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-1194] Implement Validate Checkout #1999

Merged
merged 27 commits into from Mar 26, 2024

Conversation

scottkicks
Copy link
Contributor

@scottkicks scottkicks commented Mar 26, 2024

πŸ“² What

When tapping pledge, we need to run this mutation so the backend can do some extra validations before we ask Stripe to do its thing.

For New Payment Methods

  • We'll use the payment intent that is passed back to the VM via PaymentSourceSelected
  • We can then call Stripe's STPAPIClient.shared.retrievePaymentIntent to get the required paymentSourceId (paymentMethodId in Stripe)

For Pre-Existing Payment Methods that were originally created with a setup intent

  • We'll need to call Create Payment Intent again to pass to this mutation
  • We'll also need to use the selected existing card's id to filter the user's stored cards and grab the stripeCardId

I opted to make separate calls to this mutation depending on the case since they require enough distinct information to warrant it. There might be an opportunity to remove some duplicate code so if there are any nits please call them out!

The mutation accepts the following params:

  • the checkoutId (from the [MBL-1273] Implement Create Checkout MutationΒ #1982 on ConfirmDetailsViewController). One update to this was made. Validate Checkout requires that the checkoutId be decoded to base 64.
  • the paymentMethodId that we can pull in using, STPAPIClient.shared.retrievePaymentIntent(withClientSecret: clientSecret) for new cards, or the stripeCardId property on existing cards.
  • and the paymentIntentClientSecret that we create using the backend.

Extras

I also updated PostCampaignCheckoutViewController to use our MessageBannerViewControllerPresenting to show error messages when an API call fails.

  • It's important to note that in the case of a failure, the expected UX is to navigate the backer back to the ConfirmDetails screen so that we can being the process of creating a checkout Id/validating/etc. before attempting another pledge. This is because once we confirm the payment with Stripe, the backend will need to invalidate that request, essentially processing a refund to the backer and invalidating our previous validations.

πŸ€” Why

Gotta validate, ya dig?

βœ… Acceptance criteria

  • Tapping Pledge, after adding a card or selecting an existing card, successfully runs the validateCheckout mutation with the correct values.

⏰ TODO

  • Next step is to confirm the payment intent via Stripe.confirmPayment()

@scottkicks scottkicks force-pushed the scott/pcp/validate-checkout-confirm-payment branch from fec43c3 to 7005ac7 Compare March 26, 2024 13:52
@scottkicks scottkicks marked this pull request as ready for review March 26, 2024 14:00
Copy link
Contributor

@amy-at-kickstarter amy-at-kickstarter left a comment

Choose a reason for hiding this comment

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

Mostly LGTM, but a couple things to address first:

  1. How will we handle saved credit cards (which have no PaymentIntent)?
  2. Why do we need to decode the GraphID of a Checkout? Can this be resolved at the mutation level, instead of doing it in the client?

A few non-blocking nits:

  1. Is the VC the correct place to call Stripe? Should it be in the VM or will that break testability?
  2. Some miscellanea/style comments

Also left a few nits/comments, but those aren't blocking.

@scottkicks
Copy link
Contributor Author

scottkicks commented Mar 26, 2024

@amy-at-kickstarter Looking into your other comments.

Why do we need to decode the GraphID of a Checkout? Can this be resolved at the mutation level, instead of doing it in the client?

I spoke with the backend, and they would rather we did this. It sounded like they could (they do in other cases), but they aren't willing to spend the time updating the different pieces involved right now.

@scottkicks scottkicks force-pushed the scott/pcp/validate-checkout-confirm-payment branch from da13cfc to 4d7210e Compare March 26, 2024 19:30
@@ -211,7 +211,7 @@ public class ConfirmDetailsViewModel: ConfirmDetailsViewModelType, ConfirmDetail

/// Hide when there is a reward and shipping is enabled (accounts for digital rewards), and in a pledge context
self.pledgeSummaryViewHidden = Signal.zip(baseReward, context).map { baseReward, context in
(baseReward.isNoReward == false && baseReward.shipping.enabled) && context == .pledge
baseReward.isNoReward == false && context == .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.

unrelated fix for showing the summary view on details

Copy link
Contributor

Choose a reason for hiding this comment

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

πŸ‘

Copy link
Contributor

@amy-at-kickstarter amy-at-kickstarter left a comment

Choose a reason for hiding this comment

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

LGTM with some questions and nits

.observeValues { [weak self] _ in
guard let self else { return }

// TODO: Confirm paymentIntent using Stripe.confirmPayment()
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this // TODO: correct for the existing card case, too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes we'll have to confirm the payment either way using the intent

return
}

let paymentMethodId = intent.paymentMethodId!
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Could also throw this let into the above guard statement, just in case.

}
case let .savedCreditCard(savedCardId):
self.viewModel.inputs
.creditCardSelected(source: paymentSource, paymentMethodId: savedCardId, isNewPaymentMethod: false)
Copy link
Contributor

Choose a reason for hiding this comment

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

πŸ‘

@@ -9,6 +9,7 @@ public struct UserCreditCards: Decodable {
public var id: String
public var lastFour: String
public var type: CreditCardType?
public var stripeCardId: String?
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, yeah, I only added it to the GraphQL object. Good find.

@@ -211,7 +211,7 @@ public class ConfirmDetailsViewModel: ConfirmDetailsViewModelType, ConfirmDetail

/// Hide when there is a reward and shipping is enabled (accounts for digital rewards), and in a pledge context
self.pledgeSummaryViewHidden = Signal.zip(baseReward, context).map { baseReward, context in
(baseReward.isNoReward == false && baseReward.shipping.enabled) && context == .pledge
baseReward.isNoReward == false && context == .pledge
Copy link
Contributor

Choose a reason for hiding this comment

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

πŸ‘

}

return checkoutId
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Any resolution on why this needs to be decoded from GraphQL? At a minimum, this should really have a //TODO: explaining the hack.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yea i left an unfortunate comment earlier.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

its silly that they would give us a string, that we only use to send to one of their mutations, and not format it correctly for us

}

let storedCardsValues = storedCardsEvent.values()
.filter(second >>> isFalse)
Copy link
Contributor

Choose a reason for hiding this comment

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

What does this do?

Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Would prefer this to be a non-Prelude operator for clarity.

let selectedExistingCard = self.creditCardSelectedProperty.signal.skipNil()
.filter { $0.isNewPaymentMethod == false }

let newPaymentIntentEvent = initialData
Copy link
Contributor

Choose a reason for hiding this comment

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

This only needs to happen when you're not adding a new card via the payment method selector, since in that case, we've already made a PI for you. Should this be attached to a different event than initialData?

let selectedNewCreditCard = self.creditCardSelectedProperty.signal.skipNil()
.filter { $0.isNewPaymentMethod }

// Runs validation for new cards that were created with payment intents.
Copy link
Contributor

Choose a reason for hiding this comment

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

Loving these comments

@scottkicks scottkicks merged commit 140fa8f into main Mar 26, 2024
5 checks passed
@scottkicks scottkicks deleted the scott/pcp/validate-checkout-confirm-payment branch March 26, 2024 20:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants