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

Send CBC preferred network in confirm call #2965

Merged
merged 11 commits into from
Oct 4, 2023
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
41 changes: 41 additions & 0 deletions Stripe/StripeiOSTests/STPCardBrandTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
}
31 changes: 31 additions & 0 deletions Stripe/StripeiOSTests/STPPaymentMethodCardParamsTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,20 @@ final class CardSection: ContainerElement {
return params
}
: nil
var cardBrandDropDown: DropdownFieldElement?
var cardBrandDropDown: PaymentMethodElementWrapper<DropdownFieldElement>?
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
}
Expand Down Expand Up @@ -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 }
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) ?? ""
Expand All @@ -60,6 +62,7 @@ public class STPPaymentMethodCardParams: NSObject, STPFormEncodable {
"expMonth = \(expMonth ?? 0)",
"expYear = \(expYear ?? 0)",
"cvc = \(((cvc) != nil ? "<redacted>" : nil) ?? "")",
"networks = \(networks?.description ?? "")",
// Token
"token = \(token ?? "")",
]
Expand All @@ -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",
]
}

Expand All @@ -93,6 +97,7 @@ public class STPPaymentMethodCardParams: NSObject, STPFormEncodable {
copyCardParams.expMonth = expMonth
copyCardParams.expYear = expYear
copyCardParams.cvc = cvc
copyCardParams.networks = networks
return copyCardParams
}

Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,10 @@ extension DropdownFieldElement: PickerFieldViewDelegate {
didUpdate?(selectedIndex)
}
previouslySelectedIndex = selectedIndex
delegate?.continueToNextField(element: self)
DispatchQueue.main.async { [weak self] in
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixes a bug I was seeing and #2936

guard let self = self else { return }
self.delegate?.continueToNextField(element: self)
}
}

func didCancel(_ pickerFieldView: PickerFieldView) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
}
}