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

[POC] Link Brand Card #3964

Draft
wants to merge 5 commits 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
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,15 @@ struct PaymentSheetTestPlaygroundSettings: Codable, Equatable {

case link_pm = "Link PM"
case passthrough
case link_card_brand = "Link Card"

var value: String {
switch self {
case .link_pm: "LINK_PAYMENT_METHOD"
case .passthrough: "PASSTHROUGH"
case .link_card_brand: "LINK_CARD_BRAND"
}
}
}

enum UserOverrideCountry: String, PickerEnum {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ extension PlaygroundController {
"mode": settings.mode.rawValue,
"automatic_payment_methods": settings.apmsEnabled == .on,
"use_link": settings.linkMode == .link_pm,
"link_mode": settings.linkMode.value,
"use_manual_confirmation": settings.integrationType == .deferred_mc,
"require_cvc_recollection": settings.requireCVCRecollection == .on,
"customer_session_component_name": "mobile_payment_element",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,14 @@ protocol FinancialConnectionsAPI {
bankAccountId: String
) -> Future<FinancialConnectionsPaymentDetails>

// TODO: Figure out return type for this request.
// We'll need to sort out the down-scoped client secret first.
func sharePaymentDetails(
consumerSessionClientSecret: String,
paymentDetailsId: String,
expectedPaymentMethodType: String
) -> Future<FinancialConnectionsPaymentMethod>

func paymentMethods(
consumerSessionClientSecret: String,
paymentDetailsId: String
Expand Down Expand Up @@ -944,6 +952,27 @@ extension FinancialConnectionsAPIClient: FinancialConnectionsAPI {
)
}

func sharePaymentDetails(
consumerSessionClientSecret: String,
paymentDetailsId: String,
expectedPaymentMethodType: String
) -> Future<FinancialConnectionsPaymentMethod> {
let parameters: [String: Any] = [
"request_surface": requestSurface,
"id": paymentDetailsId,
"credentials": [
"consumer_session_client_secret": consumerSessionClientSecret
],
"expected_payment_method_type": expectedPaymentMethodType,
"expand": ["payment_method"],
]
return post(
resource: APIEndpointSharePaymentDetails,
parameters: parameters,
useConsumerPublishableKeyIfNeeded: false
)
}

func paymentMethods(
consumerSessionClientSecret: String,
paymentDetailsId: String
Expand Down Expand Up @@ -995,4 +1024,5 @@ private let APIEndpointPollAccountNumbers = "link_account_sessions/poll_account_
private let APIEndpointLinkAccountsSignUp = "consumers/accounts/sign_up"
private let APIEndpointAttachLinkConsumerToLinkAccountSession = "consumers/attach_link_consumer_to_link_account_session"
private let APIEndpointPaymentDetails = "consumers/payment_details"
private let APIEndpointSharePaymentDetails = "consumers/payment_details/share"
private let APIEndpointPaymentMethods = "payment_methods"
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ struct FinancialConnectionsSessionManifest: Decodable {
!livemode
}

var isPantherPayment: Bool {
let isLinkPaymentMethod = paymentMethodType == .link
return isProductInstantDebits && isLinkPaymentMethod
}

init(
accountholderCustomerEmailAddress: String? = nil,
accountholderIsLinkConsumer: Bool? = nil,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,10 +513,18 @@ extension NativeFlowController {
}

bankAccountDetails = paymentDetails.redactedPaymentDetails.bankAccountDetails
return self.dataManager.createPaymentMethod(
consumerSessionClientSecret: consumerSession.clientSecret,
paymentDetailsId: paymentDetails.redactedPaymentDetails.id
)
if self.dataManager.manifest.isPantherPayment {
return self.dataManager.apiClient.sharePaymentDetails(
consumerSessionClientSecret: consumerSession.clientSecret,
paymentDetailsId: paymentDetails.redactedPaymentDetails.id,
expectedPaymentMethodType: "card"
)
} else {
return self.dataManager.createPaymentMethod(
consumerSessionClientSecret: consumerSession.clientSecret,
paymentDetailsId: paymentDetails.redactedPaymentDetails.id
)
}
}
.observe { result in
switch result {
Expand Down Expand Up @@ -801,7 +809,7 @@ extension NativeFlowController: ManualEntryViewControllerDelegate {
// to the Link signup/save call later in the flow. We don't need them anymore since we know
// they've failed us in some way at this point.
dataManager.linkedAccounts = nil

dataManager.paymentAccountResource = paymentAccountResource
dataManager.accountNumberLast4 = accountNumberLast4

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
47AD56A9889DF5EFBBA9CEFB /* PollingViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ADE49E72DD5EDA448D12D88 /* PollingViewTests.swift */; };
47B19F96CCEA290541E3B988 /* CardSectionElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D03000A6807B09BFD8E6CB1 /* CardSectionElement.swift */; };
48DA2EFE0944E737B0F197B0 /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = B2AFFAD776D5F21DF837F1BD /* OHHTTPStubs */; };
493CA9F12C7E14F90089058D /* ConsumerSession+PaymentMethodType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 493CA9F02C7E14F90089058D /* ConsumerSession+PaymentMethodType.swift */; };
49803444CD948F1ED28FF021 /* PaymentSheetFormFactory+FormSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7A1EFF100C589FDFF4D516 /* PaymentSheetFormFactory+FormSpec.swift */; };
49F62EDF394F18E5BB201D53 /* StripePaymentSheet.h in Headers */ = {isa = PBXBuildFile; fileRef = 7AA6166F234C3A2129CBD573 /* StripePaymentSheet.h */; settings = {ATTRIBUTES = (Public, ); }; };
4A1A0A542B824C830A200BE0 /* StubbedBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBF8498CCD12A5190F9267CD /* StubbedBackend.swift */; };
Expand Down Expand Up @@ -426,6 +427,7 @@
45B6DC9BD9183495E5649369 /* LinkAccountService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkAccountService.swift; sourceTree = "<group>"; };
47C5DB8C01BA7137369C8B4D /* TextFieldElement+Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TextFieldElement+Card.swift"; sourceTree = "<group>"; };
492B254E43F3BB9F9CEAEA06 /* PaymentSheetLoaderStubbedTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentSheetLoaderStubbedTest.swift; sourceTree = "<group>"; };
493CA9F02C7E14F90089058D /* ConsumerSession+PaymentMethodType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConsumerSession+PaymentMethodType.swift"; sourceTree = "<group>"; };
4BEFE8C0CFEAE73F9FD736D3 /* STPStringUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPStringUtils.swift; sourceTree = "<group>"; };
4C6AA41454A6757B3E26AE67 /* StripePaymentSheetTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StripePaymentSheetTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
4D595AA033BC84CB4E1C277F /* PaymentSheetFormFactorySnapshotTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentSheetFormFactorySnapshotTest.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -941,6 +943,7 @@
F1E614E8481658A027599A92 /* STPAPIClient+Link.swift */,
B662953D2C63F6C2007B6B14 /* PaymentDetailsShareResponse.swift */,
441C3414745D483C9A47ED0B /* VerificationSession.swift */,
493CA9F02C7E14F90089058D /* ConsumerSession+PaymentMethodType.swift */,
);
path = Link;
sourceTree = "<group>";
Expand Down Expand Up @@ -1788,6 +1791,7 @@
229A4A578609A3711F02682E /* STPCardBrandChoice.swift in Sources */,
3EDFACA133567159875143C5 /* STPElementsSession.swift in Sources */,
1BFC617EED154D32BFCADAE7 /* SeparatorLabel.swift in Sources */,
493CA9F12C7E14F90089058D /* ConsumerSession+PaymentMethodType.swift in Sources */,
01D46644D87983FC4387B92C /* InstantDebitsOnlyFinancialConnectionsAuthManager.swift in Sources */,
367BB57FA826A82EEF074A70 /* PayWithLinkWebController.swift in Sources */,
F3A34AD1CC2CBB899738C9D7 /* LinkInlineSignupElement.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,12 @@ class PaymentSheetLinkAccount: PaymentSheetLinkAccountInfoProtocol {
}
}

func sharePaymentDetails(id: String, cvc: String?, completion: @escaping (Result<PaymentDetailsShareResponse, Error>) -> Void) {
func sharePaymentDetails(
id: String,
cvc: String?,
paymentMethodType: ConsumerSession.PaymentMethodType?,
completion: @escaping (Result<PaymentDetailsShareResponse, Error>) -> Void
) {
guard let session = currentSession else {
assertionFailure()
return completion(
Expand All @@ -182,6 +187,7 @@ class PaymentSheetLinkAccount: PaymentSheetLinkAccountInfoProtocol {
id: id,
cvc: cvc,
consumerAccountPublishableKey: publishableKey,
paymentMethodType: paymentMethodType,
completion: completionWrapper
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// ConsumerSession+PaymentMethodType.swift
// StripePaymentSheet
//
// Created by Mat Schmid on 2024-08-27.
//

import Foundation

extension ConsumerSession {
enum PaymentMethodType: String {
case card = "CARD"
case bankAccount = "BANK_ACCOUNT"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,15 @@ extension ConsumerSession {
id: String,
cvc: String?,
consumerAccountPublishableKey: String?,
paymentMethodType: PaymentMethodType?,
completion: @escaping (Result<PaymentDetailsShareResponse, Error>) -> Void
) {
apiClient.sharePaymentDetails(
for: clientSecret,
id: id,
consumerAccountPublishableKey: consumerAccountPublishableKey,
cvc: cvc,
paymentMethodType: paymentMethodType?.rawValue,
completion: completion)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,28 @@ typealias ConsumerSessionWithPaymentDetails = (session: ConsumerSession, payment
For internal SDK use only
*/
final class ConsumerPaymentDetails: Decodable {
enum PaymentDetailsType: String, Decodable {
case card = "CARD"
case bankAccount = "BANK_ACCOUNT"
case invalid = "PAYMENT_DETAILS_TYPE_INVALID"
}

let stripeID: String
let paymentDetailsType: PaymentDetailsType

init(stripeID: String) {
init(stripeID: String, paymentDetailsType: PaymentDetailsType) {
self.stripeID = stripeID
self.paymentDetailsType = paymentDetailsType
}

private enum CodingKeys: String, CodingKey {
case stripeID = "id"
case paymentDetailsType = "type"
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.stripeID = try container.decode(String.self, forKey: .stripeID)
self.paymentDetailsType = try container.decode(PaymentDetailsType.self, forKey: .paymentDetailsType)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ extension STPAPIClient {
id: String,
consumerAccountPublishableKey: String?,
cvc: String?,
paymentMethodType: String?,
completion: @escaping (Result<PaymentDetailsShareResponse, Error>) -> Void
) {
let endpoint: String = "consumers/payment_details/share"
Expand All @@ -215,6 +216,10 @@ extension STPAPIClient {
parameters["payment_method_options"] = ["card": ["cvc": cvc]]
}

if let paymentMethodType {
parameters["expected_payment_method_type"] = paymentMethodType
}

APIRequest<PaymentDetailsShareResponse>.post(
with: self,
endpoint: endpoint,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ extension STPElementsSession {
linkSettings?.fundingSources
}

var linkMode: LinkSettings.LinkMode? {
linkSettings?.linkMode
}

var disableLinkSignup: Bool {
linkSettings?.disableSignup ?? false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ class IntentConfirmParams {
case .instantDebits:
let params = STPPaymentMethodParams(type: .link)
self.init(params: params, type: type)
case .linkCardBrand:
let params = STPPaymentMethodParams(type: .card)
self.init(params: params, type: type)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ extension PaymentSheet {
enum PaymentMethodType: Equatable, Hashable {
case stripe(STPPaymentMethodType)
case external(ExternalPaymentMethod)

// Synthetic payment methods:
case instantDebits
case linkCardBrand

static var analyticLogForIcon: Set<PaymentMethodType> = []
static let analyticLogForIconSemaphore = DispatchSemaphore(value: 1)
Expand All @@ -27,7 +30,7 @@ extension PaymentSheet {
return paymentMethodType.displayName
case .external(let externalPaymentMethod):
return externalPaymentMethod.label
case .instantDebits:
case .instantDebits, .linkCardBrand:
return String.Localized.bank
}
}
Expand All @@ -42,6 +45,8 @@ extension PaymentSheet {
return externalPaymentMethod.type
case .instantDebits:
return "instant_debits"
case .linkCardBrand:
return "link_card_bank"
}
}

Expand Down Expand Up @@ -103,7 +108,7 @@ extension PaymentSheet {
}
return DownloadManager.sharedManager.imagePlaceHolder()
}
case .instantDebits:
case .instantDebits, .linkCardBrand:
return Image.pm_type_us_bank.makeImage(overrideUserInterfaceStyle: forDarkBackground ? .dark : .light)
}
}
Expand All @@ -114,7 +119,7 @@ extension PaymentSheet {
return stpPaymentMethodType.iconRequiresTinting
case .external:
return false
case .instantDebits:
case .instantDebits, .linkCardBrand:
return true
}
}
Expand Down Expand Up @@ -173,6 +178,19 @@ extension PaymentSheet {
if availabilityStatus == .supported {
recommendedPaymentMethodTypes.append(.instantDebits)
}
} else if
elementsSession.linkFundingSources?.contains(.bankAccount) == true,
!elementsSession.orderedPaymentMethodTypes.contains(.USBankAccount),
elementsSession.linkSettings?.linkMode == .linkCardBrand
{
let availabilityStatus = configurationSatisfiesRequirements(
requirements: [.financialConnectionsSDK],
configuration: configuration,
intent: intent
)
if availabilityStatus == .supported {
recommendedPaymentMethodTypes.append(.linkCardBrand)
}
}

if let merchantPaymentMethodOrder = configuration.paymentMethodOrder?.map({ $0.lowercased() }) {
Expand All @@ -196,8 +214,10 @@ extension PaymentSheet {
}
// 3. Append the remaining PMs in recommendedPaymentMethodTypes
reorderedPaymentMethodTypes.append(contentsOf: recommendedPaymentMethodTypes)
print("**** reorderedPaymentMethodTypes", reorderedPaymentMethodTypes)
return reorderedPaymentMethodTypes
} else {
print("**** recommendedPaymentMethodTypes", recommendedPaymentMethodTypes)
return recommendedPaymentMethodTypes
}
}
Expand Down Expand Up @@ -237,9 +257,9 @@ extension PaymentSheet {
case .bacsDebit:
return [.returnURL, .userSupportsDelayedPaymentMethods]
case .cardPresent, .blik, .weChatPay, .grabPay, .FPX, .giropay, .przelewy24, .EPS,
.netBanking, .OXXO, .afterpayClearpay, .UPI, .link, .affirm, .paynow, .zip, .alma,
.mobilePay, .unknown, .alipay, .konbini, .promptPay, .swish, .twint, .multibanco,
.sunbit, .billie, .satispay:
.netBanking, .OXXO, .afterpayClearpay, .UPI, .link, .affirm, .paynow,
.zip, .alma, .mobilePay, .unknown, .alipay, .konbini, .promptPay, .swish, .twint,
.multibanco, .sunbit, .billie, .satispay:
return [.unsupportedForSetup]
@unknown default:
return [.unsupportedForSetup]
Expand Down
Loading
Loading