Skip to content

Commit

Permalink
Send CBC preferred network in confirm call (#2965)
Browse files Browse the repository at this point in the history
## Summary
- We now send card[networks][preferred] the network call when confirming
a CBC intent
- When no card brand is selected we omit `networks` from card params
entirley.

## Motivation

https://docs.google.com/document/d/1T12LgVTkEv-LaDtQO3lGgNDt3nV_mwrUOJWgfUmTa9U/edit

## Testing
- New unit tests
- Manual

## Changelog
See diff
  • Loading branch information
porter-stripe authored Oct 4, 2023
1 parent 834866c commit b5ea455
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 10 deletions.
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
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
}
}
}

0 comments on commit b5ea455

Please sign in to comment.