From 3c24ab5fbfa25a80f815941074b9398f54645ae5 Mon Sep 17 00:00:00 2001 From: Isa Martin Date: Mon, 12 Aug 2024 14:15:11 -0700 Subject: [PATCH 1/7] - New screen for crowdfund checkout --- .../ui/fragments/CrowdfundCheckout.kt | 49 +++++++++++++++++++ .../projectpage/CrowdfundCheckoutViewModel.kt | 16 ++++++ .../layout/fragment_crowdfund_checkout.xml | 16 ++++++ 3 files changed, 81 insertions(+) create mode 100644 app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckout.kt create mode 100644 app/src/main/java/com/kickstarter/viewmodels/projectpage/CrowdfundCheckoutViewModel.kt create mode 100644 app/src/main/res/layout/fragment_crowdfund_checkout.xml diff --git a/app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckout.kt b/app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckout.kt new file mode 100644 index 0000000000..ac66abc13f --- /dev/null +++ b/app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckout.kt @@ -0,0 +1,49 @@ +package com.kickstarter.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.kickstarter.databinding.FragmentCrowdfundCheckoutBinding +import com.kickstarter.ui.compose.designsystem.KSTheme +import com.kickstarter.ui.compose.designsystem.KickstarterApp +import com.kickstarter.viewmodels.projectpage.CrowdfundCheckoutViewModel.Factory +import com.kickstarter.viewmodels.projectpage.CrowdfundCheckoutViewModel + +class CrowdfundCheckout: Fragment() { + + private var binding: FragmentCrowdfundCheckoutBinding? = null + + private lateinit var viewModelFactory: Factory + private val viewModel: CrowdfundCheckoutViewModel by viewModels { + viewModelFactory + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) + binding = FragmentCrowdfundCheckoutBinding.inflate(inflater, container, false) + return binding?.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding?.composeView?.apply { + // Dispose of the Composition when the view's LifecycleOwner is destroyed + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + // Compose world + setContent { + KickstarterApp( + useDarkTheme = true + ) { + KSTheme { + + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kickstarter/viewmodels/projectpage/CrowdfundCheckoutViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/projectpage/CrowdfundCheckoutViewModel.kt new file mode 100644 index 0000000000..95b268533b --- /dev/null +++ b/app/src/main/java/com/kickstarter/viewmodels/projectpage/CrowdfundCheckoutViewModel.kt @@ -0,0 +1,16 @@ +package com.kickstarter.viewmodels.projectpage + +import android.os.Bundle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.kickstarter.libs.Environment + +class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = null) : ViewModel() { + + class Factory(private val environment: Environment, private val bundle: Bundle? = null) : + ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return CrowdfundCheckoutViewModel(environment, bundle) as T + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_crowdfund_checkout.xml b/app/src/main/res/layout/fragment_crowdfund_checkout.xml new file mode 100644 index 0000000000..6a340c0f83 --- /dev/null +++ b/app/src/main/res/layout/fragment_crowdfund_checkout.xml @@ -0,0 +1,16 @@ + + + + + + + From 261e4985fcfff517b59daba47d6cdf8fc2690f54 Mon Sep 17 00:00:00 2001 From: Isa Martin Date: Mon, 12 Aug 2024 17:15:25 -0700 Subject: [PATCH 2/7] - New screen loading saved payment methods and user information --- .../libs/utils/extensions/FragmentExt.kt | 6 +- .../kickstarter/ui/extensions/ActivityExt.kt | 6 +- .../ui/fragments/BackingAddOnsFragment.kt | 6 + .../ui/fragments/CrowdfundCheckout.kt | 49 ------ .../ui/fragments/CrowdfundCheckoutFragment.kt | 108 +++++++++++++ .../projectpage/CrowdfundCheckoutViewModel.kt | 146 +++++++++++++++++- 6 files changed, 269 insertions(+), 52 deletions(-) delete mode 100644 app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckout.kt create mode 100644 app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckoutFragment.kt diff --git a/app/src/main/java/com/kickstarter/libs/utils/extensions/FragmentExt.kt b/app/src/main/java/com/kickstarter/libs/utils/extensions/FragmentExt.kt index 09f7be8131..3ec76853cc 100644 --- a/app/src/main/java/com/kickstarter/libs/utils/extensions/FragmentExt.kt +++ b/app/src/main/java/com/kickstarter/libs/utils/extensions/FragmentExt.kt @@ -5,13 +5,17 @@ import androidx.fragment.app.Fragment import com.kickstarter.ui.ArgumentsKey import com.kickstarter.ui.data.PledgeData import com.kickstarter.ui.data.PledgeReason +import com.kickstarter.ui.fragments.CrowdfundCheckoutFragment import com.kickstarter.ui.fragments.PledgeFragment fun Fragment.selectPledgeFragment( pledgeData: PledgeData, pledgeReason: PledgeReason ): Fragment { - return PledgeFragment().withData(pledgeData, pledgeReason) + val fragment = if (pledgeReason == PledgeReason.FIX_PLEDGE) { + PledgeFragment() + } else CrowdfundCheckoutFragment() + return fragment.withData(pledgeData, pledgeReason) } fun Fragment.withData(pledgeData: PledgeData?, pledgeReason: PledgeReason?): Fragment { diff --git a/app/src/main/java/com/kickstarter/ui/extensions/ActivityExt.kt b/app/src/main/java/com/kickstarter/ui/extensions/ActivityExt.kt index bfa376f763..aee1893190 100644 --- a/app/src/main/java/com/kickstarter/ui/extensions/ActivityExt.kt +++ b/app/src/main/java/com/kickstarter/ui/extensions/ActivityExt.kt @@ -44,6 +44,7 @@ import com.kickstarter.ui.activities.MessagesActivity import com.kickstarter.ui.data.PledgeData import com.kickstarter.ui.data.PledgeReason import com.kickstarter.ui.data.ProjectData +import com.kickstarter.ui.fragments.CrowdfundCheckoutFragment import com.kickstarter.ui.fragments.PledgeFragment import timber.log.Timber @@ -79,7 +80,10 @@ fun Activity.selectPledgeFragment( pledgeData: PledgeData, pledgeReason: PledgeReason, ): Fragment { - return PledgeFragment().withData(pledgeData, pledgeReason) + val fragment = if (pledgeReason == PledgeReason.FIX_PLEDGE) { + PledgeFragment() + } else CrowdfundCheckoutFragment() + return fragment.withData(pledgeData, pledgeReason) } fun Activity.showSnackbar(anchor: View, stringResId: Int) { diff --git a/app/src/main/java/com/kickstarter/ui/fragments/BackingAddOnsFragment.kt b/app/src/main/java/com/kickstarter/ui/fragments/BackingAddOnsFragment.kt index 7766887855..3e4065fec1 100644 --- a/app/src/main/java/com/kickstarter/ui/fragments/BackingAddOnsFragment.kt +++ b/app/src/main/java/com/kickstarter/ui/fragments/BackingAddOnsFragment.kt @@ -24,6 +24,7 @@ import com.kickstarter.ui.compose.designsystem.KSTheme.dimensions import com.kickstarter.ui.compose.designsystem.KickstarterApp import com.kickstarter.ui.data.PledgeData import com.kickstarter.ui.data.PledgeReason +import com.kickstarter.ui.extensions.showErrorToast import com.kickstarter.viewmodels.projectpage.AddOnsViewModel class BackingAddOnsFragment : Fragment() { @@ -46,6 +47,11 @@ class BackingAddOnsFragment : Fragment() { viewModelC.provideBundle(arguments) env } + + viewModelC.provideErrorAction { message -> + showErrorToast(context, this, message ?: getString(R.string.general_error_something_wrong)) + } + // Dispose of the Composition when the view's LifecycleOwner // is destroyed setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) diff --git a/app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckout.kt b/app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckout.kt deleted file mode 100644 index ac66abc13f..0000000000 --- a/app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckout.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.kickstarter.ui.fragments - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import com.kickstarter.databinding.FragmentCrowdfundCheckoutBinding -import com.kickstarter.ui.compose.designsystem.KSTheme -import com.kickstarter.ui.compose.designsystem.KickstarterApp -import com.kickstarter.viewmodels.projectpage.CrowdfundCheckoutViewModel.Factory -import com.kickstarter.viewmodels.projectpage.CrowdfundCheckoutViewModel - -class CrowdfundCheckout: Fragment() { - - private var binding: FragmentCrowdfundCheckoutBinding? = null - - private lateinit var viewModelFactory: Factory - private val viewModel: CrowdfundCheckoutViewModel by viewModels { - viewModelFactory - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - super.onCreateView(inflater, container, savedInstanceState) - binding = FragmentCrowdfundCheckoutBinding.inflate(inflater, container, false) - return binding?.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding?.composeView?.apply { - // Dispose of the Composition when the view's LifecycleOwner is destroyed - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - // Compose world - setContent { - KickstarterApp( - useDarkTheme = true - ) { - KSTheme { - - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckoutFragment.kt b/app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckoutFragment.kt new file mode 100644 index 0000000000..b96f0a1fb3 --- /dev/null +++ b/app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckoutFragment.kt @@ -0,0 +1,108 @@ +package com.kickstarter.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.kickstarter.R +import com.kickstarter.databinding.FragmentCrowdfundCheckoutBinding +import com.kickstarter.libs.utils.extensions.getEnvironment +import com.kickstarter.libs.utils.extensions.pledgeAmountTotal +import com.kickstarter.libs.utils.extensions.shippingCostIfShipping +import com.kickstarter.models.Project +import com.kickstarter.models.Reward +import com.kickstarter.models.ShippingRule +import com.kickstarter.ui.activities.compose.projectpage.CheckoutScreen +import com.kickstarter.ui.compose.designsystem.KSTheme +import com.kickstarter.ui.compose.designsystem.KickstarterApp +import com.kickstarter.ui.data.PledgeReason +import com.kickstarter.ui.extensions.showErrorToast +import com.kickstarter.viewmodels.projectpage.CheckoutUIState +import com.kickstarter.viewmodels.projectpage.CrowdfundCheckoutViewModel +import com.kickstarter.viewmodels.projectpage.CrowdfundCheckoutViewModel.Factory + +class CrowdfundCheckoutFragment : Fragment() { + + private var binding: FragmentCrowdfundCheckoutBinding? = null + + private lateinit var viewModelFactory: Factory + private val viewModel: CrowdfundCheckoutViewModel by viewModels { + viewModelFactory + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + super.onCreateView(inflater, container, savedInstanceState) + binding = FragmentCrowdfundCheckoutBinding.inflate(inflater, container, false) + + val view = binding?.root + binding?.composeView?.apply { + val environment = this.context.getEnvironment()?.let { env -> + viewModelFactory = Factory(env, bundle = arguments) + viewModel.provideBundle(arguments) + env + } + + viewModel.provideErrorAction { message -> + showErrorToast(context, this, message ?: getString(R.string.general_error_something_wrong)) + } + + // Dispose of the Composition when the view's LifecycleOwner is destroyed + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + // Compose world + setContent { + KickstarterApp( + useDarkTheme = true + ) { + + val checkoutStates = viewModel.crowdfundCheckoutUIState.collectAsStateWithLifecycle( + initialValue = CheckoutUIState() + ).value + + val rwList = checkoutStates.selectedRewards + val email = checkoutStates.userEmail + val storedCards = checkoutStates.storeCards + val isLoading = checkoutStates.isLoading + + // TODO: all of this will come from Backing on Manage/Update + val pledgeData = viewModel.getPledgeData() + val pledgeReason = viewModel.getPledgeReason() ?: PledgeReason.PLEDGE + val shippingRule = pledgeData?.shippingRule() ?: ShippingRule.builder().build() + val shippingAmount = pledgeData?.shippingCostIfShipping() ?: 0.0 + val totalAmount = pledgeData?.pledgeAmountTotal() ?: 0.0 + val bonus = pledgeData?.bonusAmount() ?: 0.0 + val project = pledgeData?.projectData()?.project() ?: Project.builder().build() + val selectedRw = pledgeData?.reward() ?: Reward.builder().build() + + KSTheme { + CheckoutScreen( + rewardsList = rwList.map { Pair(it.title() ?: "", it.pledgeAmount().toString()) }, + environment = requireNotNull(environment), + shippingAmount = shippingAmount, + selectedReward = selectedRw, + currentShippingRule = shippingRule, + totalAmount = totalAmount, + totalBonusSupport = bonus, + storedCards = storedCards, + project = project, + email = email, + pledgeReason = pledgeReason, + rewardsHaveShippables = true, // TODO: pledeData extension for this + onPledgeCtaClicked = { + viewModel.pledge() + }, + isLoading = isLoading, + newPaymentMethodClicked = { }, + onDisclaimerItemClicked = {}, + onAccountabilityLinkClicked = {} + ) + } + } + } + } + return view + } +} diff --git a/app/src/main/java/com/kickstarter/viewmodels/projectpage/CrowdfundCheckoutViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/projectpage/CrowdfundCheckoutViewModel.kt index 95b268533b..a17a35a747 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/projectpage/CrowdfundCheckoutViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/projectpage/CrowdfundCheckoutViewModel.kt @@ -3,9 +3,153 @@ package com.kickstarter.viewmodels.projectpage import android.os.Bundle import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope import com.kickstarter.libs.Environment +import com.kickstarter.libs.utils.extensions.checkoutTotalAmount +import com.kickstarter.libs.utils.extensions.pledgeAmountTotal +import com.kickstarter.libs.utils.extensions.rewardsAndAddOnsList +import com.kickstarter.libs.utils.extensions.shippingCostIfShipping +import com.kickstarter.models.Reward +import com.kickstarter.models.StoredCard +import com.kickstarter.models.User +import com.kickstarter.ui.ArgumentsKey +import com.kickstarter.ui.data.CheckoutData +import com.kickstarter.ui.data.PledgeData +import com.kickstarter.ui.data.PledgeReason +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.rx2.asFlow +import type.CreditCardPaymentType + +data class CheckoutUIState( + val storeCards: List = listOf(), + val userEmail: String = "", + val isLoading: Boolean = false, + val selectedRewards: List = emptyList(), + val shippingAmount: Double = 0.0, + val checkoutTotal: Double = 0.0, + val isPledgeButtonEnabled: Boolean = true +) class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = null) : ViewModel() { + val analytics = requireNotNull(environment.analytics()) + val apolloClient = requireNotNull(environment.apolloClientV2()) + val currentUser = requireNotNull(environment.currentUserV2()?.loggedInUser()?.asFlow()) + + private var pledgeData: PledgeData? = null + private var checkoutData: CheckoutData? = null // TOD potentially needs to change with user card input + private var pledgeReason: PledgeReason? = null + private val storedCards = mutableListOf() + private var user: User? = null + private var selectedRewards = emptyList() + private var isPledgeButtonEnabled = false + + private var errorAction: (message: String?) -> Unit = {} + + private var scope: CoroutineScope = viewModelScope + private var dispatcher: CoroutineDispatcher = Dispatchers.IO + + private var _crowdfundCheckoutUIState = MutableStateFlow(CheckoutUIState()) + val crowdfundCheckoutUIState: StateFlow + get() = _crowdfundCheckoutUIState + .asStateFlow() + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = CheckoutUIState(isLoading = false) + ) + + /** + * By default run in + * scope: viewModelScope + * dispatcher: Dispatchers.IO + */ + fun provideScopeAndDispatcher(scope: CoroutineScope, dispatcher: CoroutineDispatcher) { + this.scope = scope + this.dispatcher = dispatcher + } + + fun getPledgeData() = this.pledgeData + + fun getPledgeReason() = this.pledgeReason + + fun provideBundle(arguments: Bundle?) { + val pData = arguments?.getParcelable(ArgumentsKey.PLEDGE_PLEDGE_DATA) as PledgeData? + pledgeReason = arguments?.getSerializable(ArgumentsKey.PLEDGE_PLEDGE_REASON) as PledgeReason + + if (pData != null) { + selectedRewards = pData.rewardsAndAddOnsList() + pledgeData = pData + checkoutData = CheckoutData.builder() + .amount(pData.pledgeAmountTotal()) + .paymentType(CreditCardPaymentType.CREDIT_CARD) + .bonusAmount(pData.bonusAmount()) + .shippingAmount(pData.shippingCostIfShipping()) + .build() + + collectUserInformation() + sendPageViewedEvent() + } + } + + fun provideErrorAction(errorAction: (message: String?) -> Unit) { + this.errorAction = errorAction + } + + // TODO: can potentially be extracted to an UserUseCase + private fun collectUserInformation() { + scope.launch(dispatcher) { + emitCurrentState(isLoading = true) + currentUser.combine(apolloClient.userPrivacy().asFlow()) { cUser, privacy -> + cUser.toBuilder() + .email(privacy.email) + .name(privacy.name) + .build() + }.combine(apolloClient.getStoredCards().asFlow()) { updatedUser, cards -> + user = updatedUser + storedCards.addAll(cards) + }.catch { + errorAction.invoke(it.message) + emitCurrentState(isLoading = false) + }.collectLatest { + emitCurrentState(isLoading = false) + } + } + } + + private fun sendPageViewedEvent() { + if (checkoutData != null && pledgeData != null) { + analytics.trackCheckoutScreenViewed(checkoutData!!, pledgeData!!) + } + } + + private suspend fun emitCurrentState(isLoading: Boolean = false) { + _crowdfundCheckoutUIState.emit( + CheckoutUIState( + storeCards = storedCards.toList(), + userEmail = user?.email() ?: "", + isLoading = isLoading, + selectedRewards = selectedRewards, + shippingAmount = this.pledgeData?.shippingCostIfShipping() ?: 0.0, + checkoutTotal = this.pledgeData?.checkoutTotalAmount() ?: 0.0, + isPledgeButtonEnabled = isLoading, + ) + ) + } + + // TODO: call pledge mutation + fun pledge() { + } class Factory(private val environment: Environment, private val bundle: Bundle? = null) : ViewModelProvider.Factory { @@ -13,4 +157,4 @@ class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = return CrowdfundCheckoutViewModel(environment, bundle) as T } } -} \ No newline at end of file +} From 949c64a1132fb80dc27bfe7aeb29f55a5a71c302 Mon Sep 17 00:00:00 2001 From: Isa Martin Date: Tue, 13 Aug 2024 13:39:14 -0700 Subject: [PATCH 3/7] - Able to pledge successfully --- .../com/kickstarter/libs/utils/RewardUtils.kt | 21 +++ .../services/mutations/UpdateBackingData.kt | 41 ++++++ .../compose/projectpage/CheckoutScreen.kt | 7 +- .../ui/fragments/CrowdfundCheckoutFragment.kt | 23 ++- .../projectpage/CrowdfundCheckoutViewModel.kt | 139 +++++++++++++++++- 5 files changed, 221 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/kickstarter/libs/utils/RewardUtils.kt b/app/src/main/java/com/kickstarter/libs/utils/RewardUtils.kt index 40acc65ab5..adabd6b6eb 100644 --- a/app/src/main/java/com/kickstarter/libs/utils/RewardUtils.kt +++ b/app/src/main/java/com/kickstarter/libs/utils/RewardUtils.kt @@ -232,4 +232,25 @@ object RewardUtils { fun getFinalBonusSupportAmount(addedBonusSupport: Double, initialBonusSupport: Double): Double { return if (addedBonusSupport > 0) addedBonusSupport else initialBonusSupport } + + /** For the checkout we need to send a list repeating as much addOns items + * as the user has selected: + * User selection [R, 2xa, 3xb] + * Checkout data [R, a, a, b, b, b] + */ + fun extendAddOns(flattenedList: List): List { + val mutableList = mutableListOf() + + flattenedList.map { + if (!it.isAddOn()) mutableList.add(it) + else { + val q = it.quantity() ?: 1 + for (i in 1..q) { + mutableList.add(it) + } + } + } + + return mutableList.toList() + } } diff --git a/app/src/main/java/com/kickstarter/services/mutations/UpdateBackingData.kt b/app/src/main/java/com/kickstarter/services/mutations/UpdateBackingData.kt index 90e888f311..9924802338 100644 --- a/app/src/main/java/com/kickstarter/services/mutations/UpdateBackingData.kt +++ b/app/src/main/java/com/kickstarter/services/mutations/UpdateBackingData.kt @@ -2,6 +2,9 @@ package com.kickstarter.services.mutations import com.kickstarter.models.Backing import com.kickstarter.models.Reward +import com.kickstarter.models.StoredCard +import com.kickstarter.models.extensions.isFromPaymentSheet +import com.kickstarter.viewmodels.PledgeFragmentViewModel data class UpdateBackingData( val backing: Backing, @@ -11,3 +14,41 @@ data class UpdateBackingData( val paymentSourceId: String? = null, val intentClientSecret: String? = null ) + +/** + * Obtain the data model input that will be send to UpdateBacking mutation + * - When updating payment method with a new payment method using payment sheet + * - When updating payment method with a previously existing payment source + * - Updating any other parameter like location, amount or rewards + */ +fun getUpdateBackingData( + backing: Backing, + amount: String? = null, + locationId: String? = null, + rewardsList: List = listOf(), + pMethod: StoredCard? = null +): UpdateBackingData { + return pMethod?.let { card -> + // - Updating the payment method, a new one from PaymentSheet or already existing one + if (card.isFromPaymentSheet()) UpdateBackingData( + backing, + amount, + locationId, + rewardsList, + intentClientSecret = card.clientSetupId() + ) + else UpdateBackingData( + backing, + amount, + locationId, + rewardsList, + paymentSourceId = card.id() + ) + // - Updating amount, location or rewards + } ?: UpdateBackingData( + backing, + amount, + locationId, + rewardsList + ) +} diff --git a/app/src/main/java/com/kickstarter/ui/activities/compose/projectpage/CheckoutScreen.kt b/app/src/main/java/com/kickstarter/ui/activities/compose/projectpage/CheckoutScreen.kt index 07d53edbc7..635a900cdf 100644 --- a/app/src/main/java/com/kickstarter/ui/activities/compose/projectpage/CheckoutScreen.kt +++ b/app/src/main/java/com/kickstarter/ui/activities/compose/projectpage/CheckoutScreen.kt @@ -117,7 +117,8 @@ fun CheckoutScreenPreview() { onPledgeCtaClicked = { }, newPaymentMethodClicked = { }, onDisclaimerItemClicked = {}, - onAccountabilityLinkClicked = {} + onAccountabilityLinkClicked = {}, + onSelectedPaymentMethod = {} ) } } @@ -141,7 +142,8 @@ fun CheckoutScreen( onPledgeCtaClicked: (selectedCard: StoredCard?) -> Unit, newPaymentMethodClicked: () -> Unit, onDisclaimerItemClicked: (disclaimerItem: DisclaimerItems) -> Unit, - onAccountabilityLinkClicked: () -> Unit + onAccountabilityLinkClicked: () -> Unit, + onSelectedPaymentMethod: (StoredCard?) -> Unit = {} ) { val selectedOption = remember { mutableStateOf( @@ -153,6 +155,7 @@ fun CheckoutScreen( val onOptionSelected: (StoredCard?) -> Unit = { selectedOption.value = it + onSelectedPaymentMethod.invoke(it) } // - After adding new payment method, selected card should be updated to the newly added diff --git a/app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckoutFragment.kt b/app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckoutFragment.kt index b96f0a1fb3..1d84af18df 100644 --- a/app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckoutFragment.kt +++ b/app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckoutFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels @@ -13,6 +14,7 @@ import com.kickstarter.databinding.FragmentCrowdfundCheckoutBinding import com.kickstarter.libs.utils.extensions.getEnvironment import com.kickstarter.libs.utils.extensions.pledgeAmountTotal import com.kickstarter.libs.utils.extensions.shippingCostIfShipping +import com.kickstarter.models.Checkout import com.kickstarter.models.Project import com.kickstarter.models.Reward import com.kickstarter.models.ShippingRule @@ -21,10 +23,12 @@ import com.kickstarter.ui.compose.designsystem.KSTheme import com.kickstarter.ui.compose.designsystem.KickstarterApp import com.kickstarter.ui.data.PledgeReason import com.kickstarter.ui.extensions.showErrorToast +import com.kickstarter.ui.fragments.PledgeFragment.PledgeDelegate import com.kickstarter.viewmodels.projectpage.CheckoutUIState import com.kickstarter.viewmodels.projectpage.CrowdfundCheckoutViewModel import com.kickstarter.viewmodels.projectpage.CrowdfundCheckoutViewModel.Factory + class CrowdfundCheckoutFragment : Fragment() { private var binding: FragmentCrowdfundCheckoutBinding? = null @@ -47,7 +51,9 @@ class CrowdfundCheckoutFragment : Fragment() { } viewModel.provideErrorAction { message -> - showErrorToast(context, this, message ?: getString(R.string.general_error_something_wrong)) + activity?.runOnUiThread { + showErrorToast(context, this, message ?: getString(R.string.general_error_something_wrong)) + } } // Dispose of the Composition when the view's LifecycleOwner is destroyed @@ -77,6 +83,14 @@ class CrowdfundCheckoutFragment : Fragment() { val project = pledgeData?.projectData()?.project() ?: Project.builder().build() val selectedRw = pledgeData?.reward() ?: Reward.builder().build() + val resultCheckoutStates = viewModel.checkoutResultState.collectAsStateWithLifecycle( + initialValue = Checkout.builder().build() + ) + + if(resultCheckoutStates.value.backing().requiresAction()) { + (activity as PledgeDelegate?)?.pledgeSuccessfullyUpdated() + } + KSTheme { CheckoutScreen( rewardsList = rwList.map { Pair(it.title() ?: "", it.pledgeAmount().toString()) }, @@ -95,9 +109,12 @@ class CrowdfundCheckoutFragment : Fragment() { viewModel.pledge() }, isLoading = isLoading, - newPaymentMethodClicked = { }, + newPaymentMethodClicked = {}, onDisclaimerItemClicked = {}, - onAccountabilityLinkClicked = {} + onAccountabilityLinkClicked = {}, + onSelectedPaymentMethod = { paymentMethodSeelcted -> + viewModel.userChangedPaymentMethodSelected(paymentMethodSeelcted) + } ) } } diff --git a/app/src/main/java/com/kickstarter/viewmodels/projectpage/CrowdfundCheckoutViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/projectpage/CrowdfundCheckoutViewModel.kt index a17a35a747..dd96f51479 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/projectpage/CrowdfundCheckoutViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/projectpage/CrowdfundCheckoutViewModel.kt @@ -1,21 +1,34 @@ package com.kickstarter.viewmodels.projectpage import android.os.Bundle +import android.util.Pair import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.kickstarter.libs.Environment +import com.kickstarter.libs.RefTag +import com.kickstarter.libs.rx.transformers.Transformers.takeWhenV2 +import com.kickstarter.libs.utils.RefTagUtils +import com.kickstarter.libs.utils.RewardUtils import com.kickstarter.libs.utils.extensions.checkoutTotalAmount import com.kickstarter.libs.utils.extensions.pledgeAmountTotal import com.kickstarter.libs.utils.extensions.rewardsAndAddOnsList import com.kickstarter.libs.utils.extensions.shippingCostIfShipping +import com.kickstarter.models.Checkout +import com.kickstarter.models.Project import com.kickstarter.models.Reward import com.kickstarter.models.StoredCard import com.kickstarter.models.User +import com.kickstarter.models.extensions.getBackingData +import com.kickstarter.services.mutations.CreateBackingData +import com.kickstarter.services.mutations.UpdateBackingData +import com.kickstarter.services.mutations.getUpdateBackingData import com.kickstarter.ui.ArgumentsKey import com.kickstarter.ui.data.CheckoutData import com.kickstarter.ui.data.PledgeData import com.kickstarter.ui.data.PledgeReason +import com.kickstarter.viewmodels.getUpdateBackingData +import io.reactivex.Observable import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -26,6 +39,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.rx2.asFlow @@ -38,21 +52,27 @@ data class CheckoutUIState( val selectedRewards: List = emptyList(), val shippingAmount: Double = 0.0, val checkoutTotal: Double = 0.0, - val isPledgeButtonEnabled: Boolean = true + val isPledgeButtonEnabled: Boolean = true, + val selectedPaymentMethod: StoredCard = StoredCard.builder().build() ) class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = null) : ViewModel() { val analytics = requireNotNull(environment.analytics()) val apolloClient = requireNotNull(environment.apolloClientV2()) val currentUser = requireNotNull(environment.currentUserV2()?.loggedInUser()?.asFlow()) + val cookieManager = requireNotNull(environment.cookieManager()) + val sharedPreferences = requireNotNull(environment.sharedPreferences()) private var pledgeData: PledgeData? = null private var checkoutData: CheckoutData? = null // TOD potentially needs to change with user card input private var pledgeReason: PledgeReason? = null - private val storedCards = mutableListOf() + private var storedCards = emptyList() + private var project = Project.builder().build() private var user: User? = null private var selectedRewards = emptyList() private var isPledgeButtonEnabled = false + private var selectedPaymentMethod: StoredCard = StoredCard.builder().build() + private var refTag: RefTag? = null private var errorAction: (message: String?) -> Unit = {} @@ -69,6 +89,17 @@ class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = initialValue = CheckoutUIState(isLoading = false) ) + // - CreateBacking/UpdateBacking Result States + private var _checkoutResultState = MutableStateFlow(Checkout.builder().build()) + val checkoutResultState: StateFlow + get() = _checkoutResultState + .asStateFlow() + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + initialValue = Checkout.builder().build() + ) + /** * By default run in * scope: viewModelScope @@ -79,17 +110,28 @@ class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = this.dispatcher = dispatcher } + /** + * PledgeData information that is given to the VM via + * constructor on the bundle object. + */ fun getPledgeData() = this.pledgeData + /** + * PledgeReason information that is given to the VM via + * constructor on the bundle object. + */ fun getPledgeReason() = this.pledgeReason fun provideBundle(arguments: Bundle?) { val pData = arguments?.getParcelable(ArgumentsKey.PLEDGE_PLEDGE_DATA) as PledgeData? - pledgeReason = arguments?.getSerializable(ArgumentsKey.PLEDGE_PLEDGE_REASON) as PledgeReason + pledgeReason = arguments?.getSerializable(ArgumentsKey.PLEDGE_PLEDGE_REASON) as PledgeReason? if (pData != null) { selectedRewards = pData.rewardsAndAddOnsList() pledgeData = pData + project = pData.projectData().project() + refTag = RefTagUtils.storedCookieRefTagForProject(project, cookieManager, sharedPreferences) + checkoutData = CheckoutData.builder() .amount(pData.pledgeAmountTotal()) .paymentType(CreditCardPaymentType.CREDIT_CARD) @@ -117,7 +159,8 @@ class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = .build() }.combine(apolloClient.getStoredCards().asFlow()) { updatedUser, cards -> user = updatedUser - storedCards.addAll(cards) + selectedPaymentMethod = if (cards.isEmpty()) cards.first() else StoredCard.builder().build() + storedCards = cards }.catch { errorAction.invoke(it.message) emitCurrentState(isLoading = false) @@ -143,14 +186,100 @@ class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = shippingAmount = this.pledgeData?.shippingCostIfShipping() ?: 0.0, checkoutTotal = this.pledgeData?.checkoutTotalAmount() ?: 0.0, isPledgeButtonEnabled = isLoading, + selectedPaymentMethod = selectedPaymentMethod ) ) } - // TODO: call pledge mutation + fun userChangedPaymentMethodSelected(paymentMethodSelected: StoredCard?) { + paymentMethodSelected?.let { + selectedPaymentMethod = it + } + } + fun pledge() { + scope.launch(dispatcher) { + when (pledgeReason) { + PledgeReason.PLEDGE -> createBacking() + PledgeReason.UPDATE_PLEDGE, + PledgeReason.UPDATE_REWARD, + PledgeReason.UPDATE_PAYMENT -> updateBacking() + else -> { + errorAction.invoke(null) + } + } + } } + private suspend fun createBacking() { + val backingData = selectedPaymentMethod.getBackingData( + proj = project, + amount = pledgeData?.checkoutTotalAmount().toString(), + locationId = pledgeData?.shippingRule()?.location()?.id()?.toString(), + rewards = pledgeData?.rewardsAndAddOnsList() ?: emptyList(), + cookieRefTag = refTag + ) + + this.apolloClient.createBacking(backingData).asFlow() + .onStart { + isPledgeButtonEnabled = false + emitCurrentState(isLoading = true) + }.catch { + errorAction.invoke(it.message) + isPledgeButtonEnabled = true + emitCurrentState(isLoading = false) + } + .collectLatest { + _checkoutResultState.emit(it) + emitCurrentState(isLoading = false) + } + } + + private suspend fun updateBacking() { + val locationId = pledgeData?.shippingRule()?.location()?.id()?.toString() + val amount = pledgeData?.checkoutTotalAmount().toString() + val extendedList = + RewardUtils.extendAddOns(pledgeData?.rewardsAndAddOnsList() ?: emptyList()) + if (project.isBacking()) { + project.backing()?.let { backing -> + val backingData = + if (backing.amount() == (pledgeData?.checkoutTotalAmount() ?: 0)) { + getUpdateBackingData( + backing, + null, + locationId, + extendedList, + selectedPaymentMethod + ) + } else { + getUpdateBackingData( + backing, + amount, + locationId, + extendedList, + selectedPaymentMethod + ) + } + + apolloClient.updateBacking(backingData).asFlow() + .onStart { + isPledgeButtonEnabled = false + emitCurrentState(isLoading = true) + }.catch { + errorAction.invoke(it.message) + isPledgeButtonEnabled = true + emitCurrentState(isLoading = false) + } + .collectLatest { + _checkoutResultState.emit(it) + emitCurrentState(isLoading = false) + } + } + + } + } + + class Factory(private val environment: Environment, private val bundle: Bundle? = null) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { From 9a59f75da8ec9da57a31ab91df2b93fee170a056 Mon Sep 17 00:00:00 2001 From: Isa Martin Date: Tue, 13 Aug 2024 14:59:15 -0700 Subject: [PATCH 4/7] - Create Backing mutation called - Update backing mutation called - Analytic events sent - Third party events sent --- .../services/mutations/UpdateBackingData.kt | 1 - .../compose/projectpage/CheckoutScreen.kt | 10 +- .../ui/fragments/CrowdfundCheckoutFragment.kt | 26 ++-- .../projectpage/CrowdfundCheckoutViewModel.kt | 139 +++++++++++++++--- 4 files changed, 135 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/kickstarter/services/mutations/UpdateBackingData.kt b/app/src/main/java/com/kickstarter/services/mutations/UpdateBackingData.kt index 9924802338..8b7adf352e 100644 --- a/app/src/main/java/com/kickstarter/services/mutations/UpdateBackingData.kt +++ b/app/src/main/java/com/kickstarter/services/mutations/UpdateBackingData.kt @@ -4,7 +4,6 @@ import com.kickstarter.models.Backing import com.kickstarter.models.Reward import com.kickstarter.models.StoredCard import com.kickstarter.models.extensions.isFromPaymentSheet -import com.kickstarter.viewmodels.PledgeFragmentViewModel data class UpdateBackingData( val backing: Backing, diff --git a/app/src/main/java/com/kickstarter/ui/activities/compose/projectpage/CheckoutScreen.kt b/app/src/main/java/com/kickstarter/ui/activities/compose/projectpage/CheckoutScreen.kt index 635a900cdf..1fc1fe1c25 100644 --- a/app/src/main/java/com/kickstarter/ui/activities/compose/projectpage/CheckoutScreen.kt +++ b/app/src/main/java/com/kickstarter/ui/activities/compose/projectpage/CheckoutScreen.kt @@ -118,7 +118,7 @@ fun CheckoutScreenPreview() { newPaymentMethodClicked = { }, onDisclaimerItemClicked = {}, onAccountabilityLinkClicked = {}, - onSelectedPaymentMethod = {} + onChangedPaymentMethod = {} ) } } @@ -135,7 +135,7 @@ fun CheckoutScreen( shippingAmount: Double = 0.0, pledgeReason: PledgeReason, totalAmount: Double, - currentShippingRule: ShippingRule, + currentShippingRule: ShippingRule?, totalBonusSupport: Double = 0.0, rewardsHaveShippables: Boolean, isLoading: Boolean = false, @@ -143,7 +143,7 @@ fun CheckoutScreen( newPaymentMethodClicked: () -> Unit, onDisclaimerItemClicked: (disclaimerItem: DisclaimerItems) -> Unit, onAccountabilityLinkClicked: () -> Unit, - onSelectedPaymentMethod: (StoredCard?) -> Unit = {} + onChangedPaymentMethod: (StoredCard?) -> Unit = {} ) { val selectedOption = remember { mutableStateOf( @@ -155,7 +155,7 @@ fun CheckoutScreen( val onOptionSelected: (StoredCard?) -> Unit = { selectedOption.value = it - onSelectedPaymentMethod.invoke(it) + onChangedPaymentMethod.invoke(it) } // - After adding new payment method, selected card should be updated to the newly added @@ -288,7 +288,7 @@ fun CheckoutScreen( totalAmountConvertedString ) ?: "About $totalAmountConvertedString" - val shippingLocation = currentShippingRule.location()?.displayableName() ?: "" + val shippingLocation = currentShippingRule?.location()?.displayableName() ?: "" val deliveryDateString = if (selectedReward?.estimatedDeliveryOn().isNotNull()) { stringResource(id = R.string.Estimated_delivery) + " " + DateTimeUtils.estimatedDeliveryOn( diff --git a/app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckoutFragment.kt b/app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckoutFragment.kt index 1d84af18df..7d5bed661a 100644 --- a/app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckoutFragment.kt +++ b/app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckoutFragment.kt @@ -4,20 +4,17 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.kickstarter.R import com.kickstarter.databinding.FragmentCrowdfundCheckoutBinding +import com.kickstarter.libs.utils.RewardUtils import com.kickstarter.libs.utils.extensions.getEnvironment -import com.kickstarter.libs.utils.extensions.pledgeAmountTotal -import com.kickstarter.libs.utils.extensions.shippingCostIfShipping import com.kickstarter.models.Checkout import com.kickstarter.models.Project import com.kickstarter.models.Reward -import com.kickstarter.models.ShippingRule import com.kickstarter.ui.activities.compose.projectpage.CheckoutScreen import com.kickstarter.ui.compose.designsystem.KSTheme import com.kickstarter.ui.compose.designsystem.KickstarterApp @@ -28,7 +25,6 @@ import com.kickstarter.viewmodels.projectpage.CheckoutUIState import com.kickstarter.viewmodels.projectpage.CrowdfundCheckoutViewModel import com.kickstarter.viewmodels.projectpage.CrowdfundCheckoutViewModel.Factory - class CrowdfundCheckoutFragment : Fragment() { private var binding: FragmentCrowdfundCheckoutBinding? = null @@ -72,14 +68,13 @@ class CrowdfundCheckoutFragment : Fragment() { val email = checkoutStates.userEmail val storedCards = checkoutStates.storeCards val isLoading = checkoutStates.isLoading + val shippingAmount = checkoutStates.shippingAmount + val totalAmount = checkoutStates.checkoutTotal + val shippingRule = checkoutStates.shippingRule + val bonus = checkoutStates.bonusAmount - // TODO: all of this will come from Backing on Manage/Update val pledgeData = viewModel.getPledgeData() val pledgeReason = viewModel.getPledgeReason() ?: PledgeReason.PLEDGE - val shippingRule = pledgeData?.shippingRule() ?: ShippingRule.builder().build() - val shippingAmount = pledgeData?.shippingCostIfShipping() ?: 0.0 - val totalAmount = pledgeData?.pledgeAmountTotal() ?: 0.0 - val bonus = pledgeData?.bonusAmount() ?: 0.0 val project = pledgeData?.projectData()?.project() ?: Project.builder().build() val selectedRw = pledgeData?.reward() ?: Reward.builder().build() @@ -87,11 +82,12 @@ class CrowdfundCheckoutFragment : Fragment() { initialValue = Checkout.builder().build() ) - if(resultCheckoutStates.value.backing().requiresAction()) { + if (resultCheckoutStates.value.backing().requiresAction()) { (activity as PledgeDelegate?)?.pledgeSuccessfullyUpdated() } KSTheme { + // TODO: update to display local pickup CheckoutScreen( rewardsList = rwList.map { Pair(it.title() ?: "", it.pledgeAmount().toString()) }, environment = requireNotNull(environment), @@ -104,7 +100,9 @@ class CrowdfundCheckoutFragment : Fragment() { project = project, email = email, pledgeReason = pledgeReason, - rewardsHaveShippables = true, // TODO: pledeData extension for this + rewardsHaveShippables = rwList.any { + RewardUtils.isShippable(it) + }, onPledgeCtaClicked = { viewModel.pledge() }, @@ -112,8 +110,8 @@ class CrowdfundCheckoutFragment : Fragment() { newPaymentMethodClicked = {}, onDisclaimerItemClicked = {}, onAccountabilityLinkClicked = {}, - onSelectedPaymentMethod = { paymentMethodSeelcted -> - viewModel.userChangedPaymentMethodSelected(paymentMethodSeelcted) + onChangedPaymentMethod = { paymentMethodSelected -> + viewModel.userChangedPaymentMethodSelected(paymentMethodSelected) } ) } diff --git a/app/src/main/java/com/kickstarter/viewmodels/projectpage/CrowdfundCheckoutViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/projectpage/CrowdfundCheckoutViewModel.kt index dd96f51479..6b6142ab15 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/projectpage/CrowdfundCheckoutViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/projectpage/CrowdfundCheckoutViewModel.kt @@ -7,27 +7,30 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.kickstarter.libs.Environment import com.kickstarter.libs.RefTag -import com.kickstarter.libs.rx.transformers.Transformers.takeWhenV2 import com.kickstarter.libs.utils.RefTagUtils import com.kickstarter.libs.utils.RewardUtils +import com.kickstarter.libs.utils.ThirdPartyEventValues import com.kickstarter.libs.utils.extensions.checkoutTotalAmount import com.kickstarter.libs.utils.extensions.pledgeAmountTotal import com.kickstarter.libs.utils.extensions.rewardsAndAddOnsList import com.kickstarter.libs.utils.extensions.shippingCostIfShipping +import com.kickstarter.models.Backing import com.kickstarter.models.Checkout +import com.kickstarter.models.Location import com.kickstarter.models.Project import com.kickstarter.models.Reward +import com.kickstarter.models.ShippingRule import com.kickstarter.models.StoredCard import com.kickstarter.models.User import com.kickstarter.models.extensions.getBackingData -import com.kickstarter.services.mutations.CreateBackingData -import com.kickstarter.services.mutations.UpdateBackingData import com.kickstarter.services.mutations.getUpdateBackingData import com.kickstarter.ui.ArgumentsKey import com.kickstarter.ui.data.CheckoutData import com.kickstarter.ui.data.PledgeData +import com.kickstarter.ui.data.PledgeFlowContext import com.kickstarter.ui.data.PledgeReason import com.kickstarter.viewmodels.getUpdateBackingData +import com.kickstarter.viewmodels.usecases.SendThirdPartyEventUseCaseV2 import io.reactivex.Observable import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -37,8 +40,10 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -53,7 +58,9 @@ data class CheckoutUIState( val shippingAmount: Double = 0.0, val checkoutTotal: Double = 0.0, val isPledgeButtonEnabled: Boolean = true, - val selectedPaymentMethod: StoredCard = StoredCard.builder().build() + val selectedPaymentMethod: StoredCard = StoredCard.builder().build(), + val bonusAmount: Double = 0.0, + val shippingRule: ShippingRule? = null ) class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = null) : ViewModel() { @@ -62,17 +69,24 @@ class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = val currentUser = requireNotNull(environment.currentUserV2()?.loggedInUser()?.asFlow()) val cookieManager = requireNotNull(environment.cookieManager()) val sharedPreferences = requireNotNull(environment.sharedPreferences()) + val ffClient = requireNotNull(environment.featureFlagClient()) private var pledgeData: PledgeData? = null private var checkoutData: CheckoutData? = null // TOD potentially needs to change with user card input private var pledgeReason: PledgeReason? = null private var storedCards = emptyList() private var project = Project.builder().build() + private var backing: Backing? = null private var user: User? = null private var selectedRewards = emptyList() private var isPledgeButtonEnabled = false private var selectedPaymentMethod: StoredCard = StoredCard.builder().build() + private var shippingRule: ShippingRule? = null private var refTag: RefTag? = null + private var shippingAmount = 0.0 + private var totalAmount = 0.0 + private var bonusAmount = 0.0 + private var thirdPartyEventSent = Pair(false, "") private var errorAction: (message: String?) -> Unit = {} @@ -127,17 +141,76 @@ class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = pledgeReason = arguments?.getSerializable(ArgumentsKey.PLEDGE_PLEDGE_REASON) as PledgeReason? if (pData != null) { - selectedRewards = pData.rewardsAndAddOnsList() pledgeData = pData project = pData.projectData().project() - refTag = RefTagUtils.storedCookieRefTagForProject(project, cookieManager, sharedPreferences) + backing = project.backing() + refTag = RefTagUtils.storedCookieRefTagForProject( + project, + cookieManager, + sharedPreferences + ) + + if (backing == null) { + selectedRewards = pData.rewardsAndAddOnsList() + pledgeData = pData + refTag = RefTagUtils.storedCookieRefTagForProject( + project, + cookieManager, + sharedPreferences + ) + + shippingRule = pData.shippingRule() + shippingAmount = pData.shippingCostIfShipping() + bonusAmount = pData.bonusAmount() + totalAmount = pData.checkoutTotalAmount() + + checkoutData = CheckoutData.builder() + .amount(pData.pledgeAmountTotal()) + .paymentType(CreditCardPaymentType.CREDIT_CARD) + .bonusAmount(bonusAmount) + .shippingAmount(pData.shippingCostIfShipping()) + .build() + } else { + // TODO: explore make re-usable into a separate Utils/extension extracting function all information from backing code + val list = mutableListOf() + backing?.reward()?.let { + list.add(it) + } + backing?.addOns()?.let { + list.addAll(it) + } + backing?.location()?.let { + shippingRule = ShippingRule.builder() + .location(it) + .build() + } + + if (backing?.location() == null && backing?.locationName() != null && backing?.locationId() != null) { + val location = Location.builder() + .name(backing?.locationName()) + .displayableName(backing?.locationName()) + .id(backing?.locationId()) + .build() + shippingRule = ShippingRule.builder() + .location(location) + .build() + } + + selectedRewards = list.toList() + + shippingAmount = (backing?.shippingAmount() ?: 0.0).toDouble() + + bonusAmount = (backing?.bonusAmount() ?: 0.0).toDouble() + val pAmount = (backing?.amount() ?: 0.0).toDouble() + totalAmount = pAmount + bonusAmount + shippingAmount - checkoutData = CheckoutData.builder() - .amount(pData.pledgeAmountTotal()) - .paymentType(CreditCardPaymentType.CREDIT_CARD) - .bonusAmount(pData.bonusAmount()) - .shippingAmount(pData.shippingCostIfShipping()) - .build() + checkoutData = CheckoutData.builder() + .amount(totalAmount) + .paymentType(CreditCardPaymentType.CREDIT_CARD) + .bonusAmount(bonusAmount) + .shippingAmount(shippingAmount) + .build() + } collectUserInformation() sendPageViewedEvent() @@ -159,7 +232,6 @@ class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = .build() }.combine(apolloClient.getStoredCards().asFlow()) { updatedUser, cards -> user = updatedUser - selectedPaymentMethod = if (cards.isEmpty()) cards.first() else StoredCard.builder().build() storedCards = cards }.catch { errorAction.invoke(it.message) @@ -172,7 +244,9 @@ class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = private fun sendPageViewedEvent() { if (checkoutData != null && pledgeData != null) { - analytics.trackCheckoutScreenViewed(checkoutData!!, pledgeData!!) + if (pledgeData?.pledgeFlowContext() == PledgeFlowContext.NEW_PLEDGE) + analytics.trackCheckoutScreenViewed(requireNotNull(checkoutData), requireNotNull(pledgeData)) + else analytics.trackUpdatePledgePageViewed(requireNotNull(checkoutData), requireNotNull(pledgeData)) } } @@ -183,10 +257,12 @@ class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = userEmail = user?.email() ?: "", isLoading = isLoading, selectedRewards = selectedRewards, - shippingAmount = this.pledgeData?.shippingCostIfShipping() ?: 0.0, - checkoutTotal = this.pledgeData?.checkoutTotalAmount() ?: 0.0, + shippingAmount = shippingAmount, + checkoutTotal = totalAmount, isPledgeButtonEnabled = isLoading, - selectedPaymentMethod = selectedPaymentMethod + selectedPaymentMethod = selectedPaymentMethod, + bonusAmount = bonusAmount, + shippingRule = shippingRule ) ) } @@ -195,8 +271,25 @@ class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = paymentMethodSelected?.let { selectedPaymentMethod = it } + + // - Send event on background thread + scope.launch(dispatcher) { + SendThirdPartyEventUseCaseV2(sharedPreferences, ffClient) + .sendThirdPartyEvent( + project = Observable.just(project), + currentUser = requireNotNull(environment.currentUserV2()), + apolloClient = apolloClient, + draftPledge = Pair(pledgeData?.pledgeAmountTotal(), shippingAmount), + checkoutAndPledgeData = Observable.just(Pair(checkoutData, pledgeData)), + eventName = ThirdPartyEventValues.EventName.ADD_PAYMENT_INFO + ).asFlow().collect { + thirdPartyEventSent = it + } + } } + fun isThirdPartyEventSent(): Pair = this.thirdPartyEventSent + fun pledge() { scope.launch(dispatcher) { when (pledgeReason) { @@ -212,6 +305,10 @@ class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = } private suspend fun createBacking() { + if (checkoutData != null && pledgeData != null) { + analytics.trackPledgeSubmitCTA(requireNotNull(checkoutData), requireNotNull(pledgeData)) + } + val backingData = selectedPaymentMethod.getBackingData( proj = project, amount = pledgeData?.checkoutTotalAmount().toString(), @@ -230,6 +327,7 @@ class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = emitCurrentState(isLoading = false) } .collectLatest { + checkoutData = checkoutData?.toBuilder()?.id(it.id())?.build() _checkoutResultState.emit(it) emitCurrentState(isLoading = false) } @@ -269,17 +367,16 @@ class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = errorAction.invoke(it.message) isPledgeButtonEnabled = true emitCurrentState(isLoading = false) - } - .collectLatest { + }.onCompletion { + }.collectLatest { + checkoutData = checkoutData?.toBuilder()?.id(it.id())?.build() _checkoutResultState.emit(it) emitCurrentState(isLoading = false) } } - } } - class Factory(private val environment: Environment, private val bundle: Bundle? = null) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { From 64d6fd8ec6cb82f0967db16de6d1fe8c777a4caa Mon Sep 17 00:00:00 2001 From: Isa Martin Date: Tue, 13 Aug 2024 15:54:53 -0700 Subject: [PATCH 5/7] - Recover from bug, selected shipping rule was not being sent to addOns screen --- .../java/com/kickstarter/ui/fragments/RewardsFragment.kt | 5 +++++ .../com/kickstarter/viewmodels/RewardsFragmentViewModel.kt | 4 ++++ 2 files changed, 9 insertions(+) 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..6f727d0dba 100644 --- a/app/src/main/java/com/kickstarter/ui/fragments/RewardsFragment.kt +++ b/app/src/main/java/com/kickstarter/ui/fragments/RewardsFragment.kt @@ -23,6 +23,7 @@ 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.mock.factories.ShippingRuleFactory import com.kickstarter.ui.activities.compose.projectpage.RewardCarouselScreen import com.kickstarter.ui.compose.designsystem.KSTheme import com.kickstarter.ui.compose.designsystem.KickstarterApp @@ -94,6 +95,10 @@ class RewardsFragment : Fragment() { initialValue = ShippingRulesState() ).value + if (rules.selectedShippingRule != ShippingRuleFactory.emptyShippingRule()) { + viewModel.setInitialShippingRule(rules.selectedShippingRule) + } + val rewards = rules.filteredRw val listState = rememberLazyListState() diff --git a/app/src/main/java/com/kickstarter/viewmodels/RewardsFragmentViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/RewardsFragmentViewModel.kt index 3c05d13873..26a61458de 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/RewardsFragmentViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/RewardsFragmentViewModel.kt @@ -337,6 +337,10 @@ class RewardsFragmentViewModel { this.selectedShippingRule = shippingRule } + fun setInitialShippingRule(rule: ShippingRule) { + this.selectedShippingRule = rule + } + override fun onCleared() { disposables.clear() super.onCleared() From a586382de388c426369964f4741c8b1dd34f9d86 Mon Sep 17 00:00:00 2001 From: Isa Martin Date: Tue, 13 Aug 2024 17:32:07 -0700 Subject: [PATCH 6/7] - Pledge & change payment method already in place --- .../ui/fragments/CrowdfundCheckoutFragment.kt | 19 +- .../projectpage/CrowdfundCheckoutViewModel.kt | 207 ++++++++++-------- 2 files changed, 125 insertions(+), 101 deletions(-) diff --git a/app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckoutFragment.kt b/app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckoutFragment.kt index 7d5bed661a..437263425d 100644 --- a/app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckoutFragment.kt +++ b/app/src/main/java/com/kickstarter/ui/fragments/CrowdfundCheckoutFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels @@ -12,7 +13,6 @@ import com.kickstarter.R import com.kickstarter.databinding.FragmentCrowdfundCheckoutBinding import com.kickstarter.libs.utils.RewardUtils import com.kickstarter.libs.utils.extensions.getEnvironment -import com.kickstarter.models.Checkout import com.kickstarter.models.Project import com.kickstarter.models.Reward import com.kickstarter.ui.activities.compose.projectpage.CheckoutScreen @@ -78,16 +78,23 @@ class CrowdfundCheckoutFragment : Fragment() { val project = pledgeData?.projectData()?.project() ?: Project.builder().build() val selectedRw = pledgeData?.reward() ?: Reward.builder().build() - val resultCheckoutStates = viewModel.checkoutResultState.collectAsStateWithLifecycle( - initialValue = Checkout.builder().build() - ) + val checkoutSuccess = viewModel.checkoutResultState.collectAsStateWithLifecycle().value + val id = checkoutSuccess.first?.id() ?: -1 - if (resultCheckoutStates.value.backing().requiresAction()) { - (activity as PledgeDelegate?)?.pledgeSuccessfullyUpdated() + LaunchedEffect(id) { + if (id > 0) { + if (pledgeReason == PledgeReason.PLEDGE) + (activity as PledgeDelegate?)?.pledgeSuccessfullyCreated(checkoutSuccess) + if (pledgeReason == PledgeReason.UPDATE_PAYMENT) + (activity as PledgeDelegate?)?.pledgePaymentSuccessfullyUpdated() + if (pledgeReason == PledgeReason.UPDATE_REWARD || pledgeReason == PledgeReason.UPDATE_PLEDGE) + (activity as PledgeDelegate?)?.pledgeSuccessfullyUpdated() + } } KSTheme { // TODO: update to display local pickup + // TODO: hide bonus support if 0 CheckoutScreen( rewardsList = rwList.map { Pair(it.title() ?: "", it.pledgeAmount().toString()) }, environment = requireNotNull(environment), diff --git a/app/src/main/java/com/kickstarter/viewmodels/projectpage/CrowdfundCheckoutViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/projectpage/CrowdfundCheckoutViewModel.kt index 6b6142ab15..436e2fdc06 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/projectpage/CrowdfundCheckoutViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/projectpage/CrowdfundCheckoutViewModel.kt @@ -8,14 +8,12 @@ import androidx.lifecycle.viewModelScope import com.kickstarter.libs.Environment import com.kickstarter.libs.RefTag import com.kickstarter.libs.utils.RefTagUtils -import com.kickstarter.libs.utils.RewardUtils import com.kickstarter.libs.utils.ThirdPartyEventValues import com.kickstarter.libs.utils.extensions.checkoutTotalAmount import com.kickstarter.libs.utils.extensions.pledgeAmountTotal import com.kickstarter.libs.utils.extensions.rewardsAndAddOnsList import com.kickstarter.libs.utils.extensions.shippingCostIfShipping import com.kickstarter.models.Backing -import com.kickstarter.models.Checkout import com.kickstarter.models.Location import com.kickstarter.models.Project import com.kickstarter.models.Reward @@ -29,7 +27,6 @@ import com.kickstarter.ui.data.CheckoutData import com.kickstarter.ui.data.PledgeData import com.kickstarter.ui.data.PledgeFlowContext import com.kickstarter.ui.data.PledgeReason -import com.kickstarter.viewmodels.getUpdateBackingData import com.kickstarter.viewmodels.usecases.SendThirdPartyEventUseCaseV2 import io.reactivex.Observable import kotlinx.coroutines.CoroutineDispatcher @@ -38,12 +35,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -96,7 +90,6 @@ class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = private var _crowdfundCheckoutUIState = MutableStateFlow(CheckoutUIState()) val crowdfundCheckoutUIState: StateFlow get() = _crowdfundCheckoutUIState - .asStateFlow() .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(), @@ -104,14 +97,13 @@ class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = ) // - CreateBacking/UpdateBacking Result States - private var _checkoutResultState = MutableStateFlow(Checkout.builder().build()) - val checkoutResultState: StateFlow + private var _checkoutResultState = MutableStateFlow>(Pair(null, null)) + val checkoutResultState: StateFlow> get() = _checkoutResultState - .asStateFlow() .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(), - initialValue = Checkout.builder().build() + initialValue = Pair(null, null) ) /** @@ -139,6 +131,7 @@ class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = fun provideBundle(arguments: Bundle?) { val pData = arguments?.getParcelable(ArgumentsKey.PLEDGE_PLEDGE_DATA) as PledgeData? pledgeReason = arguments?.getSerializable(ArgumentsKey.PLEDGE_PLEDGE_REASON) as PledgeReason? + val flowContext = pledgeReason?.let { PledgeFlowContext.forPledgeReason(it) } if (pData != null) { pledgeData = pData @@ -150,66 +143,16 @@ class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = sharedPreferences ) - if (backing == null) { - selectedRewards = pData.rewardsAndAddOnsList() - pledgeData = pData - refTag = RefTagUtils.storedCookieRefTagForProject( - project, - cookieManager, - sharedPreferences - ) - - shippingRule = pData.shippingRule() - shippingAmount = pData.shippingCostIfShipping() - bonusAmount = pData.bonusAmount() - totalAmount = pData.checkoutTotalAmount() - - checkoutData = CheckoutData.builder() - .amount(pData.pledgeAmountTotal()) - .paymentType(CreditCardPaymentType.CREDIT_CARD) - .bonusAmount(bonusAmount) - .shippingAmount(pData.shippingCostIfShipping()) - .build() - } else { - // TODO: explore make re-usable into a separate Utils/extension extracting function all information from backing code - val list = mutableListOf() - backing?.reward()?.let { - list.add(it) - } - backing?.addOns()?.let { - list.addAll(it) - } - backing?.location()?.let { - shippingRule = ShippingRule.builder() - .location(it) - .build() + when (flowContext) { + PledgeFlowContext.NEW_PLEDGE, + PledgeFlowContext.CHANGE_REWARD -> getPledgeInfoFrom(pData) + PledgeFlowContext.MANAGE_REWARD -> { + backing?.let { getPledgeInfoFrom(it) } } - if (backing?.location() == null && backing?.locationName() != null && backing?.locationId() != null) { - val location = Location.builder() - .name(backing?.locationName()) - .displayableName(backing?.locationName()) - .id(backing?.locationId()) - .build() - shippingRule = ShippingRule.builder() - .location(location) - .build() + else -> { + errorAction.invoke(null) } - - selectedRewards = list.toList() - - shippingAmount = (backing?.shippingAmount() ?: 0.0).toDouble() - - bonusAmount = (backing?.bonusAmount() ?: 0.0).toDouble() - val pAmount = (backing?.amount() ?: 0.0).toDouble() - totalAmount = pAmount + bonusAmount + shippingAmount - - checkoutData = CheckoutData.builder() - .amount(totalAmount) - .paymentType(CreditCardPaymentType.CREDIT_CARD) - .bonusAmount(bonusAmount) - .shippingAmount(shippingAmount) - .build() } collectUserInformation() @@ -217,6 +160,69 @@ class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = } } + private fun getPledgeInfoFrom(backing: Backing) { + // TODO: explore make re-usable into a separate Utils/extension extracting function all information from backing code + val list = mutableListOf() + backing.reward()?.let { + list.add(it) + } + backing.addOns()?.let { + list.addAll(it) + } + backing.location()?.let { + shippingRule = ShippingRule.builder() + .location(it) + .build() + } + + if (backing.location() == null && backing.locationName() != null && backing.locationId() != null) { + val location = Location.builder() + .name(backing.locationName()) + .displayableName(backing.locationName()) + .id(backing.locationId()) + .build() + shippingRule = ShippingRule.builder() + .location(location) + .build() + } + + selectedRewards = list.toList() + + shippingAmount = (backing.shippingAmount() ?: 0.0).toDouble() + + bonusAmount = (backing.bonusAmount() ?: 0.0).toDouble() + totalAmount = (backing.amount() ?: 0.0).toDouble() + + checkoutData = CheckoutData.builder() + .amount(totalAmount) + .paymentType(CreditCardPaymentType.CREDIT_CARD) + .bonusAmount(bonusAmount) + .shippingAmount(shippingAmount) + .build() + } + + private fun getPledgeInfoFrom(pData: PledgeData) { + selectedRewards = pData.rewardsAndAddOnsList() + pledgeData = pData + refTag = RefTagUtils.storedCookieRefTagForProject( + project, + cookieManager, + sharedPreferences + ) + + shippingRule = pData.shippingRule() + shippingAmount = pData.shippingCostIfShipping() + bonusAmount = pData.bonusAmount() + totalAmount = pData.checkoutTotalAmount() + + checkoutData = CheckoutData.builder() + .amount(pData.pledgeAmountTotal()) + .paymentType(CreditCardPaymentType.CREDIT_CARD) + .bonusAmount(bonusAmount) + .shippingAmount(pData.shippingCostIfShipping()) + .build() + } + fun provideErrorAction(errorAction: (message: String?) -> Unit) { this.errorAction = errorAction } @@ -328,38 +334,50 @@ class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = } .collectLatest { checkoutData = checkoutData?.toBuilder()?.id(it.id())?.build() - _checkoutResultState.emit(it) + _checkoutResultState.emit(Pair(checkoutData, pledgeData)) emitCurrentState(isLoading = false) } } private suspend fun updateBacking() { - val locationId = pledgeData?.shippingRule()?.location()?.id()?.toString() - val amount = pledgeData?.checkoutTotalAmount().toString() - val extendedList = - RewardUtils.extendAddOns(pledgeData?.rewardsAndAddOnsList() ?: emptyList()) - if (project.isBacking()) { - project.backing()?.let { backing -> - val backingData = - if (backing.amount() == (pledgeData?.checkoutTotalAmount() ?: 0)) { - getUpdateBackingData( - backing, - null, - locationId, - extendedList, - selectedPaymentMethod - ) - } else { - getUpdateBackingData( - backing, - amount, - locationId, - extendedList, - selectedPaymentMethod - ) + project.backing()?.let { backing -> + val backingData = when (pledgeReason) { + PledgeReason.UPDATE_PAYMENT -> { + val locationId = backing.locationId() ?: 0 + val rwl = mutableListOf() + backing.reward()?.let { + rwl.add(it) } + backing.addOns()?.let { + rwl.addAll(it) + } + + getUpdateBackingData( + backing, + null, + locationId.toString(), + rwl, + selectedPaymentMethod + ) + } + PledgeReason.UPDATE_REWARD -> { + getUpdateBackingData( + backing, + pledgeData?.checkoutTotalAmount().toString(), + pledgeData?.shippingRule()?.location()?.id().toString(), + pledgeData?.rewardsAndAddOnsList() ?: emptyList(), + selectedPaymentMethod + ) + } + PledgeReason.FIX_PLEDGE, // Managed on PledgeFragment/ViewModel + PledgeReason.PLEDGE, // Error + PledgeReason.UPDATE_PLEDGE, // Error + PledgeReason.LATE_PLEDGE, // Error + null -> { null } + } - apolloClient.updateBacking(backingData).asFlow() + backingData?.let { + apolloClient.updateBacking(it).asFlow() .onStart { isPledgeButtonEnabled = false emitCurrentState(isLoading = true) @@ -367,10 +385,9 @@ class CrowdfundCheckoutViewModel(val environment: Environment, bundle: Bundle? = errorAction.invoke(it.message) isPledgeButtonEnabled = true emitCurrentState(isLoading = false) - }.onCompletion { }.collectLatest { checkoutData = checkoutData?.toBuilder()?.id(it.id())?.build() - _checkoutResultState.emit(it) + _checkoutResultState.emit(Pair(checkoutData, pledgeData)) emitCurrentState(isLoading = false) } } From 717fdc1dd86447fa81226f9faf40d58f7f8de14a Mon Sep 17 00:00:00 2001 From: Isa Martin Date: Tue, 13 Aug 2024 17:45:10 -0700 Subject: [PATCH 7/7] - Tests for FragmentSelection on Checkout Flow --- .../libs/utils/extensions/FragmentExtTest.kt | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/app/src/test/java/com/kickstarter/libs/utils/extensions/FragmentExtTest.kt b/app/src/test/java/com/kickstarter/libs/utils/extensions/FragmentExtTest.kt index 190ff97e9b..2a83c1a366 100644 --- a/app/src/test/java/com/kickstarter/libs/utils/extensions/FragmentExtTest.kt +++ b/app/src/test/java/com/kickstarter/libs/utils/extensions/FragmentExtTest.kt @@ -2,6 +2,7 @@ package com.kickstarter.libs.utils.extensions import androidx.fragment.app.Fragment import com.kickstarter.KSRobolectricTestCase +import com.kickstarter.mock.factories.BackingFactory import com.kickstarter.mock.factories.ProjectDataFactory import com.kickstarter.mock.factories.ProjectFactory import com.kickstarter.models.Reward @@ -9,6 +10,7 @@ import com.kickstarter.ui.ArgumentsKey import com.kickstarter.ui.data.PledgeData import com.kickstarter.ui.data.PledgeFlowContext import com.kickstarter.ui.data.PledgeReason +import com.kickstarter.ui.fragments.CrowdfundCheckoutFragment import com.kickstarter.ui.fragments.PledgeFragment import org.junit.Test @@ -45,14 +47,14 @@ class FragmentExtTest : KSRobolectricTestCase() { } @Test - fun testPledgeFragmentInstance() { + fun testPledgeFragmentInstance_ForNewPledge() { val project = ProjectFactory.project() val projectData = ProjectDataFactory.project(project) val reward = Reward.builder().build() val addOns = listOf(reward) val pledgeData = PledgeData.builder() - .pledgeFlowContext(PledgeFlowContext.MANAGE_REWARD) + .pledgeFlowContext(PledgeFlowContext.NEW_PLEDGE) .projectData(projectData) .reward(reward) .addOns(addOns) @@ -60,7 +62,7 @@ class FragmentExtTest : KSRobolectricTestCase() { val fragment = Fragment().selectPledgeFragment(pledgeData, PledgeReason.PLEDGE) - assertTrue(fragment is PledgeFragment) + assertTrue(fragment is CrowdfundCheckoutFragment) val arg1 = fragment.arguments?.get(ArgumentsKey.PLEDGE_PLEDGE_DATA) as? PledgeData val arg2 = fragment.arguments?.get(ArgumentsKey.PLEDGE_PLEDGE_REASON) @@ -68,4 +70,31 @@ class FragmentExtTest : KSRobolectricTestCase() { assertEquals(arg1, pledgeData) assertEquals(arg2, PledgeReason.PLEDGE) } + + @Test + fun testPledgeFragmentInstance_ForFixPledge() { + val project = ProjectFactory.project() + val backing = BackingFactory.backing(project) + val updatedProj = project.toBuilder().backing(backing).isBacking(true).build() + val projectData = ProjectDataFactory.project(updatedProj) + val reward = Reward.builder().build() + val addOns = listOf(reward) + + val pledgeData = PledgeData.builder() + .pledgeFlowContext(PledgeFlowContext.FIX_ERRORED_PLEDGE) + .projectData(projectData) + .reward(reward) + .addOns(addOns) + .build() + + val fragment = Fragment().selectPledgeFragment(pledgeData, PledgeReason.FIX_PLEDGE) + + assertTrue(fragment is PledgeFragment) + + val arg1 = fragment.arguments?.get(ArgumentsKey.PLEDGE_PLEDGE_DATA) as? PledgeData + val arg2 = fragment.arguments?.get(ArgumentsKey.PLEDGE_PLEDGE_REASON) + + assertEquals(arg1, pledgeData) + assertEquals(arg2, PledgeReason.FIX_PLEDGE) + } }