Skip to content

Commit

Permalink
Merge branch 'master' into joyceqin-bideprecation
Browse files Browse the repository at this point in the history
  • Loading branch information
joyceqin-stripe authored Oct 11, 2024
2 parents ddabe4f + 7b829a1 commit 9dc9759
Show file tree
Hide file tree
Showing 20 changed files with 257 additions and 134 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ final class InstantDebitsUITests: XCTestCase {
XCTAssertTrue(featuredLegacyTestInstitution.waitForExistence(timeout: 60.0))
featuredLegacyTestInstitution.tap()

app.fc_nativePrepaneContinueButton.tap()
app.fc_nativeConnectAccountsButton.tap()
app.fc_nativeSuccessDoneButton.tap()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import UIKit

class EmbeddedPlaygroundViewController: UIViewController {
private let appearance: PaymentSheet.Appearance
private let intentConfig: PaymentSheet.IntentConfiguration

private let configuration: EmbeddedPaymentElement.Configuration

private var embeddedPaymentElement: EmbeddedPaymentElement!
private let intentConfig: EmbeddedPaymentElement.IntentConfiguration

private(set) var embeddedPaymentElement: EmbeddedPaymentElement?

private lazy var loadingIndicator: UIActivityIndicatorView = {
let indicator = UIActivityIndicatorView(style: .medium)
Expand All @@ -33,13 +35,19 @@ class EmbeddedPlaygroundViewController: UIViewController {
checkoutButton.translatesAutoresizingMaskIntoConstraints = false
return checkoutButton
}()


private let settingsViewContainer = UIStackView()

private let paymentOptionView = EmbeddedPaymentOptionView()

init(configuration: EmbeddedPaymentElement.Configuration, intentConfig: PaymentSheet.IntentConfiguration, appearance: PaymentSheet.Appearance) {
init(
configuration: EmbeddedPaymentElement.Configuration,
intentConfig: EmbeddedPaymentElement.IntentConfiguration,
appearance: PaymentSheet.Appearance
) {
self.appearance = appearance
self.intentConfig = intentConfig
self.configuration = configuration
self.intentConfig = intentConfig

super.init(nibName: nil, bundle: nil)
}
Expand All @@ -61,40 +69,57 @@ class EmbeddedPlaygroundViewController: UIViewController {
setupLoadingIndicator()
loadingIndicator.startAnimating()

Task {
Task { @MainActor in
do {
try await setupUI()
} catch {
presentError(error)
let alert = UIAlertController(
title: "Error loading Embedded Payment Element",
message: error.localizedDescription,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
self.present(alert, animated: true)
}

loadingIndicator.stopAnimating()
}
}

private func setupUI() async throws {
embeddedPaymentElement = try await EmbeddedPaymentElement.create(intentConfiguration: intentConfig,
configuration: configuration)
let embeddedPaymentElement = try await EmbeddedPaymentElement.create(
intentConfiguration: intentConfig,
configuration: configuration
)
embeddedPaymentElement.delegate = self
embeddedPaymentElement.view.translatesAutoresizingMaskIntoConstraints = false
paymentOptionView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(embeddedPaymentElement.view)
self.view.addSubview(paymentOptionView)
self.view.addSubview(checkoutButton)
self.embeddedPaymentElement = embeddedPaymentElement

// Scroll view contains our content
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scrollView)

// All our content is in a stack view
let stackView = UIStackView(arrangedSubviews: [settingsViewContainer, embeddedPaymentElement.view, paymentOptionView, checkoutButton])
stackView.axis = .vertical
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.isLayoutMarginsRelativeArrangement = true
stackView.layoutMargins = .init(top: 0, left: 16, bottom: 0, right: 16)
stackView.spacing = 16
scrollView.addSubview(stackView)

NSLayoutConstraint.activate([
embeddedPaymentElement.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
embeddedPaymentElement.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
embeddedPaymentElement.view.widthAnchor.constraint(equalTo: view.widthAnchor),
paymentOptionView.topAnchor.constraint(equalTo: embeddedPaymentElement.view.bottomAnchor, constant: 25),
paymentOptionView.widthAnchor.constraint(equalTo: view.widthAnchor),
paymentOptionView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
checkoutButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.9),
checkoutButton.heightAnchor.constraint(equalToConstant: 50),
checkoutButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
checkoutButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20),
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),

scrollView.contentLayoutGuide.topAnchor.constraint(equalTo: stackView.topAnchor),
scrollView.contentLayoutGuide.bottomAnchor.constraint(equalTo: stackView.bottomAnchor),
scrollView.contentLayoutGuide.leadingAnchor.constraint(equalTo: stackView.leadingAnchor),
scrollView.contentLayoutGuide.trailingAnchor.constraint(equalTo: stackView.trailingAnchor),
scrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: view.widthAnchor),
])

paymentOptionView.configure(with: embeddedPaymentElement.paymentOption, showMandate: !configuration.embeddedViewDisplaysMandateText)
}

Expand All @@ -107,31 +132,29 @@ class EmbeddedPlaygroundViewController: UIViewController {
])
}

