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

[DRAFT] Experiment: try moving ApplePay out of late pledge view model #2049

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -235,7 +235,7 @@ final class PostCampaignCheckoutViewController: UIViewController,
STPAPIClient.shared.configuration.appleMerchantIdentifier = merchantIdentifier
}

self.viewModel.outputs.goToApplePayPaymentAuthorization
self.viewModel.applePayModel.goToApplePayPaymentAuthorization
.observeForControllerAction()
.observeValues { [weak self] paymentAuthorizationData in
self?.goToPaymentAuthorization(paymentAuthorizationData)
Expand Down Expand Up @@ -332,7 +332,7 @@ extension PostCampaignCheckoutViewController: PledgeViewCTAContainerViewDelegate

func applePayButtonTapped() {
self.paymentMethodsViewController.cancelModalPresentation(true)
self.viewModel.inputs.applePayButtonTapped()
self.viewModel.applePayModel.applePayButtonTapped()
}

func submitButtonTapped() {
Expand Down Expand Up @@ -448,7 +448,7 @@ extension PostCampaignCheckoutViewController: STPApplePayContextDelegate {
token: ""
)

self.viewModel.inputs.applePayContextDidCreatePayment(params: params)
self.viewModel.applePayModel.applePayContextDidCreatePayment(params: params)
completion(paymentIntentClientSecret, nil)
}

Expand All @@ -459,7 +459,7 @@ extension PostCampaignCheckoutViewController: STPApplePayContextDelegate {
) {
switch status {
case .success:
self.viewModel.inputs.applePayContextDidComplete()
self.viewModel.applePayModel.applePayContextDidComplete()
case .error:
self.viewModel.inputs.checkoutTerminated()
self.messageBannerViewController?
Expand Down
4 changes: 4 additions & 0 deletions Kickstarter.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1527,6 +1527,7 @@
E17611E22B73D9A400DF2F50 /* Data+PKCE.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17611E12B73D9A400DF2F50 /* Data+PKCE.swift */; };
E17611E42B751E8100DF2F50 /* Paginator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17611E32B751E8100DF2F50 /* Paginator.swift */; };
E17611E62B75242A00DF2F50 /* PaginatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17611E52B75242A00DF2F50 /* PaginatorTests.swift */; };
E17669CF2BDAF1440091B1DC /* ApplePayCheckoutViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17669CE2BDAF1440091B1DC /* ApplePayCheckoutViewModel.swift */; };
E1801FAA2BAB6D0900EBB533 /* PaymentSourceSelected.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1801FA82BAB6CD400EBB533 /* PaymentSourceSelected.swift */; };
E182E5BA2B8CDFDE0008DD69 /* AppEnvironmentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ED1F121E830FDC00BFFA01 /* AppEnvironmentTests.swift */; };
E182E5BC2B8D36FE0008DD69 /* AppEnvironmentTests+OAuthInKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = E182E5BB2B8D36FE0008DD69 /* AppEnvironmentTests+OAuthInKeychain.swift */; };
Expand Down Expand Up @@ -3158,6 +3159,7 @@
E17611E12B73D9A400DF2F50 /* Data+PKCE.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+PKCE.swift"; sourceTree = "<group>"; };
E17611E32B751E8100DF2F50 /* Paginator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Paginator.swift; sourceTree = "<group>"; };
E17611E52B75242A00DF2F50 /* PaginatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginatorTests.swift; sourceTree = "<group>"; };
E17669CE2BDAF1440091B1DC /* ApplePayCheckoutViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ApplePayCheckoutViewModel.swift; path = Library/ViewModels/ApplePayCheckoutViewModel.swift; sourceTree = "<group>"; };
E1801FA82BAB6CD400EBB533 /* PaymentSourceSelected.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentSourceSelected.swift; sourceTree = "<group>"; };
E182E5BB2B8D36FE0008DD69 /* AppEnvironmentTests+OAuthInKeychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppEnvironmentTests+OAuthInKeychain.swift"; sourceTree = "<group>"; };
E1889D8D2B6065D6004FBE21 /* CombineTestObserverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineTestObserverTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6300,6 +6302,7 @@
A7E06C701C5A6EB300EBDCC2 = {
isa = PBXGroup;
children = (
E17669CE2BDAF1440091B1DC /* ApplePayCheckoutViewModel.swift */,
802800561C88F62500141235 /* Configs */,
E10BE8CE2B02975C00F73DC9 /* RichPushNotifications */,
E113BD822B7D255000D3A809 /* Library-Keychain-iOSTests */,
Expand Down Expand Up @@ -7683,6 +7686,7 @@
8053D3111D3848A3007B85DB /* Reachability.swift in Sources */,
77C9122723C4F99400F3D2C9 /* Double+Currency.swift in Sources */,
4751A67B272B57EE00F81DD5 /* ProjectTabDisclaimerCellViewModel.swift in Sources */,
E17669CF2BDAF1440091B1DC /* ApplePayCheckoutViewModel.swift in Sources */,
809F8B661D08B4FF005BADD9 /* UpdateDraftViewModel.swift in Sources */,
8A13D165249566FB007E2C0B /* PledgeExpandableHeaderRewardCellViewModel.swift in Sources */,
59392C1E1D7095B3001C99A4 /* ProjectUpdatesViewModel.swift in Sources */,
Expand Down
114 changes: 114 additions & 0 deletions Library/ViewModels/ApplePayCheckoutViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import Foundation
import KsApi
import ReactiveSwift

