-
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
[MBL-1210] Fix logged out and login flow for post campaign checkout #1991
Changes from all commits
f40ac82
3118a49
f5c47f0
eb85b9c
ff9505b
a445140
093bd0c
23001d5
a5f4549
237ee7e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,7 +22,7 @@ final class PostCampaignCheckoutViewController: UIViewController { | |
private lazy var pledgeCTAContainerView: PledgeViewCTAContainerView = { | ||
PledgeViewCTAContainerView(frame: .zero) | ||
|> \.translatesAutoresizingMaskIntoConstraints .~ false | ||
// TODO: Add self as delegate and add support for delegate methods. | ||
|> \.delegate .~ self | ||
}() | ||
|
||
private lazy var pledgeDisclaimerView: PledgeDisclaimerView = { | ||
|
@@ -50,6 +50,8 @@ final class PostCampaignCheckoutViewController: UIViewController { | |
|> \.translatesAutoresizingMaskIntoConstraints .~ false | ||
}() | ||
|
||
private var sessionStartedObserver: Any? | ||
|
||
private let viewModel: PostCampaignCheckoutViewModelType = PostCampaignCheckoutViewModel() | ||
|
||
// MARK: - Lifecycle | ||
|
@@ -79,6 +81,10 @@ final class PostCampaignCheckoutViewController: UIViewController { | |
self.viewModel.inputs.viewDidLoad() | ||
} | ||
|
||
deinit { | ||
self.sessionStartedObserver.doIfSome(NotificationCenter.default.removeObserver) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nifty, first time I've seen this in this codebase. Some kind of fancy RAC unsubscribe? |
||
} | ||
|
||
// MARK: - Configuration | ||
|
||
private func configureChildViewControllers() { | ||
|
@@ -179,12 +185,40 @@ final class PostCampaignCheckoutViewController: UIViewController { | |
self?.paymentMethodsViewController.configure(with: value) | ||
} | ||
|
||
self.viewModel.outputs.goToLoginSignup | ||
.observeForControllerAction() | ||
.observeValues { [weak self] intent, project, reward in | ||
self?.goToLoginSignup(with: intent, project: project, reward: reward) | ||
} | ||
|
||
self.viewModel.outputs.showWebHelp | ||
.observeForControllerAction() | ||
.observeValues { [weak self] helpType in | ||
guard let self = self else { return } | ||
self.presentHelpWebViewController(with: helpType, presentationStyle: .formSheet) | ||
} | ||
|
||
self.sessionStartedObserver = NotificationCenter.default | ||
.addObserver(forName: .ksr_sessionStarted, object: nil, queue: nil) { [weak self] _ in | ||
self?.viewModel.inputs.userSessionStarted() | ||
} | ||
|
||
self.paymentMethodsViewController.view.rac.hidden = self.viewModel.outputs.paymentMethodsViewHidden | ||
} | ||
|
||
// MARK: - Functions | ||
|
||
private func goToLoginSignup(with intent: LoginIntent, project: Project, reward: Reward?) { | ||
let loginSignupViewController = LoginToutViewController.configuredWith( | ||
loginIntent: intent, | ||
project: project, | ||
reward: reward | ||
) | ||
|
||
let navigationController = UINavigationController(rootViewController: loginSignupViewController) | ||
let navigationBarHeight = navigationController.navigationBar.bounds.height | ||
|
||
self.present(navigationController, animated: true) | ||
} | ||
} | ||
|
||
|
@@ -195,3 +229,27 @@ extension PostCampaignCheckoutViewController: PledgeDisclaimerViewDelegate { | |
self.viewModel.inputs.pledgeDisclaimerViewDidTapLearnMore() | ||
} | ||
} | ||
|
||
// MARK: - PledgeViewCTAContainerViewDelegate | ||
|
||
extension PostCampaignCheckoutViewController: PledgeViewCTAContainerViewDelegate { | ||
func goToLoginSignup() { | ||
self.paymentMethodsViewController.cancelModalPresentation(true) | ||
self.viewModel.inputs.goToLoginSignupTapped() | ||
} | ||
|
||
func applePayButtonTapped() { | ||
self.paymentMethodsViewController.cancelModalPresentation(true) | ||
// TODO: Respond to button tap | ||
} | ||
|
||
func submitButtonTapped() { | ||
self.paymentMethodsViewController.cancelModalPresentation(true) | ||
// TODO: Respond to button tap | ||
} | ||
|
||
func termsOfUseTapped(with helpType: HelpType) { | ||
self.paymentMethodsViewController.cancelModalPresentation(true) | ||
self.viewModel.inputs.termsOfUseTapped(with: helpType) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -283,6 +283,15 @@ public final class PledgePaymentMethodsViewModel: PledgePaymentMethodsViewModelT | |
self.shouldCancelPaymentSheetAppearance <~ updatedCards.signal | ||
.mapConst(true) | ||
|
||
// Only ever use the value if the view model is configured and the payment sheet could exist. | ||
// In the logged out state, the payment sheet is part of the view without being configured, | ||
// so this is a real risk. | ||
let safeShouldCancelPaymentSheet = Signal.combineLatest( | ||
self.shouldCancelPaymentSheetAppearance.signal, | ||
configureWithValue | ||
) | ||
.map(first) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Most patterns I've seen in our codebase call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It happens in the logged out state! If the user is logged out, the payment sheet doesn't actually get configured, but we still say "hey, please don't show that extra modal" if the user interacts with the pledge cta (ex: if they want to log in). Leaving it as it was causes the app to crash when that happens. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aha, that makes sense! This would be a good comment, actually. |
||
|
||
let createSetupIntentEvent = Signal.combineLatest( | ||
project, | ||
paymentSheetOnPledgeContext.filter(isTrue) | ||
|
@@ -310,7 +319,7 @@ public final class PledgePaymentMethodsViewModel: PledgePaymentMethodsViewModelT | |
} | ||
|
||
self.goToAddCardViaStripeScreen = createSetupIntentEvent.values() | ||
.withLatestFrom(self.shouldCancelPaymentSheetAppearance.signal) | ||
.withLatestFrom(safeShouldCancelPaymentSheet) | ||
.map { (data, shouldCancel) -> PaymentSheetSetupData? in | ||
shouldCancel ? nil : data | ||
} | ||
|
@@ -322,7 +331,7 @@ public final class PledgePaymentMethodsViewModel: PledgePaymentMethodsViewModelT | |
|
||
self.updateAddNewCardLoading = Signal.merge( | ||
createSetupIntentEvent.errors().mapConst(false), | ||
self.shouldCancelPaymentSheetAppearance.signal.negate() | ||
safeShouldCancelPaymentSheet.negate() | ||
) | ||
|
||
self.willSelectRowAtIndexPathReturnProperty <~ self.reloadPaymentMethods | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,7 +20,10 @@ public struct PostCampaignCheckoutData: Equatable { | |
|
||
public protocol PostCampaignCheckoutViewModelInputs { | ||
func configure(with data: PostCampaignCheckoutData) | ||
func goToLoginSignupTapped() | ||
func pledgeDisclaimerViewDidTapLearnMore() | ||
func termsOfUseTapped(with: HelpType) | ||
func userSessionStarted() | ||
func viewDidLoad() | ||
} | ||
|
||
|
@@ -31,6 +34,8 @@ public protocol PostCampaignCheckoutViewModelOutputs { | |
Never | ||
> { get } | ||
var configurePledgeViewCTAContainerView: Signal<PledgeViewCTAContainerViewData, Never> { get } | ||
var goToLoginSignup: Signal<(LoginIntent, Project, Reward?), Never> { get } | ||
var paymentMethodsViewHidden: Signal<Bool, Never> { get } | ||
var showWebHelp: Signal<HelpType, Never> { get } | ||
} | ||
|
||
|
@@ -52,16 +57,23 @@ public class PostCampaignCheckoutViewModel: PostCampaignCheckoutViewModelType, | |
|
||
let context = initialData.map(\.context) | ||
|
||
self.configurePaymentMethodsViewControllerWithValue = initialData | ||
let configurePaymentMethodsData = Signal.merge( | ||
initialData, | ||
initialData.takeWhen(self.userSessionStartedSignal) | ||
) | ||
|
||
self.configurePaymentMethodsViewControllerWithValue = configurePaymentMethodsData | ||
.compactMap { data -> PledgePaymentMethodsValue? in | ||
guard let user = AppEnvironment.current.currentUser else { return nil } | ||
guard let reward = data.rewards.first else { return nil } | ||
|
||
return (user, data.project, reward, data.context, data.refTag) | ||
} | ||
|
||
// TODO: Respond to login flow. | ||
let isLoggedIn = initialData.ignoreValues() | ||
self.goToLoginSignup = initialData.takeWhen(self.goToLoginSignupSignal) | ||
.map { (LoginIntent.backProject, $0.project, $0.rewards.first) } | ||
|
||
let isLoggedIn = Signal.merge(initialData.ignoreValues(), self.userSessionStartedSignal) | ||
.map { _ in AppEnvironment.current.currentUser } | ||
.map(isNotNil) | ||
|
||
|
@@ -70,11 +82,23 @@ public class PostCampaignCheckoutViewModel: PostCampaignCheckoutViewModelType, | |
context | ||
) | ||
.map { isLoggedIn, context in | ||
// TODO: Calculate isEnabled and willRetryPaymentMethod fields instead of defaulting to true. | ||
PledgeViewCTAContainerViewData(isLoggedIn, true, context, true) | ||
PledgeViewCTAContainerViewData( | ||
isLoggedIn: isLoggedIn, | ||
isEnabled: true, // Pledge button never needs to be disabled on checkout page. | ||
context: context, | ||
willRetryPaymentMethod: false // Only retry in the `fixPaymentMethod` context. | ||
) | ||
} | ||
|
||
self.showWebHelp = self.pledgeDisclaimerViewDidTapLearnMoreSignal.mapConst(.trust) | ||
self.paymentMethodsViewHidden = Signal.combineLatest(isLoggedIn, context) | ||
.map { isLoggedIn, context in | ||
!isLoggedIn || context.paymentMethodsViewHidden | ||
} | ||
|
||
self.showWebHelp = Signal.merge( | ||
self.termsOfUseTappedSignal, | ||
self.pledgeDisclaimerViewDidTapLearnMoreSignal.mapConst(.trust) | ||
) | ||
|
||
self.configurePledgeRewardsSummaryViewWithData = initialData | ||
.compactMap { data in | ||
|
@@ -101,12 +125,27 @@ public class PostCampaignCheckoutViewModel: PostCampaignCheckoutViewModelType, | |
self.configureWithDataProperty.value = data | ||
} | ||
|
||
private let (goToLoginSignupSignal, goToLoginSignupObserver) = Signal<Void, Never>.pipe() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. First time I've seen this, what's the goal of using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, to be honest, this is also just copy-pasted from the standard pledge version of this VC/VM. But I think the point is that we want to react to button clicks - so we want to track whenever new events happen but we don't actually need it to pass along any data. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aah, gotcha. I think we mix |
||
public func goToLoginSignupTapped() { | ||
self.goToLoginSignupObserver.send(value: ()) | ||
} | ||
|
||
private let (pledgeDisclaimerViewDidTapLearnMoreSignal, pledgeDisclaimerViewDidTapLearnMoreObserver) | ||
= Signal<Void, Never>.pipe() | ||
public func pledgeDisclaimerViewDidTapLearnMore() { | ||
self.pledgeDisclaimerViewDidTapLearnMoreObserver.send(value: ()) | ||
} | ||
|
||
private let (termsOfUseTappedSignal, termsOfUseTappedObserver) = Signal<HelpType, Never>.pipe() | ||
public func termsOfUseTapped(with helpType: HelpType) { | ||
self.termsOfUseTappedObserver.send(value: helpType) | ||
} | ||
|
||
private let (userSessionStartedSignal, userSessionStartedObserver) = Signal<Void, Never>.pipe() | ||
public func userSessionStarted() { | ||
self.userSessionStartedObserver.send(value: ()) | ||
} | ||
|
||
private let viewDidLoadProperty = MutableProperty(()) | ||
public func viewDidLoad() { | ||
self.viewDidLoadProperty.value = () | ||
|
@@ -121,6 +160,8 @@ public class PostCampaignCheckoutViewModel: PostCampaignCheckoutViewModelType, | |
PledgeSummaryViewData | ||
), Never> | ||
public let configurePledgeViewCTAContainerView: Signal<PledgeViewCTAContainerViewData, Never> | ||
public let goToLoginSignup: Signal<(LoginIntent, Project, Reward?), Never> | ||
public let paymentMethodsViewHidden: Signal<Bool, Never> | ||
public let showWebHelp: Signal<HelpType, Never> | ||
|
||
public var inputs: PostCampaignCheckoutViewModelInputs { return self } | ||
|
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.
Nit: This method name isn't totally clear to me on first read. So it means the payment sheet normally would pop up, but now it won't?
Appearance
makes me assume it's something like a modal.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.
Ah, or is this more like a dismissal?
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 this name is more clear than it was before but you're right - when I named it, I didn't know what it did either 😭. You're pretty much correct, though. Basically when you add a new card to the payment sheet, there's a loading indicator for a bit before the sheet pops up. Setting this to false (the only way it's used outside of tests) cancels that pop up (usually because we've presented a different modal instead). I think it might also dismiss the sheet itself if it's already presented by the time this is called, but I'm not sure about that one since that would only happen as a race condition. If you have any ideas for a different name, though, let me know!
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.
Hmmm...what about something like
dontShowPaymentSheet
?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'm just going to name it
cancelModalPresentation
I think. That's clear enough while still being general enough that you don't need to understand the paymentMethodsVC to understand what the goal is.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.
Sounds good to me!