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

Support Instant Debits with deferred intents #4154

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
4 changes: 4 additions & 0 deletions StripeCore/StripeCore.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
C9C320ADCCF1548D6562CE94 /* File_IdentityDocument.json in Resources */ = {isa = PBXBuildFile; fileRef = DC24A98C4020646F99456187 /* File_IdentityDocument.json */; };
CA09DC1EC4142701B31F9673 /* UIImage+StripeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45D4100901F9445AC1FD453A /* UIImage+StripeCore.swift */; };
CAF857D45689FBEF17627E80 /* BundleLocatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 937066801E91C99C50192364 /* BundleLocatorProtocol.swift */; };
CB0E9DC82CB9C79E00E083D1 /* LinkBankPaymentMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB0E9DC72CB9C79E00E083D1 /* LinkBankPaymentMethod.swift */; };
CB1FB2383FAEE0194C39E4DE /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = E86AC7DD5F4DE2780E0AC425 /* OHHTTPStubsSwift */; };
CB8A47A5FD057112CB607DE9 /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5334916D2A4F927645C2569 /* MockData.swift */; };
D144C3A657E5C16975CB2191 /* NSError+StripeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23506F3E93ECA5A96DCE7E31 /* NSError+StripeCore.swift */; };
Expand Down Expand Up @@ -318,6 +319,7 @@
C3DCE66C04A91C235972687D /* StripeiOS-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "StripeiOS-Debug.xcconfig"; sourceTree = "<group>"; };
C51179DB520568C246BF3AF0 /* URLEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLEncoder.swift; sourceTree = "<group>"; };
C666CC926642D7AA76E75B5B /* StripeCoreTestUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StripeCoreTestUtils.h; sourceTree = "<group>"; };
CB0E9DC72CB9C79E00E083D1 /* LinkBankPaymentMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkBankPaymentMethod.swift; sourceTree = "<group>"; };
CB2721EE8E075E700FF3E58A /* StripeiOS-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "StripeiOS-Release.xcconfig"; sourceTree = "<group>"; };
CC70CDF482E22A29B11466F7 /* STPAPIClient+EmptyResponseTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "STPAPIClient+EmptyResponseTest.swift"; sourceTree = "<group>"; };
CD9288E147B8C9D33CCB5045 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/Localizable.strings"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -456,6 +458,7 @@
6A05FB4A2BCF245C0001D128 /* FinancialConnectionsEvent.swift */,
493B33052CA3015600E3622F /* LinkMode.swift */,
492039922CA47A8600CE2072 /* ElementsSessionContext.swift */,
CB0E9DC72CB9C79E00E083D1 /* LinkBankPaymentMethod.swift */,
);
path = "Connections Bindings";
sourceTree = "<group>";
Expand Down Expand Up @@ -1019,6 +1022,7 @@
096274D0729AA8849FAD103C /* PaymentsSDKVariant.swift in Sources */,
DA5A05459309B9B77ACDD736 /* STPDeviceUtils.swift in Sources */,
4910B9282C3D8F3F00B030D4 /* Result+Extensions.swift in Sources */,
CB0E9DC82CB9C79E00E083D1 /* LinkBankPaymentMethod.swift in Sources */,
83790210FFC2DD764C042C8E /* STPDispatchFunctions.swift in Sources */,
72DA29CA8A750E8B00DBF3D4 /* STPError.swift in Sources */,
F628BBE9FDA9D3A217ACA753 /* STPNumericStringValidator.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@
import Foundation

@_spi(STP) public struct InstantDebitsLinkedBank: Equatable {
public let paymentMethodId: String
public let paymentMethod: LinkBankPaymentMethod
public let bankName: String?
public let last4: String?
public let linkMode: LinkMode?

public init(
paymentMethodId: String,
paymentMethod: LinkBankPaymentMethod,
bankName: String?,
last4: String?,
linkMode: LinkMode?
) {
self.paymentMethodId = paymentMethodId
self.paymentMethod = paymentMethod
self.bankName = bankName
self.last4 = last4
self.linkMode = linkMode
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// LinkBankPaymentMethod.swift
// StripeCore
//
// Created by Till Hellmund on 10/11/24.
//

import Foundation

/// This struct represents the encoded `PaymentMethod` that we receive during the Instant Debits flow.
/// We don't decode it into a proper struct to prevent said struct (which would live in StripeCore) from getting
/// out-of-sync with `STPPaymentMethod`, which this payment method will eventually be decoded into.
@_spi(STP) public struct LinkBankPaymentMethod: UnknownFieldsDecodable, Equatable {
public var _allResponseFieldsStorage: NonEncodableParameters?
public var id: String
}
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ protocol FinancialConnectionsAPI {
func paymentMethods(
consumerSessionClientSecret: String,
paymentDetailsId: String
) -> Future<FinancialConnectionsPaymentMethod>
) -> Future<LinkBankPaymentMethod>
}

extension FinancialConnectionsAPIClient: FinancialConnectionsAPI {
Expand Down Expand Up @@ -1021,7 +1021,7 @@ extension FinancialConnectionsAPIClient: FinancialConnectionsAPI {
func paymentMethods(
consumerSessionClientSecret: String,
paymentDetailsId: String
) -> Future<FinancialConnectionsPaymentMethod> {
) -> Future<LinkBankPaymentMethod> {
let parameters: [String: Any] = [
"link": [
"credentials": [
Expand All @@ -1033,7 +1033,7 @@ extension FinancialConnectionsAPIClient: FinancialConnectionsAPI {
]

return updateAndApplyFraudDetection(to: parameters)
.chained { [weak self] parametersWithTelemetry -> Future<FinancialConnectionsPaymentMethod> in
.chained { [weak self] parametersWithTelemetry -> Future<LinkBankPaymentMethod> in
guard let self else {
return Promise(
error: FinancialConnectionsSheetError.unknown(debugDescription: "FinancialConnectionsAPIClient was deallocated.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
//

import Foundation

protocol PaymentMethodIDProvider {
var id: String { get }
}
@_spi(STP) import StripeCore

struct FinancialConnectionsPaymentDetails: Decodable {
let redactedPaymentDetails: RedactedPaymentDetails
Expand All @@ -25,15 +22,6 @@ struct BankAccountDetails: Decodable {
let last4: String?
}

struct FinancialConnectionsPaymentMethod: Decodable {
let id: String
}

struct FinancialConnectionsSharePaymentDetails: Decodable {
let paymentMethod: FinancialConnectionsPaymentMethod
}

extension FinancialConnectionsPaymentMethod: PaymentMethodIDProvider {}
extension FinancialConnectionsSharePaymentDetails: PaymentMethodIDProvider {
var id: String { paymentMethod.id }
let paymentMethod: LinkBankPaymentMethod
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ final public class FinancialConnectionsSheet {
let errorDescription = "Instant Debits is not currently supported via this interface."
let sessionInfo =
"""
paymentMethodId=\(linkedBank.paymentMethodId)
paymentMethodId=\(linkedBank.paymentMethod.id)
bankName=\(linkedBank.bankName ?? "N/A")
last4=\(linkedBank.last4 ?? "N/A")
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ extension NativeFlowController {
consumerSessionClientSecret: consumerSession.clientSecret,
bankAccountId: bankAccountId
)
.chained { [weak self] paymentDetails -> Future<PaymentMethodIDProvider> in
.chained { [weak self] paymentDetails -> Future<LinkBankPaymentMethod> in
guard let self else {
return Promise(error: FinancialConnectionsSheetError.unknown(debugDescription: "data source deallocated"))
}
Expand All @@ -527,20 +527,19 @@ extension NativeFlowController {
paymentDetailsId: paymentDetails.redactedPaymentDetails.id,
expectedPaymentMethodType: linkMode.expectedPaymentMethodType
)
.transformed { $0 as PaymentMethodIDProvider }
.transformed { $0.paymentMethod }
} else {
return self.dataManager.createPaymentMethod(
consumerSessionClientSecret: consumerSession.clientSecret,
paymentDetailsId: paymentDetails.redactedPaymentDetails.id
)
.transformed { $0 as PaymentMethodIDProvider }
}
}
.observe { result in
switch result {
case .success(let paymentMethod):
let linkedBank = InstantDebitsLinkedBank(
paymentMethodId: paymentMethod.id,
paymentMethod: paymentMethod,
bankName: bankAccountDetails?.bankName,
last4: bankAccountDetails?.last4,
linkMode: linkMode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ protocol NativeFlowDataManager: AnyObject {
func createPaymentMethod(
consumerSessionClientSecret: String,
paymentDetailsId: String
) -> Future<FinancialConnectionsPaymentMethod>
) -> Future<LinkBankPaymentMethod>
func resetState(withNewManifest newManifest: FinancialConnectionsSessionManifest)
func completeFinancialConnectionsSession(terminalError: String?) -> Future<StripeAPI.FinancialConnectionsSession>
}
Expand Down Expand Up @@ -151,7 +151,7 @@ class NativeFlowAPIDataManager: NativeFlowDataManager {
func createPaymentMethod(
consumerSessionClientSecret: String,
paymentDetailsId: String
) -> Future<FinancialConnectionsPaymentMethod> {
) -> Future<LinkBankPaymentMethod> {
apiClient.paymentMethods(
consumerSessionClientSecret: consumerSessionClientSecret,
paymentDetailsId: paymentDetailsId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,22 +166,20 @@ extension FinancialConnectionsWebFlowViewController {
switch result {
case .success(.success(let returnUrl)):
if manifest.isProductInstantDebits {
if
let paymentMethodId = Self.extractValue(from: returnUrl, key: "payment_method_id")
{
if let paymentMethod = returnUrl.extractLinkBankPaymentMethod() {
let instantDebitsLinkedBank = InstantDebitsLinkedBank(
paymentMethodId: paymentMethodId,
bankName: Self.extractValue(from: returnUrl, key: "bank_name")?
paymentMethod: paymentMethod,
bankName: returnUrl.extractValue(forKey: "bank_name")?
// backend can return "+" instead of a more-common encoding of "%20" for spaces
.replacingOccurrences(of: "+", with: " "),
last4: Self.extractValue(from: returnUrl, key: "last4"),
last4: returnUrl.extractValue(forKey: "last4"),
linkMode: elementsSessionContext?.linkMode
)
self.notifyDelegateOfSuccess(result: .instantDebits(instantDebitsLinkedBank))
} else {
self.notifyDelegateOfFailure(
error: FinancialConnectionsSheetError.unknown(
debugDescription: "payment_method_id was not returned"
debugDescription: "Invalid payment_method returned"
)
)
}
Expand Down Expand Up @@ -292,9 +290,24 @@ extension FinancialConnectionsWebFlowViewController {

notifyDelegate(result: .failed(error: error))
}
}

private static func extractValue(from url: URL, key: String) -> String? {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
private extension URL {

func extractLinkBankPaymentMethod() -> LinkBankPaymentMethod? {
guard let encodedPaymentMethod = extractValue(forKey: "payment_method") else {
return nil
}

guard let data = Data(base64Encoded: encodedPaymentMethod) else {
return nil
}

return try? JSONDecoder().decode(LinkBankPaymentMethod.self, from: data)
}

func extractValue(forKey key: String) -> String? {
guard let components = URLComponents(url: self, resolvingAgainstBaseURL: false) else {
assertionFailure("Invalid URL")
return nil
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ class EmptyFinancialConnectionsAPIClient: FinancialConnectionsAPI {
Promise<StripeFinancialConnections.FinancialConnectionsSharePaymentDetails>()
}

func paymentMethods(consumerSessionClientSecret: String, paymentDetailsId: String) -> StripeCore.Future<StripeFinancialConnections.FinancialConnectionsPaymentMethod> {
Promise<StripeFinancialConnections.FinancialConnectionsPaymentMethod>()
func paymentMethods(consumerSessionClientSecret: String, paymentDetailsId: String) -> StripeCore.Future<StripeFinancialConnections.LinkBankPaymentMethod> {
Promise<StripeFinancialConnections.LinkBankPaymentMethod>()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,6 @@ extension PaymentSheet {
var eligibleForInstantDebits: Bool {
elementsSession.orderedPaymentMethodTypes.contains(.link) &&
!elementsSession.orderedPaymentMethodTypes.contains(.USBankAccount) &&
!intent.isDeferredIntent &&
elementsSession.linkFundingSources?.contains(.bankAccount) == true
}

Expand All @@ -185,7 +184,6 @@ extension PaymentSheet {
var eligibleForLinkCardBrand: Bool {
elementsSession.linkFundingSources?.contains(.bankAccount) == true &&
!elementsSession.orderedPaymentMethodTypes.contains(.USBankAccount) &&
!intent.isDeferredIntent &&
elementsSession.linkSettings?.linkMode == .linkCardBrand
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ extension PaymentSheet {
configuration: configuration
)
if case .new(let confirmParams) = paymentOption {
if let paymentMethodId = confirmParams.instantDebitsLinkedBank?.paymentMethodId {
if let paymentMethodId = confirmParams.instantDebitsLinkedBank?.paymentMethod.id {
params.paymentMethodId = paymentMethodId
params.paymentMethodParams = nil

Expand Down Expand Up @@ -180,7 +180,7 @@ extension PaymentSheet {
configuration: configuration
)
if case .new(let confirmParams) = paymentOption {
if let paymentMethodId = confirmParams.instantDebitsLinkedBank?.paymentMethodId {
if let paymentMethodId = confirmParams.instantDebitsLinkedBank?.paymentMethod.id {
setupIntentParams.paymentMethodID = paymentMethodId
setupIntentParams.paymentMethodParams = nil
setupIntentParams.mandateData = STPMandateDataParams.makeWithInferredValues()
Expand All @@ -198,12 +198,27 @@ extension PaymentSheet {
)
// MARK: ↪ Deferred Intent
case .deferredIntent(let intentConfig):
handleDeferredIntentConfirmation(
confirmType: .new(
let confirmType: ConfirmPaymentMethodType? = if let bank = confirmParams.instantDebitsLinkedBank {
if let paymentMethod = bank.paymentMethod.decode() {
.saved(paymentMethod, paymentOptions: confirmParams.confirmPaymentMethodOptions)
} else {
nil
}
} else {
.new(
params: confirmParams.paymentMethodParams,
paymentOptions: confirmParams.confirmPaymentMethodOptions,
shouldSave: confirmParams.saveForFutureUseCheckboxState == .selected
),
)
}

guard let confirmType else {
completion(.failed(error: PaymentSheetError.invalidLinkBankPaymentMethod), nil)
return
}

handleDeferredIntentConfirmation(
confirmType: confirmType,
configuration: configuration,
intentConfig: intentConfig,
authenticationContext: authenticationContext,
Expand Down Expand Up @@ -654,3 +669,10 @@ private func isEqual(_ lhs: STPPaymentIntentShippingDetails?, _ rhs: STPPaymentI

return rhs == lhsConverted
}

private extension LinkBankPaymentMethod {

func decode() -> STPPaymentMethod? {
return STPPaymentMethod.decodedObject(fromAPIResponse: allResponseFields)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public enum PaymentSheetError: Error, LocalizedError {
// MARK: Deferred intent errors
case intentConfigurationValidationFailed(message: String)
case deferredIntentValidationFailed(message: String)
case invalidLinkBankPaymentMethod

// MARK: - Link errors
case linkSignUpNotRequired
Expand Down Expand Up @@ -79,6 +80,8 @@ extension PaymentSheetError: CustomDebugStringConvertible {
return "Attempted Apple Pay but it's not supported by the device, not configured, or missing a presenter"
case .deferredIntentValidationFailed(message: let message):
return message
case .invalidLinkBankPaymentMethod:
return "The Stripe API sent an invalid payment_method parameter"
case .alreadyPresented:
return "presentingViewController is already presenting a view controller"
case .flowControllerConfirmFailed(message: let message):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,6 @@ struct OverridePrimaryButtonState {
extension PaymentMethodFormViewController {
enum Error: Swift.Error {
case usBankAccountParamsMissing
case instantDebitsDeferredIntentNotSupported
case instantDebitsParamsMissing
}

Expand Down Expand Up @@ -417,7 +416,7 @@ extension PaymentMethodFormViewController {
}
let additionalParameters: [String: Any] = [
"product": "instant_debits",
"attach_required": true,
// "attach_required": true,
"hosted_surface": "payment_element",
]
switch intent {
Expand All @@ -443,13 +442,29 @@ extension PaymentMethodFormViewController {
from: viewController,
financialConnectionsCompletion: financialConnectionsCompletion
)
case .deferredIntent: // not supported
let errorAnalytic = ErrorAnalytic(
event: .unexpectedPaymentSheetError,
error: Error.instantDebitsDeferredIntentNotSupported
case .deferredIntent(let intentConfig):
let amount: Int?
let currency: String?
switch intentConfig.mode {
case let .payment(amount: _amount, currency: _currency, _, _):
amount = _amount
currency = _currency
case let .setup(currency: _currency, _):
amount = nil
currency = _currency
}
client.collectBankAccountForDeferredIntent(
sessionId: elementsSession.sessionID,
returnURL: configuration.returnURL,
onEvent: nil,
amount: amount,
currency: currency,
onBehalfOf: intentConfig.onBehalfOf,
additionalParameters: additionalParameters,
elementsSessionContext: elementsSessionContext,
from: viewController,
financialConnectionsCompletion: financialConnectionsCompletion
)
STPAnalyticsClient.sharedClient.log(analytic: errorAnalytic)
stpAssertionFailure()
}
}
}
Loading
Loading