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 88abf89
Show file tree
Hide file tree
Showing 10 changed files with 82 additions and 36 deletions.
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 88abf89

Please sign in to comment.