Skip to content

Commit

Permalink
[Link] Add and display instant debit mandate (#786)
Browse files Browse the repository at this point in the history
* Add instant debit mandate

* Use calculated line spacing

* Cleanup

* Add snapshot test

* Fix build

* Cleanup

* Rename property

* Fix animation glitch

* Fix iOS 12 test
  • Loading branch information
ramont-stripe authored Feb 23, 2022
1 parent 5e58f85 commit fbca15e
Show file tree
Hide file tree
Showing 9 changed files with 326 additions and 28 deletions.
8 changes: 8 additions & 0 deletions Stripe.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,8 @@
D0854906279B9A3D00D3A3DC /* LinkSignupViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0854905279B9A3D00D3A3DC /* LinkSignupViewModelTests.swift */; };
D092A850274D755C00C021A0 /* LinkToast.swift in Sources */ = {isa = PBXBuildFile; fileRef = D092A84F274D755C00C021A0 /* LinkToast.swift */; };
D092A852274D8C9000C021A0 /* icon_link_toast_success@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = D092A851274D8C9000C021A0 /* icon_link_toast_success@3x.png */; };
D092E36A27BEF8BD00B72609 /* LinkInstantDebitMandateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D092E36927BEF8BD00B72609 /* LinkInstantDebitMandateView.swift */; };
D092E36F27BF380300B72609 /* LinkInstantDebitMandateViewSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D092E36E27BF380300B72609 /* LinkInstantDebitMandateViewSnapshotTests.swift */; };
D092E37727C5616D00B72609 /* LinkAccountServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D092E37627C5616D00B72609 /* LinkAccountServiceTests.swift */; };
D09A20D326DEE66800A0D4D9 /* CardBrandView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09A20D226DEE66800A0D4D9 /* CardBrandView.swift */; };
D0BBBC9326DE9243007A9F48 /* STPCardCVCInputTextFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BBBC9226DE9243007A9F48 /* STPCardCVCInputTextFieldTests.swift */; };
Expand Down Expand Up @@ -1653,6 +1655,8 @@
D0854905279B9A3D00D3A3DC /* LinkSignupViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkSignupViewModelTests.swift; sourceTree = "<group>"; };
D092A84F274D755C00C021A0 /* LinkToast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkToast.swift; sourceTree = "<group>"; };
D092A851274D8C9000C021A0 /* icon_link_toast_success@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_link_toast_success@3x.png"; sourceTree = "<group>"; };
D092E36927BEF8BD00B72609 /* LinkInstantDebitMandateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkInstantDebitMandateView.swift; sourceTree = "<group>"; };
D092E36E27BF380300B72609 /* LinkInstantDebitMandateViewSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkInstantDebitMandateViewSnapshotTests.swift; sourceTree = "<group>"; };
D092E37627C5616D00B72609 /* LinkAccountServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkAccountServiceTests.swift; sourceTree = "<group>"; };
D09A20D226DEE66800A0D4D9 /* CardBrandView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardBrandView.swift; sourceTree = "<group>"; };
D0BBBC9226DE9243007A9F48 /* STPCardCVCInputTextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardCVCInputTextFieldTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2917,6 +2921,7 @@
D0045035276908F200BA5F01 /* CheckboxButtonSnapshotTests.swift */,
D0277AE027604E3600AF2CF6 /* Link2FAViewSnapshotTests.swift */,
D08548FD279B729F00D3A3DC /* LinkInlineSignupElementSnapshotTests.swift */,
D092E36E27BF380300B72609 /* LinkInstantDebitMandateViewSnapshotTests.swift */,
D0ECE12E27A9AD560082ED18 /* LinkLegalTermsViewSnapshotTests.swift */,
D0BEB40A273CAC350031D677 /* LinkPaymentMethodPickerSnapshotTests.swift */,
D0277ADE27604A4F00AF2CF6 /* LinkToastSnapshotTests.swift */,
Expand Down Expand Up @@ -3256,6 +3261,7 @@
D0BEB41D273EDF290031D677 /* LinkWalletFooterView.swift */,
D0EC8711278E658E00CFACDC /* LinkKeyboardAvoidingScrollView.swift */,
D0ECE12C27A9A9DA0082ED18 /* LinkLegalTermsView.swift */,
D092E36927BEF8BD00B72609 /* LinkInstantDebitMandateView.swift */,
);
name = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -3855,6 +3861,7 @@
D0CA3DB0279E578B00143B1C /* OperationDebouncerTests.swift in Sources */,
E61BEEAE265F6BDC0002FA4F /* STPAnalyticsClientPaymentsTest.swift in Sources */,
31C5B88A252E9DBB00A481A7 /* STPEphemeralKeyManagerTest.m in Sources */,
D092E36F27BF380300B72609 /* LinkInstantDebitMandateViewSnapshotTests.swift in Sources */,
D069EA2A27557B4E00C1CF46 /* STPTextFieldDelegateProxyTests.swift in Sources */,
36E283DA254A17570028C186 /* STPInputTextFieldFormatterTests.swift in Sources */,
3111C714252E2A5200207E32 /* STPApplePayContextTest.swift in Sources */,
Expand Down Expand Up @@ -4119,6 +4126,7 @@
B6442338251352DE00CA2526 /* STPFile.swift in Sources */,
3111BE852513075000288D28 /* STPFormTextFieldContainer.swift in Sources */,
D0F20320273C33C100AB10BF /* PayWithLinkViewController-NewPaymentViewController.swift in Sources */,
D092E36A27BEF8BD00B72609 /* LinkInstantDebitMandateView.swift in Sources */,
31281BC9252551AA00591A95 /* STPPaymentCardTextFieldViewModel.swift in Sources */,
D060825E26E9123E0002D656 /* STPPaymentMethodBoletoParams.swift in Sources */,
3693FFFC2756DEEF002C8F40 /* ConsumerSession+LookupResponse.swift in Sources */,
Expand Down
126 changes: 126 additions & 0 deletions Stripe/LinkInstantDebitMandateView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//
// LinkInstantDebitMandateView.swift
// StripeiOS
//
// Created by Ramon Torres on 2/17/22.
// Copyright © 2022 Stripe, Inc. All rights reserved.
//

