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
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ff23611
Checkout screen should inherit MessageBannerViewControllerPresenting
scottkicks Mar 25, 2024
19cae31
Add submitButtonTapped VM input
scottkicks Mar 25, 2024
239fffe
Implement creditCardSelected delegate
scottkicks Mar 25, 2024
495c400
Rename PaymentSourceSelected.paymentSourceId to .savedCreditCard
amy-at-kickstarter Mar 25, 2024
13baaac
Merge branch 'main' into scott/pcp/validate-checkout-confirm-payment
scottkicks Mar 25, 2024
66cc579
Merge branch 'feat/adyer/rename-payment-source-selected-paymentsource…
scottkicks Mar 25, 2024
414dda9
Get stripeId and pass to view model along side the payment intent cli…
scottkicks Mar 25, 2024
9a39f83
Call Validate Checkout on submit
scottkicks Mar 25, 2024
9044f34
Add message banner delegate
scottkicks Mar 25, 2024
cfb2888
Add validateCheckoutSuccess observer
scottkicks Mar 25, 2024
6ed64f9
decode checkout id to base64
scottkicks Mar 26, 2024
d5c8d62
Pop view controller after error banner shows
scottkicks Mar 26, 2024
8f0b39b
show an error message if Stripe fails to retrieve the stripeCardId
scottkicks Mar 26, 2024
247abb1
Merge branch 'main' into scott/pcp/validate-checkout-confirm-payment
scottkicks Mar 26, 2024
6618b39
cleanup
scottkicks Mar 26, 2024
1dc2792
Merge branch 'scott/pcp/validate-checkout-confirm-payment' of https:/…
scottkicks Mar 26, 2024
7005ac7
cleanup
scottkicks Mar 26, 2024
b0e813f
actually set message banner delegate...derp
scottkicks Mar 26, 2024
10e6a69
pr feedback cleanup suggestions
scottkicks Mar 26, 2024
e7c50b4
fix tests
scottkicks Mar 26, 2024
430327b
confirm details fixes
scottkicks Mar 26, 2024
e5d3ef8
Handle pre-existing payment method selected
scottkicks Mar 26, 2024
9901a62
run validations for new and existing cards separately
scottkicks Mar 26, 2024
703594e
Merge branch 'main' into scott/pcp/validate-checkout-confirm-payment
scottkicks Mar 26, 2024
4d7210e
cleanup
scottkicks Mar 26, 2024
b5660ee
fix tests
scottkicks Mar 26, 2024
af0f20a
pr feedback
scottkicks Mar 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@ private enum PostCampaignCheckoutLayout {
}
}

