Skip to content

Commit

Permalink
Extract common payment method operations (stripe#3559)
Browse files Browse the repository at this point in the history
## Summary
- Extracts some common code between PaymentSheet and PaymentSheetFlow
controller w.r.t to modify payment methods.
- Improves overall testing for these code paths.

## Motivation
- Reuse for vertical mode

## Testing
- Existing tests
- New tests

## Changelog
N/A
  • Loading branch information
porter-stripe authored May 3, 2024
1 parent e139562 commit a45b584
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@
5C047CFEA91C1A04EAEC0CFF /* StripeUICore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 35BFC60D1945087E74B6BD89 /* StripeUICore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5C0D1B932954D0EF3F3A679F /* ManualEntryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95BDADF560DB0B1ED175EF50 /* ManualEntryButton.swift */; };
5E00512CDFBC1C93781E20AB /* PaymentSheetLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88DBDEE23A856CE8D3B49861 /* PaymentSheetLoader.swift */; };
6103F2BC2BE45990002D67F8 /* SavedPaymentMethodManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6103F2BB2BE45990002D67F8 /* SavedPaymentMethodManager.swift */; };
614A8AE72BE53C6900E8688B /* SavedPaymentMethodManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6103F2BD2BE53737002D67F8 /* SavedPaymentMethodManagerTest.swift */; };
6151DDC02B14FDCF00ED4F7E /* UpdateCardViewControllerSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6151DDBF2B14FDCF00ED4F7E /* UpdateCardViewControllerSnapshotTests.swift */; };
61C0D3B8C63EB4558AB74A7E /* StripePayments.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A1C7CFA5C9C1A8A73CFA1C0 /* StripePayments.framework */; };
623C2D9F87929D6DA9C09E23 /* STPCameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39B31D0B890A4F8E4819B15 /* STPCameraView.swift */; };
Expand Down Expand Up @@ -420,6 +422,8 @@
5EFCD0B8D104E175C9EFF7A0 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/Localizable.strings"; sourceTree = "<group>"; };
5F884F7A5FF4FC6D0E24E6FC /* PaymentSheetErrorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentSheetErrorTest.swift; sourceTree = "<group>"; };
5FD715F32B61017198E9F952 /* BottomSheetTransitioningDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetTransitioningDelegate.swift; sourceTree = "<group>"; };
6103F2BB2BE45990002D67F8 /* SavedPaymentMethodManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedPaymentMethodManager.swift; sourceTree = "<group>"; };
6103F2BD2BE53737002D67F8 /* SavedPaymentMethodManagerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedPaymentMethodManagerTest.swift; sourceTree = "<group>"; };
6139AA50F07A1E2AC7E9827F /* AUBECSMandate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AUBECSMandate.swift; sourceTree = "<group>"; };
6151DDBF2B14FDCF00ED4F7E /* UpdateCardViewControllerSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCardViewControllerSnapshotTests.swift; sourceTree = "<group>"; };
617C44F9338DE2E93E318291 /* PayWithLinkWebController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayWithLinkWebController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -885,6 +889,7 @@
4BEFE8C0CFEAE73F9FD736D3 /* STPStringUtils.swift */,
4DA1B1B7662E177725062922 /* StripePaymentSheet+Exports.swift */,
70ED08B0F303B7C2334602C3 /* StripePaymentSheetBundleLocator.swift */,
6103F2BB2BE45990002D67F8 /* SavedPaymentMethodManager.swift */,
);
path = Helpers;
sourceTree = "<group>";
Expand Down Expand Up @@ -1282,6 +1287,7 @@
0C53358C028E528F0FC626A2 /* CustomerSheet */,
989C2E3E03E42DA64A2FAE0D /* CustomerSheetSnapshotTests.swift */,
31699A822BE183D40048677F /* DownloadManagerTest.swift */,
6103F2BD2BE53737002D67F8 /* SavedPaymentMethodManagerTest.swift */,
73FB30705EC36BD0868904A2 /* Elements+TestHelpers.swift */,
64C8F350CDB5A29F62E86592 /* FlowControllerStateTests.swift */,
990304EF35A0EE37DCE20D5B /* IntentStatusPollerTest.swift */,
Expand Down Expand Up @@ -1576,6 +1582,7 @@
96B31ABDA593F9C7FC3DBF79 /* PaymentSheetPaymentMethodTypeTest.swift in Sources */,
041E3F2DFDFD8FA7D3353CDB /* PaymentSheetSnapshotTests.swift in Sources */,
1330B53140DE10F641A82099 /* PaymentSheetViewControllerSnapshotTests.swift in Sources */,
614A8AE72BE53C6900E8688B /* SavedPaymentMethodManagerTest.swift in Sources */,
47AD56A9889DF5EFBBA9CEFB /* PollingViewTests.swift in Sources */,
EEC6283DB21D04AD5B77F9D2 /* STPApplePayContext+PaymentSheetTest.swift in Sources */,
714FBCA75296C291FDB3B345 /* STPCardBrandChoiceTest.swift in Sources */,
Expand Down Expand Up @@ -1724,6 +1731,7 @@
5E00512CDFBC1C93781E20AB /* PaymentSheetLoader.swift in Sources */,
E5571A970EB9DFC4B690636F /* STPAnalyticsClient+PaymentSheet.swift in Sources */,
0B142FE21B861925B513143D /* STPApplePayContext+PaymentSheet.swift in Sources */,
6103F2BC2BE45990002D67F8 /* SavedPaymentMethodManager.swift in Sources */,
ED75C8F47475E4BE5D496C93 /* STPPaymentIntentShippingDetailsParams+PaymentSheet.swift in Sources */,
4313D6635F10EC460D2ED21E /* SavedPaymentMethodCollectionView.swift in Sources */,
CF2AD2C7F761C46AE559E563 /* SavedPaymentOptionsViewController.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// SavedPaymentMethodManager.swift
// StripePaymentSheet
//
// Created by Nick Porter on 5/2/24.
//

import Foundation
@_spi(STP) import StripePayments

/// Provides shared implementations of common operations for managing saved payment methods in PaymentSheet
final class SavedPaymentMethodManager {

let configuration: PaymentSheet.Configuration

init(configuration: PaymentSheet.Configuration) {
self.configuration = configuration
}

func update(paymentMethod: STPPaymentMethod,
with updateParams: STPPaymentMethodUpdateParams,
using ephemeralKey: String) async throws -> STPPaymentMethod {
return try await configuration.apiClient.updatePaymentMethod(with: paymentMethod.stripeId,
paymentMethodUpdateParams: updateParams,
ephemeralKeySecret: ephemeralKey)
}

func detach(paymentMethod: STPPaymentMethod, using ephemeralKey: String) {
if let customerAccessProvider = configuration.customer?.customerAccessProvider,
case .customerSession = customerAccessProvider,
paymentMethod.type == .card,
let customerId = configuration.customer?.id {
configuration.apiClient.detachPaymentMethodRemoveDuplicates(
paymentMethod.stripeId,
customerId: customerId,
fromCustomerUsing: ephemeralKey
) { (_) in
// no-op
}
} else {
configuration.apiClient.detachPaymentMethod(
paymentMethod.stripeId,
fromCustomerUsing: ephemeralKey
) { (_) in
// no-op
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ class PaymentSheetFlowControllerViewController: UIViewController {
private let isLinkEnabled: Bool
private var isHackyLinkButtonSelected: Bool = false

private lazy var savedPaymentMethodManager: SavedPaymentMethodManager = {
return SavedPaymentMethodManager(configuration: configuration)
}()

// MARK: - Views
private let addPaymentMethodViewController: AddPaymentMethodViewController
private let savedPaymentOptionsViewController: SavedPaymentOptionsViewController
Expand Down Expand Up @@ -536,9 +540,9 @@ extension PaymentSheetFlowControllerViewController: SavedPaymentOptionsViewContr
throw PaymentSheetError.unknown(debugDescription: "Failed to read ephemeral key secret")
}

return try await configuration.apiClient.updatePaymentMethod(with: paymentMethod.stripeId,
paymentMethodUpdateParams: updateParams,
ephemeralKeySecret: ephemeralKey)
return try await savedPaymentMethodManager.update(paymentMethod: paymentMethod,
with: updateParams,
using: ephemeralKey)
}

func didUpdateSelection(
Expand Down Expand Up @@ -580,24 +584,8 @@ extension PaymentSheetFlowControllerViewController: SavedPaymentOptionsViewContr
else {
return
}
if let customerAccessProvider = configuration.customer?.customerAccessProvider,
case .customerSession = customerAccessProvider,
paymentMethod.type == .card,
let customerId = configuration.customer?.id {
configuration.apiClient.detachPaymentMethodRemoveDuplicates(
paymentMethod.stripeId,
customerId: customerId,
fromCustomerUsing: ephemeralKey
) { (_) in
// no-op
}
} else {
configuration.apiClient.detachPaymentMethod(
paymentMethod.stripeId, fromCustomerUsing: ephemeralKey
) { (_) in
// no-op
}
}

savedPaymentMethodManager.detach(paymentMethod: paymentMethod, using: ephemeralKey)

if !savedPaymentOptionsViewController.canEditPaymentMethods {
savedPaymentOptionsViewController.isRemovingPaymentMethods = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ class PaymentSheetViewController: UIViewController {
private var shouldAnimateBuyButton: Bool = true
private(set) var isDismissable: Bool = true

private lazy var savedPaymentMethodManager: SavedPaymentMethodManager = {
return SavedPaymentMethodManager(configuration: configuration)
}()

// MARK: - Views

private lazy var addPaymentMethodViewController: AddPaymentMethodViewController = {
Expand Down Expand Up @@ -615,9 +619,9 @@ extension PaymentSheetViewController: SavedPaymentOptionsViewControllerDelegate
throw PaymentSheetError.unknown(debugDescription: "Failed to read ephemeral key secret")
}

return try await configuration.apiClient.updatePaymentMethod(with: paymentMethod.stripeId,
paymentMethodUpdateParams: updateParams,
ephemeralKeySecret: ephemeralKey)
return try await savedPaymentMethodManager.update(paymentMethod: paymentMethod,
with: updateParams,
using: ephemeralKey)
}

func didUpdateSelection(
Expand Down Expand Up @@ -647,25 +651,7 @@ extension PaymentSheetViewController: SavedPaymentOptionsViewControllerDelegate
return
}

if let customerAccessProvider = configuration.customer?.customerAccessProvider,
case .customerSession = customerAccessProvider,
paymentMethod.type == .card,
let customerId = configuration.customer?.id {
configuration.apiClient.detachPaymentMethodRemoveDuplicates(
paymentMethod.stripeId,
customerId: customerId,
fromCustomerUsing: ephemeralKey
) { (_) in
// no-op
}
} else {
configuration.apiClient.detachPaymentMethod(
paymentMethod.stripeId,
fromCustomerUsing: ephemeralKey
) { (_) in
// no-op
}
}
savedPaymentMethodManager.detach(paymentMethod: paymentMethod, using: ephemeralKey)

if !savedPaymentOptionsViewController.canEditPaymentMethods {
savedPaymentOptionsViewController.isRemovingPaymentMethods = false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
//
// SavedPaymentMethodManagerTest.swift
// StripePaymentSheet
//
// Created by Nick Porter on 5/3/24.
//

import Foundation
import OHHTTPStubs
import OHHTTPStubsSwift
import StripeCoreTestUtils
@_spi(STP)@_spi(CustomerSessionBetaAccess)@testable import StripePaymentSheet
import XCTest

final class SavedPaymentMethodManagerTests: XCTestCase {

let ephemeralKey = "test-eph-key"
let paymentMethod = STPPaymentMethod.stubbedPaymentMethod()

var configuration: PaymentSheet.Configuration {
let apiClient = APIStubbedTestCase.stubbedAPIClient()
var configuration = PaymentSheet.Configuration()
configuration.apiClient = apiClient
return configuration
}

// MARK: Update tests
func testUpdatePaymentMethod() async throws {
let paymentMethod = STPPaymentMethod.stubbedPaymentMethod()
let expectation = stubUpdatePaymentMethod(paymentMethod: paymentMethod,
ephemeralKey: ephemeralKey)

let sut = SavedPaymentMethodManager(configuration: configuration)
let updatedPaymentMethod = try await sut.update(paymentMethod: paymentMethod,
with: STPPaymentMethodUpdateParams(),
using: ephemeralKey)

// Verify the response was valid
XCTAssertEqual("pm_123card", updatedPaymentMethod.stripeId)
await fulfillment(of: [expectation], timeout: 5.0)
}

// MARK: Detach tests
func testDetachPaymentMethod_noCustomer() {
let expectation = stubDetachPaymentMethod(paymentMethod: STPPaymentMethod.stubbedPaymentMethod(),
ephemeralKey: ephemeralKey)

let sut = SavedPaymentMethodManager(configuration: configuration)
sut.detach(paymentMethod: paymentMethod, using: ephemeralKey)

wait(for: [expectation], timeout: 5.0)
}

func testDetachPaymentMethod_withLegacyCustomer() {
var configuration = configuration
configuration.customer = .init(id: "cus_test123", ephemeralKeySecret: ephemeralKey)

let expectation = stubDetachPaymentMethod(paymentMethod: STPPaymentMethod.stubbedPaymentMethod(),
ephemeralKey: ephemeralKey)

let sut = SavedPaymentMethodManager(configuration: configuration)
sut.detach(paymentMethod: paymentMethod, using: ephemeralKey)

wait(for: [expectation], timeout: 5.0)
}

func testDetachPaymentMethod_withCustomerSession() {
var configuration = configuration
configuration.customer = .init(id: "cus_test123", customerSessionClientSecret: "session_123")

let listPaymentMethodsExpectation = stubListPaymentMethods(customerId: configuration.customer!.id,
ephemeralKey: ephemeralKey)

let detachExpectation = stubDetachPaymentMethod(paymentMethod: STPPaymentMethod.stubbedPaymentMethod(),
ephemeralKey: ephemeralKey)

let sut = SavedPaymentMethodManager(configuration: configuration)
sut.detach(paymentMethod: paymentMethod, using: ephemeralKey)

wait(for: [listPaymentMethodsExpectation, detachExpectation], timeout: 5.0)
}
}

extension SavedPaymentMethodManagerTests {
// MARK: HTTP Stubs

func stubRequest(urlContains: String,
ephemeralKey: String,
httpMethod: String,
responseObject: Any) -> XCTestExpectation {
let exp = expectation(description: "Request \(httpMethod) \(urlContains)")

stub { urlRequest in
return urlRequest.url?.absoluteString.contains(urlContains) ?? false
&& urlRequest.allHTTPHeaderFields?["Authorization"] == "Bearer \(ephemeralKey)"
&& urlRequest.httpMethod == httpMethod
} response: { _ in
DispatchQueue.main.async {
exp.fulfill()
}
return HTTPStubsResponse(jsonObject: responseObject,
statusCode: 200,
headers: nil)
}

return exp
}

func stubUpdatePaymentMethod(paymentMethod: STPPaymentMethod, ephemeralKey: String) -> XCTestExpectation {
return stubRequest(urlContains: "/payment_methods/\(paymentMethod.stripeId)",
ephemeralKey: ephemeralKey,
httpMethod: "POST",
responseObject: STPPaymentMethod.paymentMethodJson)
}

func stubDetachPaymentMethod(paymentMethod: STPPaymentMethod, ephemeralKey: String) -> XCTestExpectation {
return stubRequest(urlContains: "/payment_methods/\(paymentMethod.stripeId)/detach",
ephemeralKey: ephemeralKey,
httpMethod: "POST",
responseObject: [:])
}

func stubListPaymentMethods(customerId: String, ephemeralKey: String) -> XCTestExpectation {
return stubRequest(urlContains: "/payment_methods?customer=\(customerId)&type=card",
ephemeralKey: ephemeralKey,
httpMethod: "GET",
responseObject: STPPaymentMethod.paymentMethodsJson)
}
}

extension STPPaymentMethod {

static var paymentMethodJson: [String: Any] {
return [
"id": "pm_123card",
"type": "card",
"card": [
"last4": "4242",
"brand": "visa",
],
]
}

static var paymentMethodsJson: [String: Any] = [
"data": [
[
"id": "pm_123card",
"type": "card",
"card": [
"last4": "4242",
"brand": "visa",
],
],
[
"id": "pm_123mastercard",
"type": "card",
"card": [
"last4": "5555",
"brand": "mastercard",
],
],
[
"id": "pm_123amex",
"type": "card",
"card": [
"last4": "6789",
"brand": "amex",
],
],
],
]

/// Creates a fake payment method for tests
static func stubbedPaymentMethod() -> STPPaymentMethod {
return STPPaymentMethod.decodedObject(fromAPIResponse: paymentMethodJson)!
}
}

0 comments on commit a45b584

Please sign in to comment.