import UIKit
@_spi(STP) import StripeUICore

protocol LinkInstantDebitMandateViewDelegate: AnyObject {
/// Called when the user taps on a link.
///
/// - Parameters:
/// - mandateView: The view that the user interacted with.
/// - url: URL of the link.
func instantDebitMandateView(_ mandateView: LinkInstantDebitMandateView, didTapOnLinkWithURL url: URL)
}

// TODO(ramont): extract common code with `LinkLegalTermsView`.

/// For internal SDK use only
@objc(STP_Internal_LinkInstantDebitMandateViewDelegate)
final class LinkInstantDebitMandateView: UIView {
struct Constants {
static let lineHeight: CGFloat = 1.5
}

// TODO(ramont): Update with final URLs
private let links: [String: URL] = [
"terms": URL(string: "https://stripe.com/legal")!
]

weak var delegate: LinkInstantDebitMandateViewDelegate?

var textColor: UIColor? {
get {
return textView.textColor
}
set {
textView.textColor = newValue
}
}

private lazy var textView: UITextView = {
let textView = UITextView()
textView.isScrollEnabled = false
textView.isEditable = false
textView.font = LinkUI.font(forTextStyle: .caption)
textView.backgroundColor = .clear
textView.attributedText = formattedLegalText()
textView.textColor = CompatibleColor.secondaryLabel
textView.textAlignment = .center
textView.textContainerInset = .zero
textView.textContainer.lineFragmentPadding = 0
textView.delegate = self
textView.clipsToBounds = false
return textView
}()

init(delegate: LinkInstantDebitMandateViewDelegate? = nil) {
super.init(frame: .zero)
self.delegate = delegate
addAndPinSubview(textView)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
textView.font = LinkUI.font(forTextStyle: .caption, compatibleWith: traitCollection)
}

private func formattedLegalText() -> NSAttributedString {
// TODO(ramont): Localize
let string = "By continuing, you agree to authorize payments pursuant to these <terms>terms</terms>."

let formattedString = NSMutableAttributedString()

STPStringUtils.parseRanges(from: string, withTags: Set<String>(links.keys)) { string, matches in
formattedString.append(NSAttributedString(string: string))

for (tag, range) in matches {
guard range.rangeValue.location != NSNotFound else {
assertionFailure("Tag '<\(tag)>' not found")
continue
}

if let url = links[tag] {
formattedString.addAttributes([.link: url], range: range.rangeValue)
}
}
}

let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = LinkUI.lineSpacing(
fromRelativeHeight: Constants.lineHeight,
textStyle: .caption
)

formattedString.addAttributes([.paragraphStyle: paragraphStyle], range: formattedString.extent)

return formattedString
}

}

