Skip to content

Commit

Permalink
Add embedded to playground (#4025)
Browse files Browse the repository at this point in the history
## Summary
- Adds an embedded toggle to the playground that when enabled results in
a new view controller being presented with the embedded integration
- Passes in the settings and appearance. In the future we may need to
pass more info in, but can add as needed.
- When embedded is selected we enforce that we are not using CSC or
horizontal mode



https://github.com/user-attachments/assets/8ddd0f80-2fb7-433a-955f-3b3363c31ec3



## Motivation
- Easier embedded testing

## Testing
- Manual

## Changelog
N/A
  • Loading branch information
porter-stripe authored Sep 25, 2024
1 parent 3b8e1f6 commit ffdd561
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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": [
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -234,12 +234,38 @@ struct PaymentSheetTestPlayground: View {
}
}
}

var uiStyleBinding: Binding<PaymentSheetTestPlaygroundSettings.UIStyle> {
Binding<PaymentSheetTestPlaygroundSettings.UIStyle> {
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<PaymentSheetTestPlaygroundSettings.IntegrationType> {
Binding<PaymentSheetTestPlaygroundSettings.IntegrationType> {
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

Expand Down Expand Up @@ -298,7 +324,7 @@ struct PaymentSheetButtons: View {
ExamplePaymentStatusView(result: result)
}
}
} else {
} else if playgroundController.settings.uiStyle == .flowController {
VStack {
HStack {
Text("PaymentSheet.FlowController")
Expand Down Expand Up @@ -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)
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ struct PaymentSheetTestPlaygroundSettings: Codable, Equatable {

case paymentSheet
case flowController
case embedded
}

enum Mode: String, PickerEnum {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -460,6 +461,7 @@ extension PlaygroundController {
paymentSheetFlowController = nil
addressViewController = nil
paymentSheet = nil
embeddedPlaygroundController = nil
lastPaymentResult = nil
isLoading = true
let settingsToLoad = self.settings
Expand Down Expand Up @@ -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<PaymentSheet.FlowController, Error>) -> Void = { result in
self.currentlyRenderedSettings = self.settings
switch result {
Expand Down Expand Up @@ -594,6 +596,10 @@ extension PlaygroundController {
completion: completion
)
}
} else if self.settings.uiStyle == .embedded {
self.embeddedPaymentElement()
self.isLoading = false
self.currentlyRenderedSettings = self.settings
}
}
}
Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,29 +93,21 @@
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="hIk-Ur-mhf">
<rect key="frame" x="0.0" y="108" width="222" height="0.0"/>
<rect key="frame" x="0.0" y="108" width="222" height="16.5"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" title="CustomerSheet (SwiftUI)"/>
<connections>
<segue destination="AjW-Xm-4P3" kind="show" destinationCreationSelector="showSwiftUICusotmerSheetSwiftUI:" id="ODf-7Z-F15"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" id="Ysb-4K-dUG">
<rect key="frame" x="0.0" y="120" width="222" height="0.0"/>
<rect key="frame" x="0.0" y="136.5" width="222" height="30"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<state key="normal" title="LinkPaymentController"/>
<connections>
<segue destination="jqF-43-4W3" kind="show" id="I4Q-i5-Ogf"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="PqJ-Uq-tvb">
<rect key="frame" x="0.0" y="132" width="222" height="34.5"/>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="plain" title="Embedded playground"/>
<connections>
<segue destination="UO7-ym-FxO" kind="show" id="8Y8-nE-pda"/>
</connections>
</button>
</subviews>
</stackView>
</subviews>
Expand Down Expand Up @@ -452,22 +444,6 @@
</objects>
<point key="canvasLocation" x="1400" y="877"/>
</scene>
<!--Embedded Playground View Controller-->
<scene sceneID="8b9-XO-txM">
<objects>
<viewController id="UO7-ym-FxO" customClass="EmbeddedPlaygroundViewController" customModule="PaymentSheetExample" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="T1q-Uc-aU5">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<viewLayoutGuide key="safeArea" id="HQL-0B-cUt"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
<navigationItem key="navigationItem" id="MhB-nP-jRJ"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="8fv-x9-cLD" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2245" y="859"/>
</scene>
<!--Checkout (Custom)-->
<scene sceneID="QR4-jL-qym">
<objects>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit ffdd561

Please sign in to comment.