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-1393] Moves Stripe Intent Logic Into It's Own Service #2050

Merged
merged 21 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bb14d01
Create StripeIntentService that creates a new payment intent
scottkicks Apr 29, 2024
72277fb
update existing .createPaymentIntent call sites
scottkicks Apr 29, 2024
58fd985
Add setup intent func to StripeIntentServiceType
scottkicks Apr 29, 2024
aacc8d7
update existing createStripeSetupIntent call sites
scottkicks Apr 29, 2024
e253233
Merge branch 'main' into scott/post-campaign-vm-refactor
scottkicks Apr 30, 2024
92e89c4
Merge branch 'main' into scott/post-campaign-vm-refactor
scottkicks May 1, 2024
c78839f
convert service to class so that it can be tested more easily
scottkicks May 7, 2024
0d334c9
tests for StripeSetupIntentService
scottkicks May 7, 2024
6f289e3
Merge branch 'main' into scott/post-campaign-vm-refactor
scottkicks May 7, 2024
0d90620
clean up tests
scottkicks May 7, 2024
8113c18
Merge branch 'main' into scott/post-campaign-vm-refactor
scottkicks May 9, 2024
5fc37eb
Merge branch 'main' into scott/post-campaign-vm-refactor
scottkicks May 13, 2024
13da714
inject stripe intent service only where needed instead of putting it …
scottkicks May 13, 2024
36dc310
remove tests
scottkicks May 13, 2024
09c1265
update MockStripeIntentService to use apiService
scottkicks May 13, 2024
4f432e8
inject MockStripeIntentService into PledgePaymentMethodsViewModelTest…
scottkicks May 13, 2024
0bd6f53
inject MockStripeIntentService into PaymentMethodSettingsViewModelTes…
scottkicks May 13, 2024
76708ed
inject MockStripeIntentService into PostCampaignCheckoutViewModelTest…
scottkicks May 13, 2024
627c785
make assertion comment clearer
scottkicks May 13, 2024
4832c8c
Merge branch 'main' into scott/post-campaign-vm-refactor
scottkicks May 13, 2024
d371d6e
cleanup from initial testing pattern
scottkicks May 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Copy link
Contributor

Choose a reason for hiding this comment

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

I like this pattern for dependencies!

private var paymentSheetFlowController: PaymentSheet.FlowController?
private weak var cancellationDelegate: PaymentMethodSettingsViewControllerDelegate?
@IBOutlet private var tableView: UITableView!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ final class PostCampaignCheckoutViewController: UIViewController,
|> \.translatesAutoresizingMaskIntoConstraints .~ false
}()

private let viewModel: PostCampaignCheckoutViewModelType = PostCampaignCheckoutViewModel()
private let viewModel: PostCampaignCheckoutViewModelType =
PostCampaignCheckoutViewModel(stripeIntentService: StripeIntentService())

// MARK: - Lifecycle

Expand Down
8 changes: 8 additions & 0 deletions Kickstarter.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -2120,6 +2122,8 @@
609309942A6055A5004297AF /* TriggerThirdPartyEvent.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = TriggerThirdPartyEvent.graphql; sourceTree = "<group>"; };
60A3ED232B853618008E1BA8 /* RewardsCollectionViewHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RewardsCollectionViewHeaderView.swift; sourceTree = "<group>"; };
60A80F522BD7F9A00052A829 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
60A80F542BE003A60052A829 /* StripeIntentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StripeIntentService.swift; sourceTree = "<group>"; };
60A80F612BEA86A80052A829 /* MockStripeIntentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStripeIntentService.swift; sourceTree = "<group>"; };
60AE9F022ABB822300FB3A96 /* ReportProjectInfoListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportProjectInfoListItem.swift; sourceTree = "<group>"; };
60C996E32ABCA5E5006BE4F4 /* ReportProjectLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportProjectLabelView.swift; sourceTree = "<group>"; };
60C996E52ABCC002006BE4F4 /* ReportProjectCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportProjectCell.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
59 changes: 59 additions & 0 deletions Library/StripeIntentService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Foundation
import KsApi
import ReactiveSwift

public protocol StripeIntentServiceType {
func createPaymentIntent(for projectId: String, pledgeTotal: Double)
-> SignalProducer<PaymentIntentEnvelope, ErrorEnvelope>
func createSetupIntent(
for projectId: String?,
context: GraphAPI.StripeIntentContextTypes
) -> SignalProducer<ClientSecretEnvelope, ErrorEnvelope>
}

/// 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<PaymentIntentEnvelope, ErrorEnvelope> {
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<ClientSecretEnvelope, ErrorEnvelope> {
AppEnvironment.current.apiService
.createStripeSetupIntent(
input: CreateSetupIntentInput(projectId: projectId, context: context)
)
}
}
46 changes: 46 additions & 0 deletions Library/TestHelpers/MockStripeIntentService.swift
Original file line number Diff line number Diff line change
@@ -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<PaymentIntentEnvelope, ErrorEnvelope> {
assert(
AppEnvironment.current.apiService as? MockService != nil,
"AppEnvironment.current.apiService should be a MockService when used in test."
)
Copy link
Contributor

Choose a reason for hiding this comment

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

πŸ‘ nice assert


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<ClientSecretEnvelope, ErrorEnvelope> {
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)
)
}
}
11 changes: 6 additions & 5 deletions Library/ViewModels/PaymentMethodSettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -147,10 +151,7 @@ public final class PaymentMethodSettingsViewModel: PaymentMethodsViewModelType,
.switchMap { SignalProducer(value: paymentSheetEnabled) }
.filter(isTrue)
.switchMap { _ -> SignalProducer<Signal<PaymentSheetSetupData, ErrorEnvelope>.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<PaymentSheetSetupData, ErrorEnvelope> in
Expand Down
15 changes: 14 additions & 1 deletion Library/ViewModels/PaymentMethodSettingsViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Void, Never>()
private let editButtonIsEnabled = TestObserver<Bool, Never>()
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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)
}
}

Expand All @@ -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)
}
}

Expand Down
28 changes: 15 additions & 13 deletions Library/ViewModels/PledgePaymentMethodsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down
Loading