extension LinkInstantDebitMandateView: UITextViewDelegate {

func textView(
_ textView: UITextView,
shouldInteractWith URL: URL,
in characterRange: NSRange,
interaction: UITextItemInteraction
) -> Bool {
if interaction == .invokeDefaultAction {
delegate?.instantDebitMandateView(self, didTapOnLinkWithURL: URL)
}

return false
}

}
9 changes: 7 additions & 2 deletions Stripe/LinkLegalTermsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ protocol LinkLegalTermsViewDelegate: AnyObject {
/// For internal SDK use only
@objc(STP_Internal_LinkLegalTermsView)
final class LinkLegalTermsView: UIView {
struct Constants {
static let lineHeight: CGFloat = 1.5
}

// TODO(ramont): Update with final URLs
private let links: [String: URL] = [
Expand Down Expand Up @@ -98,8 +101,10 @@ final class LinkLegalTermsView: UIView {
}

let paragraphStyle = NSMutableParagraphStyle()
// TODO(ramont): Implement line spacing calculation
paragraphStyle.lineSpacing = 6
paragraphStyle.lineSpacing = LinkUI.lineSpacing(
fromRelativeHeight: Constants.lineHeight,
textStyle: .caption
)

formattedString.addAttributes([.paragraphStyle: paragraphStyle], range: formattedString.extent)

Expand Down
9 changes: 9 additions & 0 deletions Stripe/LinkPaymentMethodPicker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ final class LinkPaymentMethodPicker: UIView {
func toggleExpanded(animated: Bool) {
headerView.isExpanded.toggle()

// Prevent double header animation
if headerView.isExpanded {
// TODO(ramont): revise layout margin placement and remove conditional
setNeedsLayout()
layoutIfNeeded()
} else {
headerView.layoutIfNeeded()
}

if headerView.isExpanded {
stackView.showArrangedSubview(at: 1, animated: animated)
} else {
Expand Down
5 changes: 5 additions & 0 deletions Stripe/LinkUI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,9 @@ extension LinkUI {
}
}

static func lineSpacing(fromRelativeHeight lineHeight: CGFloat, textStyle: TextStyle) -> CGFloat {
let font = self.font(forTextStyle: textStyle)
return (font.pointSize * lineHeight) - font.pointSize
}

}
50 changes: 48 additions & 2 deletions Stripe/PayWithLinkViewController-WalletViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import UIKit
import SafariServices
@_spi(STP) import StripeCore
@_spi(STP) import StripeUICore

Expand Down Expand Up @@ -37,6 +38,8 @@ extension PayWithLinkViewController {
}
}

private lazy var instantDebitMandateView = LinkInstantDebitMandateView(delegate: self)

private lazy var confirmButton: ConfirmButton = {
let button = ConfirmButton(style: .stripe, callToAction: callToAction) { [weak self] in
self?.confirm()
Expand All @@ -62,6 +65,25 @@ extension PayWithLinkViewController {
// TODO(ramont): Localize
private lazy var separator = SeparatorLabel(text: "Or")

private lazy var paymentPickerContainerView: UIStackView = {
let stackView = UIStackView(arrangedSubviews: [
paymentPicker,
instantDebitMandateView
])

stackView.axis = .vertical
stackView.spacing = LinkUI.contentSpacing
return stackView
}()

var shouldShowInstantDebitMandate: Bool {
if case .bankAccount = paymentPicker.selectedPaymentMethod?.details {
return true
}

return false
}

init(
linkAccount: PaymentSheetLinkAccount,
context: Context,
Expand All @@ -81,6 +103,7 @@ extension PayWithLinkViewController {
super.viewDidLoad()

setupUI()
updateUI(animated: false)

paymentPicker.delegate = self
paymentPicker.dataSource = self
Expand All @@ -105,7 +128,7 @@ extension PayWithLinkViewController {

func setupUI() {
let stackView = UIStackView(arrangedSubviews: [
paymentPicker,
paymentPickerContainerView,
confirmButton,
footerView,
separator,
Expand All @@ -114,7 +137,7 @@ extension PayWithLinkViewController {

stackView.axis = .vertical
stackView.spacing = LinkUI.contentSpacing
stackView.setCustomSpacing(LinkUI.extraLargeContentSpacing, after: paymentPicker)
stackView.setCustomSpacing(LinkUI.extraLargeContentSpacing, after: paymentPickerContainerView)
stackView.isLayoutMarginsRelativeArrangement = true
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.directionalLayoutMargins = LinkUI.contentMargins
Expand All @@ -133,6 +156,14 @@ extension PayWithLinkViewController {
])
}

func updateUI(animated: Bool) {
paymentPickerContainerView.toggleArrangedSubview(
instantDebitMandateView,
shouldShow: shouldShowInstantDebitMandate,
animated: animated
)
}

func confirm() {
guard let paymentDetails = paymentPicker.selectedPaymentMethod else {
assertionFailure("`confirm()` called without a selected payment method")
Expand Down Expand Up @@ -247,6 +278,7 @@ extension PayWithLinkViewController.WalletViewController: LinkPaymentMethodPicke
func paymentMethodPickerDidChange(_ pickerView: LinkPaymentMethodPicker) {
let state: ConfirmButton.Status = pickerView.selectedPaymentMethod == nil ? .disabled : .enabled
confirmButton.update(state: state, callToAction: callToAction)
updateUI(animated: true)
}

func paymentMethodPicker(
Expand Down Expand Up @@ -329,6 +361,17 @@ extension PayWithLinkViewController.WalletViewController: LinkPaymentMethodPicke

}

extension PayWithLinkViewController.WalletViewController: LinkInstantDebitMandateViewDelegate {

func instantDebitMandateView(_ mandateView: LinkInstantDebitMandateView, didTapOnLinkWithURL url: URL) {
let safariVC = SFSafariViewController(url: url)
safariVC.dismissButtonStyle = .close
safariVC.modalPresentationStyle = .overFullScreen
present(safariVC, animated: true)
}

}

extension PayWithLinkViewController.WalletViewController: UpdatePaymentViewControllerDelegate {

func didUpdate(paymentMethod: ConsumerPaymentDetails) {
Expand All @@ -343,7 +386,10 @@ extension PayWithLinkViewController.WalletViewController: UpdatePaymentViewContr
self.paymentMethods[index] = paymentMethod
self.paymentPicker.selectedIndex = index
self.paymentPicker.reloadData()

updateUI(animated: true)
}

}

/// Helper functions for ConsumerPaymentDetails
Expand Down
Loading

0 comments on commit fbca15e

Please sign in to comment.