Skip to content

Commit

Permalink
Merge branch 'main' into showSurveyInActivity
Browse files Browse the repository at this point in the history
  • Loading branch information
ifosli authored May 14, 2024
2 parents 307ffa3 + 52f6afe commit a3efb55
Show file tree
Hide file tree
Showing 12 changed files with 222 additions and 42 deletions.
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())
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 @@ -2117,6 +2119,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 @@ -6194,6 +6198,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 @@ -6346,6 +6351,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 @@ -7766,6 +7772,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 @@ -8018,6 +8025,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."
)

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

0 comments on commit a3efb55

Please sign in to comment.