Skip to content

Commit

Permalink
Restore link signup customer input
Browse files Browse the repository at this point in the history
  • Loading branch information
yuki-stripe committed Oct 4, 2024
1 parent 4dba38d commit b06b8d0
Show file tree
Hide file tree
Showing 11 changed files with 83 additions and 37 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## X.Y.Z X-Y-Z
### PaymentSheet
* [Fixed] Fixed an issue where ISK was not correctly formatted as a zero-decimal currency when using PaymentSheet or Apple Pay. (Thanks [@Thithip](https://github.com/Thithip)!)
* [Fixed] Fixed an issue where US Bank Account forms would drop form field input when `FlowController.update` is called.
* [Fixed] Fixed an issue where certain forms like US Bank Account or the Link inline signup form would drop form field input when `FlowController.update` is called.

## 23.31.0 2024-09-23
### PaymentSheet
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ extension LinkInlineSignupElementSnapshotTests {
configuration: configuration,
showCheckbox: showCheckbox,
accountService: MockAccountService(),
previousCustomerInput: nil,
linkAccount: linkAccount,
country: country
)
Expand Down
2 changes: 1 addition & 1 deletion Stripe/StripeiOSTests/LinkSignupViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import StripeCoreTestUtils
import XCTest

@testable@_spi(STP) import Stripe
@testable@_spi(STP) import StripeCore
@testable@_spi(STP) import StripePayments
@testable@_spi(STP) import StripePaymentSheet
import StripePaymentsTestUtils
Expand Down Expand Up @@ -208,6 +207,7 @@ extension LinkInlineSignupViewModelTests {
configuration: PaymentSheet.Configuration(),
showCheckbox: showCheckbox,
accountService: MockAccountService(shouldFailLookup: shouldFailLookup),
previousCustomerInput: nil,
linkAccount: linkAccount,
country: country
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import UIKit

// TODO: Refactor this to be a ContainerElement and contain its sub-elements.
final class LinkInlineSignupElement: Element {
final class LinkInlineSignupElement: PaymentMethodElement {
let collectsUserInput: Bool = true

let signupView: LinkInlineSignupView
Expand Down Expand Up @@ -40,12 +40,14 @@ final class LinkInlineSignupElement: Element {
configuration: PaymentSheet.Configuration,
linkAccount: PaymentSheetLinkAccount?,
country: String?,
showCheckbox: Bool
showCheckbox: Bool,
previousCustomerInput: IntentConfirmParams?
) {
self.init(viewModel: LinkInlineSignupViewModel(
configuration: configuration,
showCheckbox: showCheckbox,
accountService: LinkAccountService(apiClient: configuration.apiClient),
previousCustomerInput: previousCustomerInput?.linkInlineSignupCustomerInput,
linkAccount: linkAccount,
country: country
))
Expand All @@ -56,6 +58,15 @@ final class LinkInlineSignupElement: Element {
self.signupView.delegate = self
}

func updateParams(params: IntentConfirmParams) -> IntentConfirmParams? {
params.linkInlineSignupCustomerInput = .init(
phoneNumber: signupView.phoneNumberElement.phoneNumber,
name: signupView.nameElement.text,
email: signupView.emailElement.emailAddressString,
checkboxSelected: signupView.checkboxElement.isChecked
)
return params
}
}

extension LinkInlineSignupElement: LinkInlineSignupViewDelegate {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ extension LinkInlineSignupView {
private let appearance: PaymentSheet.Appearance
/// Controls the stroke color of the checkbox
private let borderColor: UIColor
let initialIsSelectedValue: Bool

var view: UIView {
return checkboxButton
Expand Down Expand Up @@ -56,15 +57,16 @@ extension LinkInlineSignupView {

let checkbox = CheckboxButton(text: text, description: description, theme: appearanceCopy.asElementsTheme)
checkbox.addTarget(self, action: #selector(didToggleCheckbox), for: .touchUpInside)
checkbox.isSelected = false
checkbox.isSelected = initialIsSelectedValue

return checkbox
}()

init(merchantName: String, appearance: PaymentSheet.Appearance, borderColor: UIColor) {
init(merchantName: String, appearance: PaymentSheet.Appearance, borderColor: UIColor, isSelected: Bool) {
self.merchantName = merchantName
self.appearance = appearance
self.borderColor = borderColor
self.initialIsSelectedValue = isSelected
}

func setUserInteraction(isUserInteractionEnabled: Bool) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ final class LinkInlineSignupView: UIView {
private(set) lazy var checkboxElement = CheckboxElement(
merchantName: viewModel.configuration.merchantDisplayName,
appearance: viewModel.configuration.appearance,
borderColor: borderColor
borderColor: borderColor,
isSelected: viewModel.saveCheckboxChecked
)

private(set) lazy var emailElement: LinkEmailElement = {
let element = LinkEmailElement(defaultValue: viewModel.emailAddress,
let element = LinkEmailElement(defaultValue: viewModel.initialEmail ?? viewModel.emailAddress,
isOptional: viewModel.isEmailOptional,
showLogo: viewModel.mode != .textFieldsOnlyPhoneFirst,
theme: theme)
Expand All @@ -44,7 +45,7 @@ final class LinkInlineSignupView: UIView {
}()

private(set) lazy var nameElement: TextFieldElement = {
let configuration = TextFieldElement.NameConfiguration(type: .full, defaultValue: viewModel.legalName)
let configuration = TextFieldElement.NameConfiguration(type: .full, defaultValue: viewModel.initialName ?? viewModel.legalName)
return TextFieldElement(configuration: configuration, theme: theme)
}()

Expand All @@ -53,15 +54,28 @@ final class LinkInlineSignupView: UIView {
// Otherwise, we'd imply consumer consent when it hasn't occurred.
switch viewModel.mode {
case .checkbox:
let defaultCountryCode = viewModel.initialPhoneNumber?.countryCode ?? viewModel.configuration.defaultBillingDetails.address.country
let defaultPhoneNumber = viewModel.initialPhoneNumber?.number ?? viewModel.configuration.defaultBillingDetails.phone
return PhoneNumberElement(
defaultCountryCode: viewModel.configuration.defaultBillingDetails.address.country,
defaultPhoneNumber: viewModel.configuration.defaultBillingDetails.phone,
defaultCountryCode: defaultCountryCode,
defaultPhoneNumber: defaultPhoneNumber,
theme: theme
)
case .textFieldsOnlyEmailFirst:
return PhoneNumberElement(isOptional: viewModel.isPhoneNumberOptional, theme: theme)
return PhoneNumberElement(
defaultCountryCode: viewModel.initialPhoneNumber?.countryCode,
defaultPhoneNumber: viewModel.initialPhoneNumber?.number,
isOptional: viewModel.isPhoneNumberOptional,
theme: theme
)
case .textFieldsOnlyPhoneFirst:
return PhoneNumberElement(isOptional: viewModel.isPhoneNumberOptional, infoView: LinkMoreInfoView(), theme: theme)
return PhoneNumberElement(
defaultCountryCode: viewModel.initialPhoneNumber?.countryCode,
defaultPhoneNumber: viewModel.initialPhoneNumber?.number,
isOptional: viewModel.isPhoneNumberOptional,
infoView: LinkMoreInfoView(),
theme: theme
)
}
}()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ protocol LinkInlineSignupViewModelDelegate: AnyObject {
func signupViewModelDidUpdate(_ viewModel: LinkInlineSignupViewModel)
}

struct LinkInlineSignupCustomerInput: Equatable {
let phoneNumber: PhoneNumber?
let name: String?
let email: String?
let checkboxSelected: Bool?
}

final class LinkInlineSignupViewModel {
enum Action: Equatable {
case signupAndPay(account: PaymentSheetLinkAccount, phoneNumber: PhoneNumber, legalName: String?)
Expand All @@ -38,8 +45,11 @@ final class LinkInlineSignupViewModel {
let configuration: PaymentSheet.Configuration

let mode: Mode
let initialEmail: String?
let initialPhoneNumber: PhoneNumber?
let initialName: String?

var saveCheckboxChecked: Bool = false {
var saveCheckboxChecked: Bool {
didSet {
if saveCheckboxChecked != oldValue {
notifyUpdate()
Expand Down Expand Up @@ -291,13 +301,18 @@ final class LinkInlineSignupViewModel {
configuration: PaymentSheet.Configuration,
showCheckbox: Bool,
accountService: LinkAccountServiceProtocol,
previousCustomerInput: LinkInlineSignupCustomerInput?,
linkAccount: PaymentSheetLinkAccount? = nil,
country: String? = nil
) {
self.configuration = configuration
self.accountService = accountService
self.linkAccount = linkAccount
self.emailAddress = linkAccount?.email
self.saveCheckboxChecked = previousCustomerInput?.checkboxSelected ?? false
self.initialEmail = previousCustomerInput?.email
self.initialPhoneNumber = previousCustomerInput?.phoneNumber
self.initialName = previousCustomerInput?.name
if let email = self.emailAddress,
!email.isEmpty {
emailWasPrefilled = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ final class IntentConfirmParams {

var financialConnectionsLinkedBank: FinancialConnectionsLinkedBank?
var instantDebitsLinkedBank: InstantDebitsLinkedBank?
/// Hack: Contains the customer input in the link inline signup element (e.g. email, checkbox state) so that it can be preserved across `FlowController.update` etc.
var linkInlineSignupCustomerInput: LinkInlineSignupCustomerInput?

var paymentSheetLabel: String {
if let last4 = (financialConnectionsLinkedBank?.last4 ?? instantDebitsLinkedBank?.last4) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ extension PaymentSheetFormFactory {
configuration: configuration,
linkAccount: linkAccount,
country: countryCode,
showCheckbox: !shouldDisplaySaveCheckbox
showCheckbox: !shouldDisplaySaveCheckbox,
previousCustomerInput: previousCustomerInput
)
elements.append(inlineSignupElement)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@
// Created by Yuki Tokuhiro on 10/2/24.
//

import XCTest
@testable@_spi(STP) import StripeCore
@testable@_spi(STP) import StripePayments
@testable@_spi(STP) import StripePaymentSheet
@testable@_spi(STP) import StripePaymentsTestUtils
@testable@_spi(STP) import StripePaymentsUI
@testable@_spi(STP) import StripeUICore
import XCTest

@MainActor
final class CardSectionElementTest: XCTestCase {
let window: UIWindow = UIWindow(frame: .init(x: 0, y: 0, width: 428, height: 926))

func testPreservesPreviousCustomerInput() async {
func testLinkSignupPreservesPreviousCustomerInput() async {
await PaymentSheetLoader.loadMiscellaneousSingletons()
func makeForm(previousCustomerInput: IntentConfirmParams?) -> PaymentMethodElement {
let intent: Intent = ._testPaymentIntent(paymentMethodTypes: [.card])
Expand All @@ -27,7 +27,7 @@ final class CardSectionElementTest: XCTestCase {
elementsSession: ._testValue(paymentMethodTypes: ["card"], isLinkPassthroughModeEnabled: true),
previousCustomerInput: previousCustomerInput,
formCache: .init(),
configuration: configuration,
configuration: .init(),
headerView: nil,
analyticsHelper: ._testValue(),
delegate: self
Expand All @@ -40,29 +40,25 @@ final class CardSectionElementTest: XCTestCase {
formVC.viewDidAppear(false)
return formVC.form
}
var configuration = PaymentSheet.Configuration()
configuration.customer = .init(id: "id", ephemeralKeySecret: "sec")
let form = makeForm(previousCustomerInput: nil)
let checkbox = form.getCheckboxElement(startingWith: "Save payment details")!
let linkInlineSignupElement: LinkInlineSignupElement = form.getElement()!
let linkInlineView = linkInlineSignupElement.signupView

XCTAssertNotNil(checkbox) // Checkbox should appear since this is a PI w/ customer
XCTAssertNotNil(linkInlineView.checkboxElement) // Checkbox should appear since this is a PI w/o customer
form.getTextFieldElement("Card number")?.setText("4242424242424242")
form.getTextFieldElement("MM / YY").setText("1232")
form.getTextFieldElement("CVC").setText("123")
form.getTextFieldElement("ZIP").setText("65432")

XCTAssertEqual(form.getAllUnwrappedSubElements().count, 14)
// XCTAssertNotNil(form.mandateString)

// Simulate selecting checkbox
checkbox.isSelected = true
checkbox.didToggleCheckbox()
linkInlineView.checkboxElement.isChecked = true
linkInlineView.checkboxElement.didToggleCheckbox()

// Set the email & phone number
linkInlineView.emailElement.emailAddressElement.setText("\(UUID().uuidString)@foo.com")
let email = "\(UUID().uuidString)@foo.com"
linkInlineView.emailElement.emailAddressElement.setText(email)
linkInlineView.phoneNumberElement.countryDropdownElement.setRawData("GB")
linkInlineView.phoneNumberElement.textFieldElement.setText("1234567890")
linkInlineView.nameElement.setText("John Doe")

// Generate params from the form
guard let intentConfirmParams = form.updateParams(params: IntentConfirmParams(type: .stripe(.card))) else {
Expand All @@ -72,17 +68,17 @@ final class CardSectionElementTest: XCTestCase {

// Re-generate the form and validate that it carries over all previous customer input
let regeneratedForm = makeForm(previousCustomerInput: intentConfirmParams)
let regeneratedLinkInlineSignupElement: LinkInlineSignupElement = regeneratedForm.getElement()!
let regeneratedLinkInlineView = linkInlineSignupElement.signupView
guard let regeneratedIntentConfirmParams = regeneratedForm.updateParams(params: IntentConfirmParams(type: .stripe(.card))) else {
XCTFail("Regenerated form failed to create params. Validation state: \(regeneratedForm.validationState) \n Form: \(regeneratedForm)")
return
}
// Ensure checkbox remains selected
XCTAssertTrue(regeneratedForm.getCheckboxElement(startingWith: "Save payment details")!.isSelected)
XCTAssertEqual(regeneratedIntentConfirmParams, intentConfirmParams)
let linkInlineSignupElement2: LinkInlineSignupElement = regeneratedForm.getElement()!
let linkInlineView2 = linkInlineSignupElement2.signupView
print(linkInlineView2)

XCTAssertTrue(regeneratedLinkInlineSignupElement.isChecked)
XCTAssertEqual(regeneratedLinkInlineView.emailElement.emailAddressString, email)
XCTAssertEqual(regeneratedLinkInlineView.nameElement.text, "John Doe")
XCTAssertEqual(regeneratedLinkInlineView.phoneNumberElement.phoneNumber, PhoneNumber(number: "1234567890", countryCode: "GB"))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -979,11 +979,15 @@ extension IntentConfirmParams: Equatable {
print("Instant debits linked banks not equal: \(lhs.instantDebitsLinkedBank.debugDescription) vs \(rhs.instantDebitsLinkedBank.debugDescription)")
return false
}
if lhs.linkInlineSignupCustomerInput != rhs.linkInlineSignupCustomerInput {
print("Link inline signup customer input not equal: \(lhs.linkInlineSignupCustomerInput.debugDescription) vs \(rhs.linkInlineSignupCustomerInput.debugDescription)")
return false
}

// Sanity check to make sure when we add new properties, we check them here
let mirror = Mirror(reflecting: lhs)
let propertyCount = mirror.children.count
XCTAssertEqual(propertyCount, 7)
XCTAssertEqual(propertyCount, 8)

return true
}
Expand Down

0 comments on commit b06b8d0

Please sign in to comment.