public class ApplePayCheckoutViewModel {
init(withConfigurationSignal configureWithData: Signal<PostCampaignCheckoutData, Never>) {
/*
Order of operations:
1) Apple pay button tapped
2) Generate a new payment intent
3) Present the payment authorization form
4) Payment authorization form calls applePayContextDidCreatePayment with ApplePay params
5) Payment authorization form calls paymentAuthorizationDidFinish
*/

let createPaymentIntentForApplePay: Signal<Signal<PaymentIntentEnvelope, ErrorEnvelope>.Event, Never> =
configureWithData
.takeWhen(self.applePayButtonTappedSignal)
.switchMap { initialData in
let projectId = initialData.project.graphID
let pledgeTotal = initialData.total

return AppEnvironment.current.apiService
.createPaymentIntentInput(input: CreatePaymentIntentInput(
projectId: projectId,
amountDollars: String(format: "%.2f", pledgeTotal),
digitalMarketingAttributed: nil
))
.ksr_delay(AppEnvironment.current.apiDelayInterval, on: AppEnvironment.current.scheduler)
.materialize()
}

self.errors = createPaymentIntentForApplePay.errors()

let newPaymentIntentForApplePay: Signal<String, Never> = createPaymentIntentForApplePay
.values()
.map { $0.clientSecret }

self.goToApplePayPaymentAuthorization = configureWithData
.signal
.combineLatest(with: newPaymentIntentForApplePay)
.map { (
data: PostCampaignCheckoutData,
paymentIntent: String
) -> PostCampaignPaymentAuthorizationData? in
let baseReward = data.baseReward

return PostCampaignPaymentAuthorizationData(
project: data.project,
hasNoReward: baseReward.isNoReward,
subtotal: baseReward.isNoReward ? baseReward.minimum : calculateAllRewardsTotal(
addOnRewards: data.rewards,
selectedQuantities: data.selectedQuantities
),
bonus: data.bonusAmount ?? 0,
shipping: data.shipping?.total ?? 0,
total: data.total,
merchantIdentifier: Secrets.ApplePay.merchantIdentifier,
paymentIntent: paymentIntent
)
}
.skipNil()

self.completeCheckoutWithApplePayInput = Signal
.combineLatest(newPaymentIntentForApplePay, configureWithData, self.applePayParamsSignal.mapConst(true))
.takeWhen(self.applePayContextDidCompleteSignal)
.map {
(
clientSecret: String,
data: PostCampaignCheckoutData,
_: Bool
) -> GraphAPI.CompleteOnSessionCheckoutInput in
let checkoutId = data.checkoutId

return GraphAPI
.CompleteOnSessionCheckoutInput(
checkoutId: encodeToBase64("Checkout-\(checkoutId)"),
paymentIntentClientSecret: clientSecret,
paymentSourceId: nil,
paymentSourceReusable: false,
/* We are no longer sending ApplePay parameters to the backend, because Stripe Tokens are
considered deprecated and are incompatible with PaymentIntent-based payments.

In the future, we may use the other parameters in the ApplePayParams object, but for now,
send nil.
*/
applePay: nil
)
}
}

// inputs

private let (applePayButtonTappedSignal, applePayButtonTappedObserver) = Signal<Void, Never>.pipe()
public func applePayButtonTapped() {
self.applePayButtonTappedObserver.send(value: ())
}

private let (applePayParamsSignal, applePayParamsObserver) = Signal<ApplePayParams, Never>.pipe()
public func applePayContextDidCreatePayment(params: ApplePayParams) {
self.applePayParamsObserver.send(value: params)
}

private let (applePayContextDidCompleteSignal, applePayContextDidCompleteObserver)
= Signal<Void, Never>.pipe()
public func applePayContextDidComplete() {
self.applePayContextDidCompleteObserver.send(value: ())
}

// outputs
public var goToApplePayPaymentAuthorization: Signal<PostCampaignPaymentAuthorizationData, Never>
public var completeCheckoutWithApplePayInput: Signal<GraphAPI.CompleteOnSessionCheckoutInput, Never>
public var errors: Signal<ErrorEnvelope, Never>
}
114 changes: 18 additions & 96 deletions Library/ViewModels/PostCampaignCheckoutViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,19 @@ public protocol PostCampaignCheckoutViewModelInputs {
func submitButtonTapped()
func termsOfUseTapped(with: HelpType)
func viewDidLoad()
func applePayButtonTapped()
func applePayContextDidCreatePayment(params: ApplePayParams)
func applePayContextDidComplete()
}

