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

Embedded update pt 3 #4168

Merged
merged 5 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import UIKit

/// For internal SDK use only.
@objc(STP_Internal_DownloadManager)
// TODO: Refactor this API shape! https://github.com/stripe/stripe-ios/pull/3487#discussion_r1561337866
// TODO: https://jira.corp.stripe.com/browse/MOBILESDK-2604 Refactor this!
@_spi(STP) public class DownloadManager: NSObject {
public typealias UpdateImageHandler = (UIImage) -> Void

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,27 @@
//
// Created by Yuki Tokuhiro on 10/10/24.
//
@_spi(STP) import StripeCore
@_spi(STP) import StripePayments
@_spi(STP) import StripePaymentsUI
@_spi(STP) import StripeUICore
import UIKit

extension EmbeddedPaymentElement {
@MainActor
static func makeView(
configuration: Configuration,
loadResult: PaymentSheetLoader.LoadResult,
analyticsHelper: PaymentSheetAnalyticsHelper,
previousPaymentOption: PaymentOption? = nil,
delegate: EmbeddedPaymentMethodsViewDelegate? = nil
) -> EmbeddedPaymentMethodsView {
// Restore the customer's previous payment method.
// Caveats:
// - Only payment method details (including checkbox state) and billing details are restored
// - Only restored if the previous input resulted in a completed form i.e. partial or invalid input is still discarded
// TODO: Restore the form, if any

let shouldShowApplePay = PaymentSheet.isApplePayEnabled(elementsSession: loadResult.elementsSession, configuration: configuration)
let shouldShowLink = PaymentSheet.isLinkEnabled(elementsSession: loadResult.elementsSession, configuration: configuration)
let savedPaymentMethodAccessoryType = RowButton.RightAccessoryButton.getAccessoryButtonType(
Expand All @@ -23,7 +35,23 @@ extension EmbeddedPaymentElement {
allowsPaymentMethodRemoval: loadResult.elementsSession.allowsRemovalOfPaymentMethodsForPaymentSheet()
)
let initialSelection: EmbeddedPaymentMethodsView.Selection? = {
// Default to the customer's default or the first saved payment method, if any
// Select the previous payment option
switch previousPaymentOption {
case .applePay:
return .applePay
case .link:
return .link
case .external(paymentMethod: let paymentMethod, billingDetails: _):
return .new(paymentMethodType: .external(paymentMethod))
case .saved(paymentMethod: let paymentMethod, confirmParams: _):
return .saved(paymentMethod: paymentMethod)
case .new(confirmParams: let confirmParams):
return .new(paymentMethodType: confirmParams.paymentMethodType)
case nil:
break
}

// If there's no previous customer input, 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:
Expand Down Expand Up @@ -66,3 +94,34 @@ extension EmbeddedPaymentElement: EmbeddedPaymentMethodsViewDelegate {
delegate?.embeddedPaymentElementDidUpdatePaymentOption(embeddedPaymentElement: self)
}
}

// MARK: - EmbeddedPaymentElement.PaymentOptionDisplayData

extension EmbeddedPaymentElement.PaymentOptionDisplayData {
init(paymentOption: PaymentOption, mandateText: NSAttributedString?) {
self.mandateText = mandateText
self.image = paymentOption.makeIcon(updateImageHandler: nil) // ☠️ This can make a blocking network request TODO: https://jira.corp.stripe.com/browse/MOBILESDK-2604 Refactor this!
switch paymentOption {
case .applePay:
label = String.Localized.apple_pay
paymentMethodType = "apple_pay"
billingDetails = nil
case .saved(let paymentMethod, _):
label = paymentMethod.paymentSheetLabel
paymentMethodType = paymentMethod.type.identifier
billingDetails = paymentMethod.billingDetails?.toPaymentSheetBillingDetails()
case .new(let confirmParams):
label = confirmParams.paymentSheetLabel
paymentMethodType = confirmParams.paymentMethodType.identifier
billingDetails = confirmParams.paymentMethodParams.billingDetails?.toPaymentSheetBillingDetails()
case .link(let option):
label = option.paymentSheetLabel
paymentMethodType = STPPaymentMethodType.link.identifier
billingDetails = option.billingDetails?.toPaymentSheetBillingDetails()
case .external(let paymentMethod, let stpBillingDetails):
label = paymentMethod.label
paymentMethodType = paymentMethod.type
billingDetails = stpBillingDetails.toPaymentSheetBillingDetails()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,16 @@ public final class EmbeddedPaymentElement {
public let paymentMethodType: String
/// If you set `configuration.embeddedViewDisplaysMandateText = false`, this text must be displayed to the customer near your “Buy” button to comply with regulations.
public let mandateText: NSAttributedString?

}

/// Contains information about the customer's selected payment option.
/// Use this to display the payment option in your own UI
public var paymentOption: PaymentOptionDisplayData? {
return embeddedPaymentMethodsView.displayData
guard let _paymentOption else {
return nil
}
return .init(paymentOption: _paymentOption, mandateText: embeddedPaymentMethodsView.mandateText)
}

/// An asynchronous failable initializer
Expand Down Expand Up @@ -103,7 +107,8 @@ public final class EmbeddedPaymentElement {
currentUpdateTask?.cancel()
_ = await currentUpdateTask?.value
// Start the new update task
let currentUpdateTask = Task { [weak self, configuration, paymentOption, analyticsHelper] in
let currentUpdateTask = Task { @MainActor [weak self, configuration, analyticsHelper] in
// ⚠️ Don't modify `self` until the end to avoid being canceled halfway through and leaving self in a partially updated state.
// 1. Reload v1/elements/session.
let loadResult: PaymentSheetLoader.LoadResult
do {
Expand All @@ -126,26 +131,28 @@ public final class EmbeddedPaymentElement {
configuration: configuration,
loadResult: loadResult,
analyticsHelper: analyticsHelper,
previousPaymentOption: self?._paymentOption,
delegate: self
// TODO: https://jira.corp.stripe.com/browse/MOBILESDK-2583 Restore previous payment option
)

// 2. Pre-load image into cache
// Hack: Accessing paymentOption has the side-effect of ensuring its `image` property is loaded (from the internet instead of disk) before we call the completion handler.
// 3. Pre-load image into cache
// Call this on a detached Task b/c this synchronously (!) loads the image from network and we don't want to block the main actor
let fetchPaymentOption = Task.detached(priority: .userInitiated) {
return await embeddedPaymentMethodsView.displayData
// This has the nasty side effect of synchronously downloading the image (see https://jira.corp.stripe.com/browse/MOBILESDK-2604)
// This caches it so that DownloadManager doesn't block the main thread when the merchant tries to access the image
return await embeddedPaymentMethodsView.selection?.paymentMethodType?.makeImage(updateHandler: nil)
}
_ = await fetchPaymentOption.value

guard let self, !Task.isCancelled else {
return .canceled
}
// At this point, we're the latest update - update self properties and inform our delegate.
// At this point, we're still the latest update and update is successful - update self properties and inform our delegate.
let oldPaymentOption = self.paymentOption
self.loadResult = loadResult
self.embeddedPaymentMethodsView = embeddedPaymentMethodsView
self.containerView.updateEmbeddedPaymentMethodsView(embeddedPaymentMethodsView)
if paymentOption != embeddedPaymentMethodsView.displayData {
if oldPaymentOption != self.paymentOption {
self.delegate?.embeddedPaymentElementDidUpdatePaymentOption(embeddedPaymentElement: self)
}
return .succeeded
Expand All @@ -169,6 +176,31 @@ public final class EmbeddedPaymentElement {
internal private(set) var loadResult: PaymentSheetLoader.LoadResult
internal private(set) var currentUpdateTask: Task<UpdateResult, Never>?
private let analyticsHelper: PaymentSheetAnalyticsHelper
internal var _paymentOption: PaymentOption? {
// TODO: Handle forms. See `PaymentSheetVerticalViewController.selectedPaymentOption`.
// TODO: Handle CVC recollection
switch embeddedPaymentMethodsView.selection {
case .applePay:
return .applePay
case .link:
return .link(option: .wallet)
case let .new(paymentMethodType: paymentMethodType):
let params = IntentConfirmParams(type: paymentMethodType)
params.setDefaultBillingDetailsIfNecessary(for: configuration)
switch paymentMethodType {
case .stripe:
return .new(confirmParams: params)
case .external(let type):
return .external(paymentMethod: type, billingDetails: params.paymentMethodParams.nonnil_billingDetails)
case .instantDebits, .linkCardBrand:
return .new(confirmParams: params)
}
case .saved(paymentMethod: let paymentMethod):
return .saved(paymentMethod: paymentMethod, confirmParams: nil)
case .none:
return nil
}
}

private init(
configuration: Configuration,
Expand Down Expand Up @@ -278,3 +310,12 @@ extension EmbeddedPaymentElement {
public typealias BillingDetailsCollectionConfiguration = PaymentSheet.BillingDetailsCollectionConfiguration
public typealias ExternalPaymentMethodConfiguration = PaymentSheet.ExternalPaymentMethodConfiguration
}

// MARK: - EmbeddedPaymentElement.PaymentOptionDisplayData

extension EmbeddedPaymentElement.PaymentOptionDisplayData {
public static func == (lhs: Self, rhs: Self) -> Bool {
// Unfortunately, we need to manually define this because the implementation of Equatable on UIImage does not work
return lhs.image.pngData() == rhs.image.pngData() && rhs.label == lhs.label && lhs.billingDetails == rhs.billingDetails && lhs.paymentMethodType == rhs.paymentMethodType && lhs.mandateText == rhs.mandateText
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
//

import Foundation
@_spi(STP) import StripeCore
@_spi(STP) import StripeUICore
import UIKit

Expand All @@ -21,11 +20,6 @@ class EmbeddedPaymentMethodsView: UIView {

typealias Selection = VerticalPaymentMethodListSelection // TODO(porter) Maybe define our own later

var displayData: EmbeddedPaymentElement.PaymentOptionDisplayData? {
guard let selection else { return nil }
return .init(selection: selection, mandateText: mandateView.attributedText)
}

private let appearance: PaymentSheet.Appearance
private(set) var selection: Selection? {
didSet {
Expand All @@ -37,8 +31,13 @@ class EmbeddedPaymentMethodsView: UIView {
}
private let mandateProvider: MandateTextProvider
private let shouldShowMandate: Bool
/// A bit hacky; this is the mandate text for the given payment method, *regardless* of whether it is shown in the view.
/// It'd be better if the source of truth of mandate text was not the view and instead an independent `func mandateText(...) -> NSAttributedString` function, but this is hard b/c US Bank Account doesn't show mandate in certain states.
var mandateText: NSAttributedString? {
mandateView.attributedText
}

lazy var stackView: UIStackView = {
private(set) lazy var stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.spacing = appearance.embeddedPaymentElement.style == .floatingButton ? appearance.embeddedPaymentElement.row.floating.spacing : 0
Expand Down Expand Up @@ -136,16 +135,24 @@ class EmbeddedPaymentMethodsView: UIView {
stackView.addArrangedSubview(linkRowButton)
}

// Add all non-card PMs (card is added above)
for paymentMethodType in paymentMethodTypes where paymentMethodType != .stripe(.card) {
stackView.addArrangedSubview(RowButton.makeForPaymentMethodType(paymentMethodType: paymentMethodType,
subtitle: VerticalPaymentMethodListViewController.subtitleText(for: paymentMethodType),
savedPaymentMethodType: savedPaymentMethod?.type,
appearance: rowButtonAppearance,
shouldAnimateOnPress: true,
isEmbedded: true,
didTap: { [weak self] rowButton in
self?.didTap(selectedRowButton: rowButton, selection: .new(paymentMethodType: paymentMethodType))
}))
let selection: Selection = .new(paymentMethodType: paymentMethodType)
let rowButton = RowButton.makeForPaymentMethodType(
paymentMethodType: paymentMethodType,
subtitle: VerticalPaymentMethodListViewController.subtitleText(for: paymentMethodType),
savedPaymentMethodType: savedPaymentMethod?.type,
appearance: rowButtonAppearance,
shouldAnimateOnPress: true,
isEmbedded: true,
didTap: { [weak self] rowButton in
self?.didTap(selectedRowButton: rowButton, selection: selection)
}
)
if initialSelection == selection {
rowButton.isSelected = true
}
stackView.addArrangedSubview(rowButton)
}

if appearance.embeddedPaymentElement.style != .floatingButton {
Expand Down Expand Up @@ -249,37 +256,3 @@ extension PaymentSheet.Appearance.EmbeddedPaymentElement.Style {
}
}
}
@_spi(STP) import StripePayments
@_spi(STP) import StripePaymentsUI

extension EmbeddedPaymentElement.PaymentOptionDisplayData {
init(selection: EmbeddedPaymentMethodsView.Selection, mandateText: NSAttributedString?) {
self.mandateText = mandateText

switch selection {
case .new(paymentMethodType: let paymentMethodType):
image = paymentMethodType.makeImage(
forDarkBackground: UITraitCollection.current.isDarkMode,
updateHandler: nil
)
label = paymentMethodType.displayName
self.paymentMethodType = paymentMethodType.identifier
billingDetails = nil // TODO(porter) Handle billing details when we present forms (maybe set this to defaultBillingDetails) if billingDetailsConfiguration.attachDefaultsToPaymentMethod is true
case .saved(paymentMethod: let paymentMethod):
image = paymentMethod.makeIcon()
label = paymentMethod.paymentSheetLabel
paymentMethodType = paymentMethod.type.identifier
billingDetails = paymentMethod.billingDetails?.toPaymentSheetBillingDetails()
case .applePay:
image = Image.apple_pay_mark.makeImage().withRenderingMode(.alwaysOriginal)
label = .Localized.apple_pay
paymentMethodType = "apple_pay"
billingDetails = nil // TODO(porter) Handle billing details when we present forms
case .link:
image = Image.link_logo.makeImage()
label = STPPaymentMethodType.link.displayName
paymentMethodType = STPPaymentMethodType.link.identifier
billingDetails = nil // TODO(porter) Handle billing details when we present forms
}
}
}
Loading
Loading