From 12923eedd350b202ba3858c8db4aeceb8098894f Mon Sep 17 00:00:00 2001 From: Isabel Martin Date: Tue, 24 Sep 2024 09:54:27 -0700 Subject: [PATCH] MBL-586: Unify rewards selection viewModels (#2133) --- .../ui/fragments/RewardsFragment.kt | 169 +++--- .../viewmodels/RewardsFragmentViewModel.kt | 379 -------------- .../projectpage/RewardsSelectionViewModel.kt | 61 ++- .../RewardsFragmentViewModelTest.kt | 490 ------------------ .../RewardsSelectionViewModelTest.kt | 96 +++- 5 files changed, 201 insertions(+), 994 deletions(-) delete mode 100644 app/src/main/java/com/kickstarter/viewmodels/RewardsFragmentViewModel.kt delete mode 100644 app/src/test/java/com/kickstarter/viewmodels/RewardsFragmentViewModelTest.kt diff --git a/app/src/main/java/com/kickstarter/ui/fragments/RewardsFragment.kt b/app/src/main/java/com/kickstarter/ui/fragments/RewardsFragment.kt index b8ce816a57..7a5f27ba1e 100644 --- a/app/src/main/java/com/kickstarter/ui/fragments/RewardsFragment.kt +++ b/app/src/main/java/com/kickstarter/ui/fragments/RewardsFragment.kt @@ -6,12 +6,9 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AlertDialog -import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.State -import androidx.compose.runtime.rxjava2.subscribeAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels @@ -19,41 +16,32 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.kickstarter.R import com.kickstarter.databinding.FragmentRewardsBinding import com.kickstarter.libs.Environment -import com.kickstarter.libs.utils.extensions.addToDisposable import com.kickstarter.libs.utils.extensions.getEnvironment import com.kickstarter.libs.utils.extensions.reduce -import com.kickstarter.libs.utils.extensions.selectPledgeFragment import com.kickstarter.ui.activities.compose.projectpage.RewardCarouselScreen import com.kickstarter.ui.compose.designsystem.KSTheme import com.kickstarter.ui.compose.designsystem.KickstarterApp import com.kickstarter.ui.data.PledgeData import com.kickstarter.ui.data.PledgeReason import com.kickstarter.ui.data.ProjectData -import com.kickstarter.viewmodels.RewardsFragmentViewModel.Factory -import com.kickstarter.viewmodels.RewardsFragmentViewModel.RewardsFragmentViewModel -import com.kickstarter.viewmodels.usecases.ShippingRulesState -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.CompositeDisposable +import com.kickstarter.viewmodels.projectpage.RewardsSelectionViewModel class RewardsFragment : Fragment() { private lateinit var dialog: AlertDialog private var binding: FragmentRewardsBinding? = null - private lateinit var viewModelFactory: Factory - private val viewModel: RewardsFragmentViewModel by viewModels { - viewModelFactory - } + private lateinit var rewardsSelectionViewModelFactory: RewardsSelectionViewModel.Factory + private val viewModel: RewardsSelectionViewModel by viewModels { rewardsSelectionViewModelFactory } private lateinit var environment: Environment - private val disposables = CompositeDisposable() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { super.onCreateView(inflater, container, savedInstanceState) this.context?.getEnvironment()?.let { env -> - viewModelFactory = Factory(env) environment = env + rewardsSelectionViewModelFactory = RewardsSelectionViewModel.Factory(env) } super.onCreateView(inflater, container, savedInstanceState) @@ -61,17 +49,6 @@ class RewardsFragment : Fragment() { return binding?.root } - @Composable - private fun ScrollToPosition( - scrollToPosition: State, - listState: LazyListState - ) { - LaunchedEffect(scrollToPosition) { - // Animate scroll to the scrollToPosition item - listState.animateScrollToItem(index = scrollToPosition.value) - } - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) createDialog() @@ -86,69 +63,53 @@ class RewardsFragment : Fragment() { ) { KSTheme { - val projectData: State = viewModel.projectData().subscribeAsState(initial = ProjectData.builder().build()) - val backing = projectData.value.backing() ?: projectData.value.project().backing() - val project = projectData.value.project() - - val rules = viewModel.countrySelectorRules().collectAsStateWithLifecycle( - initialValue = ShippingRulesState() - ).value + val rewardSelectionUIState by viewModel.rewardSelectionUIState.collectAsStateWithLifecycle() + val shippingUIState by viewModel.shippingUIState.collectAsStateWithLifecycle() + val projectData = rewardSelectionUIState.project + val indexOfBackedReward = rewardSelectionUIState.initialRewardIndex + val rewards = shippingUIState.filteredRw + val project = projectData.project() + val backing = projectData.backing() - val rewards = rules.filteredRw - val listState = rememberLazyListState() + val rewardLoading = shippingUIState.loading + val currentUserShippingRule = shippingUIState.selectedShippingRule + val shippingRules = shippingUIState.shippingRules + val listState = rememberLazyListState( + initialFirstVisibleItemIndex = indexOfBackedReward + ) RewardCarouselScreen( lazyRowState = listState, - environment = requireNotNull(environment), + environment = environment, rewards = rewards, project = project, backing = backing, onRewardSelected = { - viewModel.inputs.rewardClicked(it) + viewModel.onUserRewardSelection(it) }, - countryList = rules.shippingRules, - onShippingRuleSelected = { - viewModel.inputs.selectedShippingRule(it) + countryList = shippingRules, + onShippingRuleSelected = { shippingRule -> + viewModel.selectedShippingRule(shippingRule) }, - currentShippingRule = rules.selectedShippingRule, - isLoading = rules.loading + currentShippingRule = currentUserShippingRule, + isLoading = rewardLoading ) - ScrollToPosition(viewModel.outputs.backedRewardPosition().subscribeAsState(initial = 0), listState) + LaunchedEffect(Unit) { + viewModel.flowUIRequest.collect { + viewModel.getPledgeData()?.let { + if (viewModel.shouldShowAlert()) { + showDialog() + } else { + showAddonsFragment(it) + } + } + } + } } } } } - - this.viewModel.outputs.showPledgeFragment() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - dialog.dismiss() - // showPledgeFragment(it.first, it.second) - showAddonsFragment(Pair(it.first, it.second)) - } - .addToDisposable(disposables) - - this.viewModel.outputs.showAddOnsFragment() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - dialog.dismiss() - showAddonsFragment(it) - } - .addToDisposable(disposables) - - this.viewModel.outputs.showAlert() - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - showAlert() - } - .addToDisposable(disposables) - } - - fun setState(state: Boolean?) { - state?.let { - viewModel.isExpanded(state) - } } private fun createDialog() { @@ -159,54 +120,32 @@ class RewardsFragment : Fragment() { .setMessage(getString(R.string.It_may_not_offer_some_or_all_of_your_add_ons)) .setNegativeButton(getString(R.string.No_go_back)) { _, _ -> {} } .setPositiveButton(getString(R.string.Yes_continue)) { _, _ -> - this.viewModel.inputs.alertButtonPressed() + viewModel.getPledgeData()?.let { showAddonsFragment(it) } }.create() } } - private fun showAlert() { + private fun showDialog() { if (this.isVisible) dialog.show() } - override fun onDetach() { - disposables.clear() - super.onDetach() - } - - fun configureWith(projectData: ProjectData) { - this.viewModel.inputs.configureWith(projectData) - } - - private fun showPledgeFragment( - pledgeData: PledgeData, - pledgeReason: PledgeReason - ) { - val fragment = this.selectPledgeFragment(pledgeData, pledgeReason) - - if (this.isVisible && this.parentFragmentManager.findFragmentByTag(fragment::class.java.simpleName) == null) { - this.parentFragmentManager - .beginTransaction() - .setCustomAnimations(R.anim.slide_in_right, 0, 0, R.anim.slide_out_right) - .add( - R.id.fragment_container, - fragment, - fragment::class.java.simpleName - ) - .addToBackStack(fragment::class.java.simpleName) - .commit() - } - } - - private fun showAddonsFragment(pledgeDataAndReason: Pair) { + private fun showAddonsFragment(pledgeDataAndReason: kotlin.Pair) { if (this.isVisible && this.parentFragmentManager.findFragmentByTag(BackingAddOnsFragment::class.java.simpleName) == null) { val reducedProject = pledgeDataAndReason.first.projectData().project().reduce() - val reducedProjectData = pledgeDataAndReason.first.projectData().toBuilder().project(reducedProject).build() - val reducedPledgeData = pledgeDataAndReason.first.toBuilder().projectData(reducedProjectData).build() + val reducedProjectData = + pledgeDataAndReason.first.projectData().toBuilder().project(reducedProject).build() + val reducedPledgeData = + pledgeDataAndReason.first.toBuilder().projectData(reducedProjectData).build() - val addOnsFragment = BackingAddOnsFragment.newInstance(Pair(reducedPledgeData, pledgeDataAndReason.second)) + val addOnsFragment = BackingAddOnsFragment.newInstance( + Pair( + reducedPledgeData, + pledgeDataAndReason.second + ) + ) this.parentFragmentManager.beginTransaction() .setCustomAnimations(R.anim.slide_in_right, 0, 0, R.anim.slide_out_right) @@ -219,4 +158,14 @@ class RewardsFragment : Fragment() { .commit() } } + + fun setState(state: Boolean?) { + state?.let { + viewModel.sendEvent(expanded = it) + } + } + + fun configureWith(projectData: ProjectData) { + this.viewModel.provideProjectData(projectData) + } } diff --git a/app/src/main/java/com/kickstarter/viewmodels/RewardsFragmentViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/RewardsFragmentViewModel.kt deleted file mode 100644 index d351752669..0000000000 --- a/app/src/main/java/com/kickstarter/viewmodels/RewardsFragmentViewModel.kt +++ /dev/null @@ -1,379 +0,0 @@ -package com.kickstarter.viewmodels - -import android.util.Pair -import androidx.annotation.VisibleForTesting -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope -import com.kickstarter.libs.Environment -import com.kickstarter.libs.rx.transformers.Transformers -import com.kickstarter.libs.rx.transformers.Transformers.combineLatestPair -import com.kickstarter.libs.rx.transformers.Transformers.takeWhenV2 -import com.kickstarter.libs.utils.RewardUtils -import com.kickstarter.libs.utils.ThirdPartyEventValues -import com.kickstarter.libs.utils.extensions.addToDisposable -import com.kickstarter.libs.utils.extensions.isBacked -import com.kickstarter.libs.utils.extensions.isNotNull -import com.kickstarter.mock.factories.RewardFactory -import com.kickstarter.models.Backing -import com.kickstarter.models.Project -import com.kickstarter.models.Reward -import com.kickstarter.models.ShippingRule -import com.kickstarter.ui.data.PledgeData -import com.kickstarter.ui.data.PledgeFlowContext -import com.kickstarter.ui.data.PledgeReason -import com.kickstarter.ui.data.ProjectData -import com.kickstarter.viewmodels.usecases.GetShippingRulesUseCase -import com.kickstarter.viewmodels.usecases.SendThirdPartyEventUseCaseV2 -import com.kickstarter.viewmodels.usecases.ShippingRulesState -import io.reactivex.Observable -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.subjects.BehaviorSubject -import io.reactivex.subjects.PublishSubject -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.launch -import java.util.Locale - -class RewardsFragmentViewModel { - interface Inputs { - /** Configure with current [ProjectData]. */ - fun configureWith(projectData: ProjectData) - - /** Call when a reward is clicked. */ - fun rewardClicked(reward: Reward) - - /** Call when the Alert button has been pressed */ - fun alertButtonPressed() - - fun isExpanded(state: Boolean?) - - fun selectedShippingRule(shippingRule: ShippingRule) - } - - interface Outputs { - /** Emits the position of the backed reward. */ - fun backedRewardPosition(): Observable - - /** Emits the current [ProjectData]. */ - fun projectData(): Observable - - /** Emits when we should show the [com.kickstarter.ui.fragments.PledgeFragment]. */ - fun showPledgeFragment(): Observable> - - /** Emits when we should show the [com.kickstarter.ui.fragments.BackingAddOnsFragment]. */ - fun showAddOnsFragment(): Observable> - - /** Emits if we have to show the alert in case any AddOns selection could be lost. */ - fun showAlert(): Observable> - } - - class RewardsFragmentViewModel(val environment: Environment, private var shippingRulesUseCase: GetShippingRulesUseCase? = null) : ViewModel(), Inputs, Outputs { - - private val isExpanded = PublishSubject.create() - private val projectDataInput = BehaviorSubject.create() - private val rewardClicked = PublishSubject.create>() - private val alertButtonPressed = PublishSubject.create() - - private val backedRewardPosition = PublishSubject.create() - private val projectData = BehaviorSubject.create() - private val pledgeData = PublishSubject.create>() - private val showPledgeFragment = PublishSubject.create>() - private val showAddOnsFragment = PublishSubject.create>() - private val showAlert = PublishSubject.create>() - private var selectedShippingRule: ShippingRule? = null - - private val sharedPreferences = requireNotNull(environment.sharedPreferences()) - private val ffClient = requireNotNull(environment.featureFlagClient()) - private val apolloClient = requireNotNull(environment.apolloClientV2()) - private val currentUser = requireNotNull(environment.currentUserV2()) - private val analyticEvents = requireNotNull(environment.analytics()) - private val configObservable = requireNotNull(environment.currentConfigV2()?.observable()) - - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - val onThirdPartyEventSent = BehaviorSubject.create() - - val inputs: Inputs = this - val outputs: Outputs = this - - private val disposables = CompositeDisposable() - - init { - - this.isExpanded - .filter { it } - .compose(combineLatestPair(this.projectDataInput)) - .filter { it.second.isNotNull() } - .map { it.second } - .subscribe { - this.analyticEvents.trackRewardsCarouselViewed(it) - } - .addToDisposable(disposables) - - this.projectDataInput - .filter { sortAndFilterRewards(it).isNotNull() } - .map { sortAndFilterRewards(it) } - .subscribe { - this.projectData.onNext(it) - } - .addToDisposable(disposables) - - val project = this.projectData - .filter { it.project().isNotNull() } - .map { it.project() } - - this.isExpanded - .filter { it } - .switchMap { - SendThirdPartyEventUseCaseV2(sharedPreferences, ffClient) - .sendThirdPartyEvent( - project = project, - apolloClient = apolloClient, - currentUser = currentUser, - eventName = ThirdPartyEventValues.EventName.SCREEN_VIEW, - firebaseScreen = ThirdPartyEventValues.ScreenName.REWARDS.value, - firebasePreviousScreen = ThirdPartyEventValues.ScreenName.PROJECT.value, - ) - } - .compose(Transformers.neverErrorV2()) - .subscribe { - onThirdPartyEventSent.onNext( - it.first - ) - } - .addToDisposable(disposables) - - project - .filter { it.isBacking() && indexOfBackedReward(it).isNotNull() } - .map { indexOfBackedReward(it) } - .distinctUntilChanged() - .subscribe { this.backedRewardPosition.onNext(it) } - .addToDisposable(disposables) - - val backedReward = project - .filter { it.isBacking() } - .map { it.backing()?.let { backing -> getReward(backing) } } - .filter { it.isNotNull() } - .map { requireNotNull(it) } - - val defaultRewardClicked = Pair(Reward.builder().id(0L).minimum(0.0).build(), false) - - Observable - .combineLatest(this.rewardClicked.startWith(defaultRewardClicked), this.projectDataInput) { rewardPair, projectData -> - if (!rewardPair.second) { - return@combineLatest Unit - } else { - return@combineLatest pledgeDataAndPledgeReason( - projectData, - rewardPair.first, - selectedShippingRule - ) - } - } - .filter { it.isNotNull() && it is Pair<*, *> && it.first is PledgeData && it.second is PledgeReason } - .map { requireNotNull(it as Pair) } - .subscribe { - val pledgeAndData = it - val newRw = it.first.reward() - val reason = it.second - - when (reason) { - PledgeReason.PLEDGE -> { - if (newRw.hasAddons()) - this.showAddOnsFragment.onNext(pledgeAndData) - else - this.pledgeData.onNext(pledgeAndData) - } - else -> {} - } - this.rewardClicked.onNext(defaultRewardClicked) - } - .addToDisposable(disposables) - - Observable - .combineLatest(this.rewardClicked.startWith(defaultRewardClicked), this.projectDataInput, backedReward) { rewardPair, projectData, backedReward -> - if (!rewardPair.second) { - return@combineLatest Unit - } else { - return@combineLatest Pair(pledgeDataAndPledgeReason(projectData, rewardPair.first, selectedShippingRule), backedReward) - } - } - .filter { - it.isNotNull() && it is Pair<*, *> && it.first is Pair<*, *> && it.second is Reward - } // todo extract to a function - .map { - requireNotNull(it as Pair, Reward>) - } - .subscribe { - val pledgeAndData = it.first - val newRw = it.first.first.reward() - val prevRw = it.second - val reason = it.first.second - - when (reason) { - PledgeReason.UPDATE_REWARD -> { - if (prevRw.hasAddons() && !newRw.hasAddons()) - this.showAlert.onNext(pledgeAndData) - - if (!prevRw.hasAddons() && !newRw.hasAddons()) - this.pledgeData.onNext(pledgeAndData) - - if (prevRw.hasAddons() && newRw.hasAddons()) { - if (differentShippingTypes(prevRw, newRw)) this.showAlert.onNext(it.first) - else this.showAddOnsFragment.onNext(pledgeAndData) - } - - if (!prevRw.hasAddons() && newRw.hasAddons()) { - this.showAddOnsFragment.onNext(pledgeAndData) - } - } - else -> {} - } - this.rewardClicked.onNext(defaultRewardClicked) - } - .addToDisposable(disposables) - - this.showAlert - .compose>(takeWhenV2(alertButtonPressed)) - .subscribe { - if (it.first.reward().hasAddons()) - this.showAddOnsFragment.onNext(it) - else this.pledgeData.onNext(it) - } - .addToDisposable(disposables) - - this.pledgeData - .distinctUntilChanged() - .subscribe { - this.showPledgeFragment.onNext(it) - } - .addToDisposable(disposables) - - Observable.combineLatest(configObservable, project) { config, project -> - if (shippingRulesUseCase == null) { - shippingRulesUseCase = GetShippingRulesUseCase( - apolloClient, - project, - config, - viewModelScope, - Dispatchers.IO - ) - } - shippingRulesUseCase?.let { useCaseState -> - useCaseState.invoke() - useCaseState.getScope().launch(useCaseState.getDispatcher()) { - shippingRulesUseCase?.shippingRulesState?.collectLatest { - selectedShippingRule = it.selectedShippingRule - } - } - } - return@combineLatest Observable.empty() - }.subscribe().addToDisposable(disposables) - } - - private fun sortAndFilterRewards(pData: ProjectData): ProjectData { - val startedRewards = pData.project().rewards()?.filter { RewardUtils.hasStarted(it) } - val sortedRewards = startedRewards?.filter { RewardUtils.isAvailable(pData.project(), it) }?.toMutableList() ?: mutableListOf() - val unavailableRewards = startedRewards?.filter { !RewardUtils.isAvailable(pData.project(), it) }?.toMutableList() - - unavailableRewards?.let { sortedRewards.addAll(it) } - - val modifiedProject = pData.project().toBuilder().rewards(sortedRewards).build() - return pData.toBuilder() - .project(modifiedProject) - .build() - } - - private fun getReward(backingObj: Backing): Reward { - return backingObj.reward()?.let { rw -> - if (backingObj.addOns().isNullOrEmpty()) rw - else rw.toBuilder().hasAddons(true).build() - } ?: RewardFactory.noReward() - } - - private fun differentShippingTypes(newRW: Reward, backedRW: Reward): Boolean { - return if (newRW.id() == backedRW.id()) false - else { - ( - newRW.shippingType()?.lowercase(Locale.getDefault()) - ?: "" - ) != ( - backedRW.shippingType() - ?.lowercase(Locale.getDefault()) ?: "" - ) - } - } - - private fun pledgeDataAndPledgeReason( - projectData: ProjectData, - reward: Reward, - selectedShippingRule: ShippingRule? - ): Pair { - val pledgeReason = if (projectData.project().isBacking()) PledgeReason.UPDATE_REWARD else PledgeReason.PLEDGE - val pledgeData = PledgeData.with(PledgeFlowContext.forPledgeReason(pledgeReason), projectData, reward, shippingRule = selectedShippingRule) - return Pair(pledgeData, pledgeReason) - } - - private fun indexOfBackedReward(project: Project): Int { - project.rewards()?.run { - for ((index, reward) in withIndex()) { - if (project.backing()?.isBacked(reward) == true) { - return index - } - } - } - - return 0 - } - - override fun isExpanded(state: Boolean?) { - state?.let { - this.isExpanded.onNext(it) - } - } - override fun configureWith(projectData: ProjectData) { - this.projectDataInput.onNext(projectData) - } - - override fun rewardClicked(reward: Reward) { - this.rewardClicked.onNext(Pair(reward, true)) - } - - override fun selectedShippingRule(shippingRule: ShippingRule) { - this.shippingRulesUseCase?.filterBySelectedRule(shippingRule) - } - - override fun onCleared() { - disposables.clear() - super.onCleared() - } - - override fun alertButtonPressed() = this.alertButtonPressed.onNext(Unit) - - override fun backedRewardPosition(): Observable = this.backedRewardPosition - - override fun projectData(): Observable = this.projectData - - override fun showPledgeFragment(): Observable> = this.showPledgeFragment - - override fun showAddOnsFragment(): Observable> = this.showAddOnsFragment - - override fun showAlert(): Observable> = this.showAlert - - fun countrySelectorRules(): Flow { - val state = shippingRulesUseCase?.let { useCase -> - useCase.shippingRulesState - } ?: emptyFlow() - return state - } - } - - class Factory(private val environment: Environment, private var shippingRulesUseCase: GetShippingRulesUseCase? = null) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return RewardsFragmentViewModel(environment, shippingRulesUseCase) as T - } - } -} diff --git a/app/src/main/java/com/kickstarter/viewmodels/projectpage/RewardsSelectionViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/projectpage/RewardsSelectionViewModel.kt index 11856b7735..8a82ad7b9e 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/projectpage/RewardsSelectionViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/projectpage/RewardsSelectionViewModel.kt @@ -13,6 +13,7 @@ import com.kickstarter.models.Reward import com.kickstarter.models.ShippingRule import com.kickstarter.ui.data.PledgeData import com.kickstarter.ui.data.PledgeFlowContext +import com.kickstarter.ui.data.PledgeReason import com.kickstarter.ui.data.ProjectData import com.kickstarter.viewmodels.usecases.GetShippingRulesUseCase import com.kickstarter.viewmodels.usecases.ShippingRulesState @@ -41,6 +42,7 @@ class RewardsSelectionViewModel(private val environment: Environment, private va private val apolloClient = requireNotNull(environment.apolloClientV2()) private lateinit var currentProjectData: ProjectData + private var pReason: PledgeReason? = null private var previousUserBacking: Backing? = null private var previouslyBackedReward: Reward? = null private var indexOfBackedReward = 0 @@ -74,9 +76,17 @@ class RewardsSelectionViewModel(private val environment: Environment, private va fun provideProjectData(projectData: ProjectData) { currentProjectData = projectData - previousUserBacking = projectData.backing() + previousUserBacking = + if (projectData.backing() != null) projectData.backing() + else projectData.project().backing() previouslyBackedReward = getReward(previousUserBacking) indexOfBackedReward = indexOfBackedReward(project = projectData.project()) + pReason = when { + previousUserBacking == null && projectData.project().isInPostCampaignPledgingPhase() == true -> PledgeReason.LATE_PLEDGE + previousUserBacking != null -> PledgeReason.UPDATE_PLEDGE + previousUserBacking == null && projectData.project().isInPostCampaignPledgingPhase() == false -> PledgeReason.PLEDGE + else -> PledgeReason.PLEDGE + } viewModelScope.launch { emitCurrentState() @@ -99,11 +109,16 @@ class RewardsSelectionViewModel(private val environment: Environment, private va fun onUserRewardSelection(reward: Reward) { viewModelScope.launch { - val pledgeData = - PledgeData.with(PledgeFlowContext.NEW_PLEDGE, currentProjectData, reward) + pReason?.let { + val pledgeData = PledgeData.with( + PledgeFlowContext.forPledgeReason(it), + currentProjectData, + reward + ) + analytics.trackSelectRewardCTA(pledgeData) + } newUserReward = reward emitCurrentState() - analytics.trackSelectRewardCTA(pledgeData) // Show add-ons mutableFlowUIRequest.emit(FlowUIState(currentPage = 1, expanded = true)) @@ -130,9 +145,11 @@ class RewardsSelectionViewModel(private val environment: Environment, private va return 0 } - fun sendEvent(expanded: Boolean, currentPage: Int, projectData: ProjectData) { + fun sendEvent(expanded: Boolean, currentPage: Int = 0, projectData: ProjectData? = null) { if (expanded && currentPage == 0) { - analytics.trackRewardsCarouselViewed(projectData = projectData) + projectData?.let { + analytics.trackRewardsCarouselViewed(projectData = projectData) + } ?: analytics.trackRewardsCarouselViewed(projectData = currentProjectData) } } @@ -165,6 +182,38 @@ class RewardsSelectionViewModel(private val environment: Environment, private va } } + fun getPledgeData(): Pair? { + return this.currentProjectData.run { + pReason?.let { pReason -> + Pair( + PledgeData.with( + pledgeFlowContext = PledgeFlowContext.forPledgeReason(pReason), + projectData = this, + reward = newUserReward, + shippingRule = selectedShippingRule + ), + pReason + ) + } + } + } + + /** + * Used during Crowdfunding phase, while updating pledge + * if User changes reward and had addOns backed before + * display Alert + */ + fun shouldShowAlert(): Boolean { + val prevRw = previousUserBacking?.reward() + prevRw?.let { + if (pReason == PledgeReason.UPDATE_PLEDGE) { + return !previousUserBacking?.addOns().isNullOrEmpty() && prevRw.id() != newUserReward.id() + } + } + + return false + } + class Factory(private val environment: Environment, private var shippingRulesUseCase: GetShippingRulesUseCase? = null) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { diff --git a/app/src/test/java/com/kickstarter/viewmodels/RewardsFragmentViewModelTest.kt b/app/src/test/java/com/kickstarter/viewmodels/RewardsFragmentViewModelTest.kt deleted file mode 100644 index 013a24a45b..0000000000 --- a/app/src/test/java/com/kickstarter/viewmodels/RewardsFragmentViewModelTest.kt +++ /dev/null @@ -1,490 +0,0 @@ -package com.kickstarter.viewmodels - -import android.content.SharedPreferences -import android.util.Pair -import androidx.annotation.NonNull -import com.kickstarter.KSRobolectricTestCase -import com.kickstarter.libs.Environment -import com.kickstarter.libs.MockCurrentUserV2 -import com.kickstarter.libs.featureflag.FlagKey -import com.kickstarter.libs.utils.EventName -import com.kickstarter.libs.utils.extensions.addToDisposable -import com.kickstarter.mock.MockCurrentConfigV2 -import com.kickstarter.mock.MockFeatureFlagClient -import com.kickstarter.mock.factories.BackingFactory -import com.kickstarter.mock.factories.ConfigFactory -import com.kickstarter.mock.factories.ProjectDataFactory -import com.kickstarter.mock.factories.ProjectFactory -import com.kickstarter.mock.factories.RewardFactory -import com.kickstarter.mock.factories.ShippingRuleFactory -import com.kickstarter.mock.factories.ShippingRulesEnvelopeFactory -import com.kickstarter.mock.factories.UserFactory -import com.kickstarter.mock.services.MockApolloClientV2 -import com.kickstarter.models.Project -import com.kickstarter.models.Reward -import com.kickstarter.services.apiresponses.ShippingRulesEnvelope -import com.kickstarter.ui.SharedPreferenceKey -import com.kickstarter.ui.data.PledgeData -import com.kickstarter.ui.data.PledgeFlowContext -import com.kickstarter.ui.data.PledgeReason -import com.kickstarter.ui.data.ProjectData -import com.kickstarter.viewmodels.RewardsFragmentViewModel.Factory -import com.kickstarter.viewmodels.RewardsFragmentViewModel.RewardsFragmentViewModel -import com.kickstarter.viewmodels.usecases.GetShippingRulesUseCase -import com.kickstarter.viewmodels.usecases.ShippingRulesState -import com.kickstarter.viewmodels.usecases.TPEventInputData -import io.reactivex.Observable -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.subscribers.TestSubscriber -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runTest -import org.joda.time.DateTime -import org.junit.After -import org.junit.Test -import org.mockito.Mockito - -class RewardsFragmentViewModelTest : KSRobolectricTestCase() { - - private lateinit var vm: RewardsFragmentViewModel - private val backedRewardPosition = TestSubscriber.create() - private val projectData = TestSubscriber.create() - private val showPledgeFragment = TestSubscriber>() - private val showAddOnsFragment = TestSubscriber>() - private val showAlert = TestSubscriber>() - - private val disposables = CompositeDisposable() - private fun setUpEnvironment( - @NonNull environment: Environment, - useCase: GetShippingRulesUseCase? = null - ) { - this.vm = Factory(environment, useCase).create(RewardsFragmentViewModel::class.java) - this.vm.outputs.backedRewardPosition().subscribe { this.backedRewardPosition.onNext(it) }.addToDisposable(disposables) - this.vm.outputs.projectData().subscribe { this.projectData.onNext(it) }.addToDisposable(disposables) - this.vm.outputs.showPledgeFragment().subscribe { this.showPledgeFragment.onNext(it) }.addToDisposable(disposables) - this.vm.outputs.showAddOnsFragment().subscribe { this.showAddOnsFragment.onNext(it) }.addToDisposable(disposables) - this.vm.outputs.showAlert().subscribe { this.showAlert.onNext(it) }.addToDisposable(disposables) - } - - @After - fun cleanUp() { - disposables.clear() - } - @Test - fun init_whenViewModelInstantiated_shouldSendPagedViewedEventToClient() { - val project = ProjectFactory.project() - setUpEnvironment(environment()) - - this.vm.inputs.configureWith(ProjectDataFactory.project(project)) - - this.vm.isExpanded(true) - this.segmentTrack.assertValue(EventName.PAGE_VIEWED.eventName) - } - - @Test - fun init_whenViewModelInstantiated_shouldThirdPartyEvent() { - val mockFeatureFlagClient: MockFeatureFlagClient = - object : MockFeatureFlagClient() { - override fun getBoolean(FlagKey: FlagKey): Boolean { - return true - } - } - - var sharedPreferences: SharedPreferences = Mockito.mock(SharedPreferences::class.java) - Mockito.`when`(sharedPreferences.getBoolean(SharedPreferenceKey.CONSENT_MANAGEMENT_PREFERENCE, false)).thenReturn(true) - - val environment = environment() - .toBuilder() - .featureFlagClient(mockFeatureFlagClient) - .sharedPreferences(sharedPreferences) - .currentUserV2(MockCurrentUserV2(UserFactory.user())) - .apolloClientV2(object : MockApolloClientV2() { - override fun triggerThirdPartyEvent(eventInput: TPEventInputData): Observable> { - return Observable.just(Pair(true, "")) - } - }) - .build() - - val project = ProjectFactory.project() - setUpEnvironment(environment) - - this.vm.inputs.configureWith(ProjectDataFactory.project(project)) - - this.vm.isExpanded(true) - - assertTrue(this.vm.onThirdPartyEventSent.value!!) - } - - @Test - fun init_whenViewModelInstantiated_FragmentCollapsed_shouldNotSendPagedViewedEvent() { - val project = ProjectFactory.project() - setUpEnvironment(environment()) - - this.vm.inputs.configureWith(ProjectDataFactory.project(project)) - - this.vm.isExpanded(false) - this.segmentTrack.assertNoValues() - } - - @Test - fun testBackedRewardPosition() { - val project = ProjectFactory.project() - setUpEnvironment(environment()) - - this.vm.inputs.configureWith(ProjectDataFactory.project(project)) - this.backedRewardPosition.assertNoValues() - - val reward = RewardFactory.reward() - val backedProject = ProjectFactory.backedProject() - .toBuilder() - .backing( - BackingFactory.backing() - .toBuilder() - .rewardId(reward.id()) - .build() - ) - .rewards(listOf(RewardFactory.noReward(), reward)) - .build() - this.vm.inputs.configureWith(ProjectDataFactory.project(backedProject)) - this.backedRewardPosition.assertValue(1) - - val backedSuccessfulProject = backedProject - .toBuilder() - .state(Project.STATE_SUCCESSFUL) - .build() - this.vm.inputs.configureWith(ProjectDataFactory.project(backedSuccessfulProject)) - this.backedRewardPosition.assertValue(1) - } - - @Test - fun testProjectData() { - val project = ProjectFactory.project() - setUpEnvironment(environment()) - - val projectData = ProjectDataFactory.project(project) - this.vm.inputs.configureWith(projectData) - this.projectData.assertValue(projectData) - } - - @Test - fun testShowPledgeFragment_whenBackingProject() { - val project = ProjectFactory.project() - setUpEnvironment(environment()) - - this.vm.inputs.configureWith(ProjectDataFactory.project(project)) - - val reward = RewardFactory.reward().toBuilder().hasAddons(false).build() - this.vm.inputs.rewardClicked(reward) - this.showPledgeFragment.assertValue( - Pair( - PledgeData.builder() - .pledgeFlowContext(PledgeFlowContext.NEW_PLEDGE) - .reward(reward) - .projectData(ProjectDataFactory.project(project)) - .build(), - PledgeReason.PLEDGE - ) - ) - this.showAddOnsFragment.assertNoValues() - } - - @Test - fun testShowAlert_whenBackingProject_withAddOns_sameReward() { - val reward = RewardFactory.rewardWithShipping().toBuilder().hasAddons(true).build() - val backedProject = ProjectFactory.backedProject() - .toBuilder() - .backing( - BackingFactory.backing() - .toBuilder() - .reward(reward) - .rewardId(reward.id()) - .build() - ) - .rewards(listOf(RewardFactory.noReward(), reward)) - .build() - setUpEnvironment(environment()) - - this.vm.inputs.configureWith(ProjectDataFactory.project(backedProject)) - - this.vm.inputs.rewardClicked(reward) - this.showPledgeFragment.assertNoValues() - this.vm.outputs.showAddOnsFragment().subscribe { - assertEquals(it.first.reward(), reward) - assertEquals(it.first.projectData(), ProjectDataFactory.project(backedProject)) - assertEquals(it.second, PledgeReason.UPDATE_REWARD) - }.addToDisposable(disposables) - - this.showAlert.assertNoValues() - } - - @Test - fun testShowAlert_whenBackingProject_withAddOns_otherReward() { - val rewarda = RewardFactory.rewardWithShipping().toBuilder().id(4).hasAddons(true).build() - val rewardb = RewardFactory.rewardHasAddOns().toBuilder().id(2).hasAddons(true).build() - val backedProject = ProjectFactory.backedProject() - .toBuilder() - .backing( - BackingFactory.backing() - .toBuilder() - .reward(rewardb) - .rewardId(rewardb.id()) - .build() - ) - .rewards(listOf(RewardFactory.noReward(), rewarda, rewardb)) - .build() - setUpEnvironment(environment()) - - this.vm.inputs.configureWith(ProjectDataFactory.project(backedProject)) - - this.vm.inputs.rewardClicked(rewarda) - this.showPledgeFragment.assertNoValues() - this.showAddOnsFragment.assertNoValues() - this.showAlert.assertValue( - Pair( - PledgeData.builder() - .pledgeFlowContext(PledgeFlowContext.CHANGE_REWARD) - .reward(rewarda) - .projectData(ProjectDataFactory.project(backedProject)) - .build(), - PledgeReason.UPDATE_REWARD - ) - ) - - this.vm.inputs.alertButtonPressed() - this.showAddOnsFragment.assertValue( - Pair( - PledgeData.builder() - .pledgeFlowContext(PledgeFlowContext.CHANGE_REWARD) - .reward(rewarda) - .projectData(ProjectDataFactory.project(backedProject)) - .build(), - PledgeReason.UPDATE_REWARD - ) - ) - this.showPledgeFragment.assertNoValues() - } - - @Test - fun testFilterOutRewards_whenRewardNotStarted_filtersOutReward() { - val rwNotLimitedStart = RewardFactory.reward() - val rwLimitedStartNotStartedYet = rwNotLimitedStart.toBuilder().startsAt(DateTime.now().plusDays(1)).build() - val rwLimitedStartStarted = rwNotLimitedStart.toBuilder().startsAt(DateTime.now()).build() - - val rewards = listOf(rwNotLimitedStart, rwLimitedStartNotStartedYet, rwLimitedStartStarted) - - val project = ProjectFactory.project().toBuilder().rewards(rewards).build() - - setUpEnvironment(environment()) - // - We configure the viewModel with a project that has rewards not started yet - this.vm.inputs.configureWith(ProjectDataFactory.project(project)) - - val filteredList = listOf(rwNotLimitedStart, rwLimitedStartStarted) - val projWithFilteredRewards = project.toBuilder().rewards(filteredList).build() - val modifiedPData = ProjectData.builder().project(projWithFilteredRewards).build() - - // - We check that the viewModel has filtered out the rewards not started yet - this.projectData.assertValue(modifiedPData) - } - - @Test - fun testFilterAndSortRewards_whenRewardUnavailable_sortsRewardToEnd() { - val rwNotLimitedStart = RewardFactory.reward() - val rwLimitedStartNotStartedYet = rwNotLimitedStart.toBuilder().startsAt(DateTime.now().plusDays(1)).build() - val rwLimitedStartStarted = rwNotLimitedStart.toBuilder().startsAt(DateTime.now()).build() - val limited = RewardFactory.reward().toBuilder().startsAt(DateTime.now()).limit(5).build() - val noRemaining = RewardFactory.limitReached() - val expired = RewardFactory.ended() - - val rewards = listOf( - rwNotLimitedStart, - rwLimitedStartNotStartedYet, - noRemaining, - limited, - expired, - rwLimitedStartStarted - ) - - val project = ProjectFactory.project().toBuilder().rewards(rewards).build() - - setUpEnvironment(environment()) - // - We configure the viewModel with a project that has rewards not started yet - this.vm.inputs.configureWith(ProjectDataFactory.project(project)) - - val filteredList = listOf(rwNotLimitedStart, limited, rwLimitedStartStarted, noRemaining, expired) - val projWithFilteredRewards = project.toBuilder().rewards(filteredList).build() - val modifiedPData = ProjectData.builder().project(projWithFilteredRewards).build() - - // - We check that the viewModel has filtered out the rewards not started yet - this.projectData.assertValue(modifiedPData) - } - - @Test - fun `test countrySelectorRules state contains appropriate ShippingRules when reward shipping worldwide and default location Canada`() = runTest { - - val unlimitedReward = RewardFactory.rewardWithShipping() - - val rewards = listOf( - unlimitedReward - ) - val project = ProjectFactory.project().toBuilder().rewards(rewards).build() - - val config = ConfigFactory.configForCA() - val currentConfig = MockCurrentConfigV2() - currentConfig.config(config) - - val testShippingRulesList = ShippingRulesEnvelopeFactory.shippingRules() - val apolloClient = object : MockApolloClientV2() { - override fun getShippingRules(reward: Reward): Observable { - return Observable.just(testShippingRulesList) - } - } - - val user = UserFactory.user() - val env = environment() - .toBuilder() - .currentUserV2(MockCurrentUserV2(user)) - .apolloClientV2(apolloClient) - .currentConfig2(currentConfig) - .build() - - val state = mutableListOf() - val dispatcher = UnconfinedTestDispatcher(testScheduler) - backgroundScope.launch(dispatcher) { - val useCase = GetShippingRulesUseCase(apolloClient, project, config, this, dispatcher) - setUpEnvironment(env, useCase) - - vm.inputs.configureWith(ProjectDataFactory.project(project)) - vm.countrySelectorRules().toList(state) - } - - advanceUntilIdle() // wait until all state emissions completed - - assertEquals(state.size, 3) - assertEquals(state[0], ShippingRulesState()) // Initialization - assertEquals(state[1], ShippingRulesState(loading = true)) // starts loading - assertEquals( - state[2], - ShippingRulesState( - loading = false, - selectedShippingRule = ShippingRuleFactory.canadaShippingRule(), - shippingRules = testShippingRulesList.shippingRules() - ) - ) // completed requests - } - - @Test - fun `test call vm-selectedShippingRule() with location US, pledgeData-shipping should be US `() = runTest { - - val unlimitedReward = RewardFactory.rewardWithShipping() - - val rewards = listOf( - unlimitedReward - ) - val project = ProjectFactory.project().toBuilder().rewards(rewards).build() - - val config = ConfigFactory.configForCA() - val currentConfig = MockCurrentConfigV2() - currentConfig.config(config) - - val testShippingRulesList = ShippingRulesEnvelopeFactory.shippingRules() - val apolloClient = object : MockApolloClientV2() { - override fun getShippingRules(reward: Reward): Observable { - return Observable.just(testShippingRulesList) - } - } - - val user = UserFactory.user() - val env = environment() - .toBuilder() - .currentUserV2(MockCurrentUserV2(user)) - .apolloClientV2(apolloClient) - .currentConfig2(currentConfig) - .build() - - val state = mutableListOf() - val dispatcher = UnconfinedTestDispatcher(testScheduler) - backgroundScope.launch(dispatcher) { - val useCase = GetShippingRulesUseCase(apolloClient, project, config, this, dispatcher) - setUpEnvironment(env, useCase) - - vm.inputs.configureWith(ProjectDataFactory.project(project)) - vm.countrySelectorRules().toList(state) - } - - advanceUntilIdle() // wait until all state emissions completed - - assertEquals(state.size, 3) - assertEquals(state[0], ShippingRulesState()) // Initialization - assertEquals(state[1], ShippingRulesState(loading = true)) // starts loading - assertEquals( - state[2], - ShippingRulesState( - loading = false, - selectedShippingRule = ShippingRuleFactory.canadaShippingRule(), - shippingRules = testShippingRulesList.shippingRules() - ) - ) // completed requests - - val usShippingRule = testShippingRulesList.shippingRules().first() - backgroundScope.launch(dispatcher) { - vm.inputs.configureWith(ProjectDataFactory.project(project)) - vm.inputs.selectedShippingRule(usShippingRule) - } - - vm.showAddOnsFragment().subscribe { - assertEquals(it.first.shippingRule()?.location(), usShippingRule.location()) - }.addToDisposable(disposables) - } - - @Test - fun `test DefaultShipping Rule is sent to PledgeFragment`() { - val project = ProjectFactory.backedProject() - val reward = RewardFactory.reward() - val selectedShippingRule = ShippingRuleFactory.usShippingRule() - - setUpEnvironment(environment()) - vm.inputs.configureWith(ProjectDataFactory.project(project)) - vm.inputs.selectedShippingRule(selectedShippingRule) - vm.inputs.rewardClicked(reward) - - vm.outputs.showPledgeFragment().subscribe { - assertEquals(it.second, PledgeFlowContext.CHANGE_REWARD) - assertEquals(it.first.reward(), reward) - assertEquals(it.first.projectData(), ProjectDataFactory.project(project)) - assertEquals(it.first.shippingRule(), selectedShippingRule) - }.addToDisposable(disposables) - this.showAddOnsFragment.assertNoValues() - } - - @Test - fun `test DefaultShipping Rule is sent to AddOnsFragment`() { - val reward = RewardFactory.rewardWithShipping().toBuilder().hasAddons(true).build() - val backedProject = ProjectFactory.backedProject() - .toBuilder() - .backing( - BackingFactory.backing() - .toBuilder() - .reward(reward) - .rewardId(reward.id()) - .build() - ) - .rewards(listOf(RewardFactory.noReward(), reward)) - .build() - val selectedShippingRule = ShippingRuleFactory.usShippingRule() - - setUpEnvironment(environment()) - - this.vm.inputs.configureWith(ProjectDataFactory.project(backedProject)) - this.vm.inputs.selectedShippingRule(selectedShippingRule) - this.vm.inputs.rewardClicked(reward) - - this.showPledgeFragment.assertNoValues() - this.vm.showAddOnsFragment().subscribe { - assertEquals(it.first.shippingRule(), selectedShippingRule) - assertEquals(it.first.reward(), reward) - }.addToDisposable(disposables) - this.showAlert.assertNoValues() - } -} diff --git a/app/src/test/java/com/kickstarter/viewmodels/RewardsSelectionViewModelTest.kt b/app/src/test/java/com/kickstarter/viewmodels/RewardsSelectionViewModelTest.kt index 3448d4cca6..bf9f83db07 100644 --- a/app/src/test/java/com/kickstarter/viewmodels/RewardsSelectionViewModelTest.kt +++ b/app/src/test/java/com/kickstarter/viewmodels/RewardsSelectionViewModelTest.kt @@ -18,6 +18,7 @@ import com.kickstarter.models.Backing import com.kickstarter.models.Project import com.kickstarter.models.Reward import com.kickstarter.services.apiresponses.ShippingRulesEnvelope +import com.kickstarter.ui.data.PledgeReason import com.kickstarter.ui.data.ProjectData import com.kickstarter.viewmodels.projectpage.FlowUIState import com.kickstarter.viewmodels.projectpage.RewardSelectionUIState @@ -34,6 +35,7 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Test +@OptIn(ExperimentalCoroutinesApi::class) class RewardsSelectionViewModelTest : KSRobolectricTestCase() { private lateinit var viewModel: RewardsSelectionViewModel @@ -43,7 +45,6 @@ class RewardsSelectionViewModelTest : KSRobolectricTestCase() { RewardsSelectionViewModel.Factory(environment, useCase).create(RewardsSelectionViewModel::class.java) } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun test_providing_project_should_initialize_UIState() = runTest { createViewModel() @@ -72,7 +73,6 @@ class RewardsSelectionViewModelTest : KSRobolectricTestCase() { ) } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun test_selecting_reward_with_addOns_no_previous_backing() = runTest { createViewModel() @@ -116,7 +116,6 @@ class RewardsSelectionViewModelTest : KSRobolectricTestCase() { this@RewardsSelectionViewModelTest.segmentTrack.assertValue(EventName.CTA_CLICKED.eventName) } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun test_selecting_reward_no_addOns_no_previous_backing() = runTest { createViewModel() @@ -160,7 +159,6 @@ class RewardsSelectionViewModelTest : KSRobolectricTestCase() { this@RewardsSelectionViewModelTest.segmentTrack.assertValue(EventName.CTA_CLICKED.eventName) } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun test_selecting_reward_with_addOns_previous_backing_same_selection() = runTest { createViewModel() @@ -208,7 +206,6 @@ class RewardsSelectionViewModelTest : KSRobolectricTestCase() { this@RewardsSelectionViewModelTest.segmentTrack.assertValue(EventName.CTA_CLICKED.eventName) } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun test_selecting_reward_with_addOns_previous_backing_different_selection() = runTest { createViewModel() @@ -256,7 +253,6 @@ class RewardsSelectionViewModelTest : KSRobolectricTestCase() { this@RewardsSelectionViewModelTest.segmentTrack.assertValue(EventName.CTA_CLICKED.eventName) } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun test_selecting_reward_with_no_addOns_previous_backing_no_addOns() = runTest { createViewModel() @@ -423,9 +419,8 @@ class RewardsSelectionViewModelTest : KSRobolectricTestCase() { this@RewardsSelectionViewModelTest.segmentTrack.assertNoValues() } - @OptIn(ExperimentalCoroutinesApi::class) @Test - fun `Default Location when Backing Project is backed location, and list of shipping rules for "restricted" is all places available for all restricted rewards without duplicated`() = runTest { + fun `Default Location when Backing Project is backed location, and list of shipping rules for restricted is all places available for all restricted rewards without duplicated`() = runTest { val testShippingRulesList = ShippingRulesEnvelopeFactory.shippingRules().shippingRules() val rw1 = RewardFactory @@ -499,7 +494,6 @@ class RewardsSelectionViewModelTest : KSRobolectricTestCase() { assertEquals(shippingUiState.last().shippingRules.size, 2) // the 3 available shipping rules } - @OptIn(ExperimentalCoroutinesApi::class) @Test fun `config is from Canada and available rules are global so Default Shipping is Canada, and list of shipping Rules provided matches all available reward global shipping`() = runTest { val rw = RewardFactory @@ -545,4 +539,88 @@ class RewardsSelectionViewModelTest : KSRobolectricTestCase() { assertEquals(shippingUiState.last().selectedShippingRule.location()?.name(), "Canada") assertEquals(shippingUiState.last().shippingRules, testShippingRulesList.shippingRules()) } + + @Test + fun `When user is updating pledge, if selecting a different reward and had addOns backed, show alert`() = runTest { + val reward = RewardFactory.digitalReward() + val addOns = listOf(RewardFactory.reward(), RewardFactory.addOnMultiple()) + val backing = Backing.builder() + .project(ProjectFactory.project()) + .addOns(addOns) + .reward(reward) + .build() + val project = ProjectFactory.project().toBuilder() + .backing(backing) + .isInPostCampaignPledgingPhase(false) + .postCampaignPledgingEnabled(false) + .build() + + val otherRewardSelected = RewardFactory.reward().toBuilder() + .hasAddons(true) + .build() + + val projectData = ProjectDataFactory.project(project, null, null) + val dispatcher = UnconfinedTestDispatcher(testScheduler) + backgroundScope.launch(dispatcher) { + createViewModel(environment()) + viewModel.provideProjectData(projectData) + viewModel.onUserRewardSelection(otherRewardSelected) + } + + advanceUntilIdle() // wait until all state emissions completed + assertEquals(viewModel.shouldShowAlert(), true) + assertEquals(viewModel.getPledgeData()?.second, PledgeReason.UPDATE_PLEDGE) + } + + @Test + fun `When user is making a new pledge, should not show alert`() = runTest { + val reward = RewardFactory.digitalReward() + + val otherRewardSelected = RewardFactory.reward().toBuilder() + .hasAddons(true) + .build() + + val project = ProjectFactory.project().toBuilder() + .rewards(listOf(reward, otherRewardSelected)) + .build() + + val projectData = ProjectDataFactory.project(project, null, null) + val dispatcher = UnconfinedTestDispatcher(testScheduler) + backgroundScope.launch(dispatcher) { + createViewModel(environment()) + viewModel.provideProjectData(projectData) + viewModel.onUserRewardSelection(otherRewardSelected) + } + + advanceUntilIdle() // wait until all state emissions completed + assertEquals(viewModel.shouldShowAlert(), false) + assertEquals(viewModel.getPledgeData()?.second, PledgeReason.PLEDGE) + } + + @Test + fun `When user is pledging during late pledges, should not show alert`() = runTest { + val reward = RewardFactory.digitalReward() + + val otherRewardSelected = RewardFactory.reward().toBuilder() + .hasAddons(true) + .build() + + val project = ProjectFactory.project().toBuilder() + .rewards(listOf(reward, otherRewardSelected)) + .isInPostCampaignPledgingPhase(true) + .postCampaignPledgingEnabled(true) + .build() + + val projectData = ProjectDataFactory.project(project, null, null) + val dispatcher = UnconfinedTestDispatcher(testScheduler) + backgroundScope.launch(dispatcher) { + createViewModel(environment()) + viewModel.provideProjectData(projectData) + viewModel.onUserRewardSelection(otherRewardSelected) + } + + advanceUntilIdle() // wait until all state emissions completed + assertEquals(viewModel.shouldShowAlert(), false) + assertEquals(viewModel.getPledgeData()?.second, PledgeReason.LATE_PLEDGE) + } }