diff --git a/Example/PaymentSheet Example/PaymentSheet Example/EmbeddedPlaygroundViewController.swift b/Example/PaymentSheet Example/PaymentSheet Example/EmbeddedPlaygroundViewController.swift index 176e44efc42..aee32c16b18 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example/EmbeddedPlaygroundViewController.swift +++ b/Example/PaymentSheet Example/PaymentSheet Example/EmbeddedPlaygroundViewController.swift @@ -9,7 +9,38 @@ import Foundation @_spi(EmbeddedPaymentMethodsViewBeta) import StripePaymentSheet import UIKit +protocol EmbeddedPlaygroundViewControllerDelegate: AnyObject { + func didComplete(with result: PaymentSheetResult) +} + class EmbeddedPlaygroundViewController: UIViewController { + + private let settings: PaymentSheetTestPlaygroundSettings + private let appearance: PaymentSheet.Appearance + + weak var delegate: EmbeddedPlaygroundViewControllerDelegate? + + private lazy var checkoutButton: UIButton = { + let checkoutButton = UIButton(type: .system) + checkoutButton.backgroundColor = appearance.primaryButton.backgroundColor ?? appearance.colors.primary + checkoutButton.layer.cornerRadius = 5.0 + checkoutButton.clipsToBounds = true + checkoutButton.setTitle("Checkout", for: .normal) + checkoutButton.setTitleColor(.white, for: .normal) + checkoutButton.translatesAutoresizingMaskIntoConstraints = false + return checkoutButton + }() + + init(settings: PaymentSheetTestPlaygroundSettings, appearance: PaymentSheet.Appearance) { + self.settings = settings + self.appearance = appearance + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func viewDidLoad() { super.viewDidLoad() self.view.backgroundColor = UIColor(dynamicProvider: { traitCollection in @@ -20,9 +51,29 @@ class EmbeddedPlaygroundViewController: UIViewController { return .systemBackground }) - var appearance = PaymentSheet.Appearance.default - appearance.paymentOptionView.style = .flatRadio + // TODO: pass in an embedded configuration built from `PaymentSheetTestPlaygroundSettings` + let paymentMethodsView = EmbeddedPaymentMethodsView(savedPaymentMethod: settings.customerMode == .returning ? .mockPaymentMethod : nil, + appearance: appearance, + shouldShowApplePay: settings.applePayEnabled == .on, + shouldShowLink: settings.linkMode == .link_pm) + paymentMethodsView.translatesAutoresizingMaskIntoConstraints = false + self.view.addSubview(paymentMethodsView) + self.view.addSubview(checkoutButton) + NSLayoutConstraint.activate([ + paymentMethodsView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + paymentMethodsView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + paymentMethodsView.widthAnchor.constraint(equalTo: view.widthAnchor), + 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), + ]) + } +} + +extension STPPaymentMethod { + static var mockPaymentMethod: STPPaymentMethod? { let amex = [ "card": [ @@ -35,16 +86,6 @@ class EmbeddedPlaygroundViewController: UIViewController { "type": "card", "id": "preloaded_amex", ] as [String: Any] - let paymentMethod = STPPaymentMethod.decodedObject(fromAPIResponse: amex) - - let paymentMethodsView = EmbeddedPaymentMethodsView(savedPaymentMethod: paymentMethod, appearance: appearance, shouldShowApplePay: true, shouldShowLink: true) - paymentMethodsView.translatesAutoresizingMaskIntoConstraints = false - self.view.addSubview(paymentMethodsView) - - NSLayoutConstraint.activate([ - paymentMethodsView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - paymentMethodsView.centerYAnchor.constraint(equalTo: view.centerYAnchor), - paymentMethodsView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1), - ]) + return STPPaymentMethod.decodedObject(fromAPIResponse: amex) } } diff --git a/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlayground.swift b/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlayground.swift index 542dbc52d23..34d25b3075e 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlayground.swift +++ b/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlayground.swift @@ -23,8 +23,8 @@ struct PaymentSheetTestPlayground: View { // Note: Use group to work around XCode 14: "Extra Argument in Call" issue // (each view can hold 10 direct subviews) Group { - SettingView(setting: $playgroundController.settings.uiStyle) - SettingView(setting: $playgroundController.settings.layout) + SettingView(setting: uiStyleBinding) + SettingView(setting: $playgroundController.settings.layout).disabled(playgroundController.settings.uiStyle == .embedded) SettingView(setting: $playgroundController.settings.shippingInfo) SettingView(setting: $playgroundController.settings.applePayEnabled) SettingView(setting: $playgroundController.settings.applePayButtonType) @@ -90,7 +90,7 @@ struct PaymentSheetTestPlayground: View { }) } SettingView(setting: $playgroundController.settings.mode) - SettingPickerView(setting: $playgroundController.settings.integrationType) + SettingPickerView(setting: integrationTypeBinding).disabled(playgroundController.settings.uiStyle == .embedded) SettingView(setting: $playgroundController.settings.customerKeyType) SettingView(setting: customerModeBinding) SettingPickerView(setting: $playgroundController.settings.amount) @@ -234,12 +234,38 @@ struct PaymentSheetTestPlayground: View { } } } + + var uiStyleBinding: Binding { + Binding { + return playgroundController.settings.uiStyle + } set: { newUIStyle in + // If we switch to embedded set confirmation type to deferred CSC if in intent first confirmation type + if newUIStyle == .embedded && playgroundController.settings.integrationType == .normal { + playgroundController.settings.integrationType = .deferred_csc + } + + playgroundController.settings.uiStyle = newUIStyle + } + } + + var integrationTypeBinding: Binding { + Binding { + return playgroundController.settings.integrationType + } set: { newIntegrationType in + // If switching to CSC and embedded is selected, reset to PaymentSheet + if newIntegrationType == .normal && playgroundController.settings.uiStyle == .embedded { + playgroundController.settings.uiStyle = .paymentSheet + } + playgroundController.settings.integrationType = newIntegrationType + } + } } @available(iOS 14.0, *) struct PaymentSheetButtons: View { @EnvironmentObject var playgroundController: PlaygroundController @State var psIsPresented: Bool = false + @State var embeddedIsPresented: Bool = false @State var psFCOptionsIsPresented: Bool = false @State var psFCIsConfirming: Bool = false @@ -298,7 +324,7 @@ struct PaymentSheetButtons: View { ExamplePaymentStatusView(result: result) } } - } else { + } else if playgroundController.settings.uiStyle == .flowController { VStack { HStack { Text("PaymentSheet.FlowController") @@ -355,6 +381,55 @@ struct PaymentSheetButtons: View { ExamplePaymentStatusView(result: result) } } + } else if playgroundController.settings.uiStyle == .embedded { + VStack { + HStack { + Text("Embedded mobile payment element") + .font(.subheadline.smallCaps()) + Spacer() + if playgroundController.isLoading { + ProgressView() + } else { + if playgroundController.settings != playgroundController.currentlyRenderedSettings { + StaleView() + } + Button { + reloadPlaygroundController() + } label: { + Image(systemName: "arrow.clockwise.circle") + } + .accessibility(identifier: "Reload") + .frame(alignment: .topLeading) + } + }.padding(.horizontal) + + if let _ = playgroundController.embeddedPlaygroundController, + playgroundController.lastPaymentResult == nil || playgroundController.lastPaymentResult?.shouldAllowPresentingPaymentSheet() ?? false { + HStack { + Button { + embeddedIsPresented = true + playgroundController.presentEmbedded() + } label: { + Text("Present embedded payment element") + } + Spacer() + Button { + playgroundController.didTapShippingAddressButton() + } label: { + Text("\(playgroundController.addressDetails?.localizedDescription ?? "Address")") + .accessibility(identifier: "Address") + } + } + .padding() + } else { + Text("Embedded payment element is nil") + .foregroundColor(.gray) + .padding() + } + if let result = playgroundController.lastPaymentResult { + ExamplePaymentStatusView(result: result) + } + } } } } diff --git a/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlaygroundSettings.swift b/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlaygroundSettings.swift index 9a5e2024546..cb192e02536 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlaygroundSettings.swift +++ b/Example/PaymentSheet Example/PaymentSheet Example/PaymentSheetTestPlaygroundSettings.swift @@ -13,6 +13,7 @@ struct PaymentSheetTestPlaygroundSettings: Codable, Equatable { case paymentSheet case flowController + case embedded } enum Mode: String, PickerEnum { diff --git a/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift b/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift index 56dc40b3b9f..eac68da6c3b 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift +++ b/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift @@ -21,6 +21,7 @@ import UIKit class PlaygroundController: ObservableObject { @Published var paymentSheetFlowController: PaymentSheet.FlowController? @Published var paymentSheet: PaymentSheet? + @Published var embeddedPlaygroundController: EmbeddedPlaygroundViewController? @Published var settings: PaymentSheetTestPlaygroundSettings @Published var currentlyRenderedSettings: PaymentSheetTestPlaygroundSettings @Published var addressDetails: AddressViewController.AddressDetails? @@ -460,6 +461,7 @@ extension PlaygroundController { paymentSheetFlowController = nil addressViewController = nil paymentSheet = nil + embeddedPlaygroundController = nil lastPaymentResult = nil isLoading = true let settingsToLoad = self.settings @@ -551,7 +553,7 @@ extension PlaygroundController { self.buildPaymentSheet() self.isLoading = false self.currentlyRenderedSettings = self.settings - } else { + } else if self.settings.uiStyle == .flowController { let completion: (Result) -> Void = { result in self.currentlyRenderedSettings = self.settings switch result { @@ -594,6 +596,10 @@ extension PlaygroundController { completion: completion ) } + } else if self.settings.uiStyle == .embedded { + self.embeddedPaymentElement() + self.isLoading = false + self.currentlyRenderedSettings = self.settings } } } @@ -797,3 +803,29 @@ class AnalyticsLogObserver: ObservableObject { /// All analytic events sent by the SDK since the playground was loaded. @Published var analyticsLog: [[String: Any]] = [] } + + +// MARK: Embedded helpers +extension PlaygroundController: EmbeddedPlaygroundViewControllerDelegate { + func embeddedPaymentElement() { + embeddedPlaygroundController = EmbeddedPlaygroundViewController(settings: settings, appearance: appearance) + embeddedPlaygroundController?.delegate = self + } + + func presentEmbedded() { + guard let embeddedPlaygroundController else { return } + let closeButton = UIBarButtonItem(barButtonSystemItem: .close, target: self, action: #selector(dismissEmbedded)) + embeddedPlaygroundController.navigationItem.leftBarButtonItem = closeButton + + let navController = UINavigationController(rootViewController: embeddedPlaygroundController) + rootViewController.present(navController, animated: true) + } + + @objc func dismissEmbedded() { + embeddedPlaygroundController?.dismiss(animated: true, completion: nil) + } + + func didComplete(with result: StripePaymentSheet.PaymentSheetResult) { + lastPaymentResult = result + } +} diff --git a/Example/PaymentSheet Example/PaymentSheet Example/Resources/Base.lproj/Main.storyboard b/Example/PaymentSheet Example/PaymentSheet Example/Resources/Base.lproj/Main.storyboard index acb6507486f..e73093deda3 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example/Resources/Base.lproj/Main.storyboard +++ b/Example/PaymentSheet Example/PaymentSheet Example/Resources/Base.lproj/Main.storyboard @@ -93,7 +93,7 @@ - @@ -452,22 +444,6 @@ - - - - - - - - - - - - - - - - diff --git a/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift b/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift index 1172f110583..495b70feb4c 100644 --- a/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift +++ b/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift @@ -2289,6 +2289,9 @@ class PaymentSheetLinkUITests: PaymentSheetUITestCase { case .flowController: app.buttons["Continue"].tap() app.buttons["Confirm"].waitForExistenceAndTap() + case .embedded: + // TODO(porter) Fill in embedded UI test steps + break } XCTAssertTrue(app.staticTexts["Success!"].waitForExistence(timeout: 10.0)) // Roundabout way to validate that signup completed successfully