Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MBL-1573: Checkout Screen (Pledge and update payment method flows for crowdfund) #2096

Merged
merged 8 commits into from
Aug 14, 2024
21 changes: 21 additions & 0 deletions app/src/main/java/com/kickstarter/libs/utils/RewardUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Reward>): List<Reward> {
val mutableList = mutableListOf<Reward>()

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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice here we can decide which crowdfund flows will use the new UI + VM

PledgeFragment()
} else CrowdfundCheckoutFragment()
return fragment.withData(pledgeData, pledgeReason)
}

fun Fragment.withData(pledgeData: PledgeData?, pledgeReason: PledgeReason?): Fragment {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ 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

data class UpdateBackingData(
val backing: Backing,
Expand All @@ -11,3 +13,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(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Package level method

backing: Backing,
amount: String? = null,
locationId: String? = null,
rewardsList: List<Reward> = 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
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ fun CheckoutScreenPreview() {
onPledgeCtaClicked = { },
newPaymentMethodClicked = { },
onDisclaimerItemClicked = {},
onAccountabilityLinkClicked = {}
onAccountabilityLinkClicked = {},
onChangedPaymentMethod = {}
)
}
}
Expand All @@ -134,14 +135,15 @@ fun CheckoutScreen(
shippingAmount: Double = 0.0,
pledgeReason: PledgeReason,
totalAmount: Double,
currentShippingRule: ShippingRule,
currentShippingRule: ShippingRule?,
totalBonusSupport: Double = 0.0,
rewardsHaveShippables: Boolean,
isLoading: Boolean = false,
onPledgeCtaClicked: (selectedCard: StoredCard?) -> Unit,
newPaymentMethodClicked: () -> Unit,
onDisclaimerItemClicked: (disclaimerItem: DisclaimerItems) -> Unit,
onAccountabilityLinkClicked: () -> Unit
onAccountabilityLinkClicked: () -> Unit,
onChangedPaymentMethod: (StoredCard?) -> Unit = {}
) {
val selectedOption = remember {
mutableStateOf(
Expand All @@ -153,6 +155,7 @@ fun CheckoutScreen(

val onOptionSelected: (StoredCard?) -> Unit = {
selectedOption.value = it
onChangedPaymentMethod.invoke(it)
}

// - After adding new payment method, selected card should be updated to the newly added
Expand Down Expand Up @@ -285,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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package com.kickstarter.ui.fragments

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
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.models.Project
import com.kickstarter.models.Reward
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.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

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 ->
activity?.runOnUiThread {
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
val shippingAmount = checkoutStates.shippingAmount
val totalAmount = checkoutStates.checkoutTotal
val shippingRule = checkoutStates.shippingRule
val bonus = checkoutStates.bonusAmount

val pledgeData = viewModel.getPledgeData()
val pledgeReason = viewModel.getPledgeReason() ?: PledgeReason.PLEDGE
val project = pledgeData?.projectData()?.project() ?: Project.builder().build()
val selectedRw = pledgeData?.reward() ?: Reward.builder().build()

val checkoutSuccess = viewModel.checkoutResultState.collectAsStateWithLifecycle().value
val id = checkoutSuccess.first?.id() ?: -1

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),
shippingAmount = shippingAmount,
selectedReward = selectedRw,
currentShippingRule = shippingRule,
totalAmount = totalAmount,
totalBonusSupport = bonus,
storedCards = storedCards,
project = project,
email = email,
pledgeReason = pledgeReason,
rewardsHaveShippables = rwList.any {
RewardUtils.isShippable(it)
},
onPledgeCtaClicked = {
viewModel.pledge()
},
isLoading = isLoading,
newPaymentMethodClicked = {},
onDisclaimerItemClicked = {},
onAccountabilityLinkClicked = {},
onChangedPaymentMethod = { paymentMethodSelected ->
viewModel.userChangedPaymentMethodSelected(paymentMethodSelected)
}
)
}
}
}
}
return view
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,10 @@ class RewardsFragmentViewModel {
this.selectedShippingRule = shippingRule
}

fun setInitialShippingRule(rule: ShippingRule) {
this.selectedShippingRule = rule
}

override fun onCleared() {
disposables.clear()
super.onCleared()
Expand Down
Loading