Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async init for embedded and initial selection #4068

Merged
merged 15 commits into from
Sep 30, 2024
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,28 @@
//

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

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

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)
checkoutButton.backgroundColor = appearance.primaryButton.backgroundColor ?? appearance.colors.primary
Expand All @@ -31,9 +39,14 @@ class EmbeddedPlaygroundViewController: UIViewController {
return checkoutButton
}()

init(settings: PaymentSheetTestPlaygroundSettings, appearance: PaymentSheet.Appearance) {
self.settings = settings
init(configuration: PaymentSheet.Configuration, intentConfig: PaymentSheet.IntentConfiguration, appearance: PaymentSheet.Appearance, delegate: EmbeddedPlaygroundViewControllerDelegate?) {
self.appearance = appearance
self.intentConfig = intentConfig
self.configuration = .init(from: configuration, formSheetAction: .confirm(completion: { result in
// TODO(porter) Probably pass in formSheetAction from PlaygroundController based on some toggle in the UI
delegate?.didComplete(with: result)
}), hidesMandateText: false)

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

Expand All @@ -51,41 +64,97 @@ 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),
])
}

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)
}
}
}

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)
extension EmbeddedPaymentElement.Configuration {

/// Initializes an EmbeddedPaymentElement.Configuration from a given PaymentSheet.Configuration.
///
/// - Parameters:
/// - paymentSheetConfig: The PaymentSheet.Configuration instance to convert from.
/// - formSheetAction: The FormSheetAction specific to EmbeddedPaymentElement.Configuration.
/// - hidesMandateText: Determines whether to hide mandate text. Defaults to `false`.
public init(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How will we add Embedded-specific stuff like hidesMandateText or formSheetAction if we go from playground settings -> PaymentSheet.Configuration -> EmbeddedConfiguration?

Maybe we should make the "playground settings -> EmbeddedConfiguration" logic happen in the PlaygroundController or something?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think creating the EmbeddedConfiguration in the PlaygroundController sounds good, I was thinking we could just inject those two properties into this view controller but that doesn't scale well if we add more. Will update this in this PR.

from paymentSheetConfig: PaymentSheet.Configuration,
formSheetAction: FormSheetAction,
hidesMandateText: Bool = false
) {
self = .init(formSheetAction: formSheetAction)

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

// Handle unique properties for EmbeddedPaymentElement.Configuration
self.hidesMandateText = hidesMandateText
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ extension PlaygroundController {
)
}
} else if self.settings.uiStyle == .embedded {
self.embeddedPaymentElement()
self.makeEmbeddedPaymentElement()
self.isLoading = false
self.currentlyRenderedSettings = self.settings
}
Expand Down Expand Up @@ -804,14 +804,12 @@ 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
func makeEmbeddedPaymentElement() {
embeddedPlaygroundController = EmbeddedPlaygroundViewController(configuration: configuration, intentConfig: intentConfig, appearance: appearance, delegate: self)
}

func presentEmbedded() {
guard let embeddedPlaygroundController else { return }
let closeButton = UIBarButtonItem(barButtonSystemItem: .close, target: self, action: #selector(dismissEmbedded))
Expand All @@ -824,7 +822,7 @@ extension PlaygroundController: EmbeddedPlaygroundViewControllerDelegate {
@objc func dismissEmbedded() {
embeddedPlaygroundController?.dismiss(animated: true, completion: nil)
}

func didComplete(with result: StripePaymentSheet.PaymentSheetResult) {
lastPaymentResult = result
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,59 @@ 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
let paymentSheetConfiguration = configuration.makePaymentSheetConfiguration()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a temporary hack right? If so, let's document it


// 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 +220,41 @@ extension EmbeddedPaymentElement {
public typealias BillingDetailsCollectionConfiguration = PaymentSheet.BillingDetailsCollectionConfiguration
public typealias ExternalPaymentMethodConfiguration = PaymentSheet.ExternalPaymentMethodConfiguration
}

// TODO(porter) Create a protocol for the commonalities between PaymentSheet.Configuration <> EmbeddedPaymentElement.Configuration
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you gonna follow up on this immediately or should we make a ticket

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah will do later, just created: https://jira.corp.stripe.com/browse/MOBILESDK-2533

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
Loading