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-1355] Move Login/Signup Logic To Confirm Pledge Details #2029

Merged
merged 12 commits into from
Apr 9, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ final class ConfirmDetailsViewController: UIViewController, MessageBannerViewCon
|> \.translatesAutoresizingMaskIntoConstraints .~ false
}()

private var sessionStartedObserver: Any?

private let viewModel: ConfirmDetailsViewModelType = ConfirmDetailsViewModel()

// MARK: - Lifecycle
Expand Down Expand Up @@ -132,6 +134,10 @@ final class ConfirmDetailsViewController: UIViewController, MessageBannerViewCon
self.viewModel.inputs.viewDidLoad()
}

deinit {
self.sessionStartedObserver.doIfSome(NotificationCenter.default.removeObserver)
}

// MARK: - Configuration

private func configureChildViewControllers() {
Expand Down Expand Up @@ -275,6 +281,17 @@ final class ConfirmDetailsViewController: UIViewController, MessageBannerViewCon
self?.messageBannerViewController?.showBanner(with: .error, message: errorMessage)
}

self.viewModel.outputs.goToLoginSignup
.observeForControllerAction()
.observeValues { [weak self] intent, project, reward in
self?.goToLoginSignup(with: intent, project: project, reward: reward)
}

self.sessionStartedObserver = NotificationCenter.default
.addObserver(forName: .ksr_sessionStarted, object: nil, queue: nil) { [weak self] _ in
self?.viewModel.inputs.userSessionStarted()
}

self.pledgeAmountViewController.view.rac.hidden = self.viewModel.outputs.pledgeAmountViewHidden

self.shippingLocationViewController.view.rac.hidden = self.viewModel.outputs.shippingLocationViewHidden
Expand All @@ -301,6 +318,18 @@ final class ConfirmDetailsViewController: UIViewController, MessageBannerViewCon

// MARK: - Functions

private func goToLoginSignup(with intent: LoginIntent, project: Project, reward: Reward?) {
let loginSignupViewController = LoginToutViewController.configuredWith(
loginIntent: intent,
project: project,
reward: reward
)
Comment on lines +322 to +326
Copy link
Contributor Author

Choose a reason for hiding this comment

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

At first glance, it looks like LoginToutViewController doesn't actually use the project or reward being passed in, but I found that they're specifically for analytics.

Copy link
Contributor

Choose a reason for hiding this comment

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

I've noticed this in quite a few places - a a view model requiring way more information than seems initially necessary, because it gets passed through to analytics. This is something we might consider for future refactoring. The way we pass around "contexts" in some places feels like a better model for this.


let navigationController = UINavigationController(rootViewController: loginSignupViewController)

self.present(navigationController, animated: true)
}

private func goToCheckout(data: PostCampaignCheckoutData) {
let vc = PostCampaignCheckoutViewController.instantiate()
vc.configure(with: data)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ final class PostCampaignCheckoutViewController: UIViewController,
|> \.translatesAutoresizingMaskIntoConstraints .~ false
}()

private var sessionStartedObserver: Any?

private let viewModel: PostCampaignCheckoutViewModelType = PostCampaignCheckoutViewModel()

// MARK: - Lifecycle
Expand Down Expand Up @@ -97,7 +95,6 @@ final class PostCampaignCheckoutViewController: UIViewController,

deinit {
self.hideProcessingView()
self.sessionStartedObserver.doIfSome(NotificationCenter.default.removeObserver)
}

// MARK: - Configuration
Expand Down Expand Up @@ -210,12 +207,6 @@ 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
Expand All @@ -237,13 +228,6 @@ final class PostCampaignCheckoutViewController: UIViewController,
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()
}

self.paymentMethodsViewController.view.rac.hidden = self.viewModel.outputs.paymentMethodsViewHidden

self.viewModel.outputs.configureStripeIntegration
.observeForUI()
.observeValues { merchantIdentifier, publishableKey in
Expand Down Expand Up @@ -282,18 +266,6 @@ final class PostCampaignCheckoutViewController: UIViewController,

// 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)

self.present(navigationController, animated: true)
}

