-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
[NT-286][NT-360] SCA Support #902
Conversation
} | ||
} | ||
|
||
public enum StripePaymentHandlerActionStatus { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This type maps to STPPaymentHandler.ActionStatus
and makes it easier for us to mock in tests without having to link the Stripe SDK to our Library framework.
@@ -33,6 +33,7 @@ public protocol PledgeViewModelInputs { | |||
) | |||
func paymentAuthorizationViewControllerDidFinish() | |||
func pledgeAmountViewControllerDidUpdate(with data: PledgeAmountData) | |||
func scaFlowCompleted(with result: StripePaymentHandlerActionStatusType, error: Error?) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We pass in our protocol type here.
import Foundation | ||
import KsApi | ||
|
||
public protocol StripeSCARequiring { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This protocol helps to merge the signals in PledgeViewModel
for simplification.
let updateBackingDidCompleteApplePay = updateApplePayBackingSuccess | ||
.ignoreValues() | ||
#warning("To be removed once new CreateBacking is implemented to handle Apple Pay") | ||
let deprecatedCreateApplePayBackingCompleted = Signal.combineLatest( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Self-explanatory but this should be removed once we're handling Apple Pay through CreateBacking
.
@@ -636,17 +667,23 @@ public class PledgeViewModel: PledgeViewModelType, PledgeViewModelInputs, Pledge | |||
public let popViewController: Signal<(), Never> | |||
public let sectionSeparatorsHidden: Signal<Bool, Never> | |||
public let shippingLocationViewHidden: Signal<Bool, Never> | |||
public let showErrorBannerWithMessage: Signal<String, Never> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Errors are all emitted through one output now.
Generated by 🚫 Danger |
@@ -318,7 +315,7 @@ public class PledgeViewModel: PledgeViewModelType, PledgeViewModelInputs, Pledge | |||
) | |||
} | |||
|
|||
let createApplePayBackingEvent = createApplePayBackingData.map( | |||
let deprecatedCreateApplePayBackingEvent = createApplePayBackingData.map( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is deprecated
cuz we'll be merging it with createBacking
soon right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's correct, we should be able to remove this completely once that work is done.
STPPaymentHandler.shared().confirmSetupIntent( | ||
withParams: .init(clientSecret: secret), | ||
authenticationContext: self | ||
) { [weak self] status, _, error in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just curious what's this middle parameter here that we're ignoring?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's the SetupIntent
object. I don't believe we need it for anything.
} | ||
} | ||
|
||
// MARK: STPAuthenticationContext |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add the -
here
let createOrUpdateApplePayBackingCompleted = Signal.combineLatest( | ||
Signal.merge( | ||
updateBackingEvent.filter { $0.isTerminating }.ignoreValues(), | ||
createBackingEvent.filter { $0.isTerminating }.ignoreValues() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be deprecatedCreateApplePayBackingEvent
? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is handled on its own so that it'll be easy to remove later. This future-proofs for when CreateBacking
supports Apple Pay.
self.createBackingError = Signal.merge( | ||
createApplePayBackingError, | ||
createBackingEventError | ||
let createOrUpdateApplePayBackingCompleted = Signal.combineLatest( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondering if this should be a zip
to account for multiple groups of events?:
- User opens payment sheet and pledges - payment sheet dismisses, but pledge fails.
- User opens up payment sheet a second time and pledges - because the
createBackingEvent.filter
event has already fired once before, the nextpaymentAuthorizationDidFinish
event will cause thecreateOrUpdateApplePayBackingCompleted
to fire without waiting for the next request to terminate.
🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll try to write a failing test for it and then prove that scenario 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok @ifbarrera @Scollaco I've taken a new approach which clears the values for the create and update backing events each time a new flow is started. I think this gives us more confidence that we won't have overlapping values between flows being interrupted and new ones starting.
The tests are passing (all several thousand lines of them 😓).
Note right now I don't think the deprecatedCreateApplePayBacking needs this treatment because it's somewhat separate, but I didn't want to spend too much time on that given that we should remove it in the next PR.
.ignoreValues(), | ||
updatePaymentAuthorizationDidFinishSignal | ||
) | ||
let didCompleteApplePayBacking = createOrUpdateBackingEventValues |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious why do we need to take createOrUpdateBackingEventValues
here? That seems to require either createBackingEvent.values()
or updateBackingEvent.values()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes this is future proof for when createBacking
supports Apple Pay.
self.submitButtonEnabled.assertValues([false, true, false, true]) | ||
self.goToThanks.assertDidNotEmitValue() | ||
self.showErrorBannerWithMessage.assertValues( | ||
["The operation couldn’t be completed. (KsApi.GraphError error 5.)"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a strange error message. Shouldn't this be "Something went wrong." which is the localizedDescription
of GraphError.invalidInput
?
with: MockStripePaymentHandlerActionStatus(status: .succeeded), error: nil | ||
) | ||
|
||
self.beginSCAFlowWithClientSecret.assertValues(["client-secret"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be above the call to inputs.scaFlowCompleted
no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I see it's already on line 1205, apologies!
|
||
let updateBackingEventErrors = updateBackingEvent.errors() | ||
let createUpdateBackingEventErrors = Signal.merge( | ||
deprecatedCreateApplePayBackingEvent.errors().map { $0 as Error }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you should have to do this casting to Error
🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's because self.showErrorBannerWithMessage
merges different types of errors (GraphError
and Error
). What if here we simplify to:
let createUpdateBackingEventErrors = Signal.merge(
deprecatedCreateApplePayBackingEvent.errors(),
createBackingEvent.errors(),
updateBackingEvent.errors()
).map { $0 as Error }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved the mapping to after the merge. I think I was moving things around and did this to satisfy the compiler at some point.
Once we've removed deprecatedCreateApplePayBackingCompletedError
this should be a little simpler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved this around to better support Graph vs SCA errors 👍
KsApi/models/Checkout.swift
Outdated
@@ -12,7 +12,7 @@ public struct Checkout: Decodable { | |||
} | |||
|
|||
public struct Backing: Decodable { | |||
let requiresAction: Bool | |||
let clientSecret: String? | |||
public let requiresAction: Bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you alpha here?
|
||
public protocol StripeSCARequiring { | ||
var requiresSCAFlow: Bool { get } | ||
var clientSecret: String? { get } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And here
public enum StripePaymentHandlerActionStatus { | ||
case succeeded | ||
case canceled | ||
case failed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here too.
self.submitButtonTappedSignal.mapConst(true), | ||
willUpdateApplePayBacking.mapConst(false) | ||
let deprecatedCreateApplePayBackingCompletedError = deprecatedCreateApplePayBackingCompleted | ||
.withLatest(from: deprecatedCreateApplePayBackingEvent.errors()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For curiosity, why are we using withLatest(from:)
and not zip
here? Shouldn't deprecatedCreateApplePayBackingCompletedError
only emit when both deprecatedCreateApplePayBackingCompleted
and deprecatedCreateApplePayBackingEvent.errors()
emit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to avoid zip
just because it can often cause sneaky problems, but that's possible true for this case.
I'll look into this along with #902 (comment).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is going to be necessary - #902 (comment).
Also we did withLatest(from:)
here because the errors emit but we have a race condition between the Apple Pay sheet dismissing and the events emitting, which is why wait for deprecatedCreateApplePayBackingCompleted
, which is a combined signal to emit and then take the most recently emitted errors.
|
||
let updateBackingEventErrors = updateBackingEvent.errors() | ||
let createUpdateBackingEventErrors = Signal.merge( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you rename this to createUpdateBackingEventErrors
to match the createOrUpdateBacking...
convention adopted above?
|
||
let updateBackingEventErrors = updateBackingEvent.errors() | ||
let createUpdateBackingEventErrors = Signal.merge( | ||
deprecatedCreateApplePayBackingEvent.errors().map { $0 as Error }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's because self.showErrorBannerWithMessage
merges different types of errors (GraphError
and Error
). What if here we simplify to:
let createUpdateBackingEventErrors = Signal.merge(
deprecatedCreateApplePayBackingEvent.errors(),
createBackingEvent.errors(),
updateBackingEvent.errors()
).map { $0 as Error }
|
||
let updateApplePayBackingError = updateApplePayBackingCompleted | ||
.withLatest(from: updateBackingEventErrors) | ||
let createUpdateApplePayBackingError = createOrUpdateApplePayBackingCompleted |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you rename this variable too?
.map(second) | ||
|
||
let updateBackingError = submitButtonTapped | ||
.takePairWhen(updateBackingEventErrors) | ||
let createUpdateBackingError = submitButtonTapped |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here
# Conflicts: # Library/ViewModels/PledgeViewModel.swift # Library/ViewModels/PledgeViewModelTests.swift
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All my comments are either minor changes or suggestions. Awesome work on this, I know it was tricky one! 🎉
@@ -443,6 +440,7 @@ public class PledgeViewModel: PledgeViewModelType, PledgeViewModelInputs, Pledge | |||
createBackingEvent.filter { $0.isTerminating }.mapConst(true), | |||
updateBackingEvent.filter { $0.isTerminating }.mapConst(true) | |||
) | |||
.skipRepeats() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice 😄
@@ -459,90 +457,139 @@ public class PledgeViewModel: PledgeViewModelType, PledgeViewModelInputs, Pledge | |||
updateButtonIsLoading | |||
) | |||
|
|||
// MARK: - Success/Failure Create | |||
let submitButtonTapped = Signal.merge( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The naming of this signal is a little misleading, because really what you're capturing here is whether a normal transaction has begun (to differentiate between a normal transaction and an apple pay transaction). I wonder if something like isCreateOrUpdateBacking
would be more appropriate considering the usage below on line 508:
let createOrUpdateBackingDidCompleteNoSCA = submitButtonTapped
.takeWhen(createOrUpdateBackingEventValuesNoSCA)
.filter(isTrue)
.ignoreValues()
would become
let createOrUpdateBackingDidCompleteNoSCA = isCreateOrUpdateBacking
.takeWhen(createOrUpdateBackingEventValuesNoSCA)
.filter(isTrue)
.ignoreValues()
Just a thought.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call!
deprecatedCreateApplePayBackingEvent.values() | ||
.combineLatest(with: deprecatedCreateApplePayBackingCompleted) | ||
.ignoreValues(), | ||
didCompleteApplePayBacking.combineLatest(with: willCreateApplePayBacking).ignoreValues(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a hunch that the combineLatest
here isn't needed because createOrUpdateApplePayBackingCompleted
zips with didInitiateApplePayBacking
which merges with willCreateApplePayBacking
(phew 😅). Fine to leave it if you want, but I would just try removing it and see if any tests break.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah naming is hard. In this particular case the combined signal is to differentiate between whether this updated or created an Apple Pay Backing 🤕 .
self.submitButtonEnabled.assertValues( | ||
[false, false, false, false, true, false, true, false], "Amount and shipping rule unchanged" | ||
) | ||
self.submitButtonEnabled.assertValues([false, true, false, true, false], "Shipping rule changed") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the comment a little off here?
} | ||
} | ||
|
||
func testApplePayBackingFails_ThenSucceeds_SignalsDoNotOverlap() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this is testing with the .update
context. Can we update (see what I did there) the name of this test to reflect that?
self.goToThanks.assertDidNotEmitValue() | ||
} | ||
} | ||
|
||
func testApplePayBackingFails_ThenStoredCardSucceeds_SignalsDoNotOverlap() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here the context seems to be .update
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well done on this one, Justin! 🎉 🎉
📲 What
Adds SCA support to our create and update pledge flows.
🤔 Why
From the Stripe docs:
🛠 How
PledgeViewModel
to trigger the SCA flow.PledgeViewModel
to handle the response (succeeded/failed/canceled).StripePaymentHandlerActionStatus
(an enum) andStripePaymentHandlerActionStatusType
(a protocol) that allows us to more easily mock the Stripe response enum in tests.Note: I've renamed
createApplePayBackingEvent
todeprecatedCreateApplePayBackingEvent
in the expectation that it will be removed soon when we've updated ourCreateBacking
mutation to acceptApplePayParams
.✅ Acceptance criteria
Using the test cards listed here:
Note: Please be aware of the caveats mentioned in this thread when testing.