From 52f6afecb9344b511e81a80f304d737b1a0c4640 Mon Sep 17 00:00:00 2001 From: Scott Clampet <110618242+scottkicks@users.noreply.github.com> Date: Tue, 14 May 2024 10:26:11 -0500 Subject: [PATCH] [MBL-1393] Moves Stripe Intent Logic Into It's Own Service (#2050) * Create StripeIntentService that creates a new payment intent * update existing .createPaymentIntent call sites * Add setup intent func to StripeIntentServiceType * update existing createStripeSetupIntent call sites * convert service to class so that it can be tested more easily * tests for StripeSetupIntentService * clean up tests * inject stripe intent service only where needed instead of putting it in our already large AppEnvironment * remove tests * update MockStripeIntentService to use apiService * This way it's a better representation of the actual service * inject MockStripeIntentService into PledgePaymentMethodsViewModelTests and update tests * inject MockStripeIntentService into PaymentMethodSettingsViewModelTests and update tests * inject MockStripeIntentService into PostCampaignCheckoutViewModelTests and update tests * make assertion comment clearer * cleanup from initial testing pattern --- .../PaymentMethodSettingsViewController.swift | 3 +- .../PledgePaymentMethodsViewController.swift | 3 +- .../PostCampaignCheckoutViewController.swift | 3 +- Kickstarter.xcodeproj/project.pbxproj | 8 +++ Library/StripeIntentService.swift | 59 +++++++++++++++++++ .../TestHelpers/MockStripeIntentService.swift | 46 +++++++++++++++ .../PaymentMethodSettingsViewModel.swift | 11 ++-- .../PaymentMethodSettingsViewModelTests.swift | 15 ++++- .../PledgePaymentMethodsViewModel.swift | 28 +++++---- .../PledgePaymentMethodsViewModelTests.swift | 25 +++++++- .../PostCampaignCheckoutViewModel.swift | 31 +++++----- .../PostCampaignCheckoutViewModelTests.swift | 32 +++++++++- 12 files changed, 222 insertions(+), 42 deletions(-) create mode 100644 Library/StripeIntentService.swift create mode 100644 Library/TestHelpers/MockStripeIntentService.swift diff --git a/Kickstarter-iOS/Features/PaymentMethods/Controller/PaymentMethodSettingsViewController.swift b/Kickstarter-iOS/Features/PaymentMethods/Controller/PaymentMethodSettingsViewController.swift index d320c8394e..d85ec29971 100644 --- a/Kickstarter-iOS/Features/PaymentMethods/Controller/PaymentMethodSettingsViewController.swift +++ b/Kickstarter-iOS/Features/PaymentMethods/Controller/PaymentMethodSettingsViewController.swift @@ -13,7 +13,8 @@ protocol PaymentMethodSettingsViewControllerDelegate: AnyObject { internal final class PaymentMethodSettingsViewController: UIViewController, MessageBannerViewControllerPresenting { private let dataSource = PaymentMethodsDataSource() - private let viewModel: PaymentMethodsViewModelType = PaymentMethodSettingsViewModel() + private let viewModel: PaymentMethodsViewModelType = + PaymentMethodSettingsViewModel(stripeIntentService: StripeIntentService()) private var paymentSheetFlowController: PaymentSheet.FlowController? private weak var cancellationDelegate: PaymentMethodSettingsViewControllerDelegate? @IBOutlet private var tableView: UITableView! diff --git a/Kickstarter-iOS/Features/PledgePaymentMethods/Controller/PledgePaymentMethodsViewController.swift b/Kickstarter-iOS/Features/PledgePaymentMethods/Controller/PledgePaymentMethodsViewController.swift index b7f9baecd0..f563cdf8f3 100644 --- a/Kickstarter-iOS/Features/PledgePaymentMethods/Controller/PledgePaymentMethodsViewController.swift +++ b/Kickstarter-iOS/Features/PledgePaymentMethods/Controller/PledgePaymentMethodsViewController.swift @@ -29,7 +29,8 @@ final class PledgePaymentMethodsViewController: UIViewController { internal weak var delegate: PledgePaymentMethodsViewControllerDelegate? internal weak var messageDisplayingDelegate: PledgeViewControllerMessageDisplaying? - private let viewModel: PledgePaymentMethodsViewModelType = PledgePaymentMethodsViewModel() + private let viewModel: PledgePaymentMethodsViewModelType = + PledgePaymentMethodsViewModel(stripeIntentService: StripeIntentService()) private var paymentSheetFlowController: PaymentSheet.FlowController? // MARK: - Lifecycle diff --git a/Kickstarter-iOS/Features/PledgeView/Controllers/PostCampaignCheckoutViewController.swift b/Kickstarter-iOS/Features/PledgeView/Controllers/PostCampaignCheckoutViewController.swift index 0342f96f39..0e7ae2191e 100644 --- a/Kickstarter-iOS/Features/PledgeView/Controllers/PostCampaignCheckoutViewController.swift +++ b/Kickstarter-iOS/Features/PledgeView/Controllers/PostCampaignCheckoutViewController.swift @@ -61,7 +61,8 @@ final class PostCampaignCheckoutViewController: UIViewController, |> \.translatesAutoresizingMaskIntoConstraints .~ false }() - private let viewModel: PostCampaignCheckoutViewModelType = PostCampaignCheckoutViewModel() + private let viewModel: PostCampaignCheckoutViewModelType = + PostCampaignCheckoutViewModel(stripeIntentService: StripeIntentService()) // MARK: - Lifecycle diff --git a/Kickstarter.xcodeproj/project.pbxproj b/Kickstarter.xcodeproj/project.pbxproj index ae824ded2c..a4bae250f0 100644 --- a/Kickstarter.xcodeproj/project.pbxproj +++ b/Kickstarter.xcodeproj/project.pbxproj @@ -510,6 +510,8 @@ 609309952A6055A5004297AF /* TriggerThirdPartyEvent.graphql in Resources */ = {isa = PBXBuildFile; fileRef = 609309942A6055A5004297AF /* TriggerThirdPartyEvent.graphql */; }; 60A3ED252B85361E008E1BA8 /* RewardsCollectionViewHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60A3ED232B853618008E1BA8 /* RewardsCollectionViewHeaderView.swift */; }; 60A80F532BD7F9A00052A829 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 60A80F522BD7F9A00052A829 /* PrivacyInfo.xcprivacy */; }; + 60A80F552BE003A60052A829 /* StripeIntentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60A80F542BE003A60052A829 /* StripeIntentService.swift */; }; + 60A80F622BEA86A80052A829 /* MockStripeIntentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60A80F612BEA86A80052A829 /* MockStripeIntentService.swift */; }; 60AE9F062ABB897900FB3A96 /* ReportProjectInfoListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60AE9F022ABB822300FB3A96 /* ReportProjectInfoListItem.swift */; }; 60C996E42ABCA5E5006BE4F4 /* ReportProjectLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60C996E32ABCA5E5006BE4F4 /* ReportProjectLabelView.swift */; }; 60C996E62ABCC002006BE4F4 /* ReportProjectCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60C996E52ABCC002006BE4F4 /* ReportProjectCell.swift */; }; @@ -2120,6 +2122,8 @@ 609309942A6055A5004297AF /* TriggerThirdPartyEvent.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = TriggerThirdPartyEvent.graphql; sourceTree = ""; }; 60A3ED232B853618008E1BA8 /* RewardsCollectionViewHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RewardsCollectionViewHeaderView.swift; sourceTree = ""; }; 60A80F522BD7F9A00052A829 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 60A80F542BE003A60052A829 /* StripeIntentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StripeIntentService.swift; sourceTree = ""; }; + 60A80F612BEA86A80052A829 /* MockStripeIntentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStripeIntentService.swift; sourceTree = ""; }; 60AE9F022ABB822300FB3A96 /* ReportProjectInfoListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportProjectInfoListItem.swift; sourceTree = ""; }; 60C996E32ABCA5E5006BE4F4 /* ReportProjectLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportProjectLabelView.swift; sourceTree = ""; }; 60C996E52ABCC002006BE4F4 /* ReportProjectCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportProjectCell.swift; sourceTree = ""; }; @@ -6201,6 +6205,7 @@ 8016BFE71D0F582D00067956 /* String+Whitespace.swift */, A7ED1F221E830FDC00BFFA01 /* String+WhitespaceTests.swift */, A79BF81E1D11F6AF004C0445 /* Strings.swift */, + 60A80F542BE003A60052A829 /* StripeIntentService.swift */, 8AD48632235939AF00A1463E /* StripeTypes.swift */, 6080DA3E2AB366550088EF3D /* SwiftUI+Extensions */, 94F4A95926125C8C000C21F9 /* TimeInterval+ISO8601Date.swift */, @@ -6353,6 +6358,7 @@ D033E2C122A05B4800464E43 /* MockApplication.swift */, 6008633E29BF750700B87B39 /* MockAppTrackingTransparency.swift */, A7ED1F451E831BA200BFFA01 /* MockBundle.swift */, + 60A80F612BEA86A80052A829 /* MockStripeIntentService.swift */, 1611EF6823B275700051CDCC /* MockUUID.swift */, 1923770928DA2AE300F68635 /* Stripe+PaymentMethod.swift */, A7ED1F461E831BA200BFFA01 /* TestCase.swift */, @@ -7775,6 +7781,7 @@ 4791BDE6271762E600DFE5D5 /* ProjectFAQsCellViewModel.swift in Sources */, 0634C2F927CFEEC2003A6D6E /* ExternalSourceViewElementCellViewModel.swift in Sources */, 94BA16E426698C8B0034CC3F /* CommentTableViewFooterViewModel.swift in Sources */, + 60A80F552BE003A60052A829 /* StripeIntentService.swift in Sources */, 77D19FF5240813240058FC8E /* NavigationController.swift in Sources */, 8A8099F422E2142C00373E66 /* RewardCardContainerViewModel.swift in Sources */, A75511631C8642C3005355CF /* LocalizedString.swift in Sources */, @@ -8028,6 +8035,7 @@ D04AACA8218BB72100CF713E /* FindFriendsCellViewModelTests.swift in Sources */, D6BD66BD23CCF7B6008694BB /* EquatableHelpersTests.swift in Sources */, D04AACA6218BB72100CF713E /* ChangePasswordViewModelTests.swift in Sources */, + 60A80F622BEA86A80052A829 /* MockStripeIntentService.swift in Sources */, A7ED20041E831C5C00BFFA01 /* UpdateDraftViewModelTests.swift in Sources */, A7ED1F4B1E831BA200BFFA01 /* TestCase.swift in Sources */, A7ED1FBE1E831C5C00BFFA01 /* ProjectNotificationCellViewModelTests.swift in Sources */, diff --git a/Library/StripeIntentService.swift b/Library/StripeIntentService.swift new file mode 100644 index 0000000000..1168d7d664 --- /dev/null +++ b/Library/StripeIntentService.swift @@ -0,0 +1,59 @@ +import Foundation +import KsApi +import ReactiveSwift + +public protocol StripeIntentServiceType { + func createPaymentIntent(for projectId: String, pledgeTotal: Double) + -> SignalProducer + func createSetupIntent( + for projectId: String?, + context: GraphAPI.StripeIntentContextTypes + ) -> SignalProducer +} + +/// This is the module that creates either a Stripe payment intent or a setup intent. +public class StripeIntentService: StripeIntentServiceType { + public init() {} + + /** + Returns a signal producer that emits a `PaymentIntentEnvelope` or `ErrorEnvelope` value representing whether or not a payment intent was created and returned successfully. + The returned producer emits once and completes. + + - parameter for: The types to register that we will request permissions for. + - parameters: + - projectId: The GraphID of a project + - pledgeTotal: The final pledge total of the current pledge + */ + + public func createPaymentIntent( + for projectId: String, + pledgeTotal: Double + ) -> SignalProducer { + AppEnvironment.current.apiService + .createPaymentIntentInput(input: CreatePaymentIntentInput( + projectId: projectId, + amountDollars: String(format: "%.2f", pledgeTotal), + digitalMarketingAttributed: nil + )) + } + + /** + Returns a signal producer that contains a `ClientSecretEnvelope` or `ErrorEnvelope` representing whether or not a setup intent was created and returned successfully. + The returned producer emits once and completes. + + - parameter for: The types to register that we will request permissions for. + - parameters: + - projectId: The optional GraphID of a project + - context: The context for which this intent is being created + */ + + public func createSetupIntent( + for projectId: String?, + context: GraphAPI.StripeIntentContextTypes + ) -> SignalProducer { + AppEnvironment.current.apiService + .createStripeSetupIntent( + input: CreateSetupIntentInput(projectId: projectId, context: context) + ) + } +} diff --git a/Library/TestHelpers/MockStripeIntentService.swift b/Library/TestHelpers/MockStripeIntentService.swift new file mode 100644 index 0000000000..644e22ac69 --- /dev/null +++ b/Library/TestHelpers/MockStripeIntentService.swift @@ -0,0 +1,46 @@ +@testable import KsApi +@testable import Library +import ReactiveSwift + +public class MockStripeIntentService: StripeIntentServiceType { + public private(set) var setupIntentRequests: Int = 0 + public private(set) var paymentIntentRequests: Int = 0 + + public init() {} + + public func createPaymentIntent( + for projectId: String, + pledgeTotal: Double + ) -> SignalProducer { + assert( + AppEnvironment.current.apiService as? MockService != nil, + "AppEnvironment.current.apiService should be a MockService when used in test." + ) + + self.paymentIntentRequests += 1 + + return AppEnvironment.current.apiService + .createPaymentIntentInput(input: CreatePaymentIntentInput( + projectId: projectId, + amountDollars: String(format: "%.2f", pledgeTotal), + digitalMarketingAttributed: nil + )) + } + + public func createSetupIntent( + for projectId: String?, + context: GraphAPI.StripeIntentContextTypes + ) -> SignalProducer { + assert( + AppEnvironment.current.apiService as? MockService != nil, + "AppEnvironment.current.apiService should be a MockService when used in test." + ) + + self.setupIntentRequests += 1 + + return AppEnvironment.current.apiService + .createStripeSetupIntent( + input: CreateSetupIntentInput(projectId: projectId, context: context) + ) + } +} diff --git a/Library/ViewModels/PaymentMethodSettingsViewModel.swift b/Library/ViewModels/PaymentMethodSettingsViewModel.swift index da2cde545a..b455a5c9f4 100644 --- a/Library/ViewModels/PaymentMethodSettingsViewModel.swift +++ b/Library/ViewModels/PaymentMethodSettingsViewModel.swift @@ -38,7 +38,11 @@ public protocol PaymentMethodsViewModelType { public final class PaymentMethodSettingsViewModel: PaymentMethodsViewModelType, PaymentMethodSettingsViewModelInputs, PaymentMethodSettingsViewModelOutputs { - public init() { + let stripeIntentService: StripeIntentServiceType + + public init(stripeIntentService: StripeIntentServiceType) { + self.stripeIntentService = stripeIntentService + self.reloadData = self.viewDidLoadProperty.signal let paymentMethodsEvent = Signal.merge( @@ -147,10 +151,7 @@ public final class PaymentMethodSettingsViewModel: PaymentMethodsViewModelType, .switchMap { SignalProducer(value: paymentSheetEnabled) } .filter(isTrue) .switchMap { _ -> SignalProducer.Event, Never> in - AppEnvironment.current.apiService - .createStripeSetupIntent( - input: CreateSetupIntentInput(projectId: nil, context: .profileSettings) - ) + stripeIntentService.createSetupIntent(for: nil, context: .profileSettings) .ksr_debounce(.seconds(1), on: AppEnvironment.current.scheduler) .ksr_delay(AppEnvironment.current.apiDelayInterval, on: AppEnvironment.current.scheduler) .switchMap { envelope -> SignalProducer in diff --git a/Library/ViewModels/PaymentMethodSettingsViewModelTests.swift b/Library/ViewModels/PaymentMethodSettingsViewModelTests.swift index 7eb7c23a0a..dcd4515cad 100644 --- a/Library/ViewModels/PaymentMethodSettingsViewModelTests.swift +++ b/Library/ViewModels/PaymentMethodSettingsViewModelTests.swift @@ -8,7 +8,9 @@ import ReactiveSwift import XCTest internal final class PaymentMethodSettingsViewModelTests: TestCase { - private let vm = PaymentMethodSettingsViewModel() + private var vm = PaymentMethodSettingsViewModel(stripeIntentService: MockStripeIntentService()) + private let mockStripeIntentService = MockStripeIntentService() + private let userTemplate = GraphUser.template |> \.storedCards .~ UserCreditCards.template private let cancelLoadingState = TestObserver() private let editButtonIsEnabled = TestObserver() @@ -25,6 +27,8 @@ internal final class PaymentMethodSettingsViewModelTests: TestCase { internal override func setUp() { super.setUp() + self.vm = PaymentMethodSettingsViewModel(stripeIntentService: self.mockStripeIntentService) + self.vm.outputs.cancelAddNewCardLoadingState.observe(self.cancelLoadingState.observer) self.vm.outputs.editButtonIsEnabled.observe(self.editButtonIsEnabled.observer) self.vm.outputs.editButtonTitle.observe(self.editButtonTitle.observer) @@ -96,6 +100,9 @@ internal final class PaymentMethodSettingsViewModelTests: TestCase { self.errorLoadingPaymentMethodsOrSetupIntent .assertValue(ErrorEnvelope.couldNotParseJSON.localizedDescription) + + XCTAssertEqual(self.mockStripeIntentService.setupIntentRequests, 1) + XCTAssertEqual(self.mockStripeIntentService.paymentIntentRequests, 0) } } @@ -287,6 +294,9 @@ internal final class PaymentMethodSettingsViewModelTests: TestCase { self.scheduler.advance(by: .seconds(1)) self.goToPaymentSheet.assertValueCount(1) + + XCTAssertEqual(self.mockStripeIntentService.setupIntentRequests, 1) + XCTAssertEqual(self.mockStripeIntentService.paymentIntentRequests, 0) } } @@ -309,6 +319,9 @@ internal final class PaymentMethodSettingsViewModelTests: TestCase { self.scheduler.advance(by: .seconds(1)) self.tableViewIsEditing.assertValues([false, true, false]) + + XCTAssertEqual(self.mockStripeIntentService.setupIntentRequests, 1) + XCTAssertEqual(self.mockStripeIntentService.paymentIntentRequests, 0) } } diff --git a/Library/ViewModels/PledgePaymentMethodsViewModel.swift b/Library/ViewModels/PledgePaymentMethodsViewModel.swift index 4a6eacc619..2f5f88eaa0 100644 --- a/Library/ViewModels/PledgePaymentMethodsViewModel.swift +++ b/Library/ViewModels/PledgePaymentMethodsViewModel.swift @@ -68,7 +68,11 @@ public protocol PledgePaymentMethodsViewModelType { public final class PledgePaymentMethodsViewModel: PledgePaymentMethodsViewModelType, PledgePaymentMethodsViewModelInputs, PledgePaymentMethodsViewModelOutputs { - public init() { + let stripeIntentService: StripeIntentServiceType + + public init(stripeIntentService: StripeIntentServiceType) { + self.stripeIntentService = stripeIntentService + let configureWithValue = Signal.combineLatest( self.viewDidLoadProperty.signal, self.configureWithValueProperty.signal.skipNil() @@ -317,24 +321,22 @@ public final class PledgePaymentMethodsViewModel: PledgePaymentMethodsViewModelT let setupIntentContext = pledgeContext == .latePledge ? GraphAPI.StripeIntentContextTypes.postCampaignCheckout : GraphAPI.StripeIntentContextTypes.crowdfundingCheckout - clientSecretSignal = AppEnvironment.current.apiService - .createStripeSetupIntent( - input: CreateSetupIntentInput(projectId: project.graphID, context: setupIntentContext) - ) - .map { $0.clientSecret } + clientSecretSignal = stripeIntentService.createSetupIntent( + for: project.graphID, + context: setupIntentContext + ) + .map { $0.clientSecret } case .paymentIntent: assert( !pledgeTotal.isNaN, "Pledge total must be set when using a PaymentIntent. Did you accidentally get here via PledgeViewModel instead of PostCampaignCheckoutViewModel?" ) - clientSecretSignal = AppEnvironment.current.apiService - .createPaymentIntentInput(input: CreatePaymentIntentInput( - projectId: project.graphID, - amountDollars: String(format: "%.2f", pledgeTotal), - digitalMarketingAttributed: nil - )) - .map { $0.clientSecret } + clientSecretSignal = stripeIntentService.createPaymentIntent( + for: project.graphID, + pledgeTotal: pledgeTotal + ) + .map { $0.clientSecret } } return clientSecretSignal diff --git a/Library/ViewModels/PledgePaymentMethodsViewModelTests.swift b/Library/ViewModels/PledgePaymentMethodsViewModelTests.swift index b178e7efb6..0e59eba4c1 100644 --- a/Library/ViewModels/PledgePaymentMethodsViewModelTests.swift +++ b/Library/ViewModels/PledgePaymentMethodsViewModelTests.swift @@ -8,7 +8,10 @@ import ReactiveExtensions_TestHelpers import XCTest final class PledgePaymentMethodsViewModelTests: TestCase { - private let vm: PledgePaymentMethodsViewModelType = PledgePaymentMethodsViewModel() + private var vm: PledgePaymentMethodsViewModelType = + PledgePaymentMethodsViewModel(stripeIntentService: MockStripeIntentService()) + private var mockStripeIntentService = MockStripeIntentService() + private let userTemplate = GraphUser.template |> \.storedCards .~ UserCreditCards.template private let goToAddStripeCardIntent = TestObserver() @@ -33,6 +36,8 @@ final class PledgePaymentMethodsViewModelTests: TestCase { override func setUp() { super.setUp() + self.vm = PledgePaymentMethodsViewModel(stripeIntentService: self.mockStripeIntentService) + self.vm.outputs.notifyDelegateCreditCardSelected .observe(self.notifyDelegateCreditCardSelected.observer) self.vm.outputs.notifyDelegateLoadPaymentMethodsError @@ -1018,6 +1023,8 @@ final class PledgePaymentMethodsViewModelTests: TestCase { self.scheduler.run() XCTAssertEqual(self.goToAddStripeCardIntent.values.count, 1) + XCTAssertEqual(self.mockStripeIntentService.setupIntentRequests, 1) + XCTAssertEqual(self.mockStripeIntentService.paymentIntentRequests, 0) } } @@ -1053,6 +1060,8 @@ final class PledgePaymentMethodsViewModelTests: TestCase { self.scheduler.run() XCTAssertEqual(self.goToAddStripeCardIntent.values.count, 1) + XCTAssertEqual(self.mockStripeIntentService.setupIntentRequests, 1) + XCTAssertEqual(self.mockStripeIntentService.paymentIntentRequests, 0) } } @@ -1088,6 +1097,8 @@ final class PledgePaymentMethodsViewModelTests: TestCase { self.scheduler.run() XCTAssertEqual(self.goToAddStripeCardIntent.values.count, 1) + XCTAssertEqual(self.mockStripeIntentService.setupIntentRequests, 1) + XCTAssertEqual(self.mockStripeIntentService.paymentIntentRequests, 0) } } @@ -1123,6 +1134,8 @@ final class PledgePaymentMethodsViewModelTests: TestCase { self.scheduler.run() XCTAssertEqual(self.goToAddStripeCardIntent.values.count, 1) + XCTAssertEqual(self.mockStripeIntentService.setupIntentRequests, 1) + XCTAssertEqual(self.mockStripeIntentService.paymentIntentRequests, 0) } } @@ -1158,6 +1171,8 @@ final class PledgePaymentMethodsViewModelTests: TestCase { self.scheduler.run() XCTAssertEqual(self.goToAddStripeCardIntent.values.count, 1) + XCTAssertEqual(self.mockStripeIntentService.setupIntentRequests, 1) + XCTAssertEqual(self.mockStripeIntentService.paymentIntentRequests, 0) } } @@ -1285,6 +1300,9 @@ final class PledgePaymentMethodsViewModelTests: TestCase { self.scheduler.run() self.addNewCardLoadingState.assertValues([true, false]) + + XCTAssertEqual(self.mockStripeIntentService.setupIntentRequests, 1) + XCTAssertEqual(self.mockStripeIntentService.paymentIntentRequests, 0) } } @@ -1316,6 +1334,9 @@ final class PledgePaymentMethodsViewModelTests: TestCase { self.scheduler.run() self.addNewCardLoadingState.assertValues([false, true, true, true]) + + XCTAssertEqual(self.mockStripeIntentService.setupIntentRequests, 0) + XCTAssertEqual(self.mockStripeIntentService.paymentIntentRequests, 0) } } @@ -1407,6 +1428,8 @@ final class PledgePaymentMethodsViewModelTests: TestCase { } XCTAssertTrue(allowedDelayedPaymentMethods) + XCTAssertEqual(self.mockStripeIntentService.setupIntentRequests, 1) + XCTAssertEqual(self.mockStripeIntentService.paymentIntentRequests, 0) } } diff --git a/Library/ViewModels/PostCampaignCheckoutViewModel.swift b/Library/ViewModels/PostCampaignCheckoutViewModel.swift index de9d0eb654..3c08213c4a 100644 --- a/Library/ViewModels/PostCampaignCheckoutViewModel.swift +++ b/Library/ViewModels/PostCampaignCheckoutViewModel.swift @@ -74,7 +74,11 @@ public protocol PostCampaignCheckoutViewModelType { public class PostCampaignCheckoutViewModel: PostCampaignCheckoutViewModelType, PostCampaignCheckoutViewModelInputs, PostCampaignCheckoutViewModelOutputs { - public init() { + let stripeIntentService: StripeIntentServiceType + + public init(stripeIntentService: StripeIntentServiceType) { + self.stripeIntentService = stripeIntentService + let initialData = Signal.combineLatest( self.configureWithDataProperty.signal, self.viewDidLoadProperty.signal @@ -164,13 +168,11 @@ public class PostCampaignCheckoutViewModel: PostCampaignCheckoutViewModelType, 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 - )) - .materialize() + return stripeIntentService.createPaymentIntent( + for: projectId, + pledgeTotal: pledgeTotal + ) + .materialize() } let paymentIntentClientSecretForExistingCards = newPaymentIntentForExistingCards.values() @@ -275,14 +277,11 @@ public class PostCampaignCheckoutViewModel: PostCampaignCheckoutViewModelType, 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() + return stripeIntentService.createPaymentIntent( + for: projectId, + pledgeTotal: pledgeTotal + ) + .materialize() } let newPaymentIntentForApplePayError: Signal = createPaymentIntentForApplePay diff --git a/Library/ViewModels/PostCampaignCheckoutViewModelTests.swift b/Library/ViewModels/PostCampaignCheckoutViewModelTests.swift index dee7f68560..2629048ffe 100644 --- a/Library/ViewModels/PostCampaignCheckoutViewModelTests.swift +++ b/Library/ViewModels/PostCampaignCheckoutViewModelTests.swift @@ -6,8 +6,10 @@ import ReactiveExtensions_TestHelpers import XCTest final class PostCampaignCheckoutViewModelTests: TestCase { - fileprivate let vm = PostCampaignCheckoutViewModel() - fileprivate let goToApplePayPaymentAuthorization = TestObserver< + private var vm = PostCampaignCheckoutViewModel(stripeIntentService: MockStripeIntentService()) + private let mockStripeIntentService = MockStripeIntentService() + + private let goToApplePayPaymentAuthorization = TestObserver< PostCampaignPaymentAuthorizationData, Never >() @@ -31,6 +33,9 @@ final class PostCampaignCheckoutViewModelTests: TestCase { override func setUp() { super.setUp() + + self.vm = PostCampaignCheckoutViewModel(stripeIntentService: self.mockStripeIntentService) + self.vm.goToApplePayPaymentAuthorization.observe(self.goToApplePayPaymentAuthorization.observer) self.vm.checkoutComplete.observe(self.checkoutComplete.observer) self.vm.processingViewIsHidden.observe(self.processingViewIsHidden.observer) @@ -179,6 +184,9 @@ final class PostCampaignCheckoutViewModelTests: TestCase { XCTAssertEqual(output.bonus, 0) XCTAssertEqual(output.shipping, 0) XCTAssertEqual(output.total, 5) + + XCTAssertEqual(self.mockStripeIntentService.paymentIntentRequests, 1) + XCTAssertEqual(self.mockStripeIntentService.setupIntentRequests, 0) } } @@ -221,6 +229,9 @@ final class PostCampaignCheckoutViewModelTests: TestCase { XCTAssertEqual(output.bonus, 0) XCTAssertEqual(output.shipping, 0) XCTAssertEqual(output.total, 18) + + XCTAssertEqual(self.mockStripeIntentService.paymentIntentRequests, 1) + XCTAssertEqual(self.mockStripeIntentService.setupIntentRequests, 0) } } @@ -268,6 +279,9 @@ final class PostCampaignCheckoutViewModelTests: TestCase { XCTAssertEqual(output.bonus, 0) XCTAssertEqual(output.shipping, 72) XCTAssertEqual(output.total, 90) + + XCTAssertEqual(self.mockStripeIntentService.paymentIntentRequests, 1) + XCTAssertEqual(self.mockStripeIntentService.setupIntentRequests, 0) } } @@ -317,6 +331,9 @@ final class PostCampaignCheckoutViewModelTests: TestCase { XCTAssertEqual(output.bonus, 5) XCTAssertEqual(output.shipping, 72) XCTAssertEqual(output.total, 133) + + XCTAssertEqual(self.mockStripeIntentService.paymentIntentRequests, 1) + XCTAssertEqual(self.mockStripeIntentService.setupIntentRequests, 0) } } @@ -349,6 +366,9 @@ final class PostCampaignCheckoutViewModelTests: TestCase { let output = self.goToApplePayPaymentAuthorization.lastValue! XCTAssertEqual(output.paymentIntent, "foo") + + XCTAssertEqual(self.mockStripeIntentService.paymentIntentRequests, 1) + XCTAssertEqual(self.mockStripeIntentService.setupIntentRequests, 0) } } @@ -419,6 +439,9 @@ final class PostCampaignCheckoutViewModelTests: TestCase { self.checkoutComplete.assertDidEmitValue() self.processingViewIsHidden.assertLastValue(true) + + XCTAssertEqual(self.mockStripeIntentService.paymentIntentRequests, 1) + XCTAssertEqual(self.mockStripeIntentService.setupIntentRequests, 0) } } @@ -455,7 +478,10 @@ final class PostCampaignCheckoutViewModelTests: TestCase { self.scheduler.run() self.goToApplePayPaymentAuthorization.assertDidNotEmitValue() - self.processingViewIsHidden.assertValues([false, true]) + self.processingViewIsHidden.assertValues([true, false]) + + XCTAssertEqual(self.mockStripeIntentService.setupIntentRequests, 0) + XCTAssertEqual(self.mockStripeIntentService.setupIntentRequests, 0) } }