From fbca15e408196550bf7473b2c0d3d62d534e85e6 Mon Sep 17 00:00:00 2001 From: ramont-stripe <88752322+ramont-stripe@users.noreply.github.com> Date: Wed, 23 Feb 2022 16:37:10 -0500 Subject: [PATCH] [Link] Add and display instant debit mandate (#786) * Add instant debit mandate * Use calculated line spacing * Cleanup * Add snapshot test * Fix build * Cleanup * Rename property * Fix animation glitch * Fix iOS 12 test --- Stripe.xcodeproj/project.pbxproj | 8 ++ Stripe/LinkInstantDebitMandateView.swift | 126 ++++++++++++++++++ Stripe/LinkLegalTermsView.swift | 9 +- Stripe/LinkPaymentMethodPicker.swift | 9 ++ Stripe/LinkUI.swift | 5 + ...kViewController-WalletViewController.swift | 50 ++++++- Stripe/UIStackView+Stripe.swift | 70 ++++++---- .../testDefault@3x.png | Bin 0 -> 15605 bytes ...InstantDebitMandateViewSnapshotTests.swift | 77 +++++++++++ 9 files changed, 326 insertions(+), 28 deletions(-) create mode 100644 Stripe/LinkInstantDebitMandateView.swift create mode 100644 Tests/ReferenceImages_64/StripeiOS_Tests.LinkInstantDebitMandateViewSnapshotTests/testDefault@3x.png create mode 100644 Tests/Tests/LinkInstantDebitMandateViewSnapshotTests.swift diff --git a/Stripe.xcodeproj/project.pbxproj b/Stripe.xcodeproj/project.pbxproj index 2f867597fd1..9fa6cb1f87d 100644 --- a/Stripe.xcodeproj/project.pbxproj +++ b/Stripe.xcodeproj/project.pbxproj @@ -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 */; }; @@ -1653,6 +1655,8 @@ D0854905279B9A3D00D3A3DC /* LinkSignupViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkSignupViewModelTests.swift; sourceTree = ""; }; D092A84F274D755C00C021A0 /* LinkToast.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkToast.swift; sourceTree = ""; }; D092A851274D8C9000C021A0 /* icon_link_toast_success@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "icon_link_toast_success@3x.png"; sourceTree = ""; }; + D092E36927BEF8BD00B72609 /* LinkInstantDebitMandateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkInstantDebitMandateView.swift; sourceTree = ""; }; + D092E36E27BF380300B72609 /* LinkInstantDebitMandateViewSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkInstantDebitMandateViewSnapshotTests.swift; sourceTree = ""; }; D092E37627C5616D00B72609 /* LinkAccountServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkAccountServiceTests.swift; sourceTree = ""; }; D09A20D226DEE66800A0D4D9 /* CardBrandView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardBrandView.swift; sourceTree = ""; }; D0BBBC9226DE9243007A9F48 /* STPCardCVCInputTextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardCVCInputTextFieldTests.swift; sourceTree = ""; }; @@ -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 */, @@ -3256,6 +3261,7 @@ D0BEB41D273EDF290031D677 /* LinkWalletFooterView.swift */, D0EC8711278E658E00CFACDC /* LinkKeyboardAvoidingScrollView.swift */, D0ECE12C27A9A9DA0082ED18 /* LinkLegalTermsView.swift */, + D092E36927BEF8BD00B72609 /* LinkInstantDebitMandateView.swift */, ); name = Views; sourceTree = ""; @@ -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 */, @@ -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 */, diff --git a/Stripe/LinkInstantDebitMandateView.swift b/Stripe/LinkInstantDebitMandateView.swift new file mode 100644 index 00000000000..c96fec85a75 --- /dev/null +++ b/Stripe/LinkInstantDebitMandateView.swift @@ -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." + + let formattedString = NSMutableAttributedString() + + STPStringUtils.parseRanges(from: string, withTags: Set(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 + } + +} diff --git a/Stripe/LinkLegalTermsView.swift b/Stripe/LinkLegalTermsView.swift index 2213da8c434..fee4c70a705 100644 --- a/Stripe/LinkLegalTermsView.swift +++ b/Stripe/LinkLegalTermsView.swift @@ -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] = [ @@ -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) diff --git a/Stripe/LinkPaymentMethodPicker.swift b/Stripe/LinkPaymentMethodPicker.swift index 9fc28c2770a..58bdde3796e 100644 --- a/Stripe/LinkPaymentMethodPicker.swift +++ b/Stripe/LinkPaymentMethodPicker.swift @@ -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 { diff --git a/Stripe/LinkUI.swift b/Stripe/LinkUI.swift index 637e7395ba5..d42335d2f88 100644 --- a/Stripe/LinkUI.swift +++ b/Stripe/LinkUI.swift @@ -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 + } + } diff --git a/Stripe/PayWithLinkViewController-WalletViewController.swift b/Stripe/PayWithLinkViewController-WalletViewController.swift index ae645b1a364..b2046c77026 100644 --- a/Stripe/PayWithLinkViewController-WalletViewController.swift +++ b/Stripe/PayWithLinkViewController-WalletViewController.swift @@ -7,6 +7,7 @@ // import UIKit +import SafariServices @_spi(STP) import StripeCore @_spi(STP) import StripeUICore @@ -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() @@ -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, @@ -81,6 +103,7 @@ extension PayWithLinkViewController { super.viewDidLoad() setupUI() + updateUI(animated: false) paymentPicker.delegate = self paymentPicker.dataSource = self @@ -105,7 +128,7 @@ extension PayWithLinkViewController { func setupUI() { let stackView = UIStackView(arrangedSubviews: [ - paymentPicker, + paymentPickerContainerView, confirmButton, footerView, separator, @@ -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 @@ -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") @@ -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( @@ -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) { @@ -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 diff --git a/Stripe/UIStackView+Stripe.swift b/Stripe/UIStackView+Stripe.swift index a3d1edc8691..fb677c43cd7 100644 --- a/Stripe/UIStackView+Stripe.swift +++ b/Stripe/UIStackView+Stripe.swift @@ -19,7 +19,7 @@ extension UIStackView { /// - animated: Whether or not to animate the transition. func showArrangedSubview(at index: Int, animated: Bool) { let view = arrangedSubviews[index] - toggleArrangedSubviews([view], shouldShow: true) + toggleArrangedSubview(view, shouldShow: true, animated: animated) } /// Hides an arranged subview with optional animation. @@ -29,7 +29,17 @@ extension UIStackView { /// - animated: Whether or not to animate the transition. func hideArrangedSubview(at index: Int, animated: Bool) { let view = arrangedSubviews[index] - toggleArrangedSubviews([view], shouldShow: false) + toggleArrangedSubview(view, shouldShow: false, animated: animated) + } + + /// Toggles the visibility of an arranged subview with optional animation. + /// + /// - Parameters: + /// - view: Arranged subview to update. + /// - shouldShow: Whether or not to show the view. + /// - animated: Whether or not to animate the transition. + func toggleArrangedSubview(_ view: UIView, shouldShow: Bool, animated: Bool = true) { + toggleArrangedSubviews([view], shouldShow: shouldShow, animated: animated) } /// Removes an arranged subview at a given index. @@ -49,23 +59,16 @@ extension UIStackView { /// - animated: Whether or not to animate the removal. /// - completion: A block to be called after removing the view. func removeArrangedSubview(_ view: UIView, animated: Bool, completion: (() -> Void)? = nil) { - let removeBlock = { + toggleArrangedSubviews([view], shouldShow: false, animated: animated) { _ in view.removeFromSuperview() view.isHidden = false view.alpha = 1 - } - - if animated { - toggleArrangedSubviews([view], shouldShow: false) { _ in - removeBlock() - completion?() - } - } else { - removeBlock() completion?() } } + // MARK: - Helpers + /// Toggles the visibility of arranged subviews with animation. /// /// This method enhances the default constraint based animation by adding fade-in/out as @@ -73,27 +76,46 @@ extension UIStackView { /// /// - Parameters: /// - views: The arranged subviews to be toggled. - /// - shouldShow: Wheter or not it should show the views. + /// - shouldShow: Whether or not it should show the views. + /// - animated: Whether or not to animate the transition. /// - completion: A block to be called when the animation finishes. - func toggleArrangedSubviews( + private func toggleArrangedSubviews( _ views: [UIView], shouldShow: Bool, + animated: Bool, completion: ((Bool) -> Void)? = nil ) { - let viewsToAnimate = views.filter { $0.isHidden == shouldShow } + let viewsToUpdate = views.filter { $0.isHidden == shouldShow } - viewsToAnimate.forEach { view in - view.isHidden = shouldShow - view.alpha = shouldShow ? 0 : 1 - } + if animated { + let outTransform = CGAffineTransform(translationX: 0, y: -10) + + viewsToUpdate.forEach { view in + view.isHidden = shouldShow + view.alpha = shouldShow ? 0 : 1 + view.transform = shouldShow ? outTransform : .identity + } + + UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut) { + viewsToUpdate.forEach { view in + view.isHidden = !shouldShow + view.alpha = shouldShow ? 1 : 0 + view.transform = shouldShow ? .identity : outTransform + } + + self.setNeedsLayout() + self.layoutIfNeeded() + } completion: { done in + viewsToUpdate.forEach { view in + view.transform = .identity + } - UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut) { - viewsToAnimate.forEach { view in + completion?(done) + } + } else { + viewsToUpdate.forEach { view in view.isHidden = !shouldShow - view.alpha = shouldShow ? 1 : 0 } - } completion: { done in - completion?(done) } } diff --git a/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInstantDebitMandateViewSnapshotTests/testDefault@3x.png b/Tests/ReferenceImages_64/StripeiOS_Tests.LinkInstantDebitMandateViewSnapshotTests/testDefault@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..2eba32e5102c36d70e754d003cf8b663d9866110 GIT binary patch literal 15605 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 literal 0 HcmV?d00001 diff --git a/Tests/Tests/LinkInstantDebitMandateViewSnapshotTests.swift b/Tests/Tests/LinkInstantDebitMandateViewSnapshotTests.swift new file mode 100644 index 00000000000..448f0dd433e --- /dev/null +++ b/Tests/Tests/LinkInstantDebitMandateViewSnapshotTests.swift @@ -0,0 +1,77 @@ +// +// LinkInstantDebitMandateViewSnapshotTests.swift +// StripeiOS Tests +// +// Created by Ramon Torres on 2/17/22. +// Copyright © 2022 Stripe, Inc. All rights reserved. +// + + +import UIKit +import FBSnapshotTestCase + +@testable import Stripe +@_spi(STP) import StripeUICore +@testable @_spi(STP) import StripeCore + +class LinkInstantDebitMandateViewSnapshotTests: FBSnapshotTestCase { + + override func setUp() { + super.setUp() +// recordMode = true + } + + func testDefault() { + let sut = makeSUT() + verify(sut) + } + + // TODO(ramont): Enable when localized. +// func testLocalization() { +// performLocalizedSnapshotTest(forLanguage: "de") +// performLocalizedSnapshotTest(forLanguage: "es") +// performLocalizedSnapshotTest(forLanguage: "el-GR") +// performLocalizedSnapshotTest(forLanguage: "it") +// performLocalizedSnapshotTest(forLanguage: "ja") +// performLocalizedSnapshotTest(forLanguage: "ko") +// performLocalizedSnapshotTest(forLanguage: "zh-Hans") +// } + +} + +// MARK: - Helpers + +extension LinkInstantDebitMandateViewSnapshotTests { + + func verify( + _ view: UIView, + identifier: String? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) { + view.autosizeHeight(width: 250) + FBSnapshotVerifyView(view, identifier: identifier, file: file, line: line) + } + + func performLocalizedSnapshotTest( + forLanguage language: String, + file: StaticString = #filePath, + line: UInt = #line + ) { + STPLocalizationUtils.overrideLanguage(to: language) + let sut = makeSUT() + STPLocalizationUtils.overrideLanguage(to: nil) + verify(sut, identifier: language, file: file, line: line) + } + +} + +// MARK: - Factory + +extension LinkInstantDebitMandateViewSnapshotTests { + + func makeSUT() -> LinkInstantDebitMandateView { + return LinkInstantDebitMandateView() + } + +}