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,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
Copy link
Collaborator

Choose a reason for hiding this comment

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

ticket or immediate followup? We should probably make an embedded-only formSheetAction toggle in the playground

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I can make a ticket, in my head I was thinking the week of Oct. 7 it would be added but I can add sooner

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'll make a follow up right now and add the hide mandate API too.

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()
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 +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
Loading