Skip to content

Commit

Permalink
Support Instant Debits with deferred intents
Browse files Browse the repository at this point in the history
  • Loading branch information
tillh-stripe committed Oct 17, 2024
1 parent 92741d8 commit 6d2214a
Show file tree
Hide file tree
Showing 15 changed files with 115 additions and 56 deletions.
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 @@ -7,18 +7,19 @@

import Foundation

@_spi(STP) public struct InstantDebitsLinkedBank: Equatable {
public let paymentMethodId: String
@_spi(STP) public struct InstantDebitsLinkedBank {
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 {
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

0 comments on commit 6d2214a

Please sign in to comment.