Skip to content

Commit

Permalink
[Payment Element / LUXE] Defines iDEAL form in JSON instead of code (#…
Browse files Browse the repository at this point in the history
…876)

* Defines iDEAL form in JSON instead of code
- Adds spec for custom_dropdown
- Defines iDEAL form in JSON instead of code

* Fix complaint

* PR feedback

* Oops, fix form specs
  • Loading branch information
yuki-stripe authored Mar 18, 2022
1 parent a67196c commit 4b6d20e
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ class PaymentSheetUITest: XCTestCase {
app.buttons["new"].tap() // new customer
app.segmentedControls["apple_pay_selector"].buttons["off"].tap() // disable Apple Pay
app.segmentedControls["automatic_payment_methods_selector"].buttons["on"].tap() // enable automatic payment methods
reload()
reload(app)

var paymentMethodButton = app.buttons["Select Payment Method"]
paymentMethodButton.tap()
Expand Down Expand Up @@ -182,7 +182,7 @@ class PaymentSheetUITest: XCTestCase {
app.alerts.scrollViews.otherElements.buttons["OK"].tap()

// Reload w/ same customer
reload()
reload(app)
paymentMethodButton.tap()
try! fillCardData(app) // If the previous card was saved, we'll be on the 'saved pms' screen and this will fail
// toggle save this card on
Expand All @@ -200,7 +200,7 @@ class PaymentSheetUITest: XCTestCase {
app.alerts.scrollViews.otherElements.buttons["OK"].tap()

// Reload w/ same customer
reload()
reload(app)

// return to payment method selector
paymentMethodButton = app.staticTexts["••••4242"] // The card should be saved now
Expand Down Expand Up @@ -265,7 +265,7 @@ class PaymentSheetUITest: XCTestCase {
app.buttons["new"].tap() // new customer
app.segmentedControls["apple_pay_selector"].buttons["off"].tap() // disable Apple Pay
app.buttons["EUR"].tap() // EUR currency
reload()
reload(app)
app.buttons["Checkout (Complete)"].tap()
let payButton = app.buttons["Pay €50.99"]

Expand Down Expand Up @@ -301,7 +301,7 @@ class PaymentSheetUITest: XCTestCase {
app.staticTexts["PaymentSheet (test playground)"].tap()
app.buttons["new"].tap() // new customer
app.segmentedControls["apple_pay_selector"].buttons["off"].tap() // disable Apple Pay
reload()
reload(app)
app.buttons["Checkout (Complete)"].tap()
let payButton = app.buttons["Pay $50.99"]

Expand Down Expand Up @@ -334,7 +334,7 @@ class PaymentSheetUITest: XCTestCase {
app.buttons["new"].tap() // new customer
app.segmentedControls["automatic_payment_methods_selector"].buttons["off"].tap() // disable Apple Pay
app.segmentedControls["shipping_info_selector"].buttons["provided"].tap() // enable shipping info
reload()
reload(app)
app.buttons["Checkout (Complete)"].tap()
let payButton = app.buttons["Pay $50.99"]

Expand All @@ -353,40 +353,3 @@ class PaymentSheetUITest: XCTestCase {

}
}

// MARK: - Helpers

extension PaymentSheetUITest {
func fillCardData(_ app: XCUIApplication) throws {
let numberField = app.textFields["Card number"]
numberField.tap()
numberField.typeText("4242424242424242")
let expField = app.textFields["expiration date"]
expField.tap()
expField.typeText("1228")
let cvcField = app.textFields["CVC"]
cvcField.tap()
cvcField.typeText("123")
let postalField = app.textFields["ZIP"]
postalField.tap()
postalField.typeText("12345")
}

func waitToDisappear(_ target: Any?) {
let exists = NSPredicate(format: "exists == 0")
expectation(for: exists, evaluatedWith: target, handler: nil)
waitForExpectations(timeout: 60.0, handler: nil)
}

func reload() {
app.buttons["Reload PaymentSheet"].tap()

let checkout = app.buttons["Checkout (Complete)"]
expectation(
for: NSPredicate(format: "enabled == true"),
evaluatedWith: checkout,
handler: nil
)
waitForExpectations(timeout: 10, handler: nil)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,39 @@ func scroll(collectionView: XCUIElement, toFindCellWithId identifier:String) ->
}
return nil
}


extension XCTestCase {
func fillCardData(_ app: XCUIApplication) throws {
let numberField = app.textFields["Card number"]
numberField.tap()
numberField.typeText("4242424242424242")
let expField = app.textFields["expiration date"]
expField.tap()
expField.typeText("1228")
let cvcField = app.textFields["CVC"]
cvcField.tap()
cvcField.typeText("123")
let postalField = app.textFields["ZIP"]
postalField.tap()
postalField.typeText("12345")
}

func waitToDisappear(_ target: Any?) {
let exists = NSPredicate(format: "exists == 0")
expectation(for: exists, evaluatedWith: target, handler: nil)
waitForExpectations(timeout: 60.0, handler: nil)
}

func reload(_ app: XCUIApplication) {
app.buttons["Reload PaymentSheet"].tap()

let checkout = app.buttons["Checkout (Complete)"]
expectation(
for: NSPredicate(format: "enabled == true"),
evaluatedWith: checkout,
handler: nil
)
waitForExpectations(timeout: 10, handler: nil)
}
}
62 changes: 59 additions & 3 deletions Stripe/FormSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,75 @@

import Foundation
@_spi(STP) import StripeUICore
@_spi(STP) import StripeCore

/// A decodable representation that can used to construct a `FormElement`
struct FormSpec: Decodable {
/// The types of Elements we support
enum ElementType: String, Decodable {
case name
case email
case address
case customDropdown
}

struct ElementSpec: Decodable, Equatable {
let type: ElementType
enum ElementSpec: Decodable, Equatable {
case name
case email
case customDropdown(DropdownElementSpec)

// MARK: Decodable

private enum CodingKeys: String, CodingKey {
case type
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

switch try container.decode(String.self, forKey: .type) {
case "name":
self = .name
case "email":
self = .email
case "custom_dropdown":
self = .customDropdown(try DropdownElementSpec(from: decoder))
default:
fatalError("Unknown type")
}
}
}

let elements: [ElementSpec]
}

extension FormSpec {
struct DropdownElementSpec: Decodable, Equatable {
struct DropdownItemSpec: Decodable, Equatable {
/// The localized text to display for this item in the dropdown
let localizedDisplayText: String
/// The value to send to the Stripe API if the customer selects this dropdown item
let apiValue: String
}
/// A form URL encoded key, whose value is `DropdownItemSpec.apiValue`
let paymentMethodDataPath: String
/// The list of items to display in the dropdown
let dropdownItems: [DropdownItemSpec]
/// The dropdown's label
let label: LocalizedString
}
}

extension FormSpec {
// For now, we'll deal with localized strings by hardcoding this enum.
// In the future, the server will provide an already-localized string
enum LocalizedString: String, Decodable {
case ideal_bank = "iDEAL Bank"

var localizedValue: String {
switch self {
case .ideal_bank:
return String.Localized.ideal_bank
}
}
}
}
14 changes: 8 additions & 6 deletions Stripe/FormSpecProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,17 @@ class FormSpecProvider {
/// Loads the JSON form spec into memory
func load(completion: ((Bool) -> Void)? = nil) {
formSpecsUpdateQueue.async { [weak self] in
guard
let data = try? Data(contentsOf: formSpecsURL),
let formSpecs = try? JSONDecoder().decode([String: FormSpec].self, from: data)
else {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let data = try Data(contentsOf: formSpecsURL)
let formSpecs = try decoder.decode([String: FormSpec].self, from: data)
self?.formSpecs = formSpecs
completion?(true)
} catch {
completion?(false)
return
}
self?.formSpecs = formSpecs
completion?(true)
}
}

Expand Down
15 changes: 12 additions & 3 deletions Stripe/PaymentSheetFormFactory+FormSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,22 @@ extension PaymentSheetFormFactory {

private func makeFormElements(from spec: FormSpec) -> [Element] {
return spec.elements.map { elementSpec in
switch elementSpec.type {
switch elementSpec {
case .name:
return makeFullName()
case .email:
return makeEmail()
case .address:
return makeBillingAddressSection()
case .customDropdown(let dropdownSpec):
let dropdownField = DropdownFieldElement(
items: dropdownSpec.dropdownItems.map { $0.localizedDisplayText },
label: dropdownSpec.label.localizedValue
)
return PaymentMethodElementWrapper(dropdownField) { dropdown, params in
let values = dropdownSpec.dropdownItems.map { $0.apiValue }
let selectedValue = values[dropdown.selectedIndex]
params.paymentMethodParams.additionalAPIParameters[dropdownSpec.paymentMethodDataPath] = selectedValue
return params
}
}
}
}
Expand Down
31 changes: 0 additions & 31 deletions Stripe/PaymentSheetFormFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,6 @@ class PaymentSheetFormFactory {
switch paymentMethod {
case .bancontact:
return makeBancontact()
case .iDEAL:
return makeIdeal()
case .sofort:
return makeSofort()
case .SEPADebit:
Expand Down Expand Up @@ -286,35 +284,6 @@ extension PaymentSheetFormFactory {
}
}

func makeIdeal() -> [PaymentMethodElement] {
let name = makeFullName()
let banks = STPiDEALBank.allCases
let items = banks.map { $0.displayName } + [String.Localized.other]
let bank = PaymentMethodElementWrapper(DropdownFieldElement(
items: items,
label: String.Localized.ideal_bank
)) { bank, params in
let idealParams = params.paymentMethodParams.iDEAL ?? STPPaymentMethodiDEALParams()
idealParams.bankName = banks.stp_boundSafeObject(at: bank.selectedIndex)?.name
params.paymentMethodParams.iDEAL = idealParams
return params
}
let email = makeEmail()
let mandate = makeMandate()
let save = makeSaveCheckbox(didToggle: { selected in
email.element.isOptional = !selected
mandate.isHidden = !selected
})
switch saveMode {
case .none:
return [name, bank]
case .userSelectable:
return [name, bank, email, save, mandate]
case .merchantRequired:
return [name, bank, email, mandate]
}
}

func makeSepa() -> [PaymentMethodElement] {
let iban = PaymentMethodElementWrapper(TextFieldElement.makeIBAN()) { iban, params in
let sepa = params.paymentMethodParams.sepaDebit ?? STPPaymentMethodSEPADebitParams()
Expand Down
64 changes: 63 additions & 1 deletion Stripe/Resources/form_specs.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,67 @@
"type": "email"
}
]
}
},
"ideal": {
"elements": [
{
"type": "name"
},
{
"type": "custom_dropdown",
"payment_method_data_path": "ideal[bank]",
"dropdown_items": [
{
"localized_display_text": "ABN Amro",
"api_value": "abn_amro",
},
{
"localized_display_text": "ASN Bank",
"api_value": "asn_bank",
},
{
"localized_display_text": "bunq B.V.",
"api_value": "bunq",
},
{
"localized_display_text": "Handelsbanken",
"api_value": "handelsbanken",
},
{
"localized_display_text": "ING Bank",
"api_value": "ing",
},
{
"localized_display_text": "Knab",
"api_value": "knab",
},
{
"localized_display_text": "Rabobank",
"api_value": "rabobank",
},
{
"localized_display_text": "RegioBank",
"api_value": "regiobank",
},
{
"localized_display_text": "Revolut",
"api_value": "revolut",
},
{
"localized_display_text": "SNS Bank",
"api_value": "sns_bank",
},
{
"localized_display_text": "Triodos Bank",
"api_value": "triodos_bank",
},
{
"localized_display_text": "Van Lanschot",
"api_value": "van_lanschot",
}
],
"label": "iDEAL Bank"
}
]
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,10 @@ import UIKit
label: String?,
didUpdate: DidUpdateSelectedIndex? = nil
) {
self.init(items: items.map({ DropdownItem(pickerDisplayName: $0,
labelDisplayName: $0,
accessibilityLabel: $0) }),
defaultIndex: defaultIndex,
label: label,
didUpdate: didUpdate
)
let dropdownItems = items.map {
DropdownItem(pickerDisplayName: $0, labelDisplayName: $0, accessibilityLabel: $0)
}
self.init(items: dropdownItems, defaultIndex: defaultIndex, label: label, didUpdate: didUpdate)
}

/**
Expand Down
Loading

0 comments on commit 4b6d20e

Please sign in to comment.