private func presentError(_ error: Error) {
DispatchQueue.main.async {
let alert = UIAlertController(title: "Error",
message: error.localizedDescription,
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
self.present(alert, animated: true)
}
func setSettingsView(_ settingsView: UIView) {
settingsViewContainer.arrangedSubviews.forEach { settingsViewContainer.removeArrangedSubview($0) }
settingsViewContainer.addArrangedSubview(settingsView)
}
}

// MARK: - EmbeddedPaymentElementDelegate

extension EmbeddedPlaygroundViewController: EmbeddedPaymentElementDelegate {
func embeddedPaymentElementDidUpdateHeight(embeddedPaymentElement: StripePaymentSheet.EmbeddedPaymentElement) {
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
}

func embeddedPaymentElementDidUpdatePaymentOption(embeddedPaymentElement: EmbeddedPaymentElement) {
paymentOptionView.configure(with: embeddedPaymentElement.paymentOption, showMandate: !configuration.embeddedViewDisplaysMandateText)
}
}

// MARK: - EmbeddedPaymentOptionView

private class EmbeddedPaymentOptionView: UIView {

private let titleLabel: UILabel = {
let label = UILabel()
label.font = .preferredFont(forTextStyle: .body)
Expand All @@ -140,22 +163,22 @@ private class EmbeddedPaymentOptionView: UIView {
label.text = "Selected payment method"
return label
}()

private let imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()

private let label: UILabel = {
let label = UILabel()
label.font = .preferredFont(forTextStyle: .subheadline)
label.numberOfLines = 1
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()

private let mandateTextLabel: UILabel = {
let mandateLabel = UILabel()
mandateLabel.font = .preferredFont(forTextStyle: .footnote)
Expand All @@ -165,45 +188,45 @@ private class EmbeddedPaymentOptionView: UIView {
mandateLabel.textAlignment = .left
return mandateLabel
}()

override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}

required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}

private func setupView() {
addSubview(titleLabel)
addSubview(imageView)
addSubview(label)
addSubview(mandateTextLabel)

NSLayoutConstraint.activate([
titleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 15),
titleLabel.topAnchor.constraint(equalTo: self.topAnchor),
titleLabel.widthAnchor.constraint(equalTo: self.widthAnchor),
titleLabel.heightAnchor.constraint(equalToConstant: 25),

imageView.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
imageView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 20),
imageView.widthAnchor.constraint(equalToConstant: 25),
imageView.heightAnchor.constraint(equalToConstant: 25),

label.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 12),
label.trailingAnchor.constraint(equalTo: self.trailingAnchor),
label.topAnchor.constraint(equalTo: self.topAnchor),
label.centerYAnchor.constraint(equalTo: imageView.centerYAnchor),

mandateTextLabel.leadingAnchor.constraint(equalTo: imageView.leadingAnchor),
mandateTextLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -12),
mandateTextLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 12),
])
}

