Skip to content

Commit

Permalink
Async init for embedded and initial selection (#4068)
Browse files Browse the repository at this point in the history
## Summary
- Updates the embedded playground to take in the configuration from the
PlaygroundController, and use the actual embedded APIs.
- Implements the `create` for the EmbeddedPaymentElement. This now uses
the PaymentSheetLoader to load the intent and elements/session. This
function also determines with the load result if we should show Link or
Apple Pay, and the first selection in the embedded view.
- Updates `EmbeddedPaymentMethodsView` to take in an initial selection
and select it if present
- Adds an `IntegrationShape` to the PaymentSheetLoader to determine if
we can default to Apple Pay/Link and when to start checkout timer
- Some updated snapshot tests

## Motivation
Embedded

## Testing
- Manual

## Changelog
N/A
  • Loading branch information
porter-stripe authored Sep 30, 2024
1 parent c8cf5d2 commit c147b50
Show file tree
Hide file tree
Showing 16 changed files with 459 additions and 145 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,22 @@
//

import Foundation
@_spi(EmbeddedPaymentElementPrivateBeta) import StripePaymentSheet
@_spi(EmbeddedPaymentElementPrivateBeta) @_spi(STP) @_spi(ExperimentalAllowsRemovalOfLastSavedPaymentMethodAPI) import StripePaymentSheet
import UIKit

protocol EmbeddedPlaygroundViewControllerDelegate: AnyObject {
func didComplete(with result: PaymentSheetResult)
}

class EmbeddedPlaygroundViewController: UIViewController {

private let settings: PaymentSheetTestPlaygroundSettings
private let appearance: PaymentSheet.Appearance
private let intentConfig: PaymentSheet.IntentConfiguration
private let configuration: EmbeddedPaymentElement.Configuration

weak var delegate: EmbeddedPlaygroundViewControllerDelegate?
private var embeddedPaymentElement: EmbeddedPaymentElement!

private lazy var loadingIndicator: UIActivityIndicatorView = {
let indicator = UIActivityIndicatorView(style: .medium)
indicator.translatesAutoresizingMaskIntoConstraints = false
indicator.hidesWhenStopped = true
return indicator
}()

private lazy var checkoutButton: UIButton = {
let checkoutButton = UIButton(type: .system)
Expand All @@ -31,9 +34,11 @@ class EmbeddedPlaygroundViewController: UIViewController {
return checkoutButton
}()

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

super.init(nibName: nil, bundle: nil)
}

Expand All @@ -51,41 +56,55 @@ class EmbeddedPlaygroundViewController: UIViewController {
return .systemBackground
})

// 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)
setupLoadingIndicator()
loadingIndicator.startAnimating()

Task {
do {
try await setupUI()
} catch {
presentError(error)
}

loadingIndicator.stopAnimating()
}
}

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

