diff --git a/CHANGELOG.md b/CHANGELOG.md index 9837a2335bb..3be7c240995 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ -## X.X.X +## x.x.x x-x-x +### PaymentSheet +* [Fixed] Fixed an issue when advancing from the country dropdown that prevented user's' from typing in their postal code. ([#2936](https://github.com/stripe/stripe-ios/issues/2936)) + ### PaymentsUI * [Fixed] An issue with `STPPaymentCardTextField`, where the `paymentCardTextFieldDidChange` delegate method wasn't being called after deleting an empty sub field. diff --git a/Stripe/StripeiOSTests/STPCardBrandTest.swift b/Stripe/StripeiOSTests/STPCardBrandTest.swift index 7abfc73dab2..4584e9ee4ab 100644 --- a/Stripe/StripeiOSTests/STPCardBrandTest.swift +++ b/Stripe/StripeiOSTests/STPCardBrandTest.swift @@ -49,4 +49,45 @@ class STPCardBrandTest: XCTestCase { } } } + + func testApiValueFromBrand() { + let brands = [ + STPCardBrand.visa, + STPCardBrand.amex, + STPCardBrand.mastercard, + STPCardBrand.discover, + STPCardBrand.JCB, + STPCardBrand.dinersClub, + STPCardBrand.unionPay, + STPCardBrand.cartesBancaires, + STPCardBrand.unknown, + ] + + for brand in brands { + let string = STPCardBrandUtilities.apiValue(from: brand) + + switch brand { + case .amex: + XCTAssertEqual(string, "american_express") + case .dinersClub: + XCTAssertEqual(string, "diners_club") + case .discover: + XCTAssertEqual(string, "discover") + case .JCB: + XCTAssertEqual(string, "jcb") + case .mastercard: + XCTAssertEqual(string, "mastercard") + case .unionPay: + XCTAssertEqual(string, "unionpay") + case .visa: + XCTAssertEqual(string, "visa") + case .cartesBancaires: + XCTAssertEqual(string, "cartes_bancaires") + case .unknown: + XCTAssertEqual(string, "unknown") + @unknown default: + break + } + } + } } diff --git a/Stripe/StripeiOSTests/STPPaymentMethodCardParamsTest.swift b/Stripe/StripeiOSTests/STPPaymentMethodCardParamsTest.swift index fcdadf81953..b684214e142 100644 --- a/Stripe/StripeiOSTests/STPPaymentMethodCardParamsTest.swift +++ b/Stripe/StripeiOSTests/STPPaymentMethodCardParamsTest.swift @@ -19,11 +19,13 @@ class STPPaymentMethodCardParamsTest: XCTestCase { params1.cvc = "123" params1.expYear = 22 params1.expMonth = 12 + params1.networks = .init(preferred: "visa") let params2 = STPPaymentMethodCardParams() params2.number = "4242424242424242" params2.cvc = "123" params2.expYear = 22 params2.expMonth = 12 + params2.networks = .init(preferred: "visa") XCTAssertEqual(params1, params2) params1.additionalAPIParameters["test"] = "bla" XCTAssertNotEqual(params1, params2) @@ -65,4 +67,33 @@ class STPPaymentMethodCardParamsTest: XCTestCase { XCTAssertEqual(cardParams.addressCountry, "US") XCTAssertEqual(cardParams.addressZip, "12345") } + + func testPropertyNamesToFormFieldsMapping() { + // Test for STPPaymentMethodCardParams + let cardParams = STPPaymentMethodCardParams() + + let cardParamsExpectedMapping = [ + "number": "number", + "expMonth": "exp_month", + "expYear": "exp_year", + "cvc": "cvc", + "token": "token", + "networks": "networks", + ] + + let cardParamsMapping = type(of: cardParams).propertyNamesToFormFieldNamesMapping() + + XCTAssertEqual(cardParamsMapping, cardParamsExpectedMapping) + + // Test for STPPaymentMethodCardNetworksParams + let networksParams = STPPaymentMethodCardNetworksParams() + + let networksParamsExpectedMapping = [ + "preferred": "preferred", + ] + + let networksParamsMapping = type(of: networksParams).propertyNamesToFormFieldNamesMapping() + + XCTAssertEqual(networksParamsMapping, networksParamsExpectedMapping) + } } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/CardSectionWithScanner/CardSectionWithScannerElement.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/CardSectionWithScanner/CardSectionWithScannerElement.swift index e53b4e1a3ff..efabc25dad0 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/CardSectionWithScanner/CardSectionWithScannerElement.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/Elements/CardSectionWithScanner/CardSectionWithScannerElement.swift @@ -72,12 +72,20 @@ final class CardSection: ContainerElement { return params } : nil - var cardBrandDropDown: DropdownFieldElement? + var cardBrandDropDown: PaymentMethodElementWrapper? if cardBrandChoiceEligible { - cardBrandDropDown = DropdownFieldElement.makeCardBrandDropdown(theme: theme) + cardBrandDropDown = PaymentMethodElementWrapper(DropdownFieldElement.makeCardBrandDropdown(theme: theme)) { field, params in + guard let cardBrandCaseIndex = Int(field.selectedItem.rawData), + let cardBrand: STPCardBrand = .init(rawValue: cardBrandCaseIndex) else { + return params + } + + cardParams(for: params).networks = STPPaymentMethodCardNetworksParams(preferred: STPCardBrandUtilities.apiValue(from: cardBrand)) + return params + } } let panElement = PaymentMethodElementWrapper(TextFieldElement.PANConfiguration(defaultValue: defaultValues.pan, - cardBrandDropDown: cardBrandDropDown), theme: theme) { field, params in + cardBrandDropDown: cardBrandDropDown?.element), theme: theme) { field, params in cardParams(for: params).number = field.text return params } @@ -108,7 +116,7 @@ final class CardSection: ContainerElement { let allSubElements: [Element?] = [ nameElement, - panElement, + panElement, SectionElement.HiddenElement(cardBrandDropDown), SectionElement.MultiElementRow([expiryElement, cvcElement], theme: theme), ] let subElements = allSubElements.compactMap { $0 } @@ -120,7 +128,7 @@ final class CardSection: ContainerElement { self.nameElement = nameElement?.element self.panElement = panElement.element - self.cardBrandDropDown = cardBrandDropDown + self.cardBrandDropDown = cardBrandDropDown?.element self.cvcElement = cvcElement.element self.expiryElement = expiryElement.element cardSection.delegate = self diff --git a/StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodCardNetworksParams.swift b/StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodCardNetworksParams.swift new file mode 100644 index 00000000000..a9942435c2b --- /dev/null +++ b/StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodCardNetworksParams.swift @@ -0,0 +1,50 @@ +// +// STPPaymentMethodCardNetworksParams.swift +// StripePayments +// +// Created by Nick Porter on 9/28/23. +// + +import Foundation + +public class STPPaymentMethodCardNetworksParams: NSObject, STPFormEncodable { + + @objc public var additionalAPIParameters: [AnyHashable: Any] = [:] + + /// The network that your user selected for this payment + /// method. This must reflect an explicit user choice. If your user didn't + /// make a selection, then pass `null`. + @objc public var preferred: String? + + @objc public convenience init(preferred: String?) { + self.init() + self.preferred = preferred + } + + // MARK: - Description + /// :nodoc: + @objc public override var description: String { + let props = [ + // Object + String(format: "%@: %p", NSStringFromClass(STPPaymentMethodCardNetworksParams.self), self), + // Preferred + "preferred = \(preferred ?? "")", + ] + + return "<\(props.joined(separator: "; "))>" + } + + // MARK: - STPFormEncodable + + @objc + public class func rootObjectName() -> String? { + return "networks" + } + + @objc + public static func propertyNamesToFormFieldNamesMapping() -> [String: String] { + return [ + NSStringFromSelector(#selector(getter: preferred)): "preferred", + ] + } +} diff --git a/StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodCardParams.swift b/StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodCardParams.swift index 5ddcc41efc3..38165ed14cd 100644 --- a/StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodCardParams.swift +++ b/StripePayments/StripePayments/Source/API Bindings/Models/PaymentMethods/Types/STPPaymentMethodCardParams.swift @@ -39,8 +39,10 @@ public class STPPaymentMethodCardParams: NSObject, STPFormEncodable { @objc public var token: String? /// Card security code. It is highly recommended to always include this value. @objc public var cvc: String? - /// The last 4 digits of the card. + /// Information about the networks to use with this payment method. + @objc public var networks: STPPaymentMethodCardNetworksParams? + /// The last 4 digits of the card. @objc public var last4: String? { if number != nil && (number?.count ?? 0) >= 4 { return (number as NSString?)?.substring(from: (number?.count ?? 0) - 4) ?? "" @@ -60,6 +62,7 @@ public class STPPaymentMethodCardParams: NSObject, STPFormEncodable { "expMonth = \(expMonth ?? 0)", "expYear = \(expYear ?? 0)", "cvc = \(((cvc) != nil ? "" : nil) ?? "")", + "networks = \(networks?.description ?? "")", // Token "token = \(token ?? "")", ] @@ -82,6 +85,7 @@ public class STPPaymentMethodCardParams: NSObject, STPFormEncodable { NSStringFromSelector(#selector(getter: expYear)): "exp_year", NSStringFromSelector(#selector(getter: cvc)): "cvc", NSStringFromSelector(#selector(getter: token)): "token", + NSStringFromSelector(#selector(getter: networks)): "networks", ] } @@ -93,6 +97,7 @@ public class STPPaymentMethodCardParams: NSObject, STPFormEncodable { copyCardParams.expMonth = expMonth copyCardParams.expYear = expYear copyCardParams.cvc = cvc + copyCardParams.networks = networks return copyCardParams } @@ -119,6 +124,6 @@ public class STPPaymentMethodCardParams: NSObject, STPFormEncodable { } return number == other?.number && expMonth == other?.expMonth && expYear == other?.expYear - && cvc == other?.cvc && token == other?.token + && cvc == other?.cvc && token == other?.token && networks?.preferred == other?.networks?.preferred } } diff --git a/StripePayments/StripePayments/Source/API Bindings/Models/STPCardBrand.swift b/StripePayments/StripePayments/Source/API Bindings/Models/STPCardBrand.swift index e592a3ec2a5..227e3b66935 100644 --- a/StripePayments/StripePayments/Source/API Bindings/Models/STPCardBrand.swift +++ b/StripePayments/StripePayments/Source/API Bindings/Models/STPCardBrand.swift @@ -73,4 +73,31 @@ public class STPCardBrandUtilities: NSObject { } } + /// Returns brand API string value from given card brand. + /// + /// - Parameter brand: The `STPCardBrand` to transform into a string. + /// - Returns: A `String` representing the card brand. This could be "visa", + @objc(apiValueFromCardBrand:) public static func apiValue(from brand: STPCardBrand) -> String { + switch brand { + case .visa: + return "visa" + case .amex: + return "american_express" + case .mastercard: + return "mastercard" + case .discover: + return "discover" + case .JCB: + return "jcb" + case .dinersClub: + return "diners_club" + case .unionPay: + return "unionpay" + case .cartesBancaires: + return "cartes_bancaires" + default: + return "unknown" + } + } + } diff --git a/StripeUICore/StripeUICore/Source/Elements/DropdownFieldElement.swift b/StripeUICore/StripeUICore/Source/Elements/DropdownFieldElement.swift index 16c0a8eac56..2c3e5d06c64 100644 --- a/StripeUICore/StripeUICore/Source/Elements/DropdownFieldElement.swift +++ b/StripeUICore/StripeUICore/Source/Elements/DropdownFieldElement.swift @@ -254,7 +254,10 @@ extension DropdownFieldElement: PickerFieldViewDelegate { didUpdate?(selectedIndex) } previouslySelectedIndex = selectedIndex - delegate?.continueToNextField(element: self) + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.delegate?.continueToNextField(element: self) + } } func didCancel(_ pickerFieldView: PickerFieldView) { diff --git a/StripeUICore/StripeUICore/Source/Elements/Section/SectionElement.swift b/StripeUICore/StripeUICore/Source/Elements/Section/SectionElement.swift index faa067a985a..834088e8580 100644 --- a/StripeUICore/StripeUICore/Source/Elements/Section/SectionElement.swift +++ b/StripeUICore/StripeUICore/Source/Elements/Section/SectionElement.swift @@ -31,7 +31,7 @@ import UIKit } var viewModel: SectionViewModel { return ViewModel( - views: elements.map({ $0.view }), + views: elements.filter { !($0.view is HiddenElement.HiddenView) }.map({ $0.view }), // filter out hidden views to prevent showing the separator title: title, errorText: errorText, subLabel: subLabel, @@ -102,3 +102,28 @@ extension SectionElement: ElementDelegate { delegate?.didUpdate(element: self) } } + +// MARK: HiddenElement + +extension SectionElement { + /// A simple container element where the element's view is hidden + /// Useful when an element is a part of a section but it's view is embeded into another element + /// E.g. card brand drop down embedded into the PAN textfield + @_spi(STP) public final class HiddenElement: ContainerElement { + final class HiddenView: UIView {} + + weak public var delegate: ElementDelegate? + public lazy var view: UIView = { + return HiddenView(frame: .zero) // Hide the element's view + }() + public let elements: [Element] + + public init?(_ element: Element?) { + guard let element = element else { + return nil + } + self.elements = [element] + element.delegate = self + } + } +}