final class PostCampaignCheckoutViewController: UIViewController {
final class PostCampaignCheckoutViewController: UIViewController, MessageBannerViewControllerPresenting {
// MARK: - Properties

internal var messageBannerViewController: MessageBannerViewController?

private lazy var titleLabel = UILabel(frame: .zero)

private lazy var paymentMethodsViewController = {
PledgePaymentMethodsViewController.instantiate()
// TODO: Add self as delegate and add support for delegate methods.
|> \.messageDisplayingDelegate .~ self
|> \.delegate .~ self
}()

private lazy var pledgeCTAContainerView: PledgeViewCTAContainerView = {
Expand Down Expand Up @@ -72,6 +75,8 @@ final class PostCampaignCheckoutViewController: UIViewController {

self.title = Strings.Back_this_project()

self.messageBannerViewController = self.configureMessageBannerViewController(on: self)

self.configureChildViewControllers()
self.setupConstraints()

Expand Down Expand Up @@ -199,6 +204,20 @@ final class PostCampaignCheckoutViewController: UIViewController {
self.presentHelpWebViewController(with: helpType, presentationStyle: .formSheet)
}

self.viewModel.outputs.validateCheckoutSuccess
.observeForControllerAction()
.observeValues { [weak self] _ in
guard let self else { return }

// TODO: Confirm paymentIntent using Stripe.confirmPayment()
}

self.viewModel.outputs.showErrorBannerWithMessage
.observeForControllerAction()
.observeValues { [weak self] errorMessage in
self?.messageBannerViewController?.showBanner(with: .error, message: errorMessage)
}

self.sessionStartedObserver = NotificationCenter.default
.addObserver(forName: .ksr_sessionStarted, object: nil, queue: nil) { [weak self] _ in
self?.viewModel.inputs.userSessionStarted()
Expand Down Expand Up @@ -253,11 +272,61 @@ extension PostCampaignCheckoutViewController: PledgeViewCTAContainerViewDelegate

func submitButtonTapped() {
self.paymentMethodsViewController.cancelModalPresentation(true)
// TODO: Respond to button tap
self.viewModel.inputs.submitButtonTapped()
}

func termsOfUseTapped(with helpType: HelpType) {
self.paymentMethodsViewController.cancelModalPresentation(true)
self.viewModel.inputs.termsOfUseTapped(with: helpType)
}
}

// MARK: - PledgePaymentMethodsViewControllerDelegate

extension PostCampaignCheckoutViewController: PledgePaymentMethodsViewControllerDelegate {
func pledgePaymentMethodsViewController(
_: PledgePaymentMethodsViewController,
didSelectCreditCard paymentSource: PaymentSourceSelected
) {
switch paymentSource {
case let .paymentIntentClientSecret(clientSecret):
return STPAPIClient.shared.retrievePaymentIntent(withClientSecret: clientSecret) { intent, _ in
scottkicks marked this conversation as resolved.
Show resolved Hide resolved
guard let intent = intent else {
self.messageBannerViewController?
.showBanner(with: .error, message: Strings.Something_went_wrong_please_try_again())
return
}

let stripeCardId = intent.stripeId
let paymentIntentClientSecret = paymentSource.paymentIntentClientSecret ?? ""
scottkicks marked this conversation as resolved.
Show resolved Hide resolved

self.viewModel.inputs.creditCardSelected(with: (stripeCardId, paymentIntentClientSecret))
}
default:
break
scottkicks marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

// MARK: - PledgeViewControllerMessageDisplaying

extension PostCampaignCheckoutViewController: PledgeViewControllerMessageDisplaying {
func pledgeViewController(_: UIViewController, didErrorWith message: String) {
self.messageBannerViewController?.showBanner(with: .error, message: message)
}

func pledgeViewController(_: UIViewController, didSucceedWith message: String) {
self.messageBannerViewController?.showBanner(with: .success, message: message)
}
}

extension PostCampaignCheckoutViewController: MessageBannerViewControllerDelegate {
func messageBannerViewDidHide(type: MessageBannerType) {
switch type {
case .error:
self.navigationController?.popViewController(animated: true)
default:
break
}
}
}
13 changes: 11 additions & 2 deletions Library/ViewModels/ConfirmDetailsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,16 @@ public class ConfirmDetailsViewModel: ConfirmDetailsViewModelType, ConfirmDetail
}

let checkoutValues = createCheckoutEvents.values()
.map { $0.checkout.id }
.map { values in
var checkoutId: String?

if let decoded = decodeBase64(values.checkout.id), let range = decoded.range(of: "Checkout-") {
let id = decoded[range.upperBound...]
checkoutId = String(id)
}

return checkoutId
}
scottkicks marked this conversation as resolved.
Show resolved Hide resolved
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


self.createCheckoutSuccess = checkoutValues.withLatestFrom(
Signal.combineLatest(
Expand Down Expand Up @@ -385,7 +394,7 @@ public class ConfirmDetailsViewModel: ConfirmDetailsViewModelType, ConfirmDetail
shipping: shipping,
refTag: initialData.refTag,
context: initialData.context,
checkoutId: checkoutValue
checkoutId: checkoutValue ?? ""
)
}

Expand Down
52 changes: 50 additions & 2 deletions Library/ViewModels/PostCampaignCheckoutViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import KsApi
import PassKit
import Prelude
import ReactiveSwift
import Stripe

public struct PostCampaignCheckoutData: Equatable {
public let project: Project
Expand All @@ -20,8 +21,10 @@ public struct PostCampaignCheckoutData: Equatable {

public protocol PostCampaignCheckoutViewModelInputs {
func configure(with data: PostCampaignCheckoutData)
func creditCardSelected(with stripeCardIdAndPaymentIntent: (String, String))
func goToLoginSignupTapped()
func pledgeDisclaimerViewDidTapLearnMore()
func submitButtonTapped()
func termsOfUseTapped(with: HelpType)
func userSessionStarted()
func viewDidLoad()
Expand All @@ -34,10 +37,12 @@ public protocol PostCampaignCheckoutViewModelOutputs {
Never
> { get }
var configurePledgeViewCTAContainerView: Signal<PledgeViewCTAContainerViewData, Never> { get }
var configureStripeIntegration: Signal<StripeConfigurationData, Never> { get }
var goToLoginSignup: Signal<(LoginIntent, Project, Reward?), Never> { get }
var paymentMethodsViewHidden: Signal<Bool, Never> { get }
var showErrorBannerWithMessage: Signal<String, Never> { get }
var showWebHelp: Signal<HelpType, Never> { get }
var configureStripeIntegration: Signal<StripeConfigurationData, Never> { get }
var validateCheckoutSuccess: Signal<String, Never> { get }
}

public protocol PostCampaignCheckoutViewModelType {
Expand All @@ -57,6 +62,7 @@ public class PostCampaignCheckoutViewModel: PostCampaignCheckoutViewModelType,
.skipNil()

let context = initialData.map(\.context)
let checkoutId = initialData.map(\.checkoutId)

let configurePaymentMethodsData = Signal.merge(
initialData,
Expand Down Expand Up @@ -130,6 +136,35 @@ public class PostCampaignCheckoutViewModel: PostCampaignCheckoutViewModelType,
AppEnvironment.current.environmentType.stripePublishableKey
)
}

// MARK: Validate Checkout Details On Submit

let stripeCardIdAndPaymentIntent = self.creditCardSelectedSignal.signal

let validateCheckout = Signal.combineLatest(checkoutId, stripeCardIdAndPaymentIntent)
.takeWhen(self.submitButtonTappedProperty.signal)
.switchMap { checkoutId, stripeCardIdAndPaymentIntent in
let (stripeCardId, paymentIntent) = stripeCardIdAndPaymentIntent

return AppEnvironment.current.apiService
.validateCheckout(
checkoutId: checkoutId,
paymentSourceId: stripeCardId,
paymentIntentClientSecret: paymentIntent
scottkicks marked this conversation as resolved.
Show resolved Hide resolved
)
.ksr_delay(AppEnvironment.current.apiDelayInterval, on: AppEnvironment.current.scheduler)
.materialize()
}

self.validateCheckoutSuccess = stripeCardIdAndPaymentIntent
.takeWhen(validateCheckout.values().ignoreValues())
.map { stripeCardIdAndPaymentIntent in
scottkicks marked this conversation as resolved.
Show resolved Hide resolved
let (_, paymentIntent) = stripeCardIdAndPaymentIntent
return paymentIntent
}

self.showErrorBannerWithMessage = validateCheckout.errors()
.map { _ in Strings.Something_went_wrong_please_try_again() }
scottkicks marked this conversation as resolved.
Show resolved Hide resolved
}

// MARK: - Inputs
Expand All @@ -139,6 +174,12 @@ public class PostCampaignCheckoutViewModel: PostCampaignCheckoutViewModelType,
self.configureWithDataProperty.value = data
}

private let (creditCardSelectedSignal, creditCardSelectedObserver) = Signal<(String, String), Never>
.pipe()
public func creditCardSelected(with stripeCardIdAndPaymentIntent: (String, String)) {
self.creditCardSelectedObserver.send(value: stripeCardIdAndPaymentIntent)
}

private let (goToLoginSignupSignal, goToLoginSignupObserver) = Signal<Void, Never>.pipe()
public func goToLoginSignupTapped() {
self.goToLoginSignupObserver.send(value: ())
Expand All @@ -150,6 +191,11 @@ public class PostCampaignCheckoutViewModel: PostCampaignCheckoutViewModelType,
self.pledgeDisclaimerViewDidTapLearnMoreObserver.send(value: ())
}

private let submitButtonTappedProperty = MutableProperty(())
public func submitButtonTapped() {
self.submitButtonTappedProperty.value = ()
}

private let (termsOfUseTappedSignal, termsOfUseTappedObserver) = Signal<HelpType, Never>.pipe()
public func termsOfUseTapped(with helpType: HelpType) {
self.termsOfUseTappedObserver.send(value: helpType)
Expand All @@ -174,10 +220,12 @@ public class PostCampaignCheckoutViewModel: PostCampaignCheckoutViewModelType,
PledgeSummaryViewData
), Never>
public let configurePledgeViewCTAContainerView: Signal<PledgeViewCTAContainerViewData, Never>
public let configureStripeIntegration: Signal<StripeConfigurationData, Never>
public let goToLoginSignup: Signal<(LoginIntent, Project, Reward?), Never>
public let paymentMethodsViewHidden: Signal<Bool, Never>
public let showErrorBannerWithMessage: Signal<String, Never>
public let showWebHelp: Signal<HelpType, Never>
public let configureStripeIntegration: Signal<StripeConfigurationData, Never>
public let validateCheckoutSuccess: Signal<String, Never>

public var inputs: PostCampaignCheckoutViewModelInputs { return self }
public var outputs: PostCampaignCheckoutViewModelOutputs { return self }
Expand Down