NSLayoutConstraint.activate([
paymentMethodsView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
paymentMethodsView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
paymentMethodsView.widthAnchor.constraint(equalTo: view.widthAnchor),
embeddedPaymentElement.view.centerXAnchor.constraint(equalTo: view.centerXAnchor),
embeddedPaymentElement.view.centerYAnchor.constraint(equalTo: view.centerYAnchor),
embeddedPaymentElement.view.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": [
"id": "preloaded_amex",
"exp_month": "10",
"exp_year": "2020",
"last4": "0005",
"brand": "amex",
],
"type": "card",
"id": "preloaded_amex",
] as [String: Any]
return STPPaymentMethod.decodedObject(fromAPIResponse: amex)
private func setupLoadingIndicator() {
view.addSubview(loadingIndicator)

NSLayoutConstraint.activate([
loadingIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
loadingIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}

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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import Contacts
import PassKit
@_spi(STP) import StripeCore
@_spi(STP) import StripePayments
@_spi(CustomerSessionBetaAccess) @_spi(STP) @_spi(PaymentSheetSkipConfirmation) @_spi(ExperimentalAllowsRemovalOfLastSavedPaymentMethodAPI) @_spi(ExperimentalPaymentMethodLayoutAPI) import StripePaymentSheet
@_spi(CustomerSessionBetaAccess) @_spi(STP) @_spi(PaymentSheetSkipConfirmation) @_spi(ExperimentalAllowsRemovalOfLastSavedPaymentMethodAPI) @_spi(ExperimentalPaymentMethodLayoutAPI) @_spi(EmbeddedPaymentElementPrivateBeta) import StripePaymentSheet
import SwiftUI
import UIKit

Expand Down Expand Up @@ -177,6 +177,75 @@ class PlaygroundController: ObservableObject {
return configuration
}

var embeddedConfiguration: EmbeddedPaymentElement.Configuration {
var configuration = EmbeddedPaymentElement.Configuration(formSheetAction: .confirm(completion: { [weak self] result in
// TODO(porter) Handle two step confirm
self?.lastPaymentResult = result
}))
configuration.externalPaymentMethodConfiguration = externalPaymentMethodConfiguration
switch settings.externalPaymentMethods {
case .paypal:
configuration.paymentMethodOrder = ["card", "external_paypal"]
case .off, .all: // When using all EPMs, alphabetize the order by not setting `paymentMethodOrder`.
break
}
configuration.merchantDisplayName = "Example, Inc."
configuration.applePay = applePayConfiguration
configuration.customer = customerConfiguration
configuration.appearance = appearance
if settings.userOverrideCountry != .off {
configuration.userOverrideCountry = settings.userOverrideCountry.rawValue
}
configuration.returnURL = "payments-example://stripe-redirect"

if settings.defaultBillingAddress != .off {
configuration.defaultBillingDetails.name = "Jane Doe"
configuration.defaultBillingDetails.address = .init(
city: "San Francisco",
country: "US",
line1: "510 Townsend St.",
postalCode: "94102",
state: "California"
)
}
switch settings.defaultBillingAddress {
case .on:
configuration.defaultBillingDetails.email = "[email protected]"
configuration.defaultBillingDetails.phone = "+13105551234"
case .randomEmail:
configuration.defaultBillingDetails.email = "test-\(UUID().uuidString)@stripe.com"
configuration.defaultBillingDetails.phone = "+13105551234"
case .randomEmailNoPhone:
configuration.defaultBillingDetails.email = "test-\(UUID().uuidString)@stripe.com"
case .customEmail:
configuration.defaultBillingDetails.email = settings.customEmail
case .off:
break
}

if settings.allowsDelayedPMs == .on {
configuration.allowsDelayedPaymentMethods = true
}

if settings.shippingInfo != .off {
configuration.allowsPaymentMethodsRequiringShippingAddress = true
configuration.shippingDetails = { [weak self] in
return self?.addressDetails
}
}
configuration.primaryButtonLabel = settings.customCtaLabel

configuration.billingDetailsCollectionConfiguration.name = .init(rawValue: settings.collectName.rawValue)!
configuration.billingDetailsCollectionConfiguration.phone = .init(rawValue: settings.collectPhone.rawValue)!
configuration.billingDetailsCollectionConfiguration.email = .init(rawValue: settings.collectEmail.rawValue)!
configuration.billingDetailsCollectionConfiguration.address = .init(rawValue: settings.collectAddress.rawValue)!
configuration.billingDetailsCollectionConfiguration.attachDefaultsToPaymentMethod = settings.attachDefaults == .on
configuration.preferredNetworks = settings.preferredNetworksEnabled == .on ? [.visa, .cartesBancaires] : nil
configuration.allowsRemovalOfLastSavedPaymentMethod = settings.allowsRemovalOfLastSavedPaymentMethod == .on

return configuration
}

var addressConfiguration: AddressViewController.Configuration {
var configuration = AddressViewController.Configuration(additionalFields: .init(phone: .optional), appearance: configuration.appearance)
if case .onWithDefaults = settings.shippingInfo {
Expand Down Expand Up @@ -599,7 +668,7 @@ extension PlaygroundController {
)
}
} else if self.settings.uiStyle == .embedded {
self.embeddedPaymentElement()
self.makeEmbeddedPaymentElement()
self.isLoading = false
self.currentlyRenderedSettings = self.settings
}
Expand Down Expand Up @@ -806,14 +875,14 @@ class AnalyticsLogObserver: ObservableObject {
@Published var analyticsLog: [[String: Any]] = []
}


// MARK: Embedded helpers
extension PlaygroundController: EmbeddedPlaygroundViewControllerDelegate {
func embeddedPaymentElement() {
embeddedPlaygroundController = EmbeddedPlaygroundViewController(settings: settings, appearance: appearance)
embeddedPlaygroundController?.delegate = self
extension PlaygroundController {
func makeEmbeddedPaymentElement() {
embeddedPlaygroundController = EmbeddedPlaygroundViewController(configuration: embeddedConfiguration,
intentConfig: intentConfig,
appearance: appearance)
}

func presentEmbedded() {
guard let embeddedPlaygroundController else { return }
let closeButton = UIBarButtonItem(barButtonSystemItem: .close, target: self, action: #selector(dismissEmbedded))
Expand All @@ -826,8 +895,4 @@ extension PlaygroundController: EmbeddedPlaygroundViewControllerDelegate {
@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 @@ -55,19 +55,60 @@ public class EmbeddedPaymentElement {
intentConfiguration: IntentConfiguration,
configuration: Configuration
) async throws -> EmbeddedPaymentElement {
// TODO(https://jira.corp.stripe.com/browse/MOBILESDK-2525)
let dummyView = await EmbeddedPaymentMethodsView(
savedPaymentMethod: nil,
appearance: .default,
shouldShowApplePay: true,
shouldShowLink: true
// TODO(porter) MOBILESDK-2533 Make a protocol for our configurations
let paymentSheetConfiguration = configuration.makePaymentSheetConfiguration()

// TODO(porter) When we do analytics decide how to handle `isCustom`
let analyticsHelper = PaymentSheetAnalyticsHelper(isCustom: true, configuration: paymentSheetConfiguration)
AnalyticsHelper.shared.generateSessionID()

let loadResult = try await PaymentSheetLoader.load(mode: .deferredIntent(intentConfiguration),
configuration: paymentSheetConfiguration,
analyticsHelper: analyticsHelper,
integrationShape: .embedded)

let paymentMethodTypes = PaymentSheet.PaymentMethodType.filteredPaymentMethodTypes(from: .deferredIntent(intentConfig: intentConfiguration),
elementsSession: loadResult.elementsSession,
configuration: paymentSheetConfiguration,
logAvailability: true)
let shouldShowApplePay = PaymentSheet.isApplePayEnabled(elementsSession: loadResult.elementsSession, configuration: paymentSheetConfiguration)
let shouldShowLink = PaymentSheet.isLinkEnabled(elementsSession: loadResult.elementsSession, configuration: paymentSheetConfiguration)
let savedPaymentMethodAccessoryType = await RowButton.RightAccessoryButton.getAccessoryButtonType(
savedPaymentMethodsCount: loadResult.savedPaymentMethods.count,
isFirstCardCoBranded: loadResult.savedPaymentMethods.first?.isCoBrandedCard ?? false,
isCBCEligible: loadResult.elementsSession.isCardBrandChoiceEligible,
allowsRemovalOfLastSavedPaymentMethod: configuration.allowsRemovalOfLastSavedPaymentMethod,
allowsPaymentMethodRemoval: loadResult.elementsSession.allowsRemovalOfPaymentMethodsForPaymentSheet()
)

let initialSelection: EmbeddedPaymentMethodsView.Selection? = {
// Default to the customer's default or the first saved payment method, if any
let customerDefault = CustomerPaymentOption.defaultPaymentMethod(for: configuration.customer?.id)
switch customerDefault {
case .applePay:
return .applePay
case .link:
return .link
case .stripeId, nil:
return loadResult.savedPaymentMethods.first.map { .saved(paymentMethod: $0) }
}
}()

let embeddedPaymentMethodsView = await EmbeddedPaymentMethodsView(
initialSelection: initialSelection,
paymentMethodTypes: paymentMethodTypes,
savedPaymentMethod: loadResult.savedPaymentMethods.first,
appearance: configuration.appearance,
shouldShowApplePay: shouldShowApplePay,
shouldShowLink: shouldShowLink,
savedPaymentMethodAccessoryType: savedPaymentMethodAccessoryType
)
return .init(view: dummyView, configuration: configuration)
return .init(view: embeddedPaymentMethodsView, configuration: configuration)
}

/// The result of an `update` call
@frozen public enum UpdateResult {
/// The update succeded
/// The update succeeded
case succeeded
/// The update was canceled. This is only returned when a subsequent `update` call cancels previous ones.
case canceled
Expand Down Expand Up @@ -180,3 +221,41 @@ extension EmbeddedPaymentElement {
public typealias BillingDetailsCollectionConfiguration = PaymentSheet.BillingDetailsCollectionConfiguration
public typealias ExternalPaymentMethodConfiguration = PaymentSheet.ExternalPaymentMethodConfiguration
}

// TODO(porter) MOBILESDK-2533 Create a protocol for the commonalities between PaymentSheet.Configuration <> EmbeddedPaymentElement.Configuration
extension EmbeddedPaymentElement.Configuration {
func makePaymentSheetConfiguration() -> PaymentSheet.Configuration {
var paymentConfig = PaymentSheet.Configuration()

paymentConfig.allowsDelayedPaymentMethods = allowsDelayedPaymentMethods
paymentConfig.allowsPaymentMethodsRequiringShippingAddress = allowsPaymentMethodsRequiringShippingAddress
paymentConfig.apiClient = apiClient
paymentConfig.applePay = applePay
paymentConfig.primaryButtonColor = primaryButtonColor
paymentConfig.primaryButtonLabel = primaryButtonLabel
paymentConfig.style = style
paymentConfig.customer = customer
paymentConfig.merchantDisplayName = merchantDisplayName
paymentConfig.returnURL = returnURL
paymentConfig.defaultBillingDetails = defaultBillingDetails
paymentConfig.savePaymentMethodOptInBehavior = savePaymentMethodOptInBehavior
paymentConfig.appearance = appearance
paymentConfig.shippingDetails = shippingDetails
paymentConfig.preferredNetworks = preferredNetworks
paymentConfig.userOverrideCountry = userOverrideCountry
paymentConfig.billingDetailsCollectionConfiguration = billingDetailsCollectionConfiguration
paymentConfig.removeSavedPaymentMethodMessage = removeSavedPaymentMethodMessage
paymentConfig.externalPaymentMethodConfiguration = externalPaymentMethodConfiguration
paymentConfig.paymentMethodOrder = paymentMethodOrder
paymentConfig.allowsRemovalOfLastSavedPaymentMethod = allowsRemovalOfLastSavedPaymentMethod

/* Note:
There are 3 properties that differ today:
hidesMandateText
formSheetAction
paymentMethodLayout
*/

return paymentConfig
}
}
Loading

0 comments on commit c147b50

Please sign in to comment.