Skip to content

Commit

Permalink
MBL-1950: Double charge late pledge (#2205)
Browse files Browse the repository at this point in the history
  • Loading branch information
Arkariang authored Jan 22, 2025
1 parent 14b3529 commit 31d4d5f
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ class ProjectPageActivity :
val checkoutLoading = latePledgeCheckoutUIState.isLoading
val shippingAmount = latePledgeCheckoutUIState.shippingAmount
val checkoutTotal = latePledgeCheckoutUIState.checkoutTotal
val isPledgeButtonEnabled = latePledgeCheckoutUIState.isPledgeButtonEnabled

latePledgeCheckoutViewModel.provideErrorAction { message ->
showToastError(message)
Expand Down Expand Up @@ -714,7 +715,8 @@ class ProjectPageActivity :
},
onAccountabilityLinkClicked = {
showAccountabilityPage()
}
},
isPledgeButtonEnabled = isPledgeButtonEnabled
)

val successfulPledge =
Expand Down Expand Up @@ -1358,7 +1360,10 @@ class ProjectPageActivity :
override fun onSuccess(result: PaymentIntentResult) {
if (result.outcome == StripeIntentResult.Outcome.SUCCEEDED) {
latePledgeCheckoutViewModel.completeOnSessionCheckoutFor3DS()
} else showToastError()
} else {
latePledgeCheckoutViewModel.clear3DSValues()
showToastError()
}
}