/*

viewDidLoad
configure
selectedCard
validate
submit
complete or terminate

*/

public protocol PostCampaignCheckoutViewModelOutputs {
var configurePaymentMethodsViewControllerWithValue: Signal<PledgePaymentMethodsValue, Never> { get }
var configurePledgeRewardsSummaryViewWithData: Signal<
Expand All @@ -61,19 +69,21 @@ public protocol PostCampaignCheckoutViewModelOutputs {
var showErrorBannerWithMessage: Signal<String, Never> { get }
var showWebHelp: Signal<HelpType, Never> { get }
var validateCheckoutSuccess: Signal<PaymentSourceValidation, Never> { get }
var goToApplePayPaymentAuthorization: Signal<PostCampaignPaymentAuthorizationData, Never> { get }
var checkoutComplete: Signal<ThanksPageData, Never> { get }
var checkoutError: Signal<ErrorEnvelope, Never> { get }
}

public protocol PostCampaignCheckoutViewModelType {
var inputs: PostCampaignCheckoutViewModelInputs { get }
var outputs: PostCampaignCheckoutViewModelOutputs { get }
var applePayModel: ApplePayCheckoutViewModel { get }
}

public class PostCampaignCheckoutViewModel: PostCampaignCheckoutViewModelType,
PostCampaignCheckoutViewModelInputs,
PostCampaignCheckoutViewModelOutputs {
public let applePayModel: ApplePayCheckoutViewModel

public init() {
let initialData = Signal.combineLatest(
self.configureWithDataProperty.signal,
Expand All @@ -82,6 +92,8 @@ public class PostCampaignCheckoutViewModel: PostCampaignCheckoutViewModelType,
.map(first)
.skipNil()

self.applePayModel = ApplePayCheckoutViewModel(withConfigurationSignal: initialData)

let context = initialData.map(\.context)
let checkoutId = initialData.map(\.checkoutId)
let baseReward = initialData.map(\.rewards).map(\.first)
Expand Down Expand Up @@ -260,70 +272,6 @@ public class PostCampaignCheckoutViewModel: PostCampaignCheckoutViewModelType,
self.showErrorBannerWithMessage = validateCheckoutError
.map { _ in Strings.Something_went_wrong_please_try_again() }

// MARK: ApplePay

/*

Order of operations:
1) Apple pay button tapped
2) Generate a new payment intent
3) Present the payment authorization form
4) Payment authorization form calls applePayContextDidCreatePayment with ApplePay params
5) Payment authorization form calls paymentAuthorizationDidFinish
*/

let createPaymentIntentForApplePay: Signal<Signal<PaymentIntentEnvelope, ErrorEnvelope>.Event, Never> =
self.configureWithDataProperty.signal
.skipNil()
.takeWhen(self.applePayButtonTappedSignal)
.switchMap { initialData in
let projectId = initialData.project.graphID
let pledgeTotal = initialData.total

return AppEnvironment.current.apiService
.createPaymentIntentInput(input: CreatePaymentIntentInput(
projectId: projectId,
amountDollars: String(format: "%.2f", pledgeTotal),
digitalMarketingAttributed: nil
))
.ksr_delay(AppEnvironment.current.apiDelayInterval, on: AppEnvironment.current.scheduler)
.materialize()
}

let newPaymentIntentForApplePayError: Signal<ErrorEnvelope, Never> = createPaymentIntentForApplePay
.errors()

let newPaymentIntentForApplePay: Signal<String, Never> = createPaymentIntentForApplePay
.values()
.map { $0.clientSecret }

self.goToApplePayPaymentAuthorization = self
.configureWithDataProperty
.signal
.skipNil()
.combineLatest(with: newPaymentIntentForApplePay)
.map { (
data: PostCampaignCheckoutData,
paymentIntent: String
) -> PostCampaignPaymentAuthorizationData? in
let baseReward = data.baseReward

return PostCampaignPaymentAuthorizationData(
project: data.project,
hasNoReward: baseReward.isNoReward,
subtotal: baseReward.isNoReward ? baseReward.minimum : calculateAllRewardsTotal(
addOnRewards: data.rewards,
selectedQuantities: data.selectedQuantities
),
bonus: data.bonusAmount ?? 0,
shipping: data.shipping?.total ?? 0,
total: data.total,
merchantIdentifier: Secrets.ApplePay.merchantIdentifier,
paymentIntent: paymentIntent
)
}
.skipNil()

// MARK: CompleteOnSessionCheckout

let completeCheckoutWithCreditCardInput: Signal<GraphAPI.CompleteOnSessionCheckoutInput, Never> = Signal
Expand All @@ -344,35 +292,10 @@ public class PostCampaignCheckoutViewModel: PostCampaignCheckoutViewModelType,
)
}

let completeCheckoutWithApplePayInput: Signal<GraphAPI.CompleteOnSessionCheckoutInput, Never> = Signal
.combineLatest(newPaymentIntentForApplePay, checkoutId, self.applePayParamsSignal.mapConst(true))
.takeWhen(self.applePayContextDidCompleteSignal)
.map {
(
clientSecret: String,
checkoutId: String,
_: Bool
) -> GraphAPI.CompleteOnSessionCheckoutInput in
GraphAPI
.CompleteOnSessionCheckoutInput(
checkoutId: encodeToBase64("Checkout-\(checkoutId)"),
paymentIntentClientSecret: clientSecret,
paymentSourceId: nil,
paymentSourceReusable: false,
/* We are no longer sending ApplePay parameters to the backend, because Stripe Tokens are
considered deprecated and are incompatible with PaymentIntent-based payments.

In the future, we may use the other parameters in the ApplePayParams object, but for now,
send nil.
*/
applePay: nil
)
}

let checkoutCompleteSignal = Signal
.merge(
completeCheckoutWithCreditCardInput,
completeCheckoutWithApplePayInput
self.applePayModel.completeCheckoutWithApplePayInput
)
.switchMap { input in
AppEnvironment.current.apiService.completeOnSessionCheckout(input: input).materialize()
Expand Down Expand Up @@ -430,7 +353,7 @@ public class PostCampaignCheckoutViewModel: PostCampaignCheckoutViewModelType,
self.submitButtonTappedProperty.signal.mapConst(false),
self.applePayButtonTappedSignal.mapConst(false),
// Hide view again whenever pledge flow is completed/cancelled/errors.
newPaymentIntentForApplePayError.mapConst(true),
self.applePayModel.errors.mapConst(true),
validateCheckoutError.mapConst(true),
self.checkoutTerminatedProperty.signal.mapConst(true),
checkoutCompleteSignal.signal.mapConst(true)
Expand Down Expand Up @@ -575,7 +498,6 @@ public class PostCampaignCheckoutViewModel: PostCampaignCheckoutViewModelType,
public let showErrorBannerWithMessage: Signal<String, Never>
public let showWebHelp: Signal<HelpType, Never>
public let validateCheckoutSuccess: Signal<PaymentSourceValidation, Never>
public let goToApplePayPaymentAuthorization: Signal<PostCampaignPaymentAuthorizationData, Never>
public let checkoutComplete: Signal<ThanksPageData, Never>
public let checkoutError: Signal<ErrorEnvelope, Never>

Expand Down