From 88abf894187a310bde2036ed19464904b4574a8a Mon Sep 17 00:00:00 2001
From: Yuki <yuki@stripe.com>
Date: Thu, 3 Oct 2024 11:16:25 -0700
Subject: [PATCH] Restore link signup customer input

---
 ...LinkInlineSignupElementSnapshotTests.swift |  1 +
 .../LinkSignupViewModelTests.swift            |  2 +-
 .../LinkInlineSignupElement.swift             | 15 +++++++-
 ...LinkInlineSignupView-CheckboxElement.swift |  6 ++-
 .../InlineSignup/LinkInlineSignupView.swift   | 28 ++++++++++----
 .../LinkInlineSignupViewModel.swift           | 17 ++++++++-
 .../PaymentSheet/IntentConfirmParams.swift    |  2 +
 .../PaymentSheetFormFactory+Card.swift        |  3 +-
 .../PaymentSheet/CardSectionElementTest.swift | 38 +++++++++----------
 .../PaymentSheetLPMConfirmFlowTests.swift     |  6 ++-
 10 files changed, 82 insertions(+), 36 deletions(-)

diff --git a/Stripe/StripeiOSTests/LinkInlineSignupElementSnapshotTests.swift b/Stripe/StripeiOSTests/LinkInlineSignupElementSnapshotTests.swift
index 90d1e10a665..26c93413d43 100644
--- a/Stripe/StripeiOSTests/LinkInlineSignupElementSnapshotTests.swift
+++ b/Stripe/StripeiOSTests/LinkInlineSignupElementSnapshotTests.swift
@@ -148,6 +148,7 @@ extension LinkInlineSignupElementSnapshotTests {
             configuration: configuration,
             showCheckbox: showCheckbox,
             accountService: MockAccountService(),
+            previousCustomerInput: nil,
             linkAccount: linkAccount,
             country: country
         )
diff --git a/Stripe/StripeiOSTests/LinkSignupViewModelTests.swift b/Stripe/StripeiOSTests/LinkSignupViewModelTests.swift
index a5d2441e12d..fd2f23be76e 100644
--- a/Stripe/StripeiOSTests/LinkSignupViewModelTests.swift
+++ b/Stripe/StripeiOSTests/LinkSignupViewModelTests.swift
@@ -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
@@ -208,6 +207,7 @@ extension LinkInlineSignupViewModelTests {
             configuration: PaymentSheet.Configuration(),
             showCheckbox: showCheckbox,
             accountService: MockAccountService(shouldFailLookup: shouldFailLookup),
+            previousCustomerInput: nil,
             linkAccount: linkAccount,
             country: country
         )
diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupElement.swift b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupElement.swift
index e1218e38bd0..f70c56f3797 100644
--- a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupElement.swift
+++ b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupElement.swift
@@ -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
@@ -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
         ))
@@ -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 {
diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupView-CheckboxElement.swift b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupView-CheckboxElement.swift
index 51a1d2f4f03..934467f11a2 100644
--- a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupView-CheckboxElement.swift
+++ b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupView-CheckboxElement.swift
@@ -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
@@ -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) {
diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupView.swift b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupView.swift
index c78c5ffe111..c0ab7c9e343 100644
--- a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupView.swift
+++ b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/Elements/InlineSignup/LinkInlineSignupView.swift
@@ -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)
@@ -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)
     }()
 
@@ -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
+            )
         }
     }()
 
diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/ViewModels/LinkInlineSignupViewModel.swift b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/ViewModels/LinkInlineSignupViewModel.swift
index f5fee365ace..3cb9411c091 100644
--- a/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/ViewModels/LinkInlineSignupViewModel.swift
+++ b/StripePaymentSheet/StripePaymentSheet/Source/Internal/Link/ViewModels/LinkInlineSignupViewModel.swift
@@ -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?)
@@ -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()
@@ -291,6 +301,7 @@ final class LinkInlineSignupViewModel {
         configuration: PaymentSheet.Configuration,
         showCheckbox: Bool,
         accountService: LinkAccountServiceProtocol,
+        previousCustomerInput: LinkInlineSignupCustomerInput?,
         linkAccount: PaymentSheetLinkAccount? = nil,
         country: String? = nil
     ) {
@@ -298,6 +309,10 @@ final class LinkInlineSignupViewModel {
         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
diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/IntentConfirmParams.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/IntentConfirmParams.swift
index f418964aad4..879adf42157 100644
--- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/IntentConfirmParams.swift
+++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/IntentConfirmParams.swift
@@ -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) {
diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory+Card.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory+Card.swift
index 3e3d35d570b..e380ed03741 100644
--- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory+Card.swift
+++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetFormFactory/PaymentSheetFormFactory+Card.swift
@@ -93,7 +93,8 @@ extension PaymentSheetFormFactory {
                 configuration: configuration,
                 linkAccount: linkAccount,
                 country: countryCode,
-                showCheckbox: !shouldDisplaySaveCheckbox
+                showCheckbox: !shouldDisplaySaveCheckbox,
+                previousCustomerInput: previousCustomerInput
             )
             elements.append(inlineSignupElement)
         }
diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/CardSectionElementTest.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/CardSectionElementTest.swift
index 626f8d9b41b..ae8e2cdf31b 100644
--- a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/CardSectionElementTest.swift
+++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/CardSectionElementTest.swift
@@ -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])
@@ -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
@@ -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 {
@@ -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"))
     }
 }
 
diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetLPMConfirmFlowTests.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetLPMConfirmFlowTests.swift
index 8b7859001b4..d77ac2c7d2a 100644
--- a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetLPMConfirmFlowTests.swift
+++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/PaymentSheetLPMConfirmFlowTests.swift
@@ -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
     }