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

Detach duplicated payment methods from PS & FlowController #2935

Draft
wants to merge 21 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ class CustomerSheetUITest: XCTestCase {
XCTAssertTrue(editButton.waitForExistence(timeout: 60.0))
editButton.tap()

removeFirstPaymentMethodInList()
removeFirstPaymentMethodInList(alertBodyText: "Remove Visa ending in 4242")

let doneButton = app.staticTexts["Done"]
XCTAssertTrue(doneButton.waitForExistence(timeout: 60.0))
Expand Down Expand Up @@ -312,19 +312,19 @@ class CustomerSheetUITest: XCTestCase {
settings
)

presentCSAndAddCardFrom(buttonLabel: "None")
presentCSAndAddCardFrom(buttonLabel: "••••4242")
presentCSAndAddCardFrom(buttonLabel: "None", cardNumber: "4242424242424242")
presentCSAndAddCardFrom(buttonLabel: "••••4242", cardNumber: "5555555555554444")

let selectButton = app.staticTexts["••••4242"]
let selectButton = app.staticTexts["••••4444"]
XCTAssertTrue(selectButton.waitForExistence(timeout: 60.0))
selectButton.tap()

let editButton = app.staticTexts["Edit"]
XCTAssertTrue(editButton.waitForExistence(timeout: 60.0))
editButton.tap()

removeFirstPaymentMethodInList()
removeFirstPaymentMethodInList()
removeFirstPaymentMethodInList(alertBodyText: "Remove Mastercard ending in 4444")
removeFirstPaymentMethodInList(alertBodyText: "Remove Visa ending in 4242")

let doneButton = app.staticTexts["Done"]
XCTAssertTrue(doneButton.waitForExistence(timeout: 60.0))
Expand All @@ -349,19 +349,19 @@ class CustomerSheetUITest: XCTestCase {
settings
)

presentCSAndAddCardFrom(buttonLabel: "None", tapAdd: false)
presentCSAndAddCardFrom(buttonLabel: "••••4242")
presentCSAndAddCardFrom(buttonLabel: "None", cardNumber: "4242424242424242", tapAdd: false)
presentCSAndAddCardFrom(buttonLabel: "••••4242", cardNumber: "5555555555554444")

let selectButton = app.staticTexts["••••4242"]
let selectButton = app.staticTexts["••••4444"]
XCTAssertTrue(selectButton.waitForExistence(timeout: 60.0))
selectButton.tap()

let editButton = app.staticTexts["Edit"]
XCTAssertTrue(editButton.waitForExistence(timeout: 60.0))
editButton.tap()

removeFirstPaymentMethodInList()
removeFirstPaymentMethodInList()
removeFirstPaymentMethodInList(alertBodyText: "Remove Mastercard ending in 4444")
removeFirstPaymentMethodInList(alertBodyText: "Remove Visa ending in 4242")

let doneButton = app.staticTexts["Done"]
XCTAssertTrue(doneButton.waitForExistence(timeout: 60.0))
Expand Down Expand Up @@ -661,7 +661,7 @@ class CustomerSheetUITest: XCTestCase {
XCTAssertTrue(last4Selectedlabel.waitForExistence(timeout: 10.0))
}

func presentCSAndAddCardFrom(buttonLabel: String, tapAdd: Bool = true) {
func presentCSAndAddCardFrom(buttonLabel: String, cardNumber: String? = nil, tapAdd: Bool = true) {
let selectButton = app.staticTexts[buttonLabel]
XCTAssertTrue(selectButton.waitForExistence(timeout: 60.0))
selectButton.tap()
Expand All @@ -670,20 +670,20 @@ class CustomerSheetUITest: XCTestCase {
app.staticTexts["+ Add"].tap()
}

try! fillCardData(app, postalEnabled: true)
try! fillCardData(app, cardNumber: cardNumber, postalEnabled: true)
app.buttons["Save"].tap()

let confirmButton = app.buttons["Confirm"]
XCTAssertTrue(confirmButton.waitForExistence(timeout: 60.0))
confirmButton.tap()

dismissAlertView(alertBody: "Success: ••••4242, selected", alertTitle: "Complete", buttonToTap: "OK")
let last4 = cardNumber?.suffix(4) ?? "4242"
dismissAlertView(alertBody: "Success: ••••\(last4), selected", alertTitle: "Complete", buttonToTap: "OK")
}

func removeFirstPaymentMethodInList() {
func removeFirstPaymentMethodInList(alertBodyText: String) {
let removeButton1 = app.buttons["Remove"].firstMatch
removeButton1.tap()
dismissAlertView(alertBody: "Remove Visa ending in 4242", alertTitle: "Remove Card", buttonToTap: "Remove")
dismissAlertView(alertBody: alertBodyText, alertTitle: "Remove Card", buttonToTap: "Remove")
}

func dismissAlertView(alertBody: String, alertTitle: String, buttonToTap: String) {
Expand Down
71 changes: 55 additions & 16 deletions Stripe/StripeiOSTests/CustomerAdapterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,51 @@ class CustomerAdapterTests: APIStubbedTestCase {
}
}

func stubElementsSessions(
key: CustomerEphemeralKey,
paymentMethodJSONs: [[AnyHashable: Any]],
expectedCount: Int,
apiClient: STPAPIClient
) {
let exp = expectation(description: "listPaymentMethod")
exp.expectedFulfillmentCount = expectedCount
stub { urlRequest in
return urlRequest.url?.absoluteString.contains("/elements/sessions") ?? false
&& urlRequest.url?.query?.contains("legacy_customer_ephemeral_key=\(key.ephemeralKeySecret)") ?? false
&& urlRequest.httpMethod == "GET"
} response: { _ in
let paymentMethodsJSON = """
{
"session_id": "123",
"payment_method_preference": {
"object": "payment_method_preference",
"country_code": "US",
"ordered_payment_method_types": [
"card"
],
},
"legacy_customer" : {
"payment_methods": [
]
}
}
"""
var pmList =
try! JSONSerialization.jsonObject(
with: paymentMethodsJSON.data(using: .utf8)!,
options: []
) as! [AnyHashable: Any]
var legacyCustomer = pmList["legacy_customer"] as! [AnyHashable: Any]
legacyCustomer["payment_methods"] = paymentMethodJSONs
pmList["legacy_customer"] = legacyCustomer
DispatchQueue.main.async {
// Fulfill after response is sent
exp.fulfill()
}
return HTTPStubsResponse(jsonObject: pmList, statusCode: 200, headers: nil)
}
}

func testGetOrCreateKeyErrorForwardedToFetchPMs() async throws {
let exp = expectation(description: "fetchPMs")
let expectedError = NSError(domain: "test", code: 123, userInfo: nil)
Expand Down Expand Up @@ -113,9 +158,8 @@ class CustomerAdapterTests: APIStubbedTestCase {
let expectedPaymentMethods = [STPFixtures.paymentMethod()]
let expectedPaymentMethodsJSON = [STPFixtures.paymentMethodJSON()]
let apiClient = stubbedAPIClient()
// Expect 1 call per PM: cards
stubListPaymentMethods(key: exampleKey, paymentMethodType: "card", paymentMethodJSONs: expectedPaymentMethodsJSON, expectedCount: 1, apiClient: apiClient)
stubListPaymentMethods(key: exampleKey, paymentMethodType: "us_bank_account", paymentMethodJSONs: [], expectedCount: 1, apiClient: apiClient)

stubElementsSessions(key: exampleKey, paymentMethodJSONs: expectedPaymentMethodsJSON, expectedCount: 1, apiClient: apiClient)

let ekm = MockEphemeralKeyEndpoint(exampleKey)
let sut = StripeCustomerAdapter(customerEphemeralKeyProvider: ekm.getEphemeralKey, apiClient: apiClient)
Expand All @@ -127,34 +171,29 @@ class CustomerAdapterTests: APIStubbedTestCase {
}

func testFetchPM_CardAndUSBankAccount() async throws {
let expectedPaymentMethods_card = [STPFixtures.paymentMethod()]
let expectedPaymentMethods_cardJSON = [STPFixtures.paymentMethodJSON()]

let expectedPaymentMethods_usbank = [STPFixtures.bankAccountPaymentMethod()]
let expectedPaymentMethods_usbankJSON = [STPFixtures.bankAccountPaymentMethodJSON()]

let expectedPaymentMethods_card_usBank = [STPFixtures.paymentMethod(), STPFixtures.bankAccountPaymentMethod()]
let expectedPaymentMethods_card_usBankJSON = [STPFixtures.paymentMethodJSON(), STPFixtures.bankAccountPaymentMethodJSON()]
let apiClient = stubbedAPIClient()
stubListPaymentMethods(key: exampleKey, paymentMethodType: "card", paymentMethodJSONs: expectedPaymentMethods_cardJSON, expectedCount: 1, apiClient: apiClient)
stubListPaymentMethods(key: exampleKey, paymentMethodType: "us_bank_account", paymentMethodJSONs: expectedPaymentMethods_usbankJSON, expectedCount: 1, apiClient: apiClient)

stubElementsSessions(key: exampleKey, paymentMethodJSONs: expectedPaymentMethods_card_usBankJSON, expectedCount: 1, apiClient: apiClient)

let ekm = MockEphemeralKeyEndpoint(exampleKey)
let sut = StripeCustomerAdapter(customerEphemeralKeyProvider: ekm.getEphemeralKey,
setupIntentClientSecretProvider: { return "si_" },
apiClient: apiClient)
let pms = try await sut.fetchPaymentMethods()
XCTAssertEqual(pms.count, 2)
XCTAssertEqual(pms[0].stripeId, expectedPaymentMethods_card[0].stripeId)
XCTAssertEqual(pms[1].stripeId, expectedPaymentMethods_usbank[0].stripeId)
XCTAssertEqual(pms[0].stripeId, expectedPaymentMethods_card_usBank[0].stripeId)
XCTAssertEqual(pms[1].stripeId, expectedPaymentMethods_card_usBank[1].stripeId)
await waitForExpectations(timeout: 2)
}

func testAttachPM() async throws {
let expectedPaymentMethods = [STPFixtures.paymentMethod()]
let expectedPaymentMethodsJSON = [STPFixtures.paymentMethodJSON()]
let apiClient = stubbedAPIClient()
// Expect 1 call per PM: cards
stubListPaymentMethods(key: exampleKey, paymentMethodType: "card", paymentMethodJSONs: expectedPaymentMethodsJSON, expectedCount: 1, apiClient: apiClient)
stubListPaymentMethods(key: exampleKey, paymentMethodType: "us_bank_account", paymentMethodJSONs: [], expectedCount: 1, apiClient: apiClient)

stubElementsSessions(key: exampleKey, paymentMethodJSONs: expectedPaymentMethodsJSON, expectedCount: 1, apiClient: apiClient)

let ekm = MockEphemeralKeyEndpoint(exampleKey)
let sut = StripeCustomerAdapter(customerEphemeralKeyProvider: ekm.getEphemeralKey, apiClient: apiClient)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ extension STPAPIClient {
typealias STPIntentCompletionBlock = ((Result<Intent, Error>) -> Void)

func retrievePaymentIntentWithPreferences(
withClientSecret secret: String
withClientSecret secret: String,
customerEphemeralKey: String? = nil
) async throws -> STPPaymentIntent {
guard STPPaymentIntentParams.isClientSecretValid(secret) && !publishableKeyIsUserKey else {
throw NSError.stp_clientSecretError()
Expand All @@ -25,6 +26,9 @@ extension STPAPIClient {
parameters["type"] = "payment_intent"
parameters["expand"] = ["payment_method_preference.payment_intent.payment_method"]
parameters["locale"] = Locale.current.toLanguageTag()
if let customerEphemeralKey {
parameters["legacy_customer_ephemeral_key"] = customerEphemeralKey
}

return try await APIRequest<STPPaymentIntent>.getWith(
self,
Expand All @@ -34,24 +38,34 @@ extension STPAPIClient {
}

func retrieveElementsSession(
withIntentConfig intentConfig: PaymentSheet.IntentConfiguration
withIntentConfig intentConfig: PaymentSheet.IntentConfiguration,
customerEphemeralKey: String? = nil
) async throws -> STPElementsSession {
let parameters = intentConfig.elementsSessionParameters(publishableKey: publishableKey)
var parameters = intentConfig.elementsSessionParameters(publishableKey: publishableKey)
if let customerEphemeralKey {
parameters["legacy_customer_ephemeral_key"] = customerEphemeralKey
}

return try await APIRequest<STPElementsSession>.getWith(
self,
endpoint: APIEndpointIntentWithPreferences,
parameters: parameters
)
}

func retrieveElementsSessionForCustomerSheet() async throws -> STPElementsSession {
func retrieveElementsSessionForCustomerSheet(
customerEphemeralKey: String? = nil
) async throws -> STPElementsSession {
var parameters: [String: Any] = [:]
parameters["type"] = "deferred_intent"
parameters["locale"] = Locale.current.toLanguageTag()

var deferredIntent = [String: Any]()
deferredIntent["mode"] = "setup"
parameters["deferred_intent"] = deferredIntent
if let customerEphemeralKey {
parameters["legacy_customer_ephemeral_key"] = customerEphemeralKey
}

return try await APIRequest<STPElementsSession>.getWith(
self,
Expand All @@ -61,7 +75,8 @@ extension STPAPIClient {
}

func retrieveSetupIntentWithPreferences(
withClientSecret secret: String
withClientSecret secret: String,
customerEphemeralKey: String? = nil
) async throws -> STPSetupIntent {
guard STPSetupIntentConfirmParams.isClientSecretValid(secret) && !publishableKeyIsUserKey else {
throw NSError.stp_clientSecretError()
Expand All @@ -72,6 +87,9 @@ extension STPAPIClient {
parameters["type"] = "setup_intent"
parameters["expand"] = ["payment_method_preference.setup_intent.payment_method"]
parameters["locale"] = Locale.current.toLanguageTag()
if let customerEphemeralKey {
parameters["legacy_customer_ephemeral_key"] = customerEphemeralKey
}

return try await APIRequest<STPSetupIntent>.getWith(
self,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// STPElementsCustomerInformation.swift
// StripePaymentSheet
//

import Foundation
@_spi(STP) import StripePayments

final class STPElementsCustomerInformation: NSObject {
let paymentMethods: [STPPaymentMethod]

let allResponseFields: [AnyHashable: Any]

/// :nodoc:
@objc public override var description: String {
let props: [String] = [
String(format: "%@: %p", NSStringFromClass(STPElementsCustomerInformation.self), self),
"paymentMethods = \(String(describing: paymentMethods))",
]

return "<\(props.joined(separator: "; "))>"
}

private init(
allResponseFields: [AnyHashable: Any],
paymentMethods: [STPPaymentMethod]
) {
self.allResponseFields = allResponseFields
self.paymentMethods = paymentMethods
super.init()
}
}

// MARK: - STPAPIResponseDecodable
extension STPElementsCustomerInformation: STPAPIResponseDecodable {
public static func decodedObject(fromAPIResponse response: [AnyHashable: Any]?) -> Self? {
guard let dict = response,
let paymentMethodPrefDict = dict["legacy_customer"] as? [AnyHashable: Any],
let savedPaymentMethods = paymentMethodPrefDict["payment_methods"] as? [[AnyHashable: Any]] else {
return nil
}
let paymentMethods = savedPaymentMethods.compactMap { STPPaymentMethod.decodedObject(fromAPIResponse: $0) }
return STPElementsCustomerInformation(
allResponseFields: dict,
paymentMethods: paymentMethods
) as? Self
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ final class STPElementsSession: NSObject {
/// A map describing payment method types form specs.
let paymentMethodSpecs: [[AnyHashable: Any]]?

/// An object containing Customer's saved payment methods information
let elementsCustomerInformation: STPElementsCustomerInformation?

/// An error associated with operations surrounding STPElementsCustomerInformation
let customerError: Error?

let allResponseFields: [AnyHashable: Any]

/// :nodoc:
Expand All @@ -45,6 +51,8 @@ final class STPElementsSession: NSObject {
"countryCode = \(String(describing: countryCode))",
"merchantCountryCode = \(String(describing: merchantCountryCode))",
"paymentMethodSpecs = \(String(describing: paymentMethodSpecs))",
"elementsCustomerInformation = \(String(describing: elementsCustomerInformation))",
"customerError = \(String(describing: customerError))",
]

return "<\(props.joined(separator: "; "))>"
Expand All @@ -58,7 +66,9 @@ final class STPElementsSession: NSObject {
countryCode: String?,
merchantCountryCode: String?,
linkSettings: LinkSettings?,
paymentMethodSpecs: [[AnyHashable: Any]]?
paymentMethodSpecs: [[AnyHashable: Any]]?,
elementsCustomerInformation: STPElementsCustomerInformation?,
customerError: Error?
) {
self.allResponseFields = allResponseFields
self.sessionID = sessionID
Expand All @@ -68,6 +78,8 @@ final class STPElementsSession: NSObject {
self.merchantCountryCode = merchantCountryCode
self.linkSettings = linkSettings
self.paymentMethodSpecs = paymentMethodSpecs
self.elementsCustomerInformation = elementsCustomerInformation
self.customerError = customerError
super.init()
}
}
Expand All @@ -94,7 +106,17 @@ extension STPElementsSession: STPAPIResponseDecodable {
linkSettings: LinkSettings.decodedObject(
fromAPIResponse: dict["link_settings"] as? [AnyHashable: Any]
),
paymentMethodSpecs: dict["payment_method_specs"] as? [[AnyHashable: Any]]
paymentMethodSpecs: dict["payment_method_specs"] as? [[AnyHashable: Any]],
elementsCustomerInformation: STPElementsCustomerInformation.decodedObject(fromAPIResponse: dict),
customerError: decodedCustomerErrorObject(fromAPIResponse: dict["customer_error"] as? [AnyHashable: Any])
) as? Self
}

public static func decodedCustomerErrorObject(fromAPIResponse response: [AnyHashable: Any]?) -> Error? {
guard let dict = response,
let errorMessage = dict["customer_error"] as? String else {
return nil
}
return PaymentSheetError.fetchSavedPaymentMethodsViaElementsFailure(message: errorMessage)
}
}