override fun onError(e: Exception) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ fun CheckoutScreen(
isPlotEligible: Boolean = false,
isIncrementalPledge: Boolean = false,
paymentIncrements: List<PaymentIncrement>? = null,
isPledgeButtonEnabled: Boolean = true
) {
val selectedOption = remember {
mutableStateOf(
Expand Down Expand Up @@ -269,7 +270,7 @@ fun CheckoutScreen(
.padding(bottom = dimensions.paddingMediumSmall)
.fillMaxWidth(),
onClickAction = { onPledgeCtaClicked(selectedOption.value) },
isEnabled = project.acceptedCardType(selectedOption.value?.type()) || selectedOption.value?.isFromPaymentSheet() ?: false,
isEnabled = (project.acceptedCardType(selectedOption.value?.type()) || selectedOption.value?.isFromPaymentSheet() ?: false) && isPledgeButtonEnabled,
text = if (pledgeReason == PledgeReason.PLEDGE || pledgeReason == PledgeReason.LATE_PLEDGE) stringResource(
id = R.string.Pledge
) + " $totalAmountString" else stringResource(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ fun ProjectPledgeButtonAndFragmentContainer(
onPledgeCtaClicked: (selectedCard: StoredCard?) -> Unit,
onAddPaymentMethodClicked: () -> Unit,
onDisclaimerItemClicked: (disclaimerItem: DisclaimerItems) -> Unit,
onAccountabilityLinkClicked: () -> Unit
onAccountabilityLinkClicked: () -> Unit,
isPledgeButtonEnabled: Boolean = false
) {
Column(modifier = Modifier.systemBarsPadding()) {
Surface(
Expand Down Expand Up @@ -293,6 +294,7 @@ fun ProjectPledgeButtonAndFragmentContainer(
isLoading = isLoading,
onDisclaimerItemClicked = onDisclaimerItemClicked,
onAccountabilityLinkClicked = onAccountabilityLinkClicked,
isPledgeButtonEnabled = isPledgeButtonEnabled
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.kickstarter.libs.Environment
import com.kickstarter.libs.utils.RewardUtils
import com.kickstarter.libs.utils.extensions.checkoutTotalAmount
import com.kickstarter.libs.utils.extensions.isNotNull
import com.kickstarter.libs.utils.extensions.isTrue
import com.kickstarter.libs.utils.extensions.locationId
import com.kickstarter.libs.utils.extensions.pledgeAmountTotal
import com.kickstarter.libs.utils.extensions.rewardsAndAddOnsList
Expand All @@ -26,6 +27,7 @@ import com.kickstarter.ui.data.PledgeData
import com.stripe.android.Stripe
import com.stripe.android.confirmPaymentIntent
import com.stripe.android.model.ConfirmPaymentIntentParams
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
Expand Down Expand Up @@ -79,6 +81,9 @@ class LatePledgeCheckoutViewModel(val environment: Environment) : ViewModel() {
private var selectedReward: Reward? = null
private var mutableLatePledgeCheckoutUIState = MutableStateFlow(LatePledgeCheckoutUIState())

private var paymentIntent: String? = null
private var pledgeButtonClickedJob: Job? = null

private var mutableOnPledgeSuccessAction = MutableSharedFlow<Boolean>()
val onPledgeSuccess: SharedFlow<Boolean>
get() = mutableOnPledgeSuccessAction
Expand Down Expand Up @@ -155,6 +160,7 @@ class LatePledgeCheckoutViewModel(val environment: Environment) : ViewModel() {
).asFlow().onStart {
emitCurrentState(isLoading = true)
}.map { clientSecret ->
buttonEnabled = true
clientSecretForNewCard = clientSecret
mutableClientSecretForNewPaymentMethod.emit(clientSecretForNewCard)
}.onCompletion {
Expand All @@ -177,9 +183,11 @@ class LatePledgeCheckoutViewModel(val environment: Environment) : ViewModel() {
}.map {
refreshUserCards()
}.catch {
emitCurrentState()
errorAction.invoke(null)
}.collect()
}.onCompletion {
emitCurrentState()
}
.collect()
}
}

Expand All @@ -203,34 +211,56 @@ class LatePledgeCheckoutViewModel(val environment: Environment) : ViewModel() {
}

fun onPledgeButtonClicked(selectedCard: StoredCard?) {
// - Avoid launching any other coroutine call while previous coroutine active
if (pledgeButtonClickedJob?.isActive.isTrue()) return

this.pledgeData?.let {
val project = it.projectData().project()
createPaymentIntentForCheckout(selectedCard, project, it.checkoutTotalAmount())
pledgeButtonClickedJob = viewModelScope.launch {
val project = it.projectData().project()

buttonEnabled = false
emitCurrentState(isLoading = true)

if (paymentIntent == null)
createPaymentIntentForCheckout(selectedCard, project, it.checkoutTotalAmount())

else {
paymentIntent?.let { pi ->
selectedCard?.let { card ->
validateCheckout(
clientSecret = pi,
selectedCard = card
)
}
}
}
}
}
}

fun provideErrorAction(errorAction: (message: String?) -> Unit) {
this.errorAction = errorAction
}

private fun createPaymentIntentForCheckout(
private suspend fun createPaymentIntentForCheckout(
selectedCard: StoredCard?,
project: Project,
totalAmount: Double
) {
viewModelScope.launch {
checkoutId?.let { cId ->
backing?.let { b ->
apolloClient.createPaymentIntent(
CreatePaymentIntentInput(
project = project,
amount = totalAmount.toString(),
checkoutId = cId,
backing = b
)
).asFlow().onStart {
emitCurrentState(isLoading = true)
}.map { clientSecret ->
if (paymentIntent.isNotNull()) return
checkoutId?.let { cId ->
backing?.let { b ->
apolloClient.createPaymentIntent(
CreatePaymentIntentInput(
project = project,
amount = totalAmount.toString(),
checkoutId = cId,
backing = b
)
)
.asFlow()
.map { clientSecret ->
paymentIntent = clientSecret
selectedCard?.let {
checkoutId?.let {
validateCheckout(
Expand All @@ -246,14 +276,16 @@ class LatePledgeCheckoutViewModel(val environment: Environment) : ViewModel() {
errorAction.invoke(null)
}
}.catch {
emitCurrentState()
// - Clean up states if error happens when creating payment intent
paymentIntent = null
errorAction.invoke(null)
}.collect()
}
} ?: run {
emitCurrentState()
errorAction.invoke(null)
}.collect() // - No completation block, will continue on validateCheckout
}
} ?: run {
buttonEnabled = true
paymentIntent = null
emitCurrentState()
errorAction.invoke(null)
}
}

Expand All @@ -268,18 +300,21 @@ class LatePledgeCheckoutViewModel(val environment: Environment) : ViewModel() {
stripeConfirmPaymentIntent(clientSecret = clientSecret, selectedCard = selectedCard)
} else {
// User validation.messages for displaying an error
if (validation.messages.isNotEmpty()) {
emitCurrentState()
errorAction.invoke(validation.messages.first())
val error = if (validation.messages.isNotEmpty()) {
validation.messages.first()
} else {
emitCurrentState()
errorAction.invoke(null)
null
}
emitCurrentState()
errorAction.invoke(error)
}
}.catch {
emitCurrentState()
errorAction.invoke(null)
}.collect()
}
.onCompletion {
emitCurrentState()
}
.collect()
}

private suspend fun stripeConfirmPaymentIntent(clientSecret: String, selectedCard: StoredCard) {
Expand Down Expand Up @@ -332,20 +367,22 @@ class LatePledgeCheckoutViewModel(val environment: Environment) : ViewModel() {
paymentIntentClientSecret = clientSecretFor3DSVerification,
paymentSourceId = if (selectedCardFor3DSVerification == newStoredCard) null else selectedCardFor3DSVerification?.id() ?: "",
paymentSourceReusable = true
).asFlow().map { iDRequiresActionPair ->
if (iDRequiresActionPair.second) {
mutablePaymentRequiresAction.emit(clientSecretFor3DSVerification)
} else {
mutableOnPledgeSuccessAction.emit(true)
).asFlow()
.onStart {
emitCurrentState(isLoading = true)
}
}.onStart {
emitCurrentState(isLoading = true)
}.onCompletion {
emitCurrentState()
}.catch {
errorAction.invoke(null)
clear3DSValues()
}.collect()
.map { iDRequiresActionPair ->
if (iDRequiresActionPair.second) {
mutablePaymentRequiresAction.emit(clientSecretFor3DSVerification)
} else {
mutableOnPledgeSuccessAction.emit(true)
}
}.onCompletion {
emitCurrentState()
}.catch {
errorAction.invoke(null)
clear3DSValues()
}.collect()
} else {
errorAction.invoke(null)
clear3DSValues()
Expand All @@ -354,8 +391,13 @@ class LatePledgeCheckoutViewModel(val environment: Environment) : ViewModel() {
}

fun clear3DSValues() {
buttonEnabled = true
clientSecretFor3DSVerification = ""
selectedCardFor3DSVerification = null

viewModelScope.launch {
emitCurrentState()
}
}

private fun createCheckoutData(shippingAmount: Double, total: Double, bonusAmount: Double?, checkout: Checkout? = null): CheckoutData {
Expand All @@ -377,7 +419,7 @@ class LatePledgeCheckoutViewModel(val environment: Environment) : ViewModel() {
selectedRewards = selectedRewards.toList(),
shippingAmount = this.pledgeData?.shippingCostIfShipping() ?: 0.0,
checkoutTotal = this.pledgeData?.checkoutTotalAmount() ?: 0.0,
isPledgeButtonEnabled = buttonEnabled,
isPledgeButtonEnabled = buttonEnabled && !isLoading,
)
)
}
Expand Down Expand Up @@ -438,16 +480,17 @@ class LatePledgeCheckoutViewModel(val environment: Environment) : ViewModel() {
)
)
.asFlow()
.onStart {
emitCurrentState(isLoading = false)
}
.map { checkoutPayment ->
buttonEnabled = true
mutableCheckoutPayment.emit(checkoutPayment)
}
.catch {
buttonEnabled = false
errorAction.invoke(it.message)
}
.onCompletion {
emitCurrentState(isLoading = false)
emitCurrentState()
}
.collect()
}
Expand All @@ -468,7 +511,6 @@ class LatePledgeCheckoutViewModel(val environment: Environment) : ViewModel() {
selectedRewards.addAll(addOns)
}

emitCurrentState(isLoading = true)
createCheckout()
}
}
Expand Down
Loading

0 comments on commit 31d4d5f

Please sign in to comment.