private func confirmPayment(with validation: PaymentSourceValidation) {
guard validation.requiresConfirmation else {
// Short circuit for payment intents that have already been validated
Expand Down Expand Up @@ -353,7 +325,6 @@ extension PostCampaignCheckoutViewController: PledgeDisclaimerViewDelegate {
extension PostCampaignCheckoutViewController: PledgeViewCTAContainerViewDelegate {
func goToLoginSignup() {
self.paymentMethodsViewController.cancelModalPresentation(true)
self.viewModel.inputs.goToLoginSignupTapped()
}

func applePayButtonTapped() {
Expand Down
37 changes: 32 additions & 5 deletions Library/ViewModels/ConfirmDetailsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public protocol ConfirmDetailsViewModelInputs {
func continueCTATapped()
func pledgeAmountViewControllerDidUpdate(with data: PledgeAmountData)
func shippingRuleSelected(_ shippingRule: ShippingRule)
func userSessionStarted()
func viewDidLoad()
}

Expand All @@ -24,13 +25,14 @@ public protocol ConfirmDetailsViewModelOutputs {
var configureShippingLocationViewWithData: Signal<PledgeShippingLocationViewData, Never> { get }
var configureShippingSummaryViewWithData: Signal<PledgeShippingSummaryViewData, Never> { get }
var createCheckoutSuccess: Signal<PostCampaignCheckoutData, Never> { get }
var showErrorBannerWithMessage: Signal<String, Never> { get }
var goToLoginSignup: Signal<(LoginIntent, Project, Reward?), Never> { get }
var localPickupViewHidden: Signal<Bool, Never> { get }
var pledgeAmountViewHidden: Signal<Bool, Never> { get }
var pledgeRewardsSummaryViewHidden: Signal<Bool, Never> { get }
var pledgeSummaryViewHidden: Signal<Bool, Never> { get }
var shippingLocationViewHidden: Signal<Bool, Never> { get }
var shippingSummaryViewHidden: Signal<Bool, Never> { get }
var showErrorBannerWithMessage: Signal<String, Never> { get }
}

public protocol ConfirmDetailsViewModelType {
Expand Down Expand Up @@ -58,6 +60,18 @@ public class ConfirmDetailsViewModel: ConfirmDetailsViewModelType, ConfirmDetail

let backing = project.map { $0.personalization.backing }.skipNil()

let isLoggedIn = Signal.merge(initialData.ignoreValues(), self.userSessionStartedSignal)
.map { _ in AppEnvironment.current.currentUser }
.map(isNotNil)

self.goToLoginSignup = Signal.combineLatest(isLoggedIn, initialData)
.takeWhen(self.continueCTATappedProperty.signal)
.filter { isLoggedIn, _ in isLoggedIn == false }
.map { _, data in
let baseReward = data.rewards.first
return (LoginIntent.backProject, data.project, baseReward)
}

// MARK: Pledge Amount

self.pledgeAmountViewHidden = context.map { $0.pledgeAmountViewHidden }
Expand Down Expand Up @@ -319,9 +333,16 @@ public class ConfirmDetailsViewModel: ConfirmDetailsViewModelType, ConfirmDetail
refTag
)

let createCheckoutEvents = pledgeDetailsData
.takeWhen(self.continueCTATappedProperty.signal)
.map { project, rewards, selectedQuantities, pledgeTotal, refTag in
let isLoggedInAndContinueButtonTapped = Signal.merge(
self.continueCTATappedProperty.signal,
self.userSessionStartedSignal
)

let createCheckoutEvents = Signal.combineLatest(isLoggedIn, pledgeDetailsData)
scottkicks marked this conversation as resolved.
Show resolved Hide resolved
.takeWhen(isLoggedInAndContinueButtonTapped)
.filter { isLoggedIn, _ in isLoggedIn }
.map { _, pledgeDetailsData in
let (project, rewards, selectedQuantities, pledgeTotal, refTag) = pledgeDetailsData
let rewardsIDs: [String] = rewards.first?.isNoReward == true
? []
: rewards.flatMap { reward -> [String] in
Expand Down Expand Up @@ -429,6 +450,11 @@ public class ConfirmDetailsViewModel: ConfirmDetailsViewModelType, ConfirmDetail
self.submitButtonTappedObserver.send(value: ())
}

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 = ()
Expand All @@ -448,13 +474,14 @@ public class ConfirmDetailsViewModel: ConfirmDetailsViewModelType, ConfirmDetail
public let configureShippingLocationViewWithData: Signal<PledgeShippingLocationViewData, Never>
public let configureShippingSummaryViewWithData: Signal<PledgeShippingSummaryViewData, Never>
public let createCheckoutSuccess: Signal<PostCampaignCheckoutData, Never>
public let showErrorBannerWithMessage: Signal<String, Never>
public let goToLoginSignup: Signal<(LoginIntent, Project, Reward?), Never>
public let localPickupViewHidden: Signal<Bool, Never>
public let pledgeAmountViewHidden: Signal<Bool, Never>
public let pledgeRewardsSummaryViewHidden: Signal<Bool, Never>
public let pledgeSummaryViewHidden: Signal<Bool, Never>
public let shippingLocationViewHidden: Signal<Bool, Never>
public let shippingSummaryViewHidden: Signal<Bool, Never>
public let showErrorBannerWithMessage: Signal<String, Never>

public var inputs: ConfirmDetailsViewModelInputs { return self }
public var outputs: ConfirmDetailsViewModelOutputs { return self }
Expand Down
75 changes: 75 additions & 0 deletions Library/ViewModels/ConfirmDetailsViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ final class ConfirmDetailsViewModelTests: TestCase {

private let createCheckoutSuccess = TestObserver<PostCampaignCheckoutData, Never>()

private let goToLoginSignup = TestObserver<(LoginIntent, Project, Reward?), Never>()

private let localPickupViewHidden = TestObserver<Bool, Never>()
private let pledgeAmountViewHidden = TestObserver<Bool, Never>()
private let shippingLocationViewHidden = TestObserver<Bool, Never>()
Expand Down Expand Up @@ -57,6 +59,8 @@ final class ConfirmDetailsViewModelTests: TestCase {

self.vm.outputs.createCheckoutSuccess.observe(self.createCheckoutSuccess.observer)

self.vm.outputs.goToLoginSignup.observe(self.goToLoginSignup.observer)

self.vm.outputs.localPickupViewHidden.observe(self.localPickupViewHidden.observer)
self.vm.outputs.pledgeAmountViewHidden.observe(self.pledgeAmountViewHidden.observer)
self.vm.outputs.shippingLocationViewHidden.observe(self.shippingLocationViewHidden.observer)
Expand All @@ -65,6 +69,77 @@ final class ConfirmDetailsViewModelTests: TestCase {
self.vm.outputs.showErrorBannerWithMessage.observe(self.showErrorBannerWithMessage.observer)
}

// MARK: - Login/Signup

func testGoToLoginSignup_emitsWhenLoggedOut_createCheckoutSuccessDoesNotEmit() {
withEnvironment(currentUser: nil) {
let data = PledgeViewData(
project: Project.template,
rewards: [Reward.template],
selectedQuantities: [Reward.template.id: 1],
selectedLocationId: nil,
refTag: .projectPage,
context: .latePledge
)

self.vm.inputs.configure(with: data)
self.vm.inputs.viewDidLoad()

self.vm.inputs.continueCTATapped()

self.goToLoginSignup.assertDidEmitValue()
self.createCheckoutSuccess.assertDidNotEmitValue()
}
}

func testGoToLoginSignup_doesNotEmitWhenLoggedIn_createCheckoutIsSuccessful() {
let expectedId = "Q2hlY2tvdXQtMTk4MzM2NjQ2"
let createCheckout = CreateCheckoutEnvelope.Checkout(id: expectedId, paymentUrl: "paymentUrl")
let mockService = MockService(
createCheckoutResult:
Result.success(CreateCheckoutEnvelope(checkout: createCheckout))
)

withEnvironment(apiService: mockService, currentUser: User.template) {
let data = PledgeViewData(
project: Project.template,
rewards: [Reward.template],
selectedQuantities: [Reward.template.id: 1],
selectedLocationId: nil,
refTag: .projectPage,
context: .latePledge
)

self.vm.inputs.configure(with: data)
self.vm.inputs.viewDidLoad()

self.vm.inputs.userSessionStarted()

let expectedShipping = PledgeShippingSummaryViewData(
locationName: "Los Angeles, CA",
omitUSCurrencyCode: true,
projectCountry: .us,
total: 3
)
self.vm.inputs.shippingRuleSelected(
ShippingRule(cost: expectedShipping.total, id: nil, location: .losAngeles)
)

let expectedBonus = 5.0
self.vm.inputs.pledgeAmountViewControllerDidUpdate(
with: (amount: expectedBonus, min: 0, max: 100, isValid: true)
)

self.vm.inputs.continueCTATapped()

self.scheduler.run()

self.goToLoginSignup.assertDidNotEmitValue()

self.createCheckoutSuccess.assertDidEmitValue()
}
}

func testPledgeContext_LoggedIn() {
let mockService = MockService(serverConfig: ServerConfig.staging)

Expand Down