func configure(with data: EmbeddedPaymentElement.PaymentOptionDisplayData?, showMandate: Bool) {
titleLabel.isHidden = data == nil
imageView.image = data?.image
Expand All @@ -212,4 +235,3 @@ private class EmbeddedPaymentOptionView: UIView {
mandateTextLabel.isHidden = !showMandate
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ struct PaymentSheetTestPlayground: View {
.environmentObject(playgroundController)
}.animationUnlessTesting()
}

var paymentMethodSaveBinding: Binding<PaymentSheetTestPlaygroundSettings.PaymentMethodSave> {
Binding<PaymentSheetTestPlaygroundSettings.PaymentMethodSave> {
return playgroundController.settings.paymentMethodSave
Expand Down Expand Up @@ -289,6 +289,13 @@ struct PaymentSheetButtons: View {
playgroundController.load(reinitializeControllers: true)
}

// This exists so that the embedded playground vc (EPVC) can call the `EmbeddedPaymentElement.update` API
// We build the settings view here, rather than in EPVC, so that it can easily update the PI/SI like all other settings and ensure the PI/SI is up to date when it's eventually used at confirm-time
@ViewBuilder
var embeddedSettingsView: some View {
SettingView(setting: $playgroundController.settings.mode)
}

var titleAndReloadView: some View {
HStack {
Text(playgroundController.settings.uiStyle.rawValue)
Expand Down Expand Up @@ -391,7 +398,7 @@ struct PaymentSheetButtons: View {
HStack {
Button {
embeddedIsPresented = true
playgroundController.presentEmbedded()
playgroundController.presentEmbedded(settingsView: embeddedSettingsView)
} label: {
Text("Present embedded payment element")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -559,21 +559,22 @@ extension PlaygroundController {
self.isLoading = false
self.currentlyRenderedSettings = self.settings
}
let shouldUpdateFlowControllerInsteadOfRecreating: Bool = {
let onlyDifferenceBetweenSettingsIsMode: Bool = {
var oldModifiedWithNewMode = currentlyRenderedSettings
oldModifiedWithNewMode.mode = settings.mode
return oldModifiedWithNewMode == settings
}()
let isDeferred = settings.integrationType != .normal
return !reinitializeControllers && onlyDifferenceBetweenSettingsIsMode && isDeferred && paymentSheetFlowController != nil
let onlyDifferenceBetweenSettingsIsMode: Bool = {
var oldModifiedWithNewMode = currentlyRenderedSettings
oldModifiedWithNewMode.mode = settings.mode
return oldModifiedWithNewMode == settings
}()
let isDeferred = settings.integrationType != .normal
let shouldUpdateEmbeddedInsteadOfRecreating = !reinitializeControllers && onlyDifferenceBetweenSettingsIsMode && isDeferred && embeddedPlaygroundViewController != nil
if !shouldUpdateEmbeddedInsteadOfRecreating {
embeddedPlaygroundViewController = nil
}
let shouldUpdateFlowControllerInsteadOfRecreating = !reinitializeControllers && onlyDifferenceBetweenSettingsIsMode && isDeferred && paymentSheetFlowController != nil
if !shouldUpdateFlowControllerInsteadOfRecreating {
paymentSheetFlowController = nil
}
addressViewController = nil
paymentSheet = nil
embeddedPlaygroundViewController = nil
lastPaymentResult = nil
isLoading = true
let settingsToLoad = self.settings
Expand Down Expand Up @@ -653,11 +654,12 @@ extension PlaygroundController {
let intentID = STPPaymentIntent.id(fromClientSecret: self.clientSecret ?? "") ?? STPSetupIntent.id(fromClientSecret: self.clientSecret ?? "")// Avoid logging client secrets as a matter of best practice even though this is testmode
print("✅ Test playground finished loading with intent id: \(intentID ?? "")) and customer id: \(self.customerId ?? "") ")

if self.settings.uiStyle == .paymentSheet {
switch self.settings.uiStyle {
case .paymentSheet:
self.buildPaymentSheet()
self.isLoading = false
self.currentlyRenderedSettings = self.settings
} else if self.settings.uiStyle == .flowController {
case .flowController:
guard !shouldUpdateFlowControllerInsteadOfRecreating else {
// Update FC rather than re-creating it
self.updateFlowController()
Expand Down Expand Up @@ -707,7 +709,13 @@ extension PlaygroundController {
completion: completion
)
}
} else if self.settings.uiStyle == .embedded {
case .embedded:
guard !shouldUpdateEmbeddedInsteadOfRecreating else {
// Update embedded rather than re-creating it
self.updateEmbedded()
self.currentlyRenderedSettings = self.settings
return
}
self.makeEmbeddedPaymentElement()
self.isLoading = false
self.currentlyRenderedSettings = self.settings
Expand Down Expand Up @@ -918,20 +926,51 @@ class AnalyticsLogObserver: ObservableObject {
// MARK: Embedded helpers
extension PlaygroundController {
func makeEmbeddedPaymentElement() {
embeddedPlaygroundViewController = EmbeddedPlaygroundViewController(configuration: embeddedConfiguration,
intentConfig: intentConfig,
appearance: appearance)
embeddedPlaygroundViewController = EmbeddedPlaygroundViewController(
configuration: embeddedConfiguration,
intentConfig: intentConfig,
appearance: appearance
)
}

func presentEmbedded() {
func presentEmbedded(settingsView: some View) {
guard let embeddedPlaygroundViewController else { return }

// Include settings view
let hostingController = UIHostingController(rootView: settingsView)
embeddedPlaygroundViewController.addChild(hostingController)
hostingController.didMove(toParent: rootViewController)
embeddedPlaygroundViewController.setSettingsView(hostingController.view)

let closeButton = UIBarButtonItem(barButtonSystemItem: .close, target: self, action: #selector(dismissEmbedded))
embeddedPlaygroundViewController.navigationItem.leftBarButtonItem = closeButton

let navController = UINavigationController(rootViewController: embeddedPlaygroundViewController)
rootViewController.present(navController, animated: true)
}

func updateEmbedded() {
Task { @MainActor in
guard let embeddedPlaygroundViewController else { return }
let result = await embeddedPlaygroundViewController.embeddedPaymentElement?.update(intentConfiguration: intentConfig)
switch result {
case .canceled, nil:
// Do nothing; this happens when a subsequent `update` call cancels this one
break
case .failed(let error):
// Display error to user in an alert, let them retry
let alert = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(.init(title: "Retry", style: .default, handler: { _ in
self.updateEmbedded()
}))
alert.addAction(.init(title: "Cancel", style: .cancel))
embeddedPlaygroundViewController.present(alert, animated: true)
case .succeeded:
self.isLoading = false
}
}
}

@objc func dismissEmbedded() {
embeddedPlaygroundViewController?.dismiss(animated: true, completion: nil)
}
Expand Down
Loading

0 comments on commit 9dc9759

Please sign in to comment.