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

Implement embedded update (part 1) #4141

Merged
merged 11 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from 10 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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.tail linguist-generated=true
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@
B6B3481CBA798CF22EE8411A /* TextFieldElement+IBAN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 570931B897DCCAC0F55FB6E3 /* TextFieldElement+IBAN.swift */; };
B6BF12392C2F2E790033601E /* PaymentSheetImageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6BF12382C2F2E780033601E /* PaymentSheetImageTests.swift */; };
B6CACC9E2CB8B90700682ECE /* EmbeddedPaymentElement+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CACC9D2CB8B8E800682ECE /* EmbeddedPaymentElement+Internal.swift */; };
B6CACCA02CBD9A3300682ECE /* EmbeddedPaymentElementTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6CACC9F2CBD9A3300682ECE /* EmbeddedPaymentElementTest.swift */; };
B6D1B3662C5DFCE800A9E0D7 /* PaymentSheetAnalyticsHelperTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6D1B3652C5DFCE800A9E0D7 /* PaymentSheetAnalyticsHelperTest.swift */; };
B6E8C7FB2C184A0300B977B2 /* PaymentSheetFormFactory+Mandates.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E8C7FA2C184A0300B977B2 /* PaymentSheetFormFactory+Mandates.swift */; };
B6F4C5F82CADB0CB00AF3767 /* USBankAccountPaymentMethodElementTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F4C5F72CADB0CB00AF3767 /* USBankAccountPaymentMethodElementTest.swift */; };
Expand Down Expand Up @@ -607,6 +608,7 @@
B68CB9622B0D2169006ACDB1 /* STPAPIClient+PaymentSheetTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "STPAPIClient+PaymentSheetTest.swift"; sourceTree = "<group>"; };
B6BF12382C2F2E780033601E /* PaymentSheetImageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentSheetImageTests.swift; sourceTree = "<group>"; };
B6CACC9D2CB8B8E800682ECE /* EmbeddedPaymentElement+Internal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmbeddedPaymentElement+Internal.swift"; sourceTree = "<group>"; };
B6CACC9F2CBD9A3300682ECE /* EmbeddedPaymentElementTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedPaymentElementTest.swift; sourceTree = "<group>"; };
B6D1B3652C5DFCE800A9E0D7 /* PaymentSheetAnalyticsHelperTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentSheetAnalyticsHelperTest.swift; sourceTree = "<group>"; };
B6E8C7FA2C184A0300B977B2 /* PaymentSheetFormFactory+Mandates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PaymentSheetFormFactory+Mandates.swift"; sourceTree = "<group>"; };
B6F4C5F72CADB0CB00AF3767 /* USBankAccountPaymentMethodElementTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = USBankAccountPaymentMethodElementTest.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1425,11 +1427,12 @@
989C2E3E03E42DA64A2FAE0D /* CustomerSheetSnapshotTests.swift */,
31699A822BE183D40048677F /* DownloadManagerTest.swift */,
73FB30705EC36BD0868904A2 /* Elements+TestHelpers.swift */,
B6CACC9F2CBD9A3300682ECE /* EmbeddedPaymentElementTest.swift */,
61FB6BCC2C8901B200F8E074 /* EmbeddedPaymentMethodsViewSnapshotTests.swift */,
614068E12CB0BF10003D2F12 /* EmbeddedPaymentMethodsViewTests.swift */,
64C8F350CDB5A29F62E86592 /* FlowControllerStateTests.swift */,
6B0E5AE62C08F4B5008AAFBE /* IntentConfirmParamsTest.swift */,
61D842902CB06047009D2D51 /* FormMandateProviderTests.swift */,
6B0E5AE62C08F4B5008AAFBE /* IntentConfirmParamsTest.swift */,
990304EF35A0EE37DCE20D5B /* IntentStatusPollerTest.swift */,
FCA28FF8CD5BA829A44CDCE7 /* Link */,
33B8F21A22FA091BC9D2924B /* LinkStubs.swift */,
Expand Down Expand Up @@ -1719,6 +1722,7 @@
9C42A106FF30A22CFBB38AE8 /* Elements+TestHelpers.swift in Sources */,
D442C49095DDEC2C6ADAF392 /* FlowControllerStateTests.swift in Sources */,
619AF0852BF56C5E00D1C981 /* PaymentMethodRowButtonSnapshotTests.swift in Sources */,
B6CACCA02CBD9A3300682ECE /* EmbeddedPaymentElementTest.swift in Sources */,
2EC9C94DD8D62E4F4EFC8AB8 /* IntentStatusPollerTest.swift in Sources */,
ABC3A7CF6D5B21D0C9684A09 /* LinkPopupURLParserTests.swift in Sources */,
F94F6A157CEB937896B682D4 /* LinkURLGeneratorTests.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ extension EmbeddedPaymentElement {
@MainActor
static func makeView(
configuration: Configuration,
loadResult: PaymentSheetLoader.LoadResult
loadResult: PaymentSheetLoader.LoadResult,
delegate: EmbeddedPaymentMethodsViewDelegate? = nil
) -> EmbeddedPaymentMethodsView {
let shouldShowApplePay = PaymentSheet.isApplePayEnabled(elementsSession: loadResult.elementsSession, configuration: configuration)
let shouldShowLink = PaymentSheet.isLinkEnabled(elementsSession: loadResult.elementsSession, configuration: configuration)
Expand Down Expand Up @@ -46,7 +47,8 @@ extension EmbeddedPaymentElement {
shouldShowLink: shouldShowLink,
savedPaymentMethodAccessoryType: savedPaymentMethodAccessoryType,
mandateProvider: mandateProvider,
shouldShowMandate: configuration.embeddedViewDisplaysMandateText
shouldShowMandate: configuration.embeddedViewDisplaysMandateText,
delegate: delegate
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import UIKit
/// An object that manages a view that displays payment methods and completes a checkout.
@_spi(EmbeddedPaymentElementPrivateBeta)
@MainActor
public class EmbeddedPaymentElement {
public final class EmbeddedPaymentElement {

/// A view that displays payment methods. It can present a sheet to collect more details or display saved payment methods.
public var view: UIView {
// TODO: Make this a _container view_ so that we can swap out the inner `embeddedPaymentMethodsView` when `update` is called.
return embeddedPaymentMethodsView
}

Expand All @@ -29,7 +30,8 @@ public class EmbeddedPaymentElement {
/// See `EmbeddedPaymentElementDelegate`.
public weak var delegate: EmbeddedPaymentElementDelegate?

public struct PaymentOptionDisplayData {
/// Contains details about a payment method that can be displayed to the customer
public struct PaymentOptionDisplayData: Equatable {
/// An image representing a payment method; e.g. the Apple Pay logo or a VISA logo
public let image: UIImage
/// A user facing string representing the payment method; e.g. "Apple Pay" or "····4242" for a card
Expand Down Expand Up @@ -90,13 +92,65 @@ public class EmbeddedPaymentElement {
/// Call this method when the IntentConfiguration values you used to initialize `EmbeddedPaymentElement` (amount, currency, etc.) change.
/// This ensures the appropriate payment methods are displayed, collect the right fields, etc.
/// - Parameter intentConfiguration: An updated IntentConfiguration.
/// - Returns: The result of the update. Any calls made to `update` before this call that are still in progress will return a `.canceled` result.
/// - Returns: The result of the update.
/// - Note: Upon completion, `paymentOption` may become nil if it's no longer available.
/// - Note: If you call `update` while a previous call to `update` is still in progress, the previous call returns `.canceled`.
public func update(
intentConfiguration: IntentConfiguration
) async -> UpdateResult {
// TODO(https://jira.corp.stripe.com/browse/MOBILESDK-2524)
return .succeeded
// Cancel the old task and let it finish so that merchants receive update results in order
currentUpdateTask?.cancel()
_ = await currentUpdateTask?.value
// Start the new update task
let currentUpdateTask = Task { [weak self, configuration, paymentOption] in
// 1. Reload v1/elements/session.
let loadResult: PaymentSheetLoader.LoadResult
do {
// TODO: Change dummy analytics helper
let dummyAnalyticsHelper = PaymentSheetAnalyticsHelper(isCustom: false, configuration: .init())
// TODO(nice to have): Make `load` respect task cancellation to reduce network consumption
loadResult = try await PaymentSheetLoader.load(
mode: .deferredIntent(intentConfiguration),
configuration: configuration,
analyticsHelper: dummyAnalyticsHelper,
integrationShape: .embedded
)
} catch {
return UpdateResult.failed(error: error)
}
guard !Task.isCancelled else {
return UpdateResult.canceled
}

// 2. Re-initialize embedded view to update the UI to match the newly loaded data.
let embeddedPaymentMethodsView = Self.makeView(
configuration: configuration,
loadResult: loadResult,
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.
// 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
}
_ = 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.
self.loadResult = loadResult
self.embeddedPaymentMethodsView = embeddedPaymentMethodsView
if paymentOption != embeddedPaymentMethodsView.displayData {
self.delegate?.embeddedPaymentElementDidUpdatePaymentOption(embeddedPaymentElement: self)
}
return .succeeded
}
self.currentUpdateTask = currentUpdateTask
return await currentUpdateTask.value
}

/// Completes the payment or setup.
Expand All @@ -109,18 +163,16 @@ public class EmbeddedPaymentElement {

// MARK: - Internal

private let embeddedPaymentMethodsView: EmbeddedPaymentMethodsView

private let loadResult: PaymentSheetLoader.LoadResult
internal private(set) var embeddedPaymentMethodsView: EmbeddedPaymentMethodsView
internal private(set) var loadResult: PaymentSheetLoader.LoadResult
internal private(set) var currentUpdateTask: Task<UpdateResult, Never>?

private init(
configuration: Configuration,
loadResult: PaymentSheetLoader.LoadResult,
delegate: EmbeddedPaymentElementDelegate? = nil
loadResult: PaymentSheetLoader.LoadResult
) {
self.configuration = configuration
self.loadResult = loadResult
self.delegate = delegate
self.embeddedPaymentMethodsView = Self.makeView(
configuration: configuration,
loadResult: loadResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,23 @@ class EmbeddedPaymentMethodsView: UIView {

weak var delegate: EmbeddedPaymentMethodsViewDelegate?

init(initialSelection: Selection?,
paymentMethodTypes: [PaymentSheet.PaymentMethodType],
savedPaymentMethod: STPPaymentMethod?,
appearance: PaymentSheet.Appearance,
shouldShowApplePay: Bool,
shouldShowLink: Bool,
savedPaymentMethodAccessoryType: RowButton.RightAccessoryButton.AccessoryType?,
mandateProvider: MandateTextProvider,
shouldShowMandate: Bool = true) {
init(
initialSelection: Selection?,
paymentMethodTypes: [PaymentSheet.PaymentMethodType],
savedPaymentMethod: STPPaymentMethod?,
appearance: PaymentSheet.Appearance,
shouldShowApplePay: Bool,
shouldShowLink: Bool,
savedPaymentMethodAccessoryType: RowButton.RightAccessoryButton.AccessoryType?,
mandateProvider: MandateTextProvider,
shouldShowMandate: Bool = true,
delegate: EmbeddedPaymentMethodsViewDelegate? = nil
) {
self.appearance = appearance
self.selection = initialSelection
self.mandateProvider = mandateProvider
self.shouldShowMandate = shouldShowMandate
self.delegate = delegate
super.init(frame: .zero)

let rowButtonAppearance = appearance.embeddedPaymentElement.style.appearanceForStyle(appearance: appearance)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ extension UIStackView {
func addSeparators(color: UIColor, backgroundColor: UIColor, thickness: CGFloat = 1, inset: UIEdgeInsets, addTopSeparator: Bool = true, addBottomSeparator: Bool = true) {
let numberOfSeparators = arrangedSubviews.count - 1

for i in 1...numberOfSeparators {
addSeparator(color: color, backgroundColor: backgroundColor, thickness: thickness, inset: inset, at: i * 2 - 1)
if numberOfSeparators > 0 {
for i in 1...numberOfSeparators {
addSeparator(color: color, backgroundColor: backgroundColor, thickness: thickness, inset: inset, at: i * 2 - 1)
}
}

if addTopSeparator {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,12 @@ final class PaymentSheetLoader {
static func fetchElementsSessionAndIntent(mode: PaymentSheet.InitializationMode, configuration: PaymentElementConfiguration, analyticsHelper: PaymentSheetAnalyticsHelper) async throws -> ElementSessionAndIntent {
let intent: Intent
let elementsSession: STPElementsSession
let clientDefaultPaymentMethod = defaultStripePaymentMethodId(forCustomerID: configuration.customer?.id)
let clientDefaultPaymentMethod: String? = {
guard let customer = configuration.customer else {
return nil
}
return defaultStripePaymentMethodId(forCustomerID: customer.id)
}()

switch mode {
case .paymentIntentClientSecret(let clientSecret):
Expand Down
Loading
Loading