Skip to content

Commit

Permalink
[Link] Pass payment information on intent confirmation (#855)
Browse files Browse the repository at this point in the history
* Add parameters

* Migrate to the new API

* Remove Link payment details from authentication context

* Cleanup and add test

* Cleanup

* Use testing publishable key

* Allow sending CVC

* Cleanup

* Handle client-side error
  • Loading branch information
ramont-stripe authored Mar 15, 2022
1 parent 33c5a46 commit 7b2126b
Show file tree
Hide file tree
Showing 13 changed files with 158 additions and 233 deletions.
4 changes: 4 additions & 0 deletions Stripe.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,7 @@
D0CA3DB0279E578B00143B1C /* OperationDebouncerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CA3DAF279E578B00143B1C /* OperationDebouncerTests.swift */; };
D0CC64B626E2A16500FC1442 /* STPPostalCodeInputTextFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CC64B526E2A16500FC1442 /* STPPostalCodeInputTextFieldTests.swift */; };
D0CFE56B2788CCD600E73511 /* CircularButtonSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0CFE56A2788CCD600E73511 /* CircularButtonSnapshotTests.swift */; };
D0D217CB27DC295D006220A9 /* PaymentSheetLinkAccountTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D217CA27DC295D006220A9 /* PaymentSheetLinkAccountTests.swift */; };
D0D28E622757398200098245 /* Button+Link.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D28E612757398200098245 /* Button+Link.swift */; };
D0D28E662757445100098245 /* Link2FAView-Header.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D28E652757445100098245 /* Link2FAView-Header.swift */; };
D0D28E682757D7CA00098245 /* LinkUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D28E672757D7CA00098245 /* LinkUI.swift */; };
Expand Down Expand Up @@ -1704,6 +1705,7 @@
D0CA3DAF279E578B00143B1C /* OperationDebouncerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationDebouncerTests.swift; sourceTree = "<group>"; };
D0CC64B526E2A16500FC1442 /* STPPostalCodeInputTextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPPostalCodeInputTextFieldTests.swift; sourceTree = "<group>"; };
D0CFE56A2788CCD600E73511 /* CircularButtonSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularButtonSnapshotTests.swift; sourceTree = "<group>"; };
D0D217CA27DC295D006220A9 /* PaymentSheetLinkAccountTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentSheetLinkAccountTests.swift; sourceTree = "<group>"; };
D0D28E612757398200098245 /* Button+Link.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Button+Link.swift"; sourceTree = "<group>"; };
D0D28E652757445100098245 /* Link2FAView-Header.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Link2FAView-Header.swift"; sourceTree = "<group>"; };
D0D28E672757D7CA00098245 /* LinkUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkUI.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3039,6 +3041,7 @@
3137A74126FD766F008BEF7B /* Objective-C Bridge */,
D0CA3DAF279E578B00143B1C /* OperationDebouncerTests.swift */,
E620F1D7265F5FA200233581 /* PaymentAnalyticTest.swift */,
D0D217CA27DC295D006220A9 /* PaymentSheetLinkAccountTests.swift */,
F3064296268C42BE0076CDDA /* PaymentSheet+APITest.swift */,
61B4BC3226D53E790099D768 /* PaymentSheet+PaymentMethodAvailabilityTest.swift */,
B66FA0BC267EE1DB008D7F1D /* PaymentSheetFormFactoryTest.swift */,
Expand Down Expand Up @@ -3928,6 +3931,7 @@
3111C5DF252CC5C700207E32 /* STPPaymentMethodTest.swift in Sources */,
3111C5F7252D1BE600207E32 /* STPSetupIntentConfirmParamsTest.swift in Sources */,
44BDCFDF245A46CC007EE6D5 /* STPPaymentMethodBancontactParamsTests.m in Sources */,
D0D217CB27DC295D006220A9 /* PaymentSheetLinkAccountTests.swift in Sources */,
D0277AE127604E3600AF2CF6 /* Link2FAViewSnapshotTests.swift in Sources */,
3111C552252BD00400207E32 /* NSDecimalNumber+StripeTest.swift in Sources */,
B6D13947230C68FF007AFF8A /* STPConnectAccountAddressTest.m in Sources */,
Expand Down
1 change: 0 additions & 1 deletion Stripe/BottomSheetViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ class BottomSheetViewController: UIViewController, PanModalPresentable {
}
}

var linkPaymentDetails: (PaymentSheetLinkAccount, ConsumerPaymentDetails)? = nil
let didCancelNative3DS2: () -> ()

required init(
Expand Down
25 changes: 0 additions & 25 deletions Stripe/ConsumerSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -214,31 +214,6 @@ extension ConsumerSession {
updateParams: updateParams,
completion: completion)
}

func completePayment(with apiClient: STPAPIClient = STPAPIClient.shared,
for paymentIntent: STPPaymentIntent,
paymentDetails: ConsumerPaymentDetails,
completion: @escaping STPPaymentIntentCompletionBlock) {
apiClient.completePayment(for: paymentIntent.stripeId,
paymentIntentClientSecret: paymentIntent.clientSecret,
consumerSessionClientSecret: clientSecret,
paymentDetailsID: paymentDetails.stripeID,
cvc: paymentDetails.cvc,
completion: completion)
}

func completeSetup(with apiClient: STPAPIClient = STPAPIClient.shared,
for setupIntent: STPSetupIntent,
paymentDetails: ConsumerPaymentDetails,
completion: @escaping STPSetupIntentCompletionBlock) {
apiClient.completeSetup(for: setupIntent.stripeID,
setupIntentClientSecret: setupIntent.clientSecret,
consumerSessionClientSecret: clientSecret,
paymentDetailsID: paymentDetails.stripeID,
cvc: paymentDetails.cvc,
completion: completion)
}


func logout(
with apiClient: STPAPIClient = STPAPIClient.shared,
Expand Down
2 changes: 0 additions & 2 deletions Stripe/Enums+CustomStringConvertible.swift
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,6 @@ extension STPIntentActionType: CustomStringConvertible {
return "alipayHandleRedirect"
case .boletoDisplayDetails:
return "boletoDisplayDetails"
case .linkAuthenticateAccount:
return "linkAuthenticateAccount"
case .redirectToURL:
return "redirectToURL"
case .unknown:
Expand Down
57 changes: 25 additions & 32 deletions Stripe/PaymentSheet+API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,6 @@ extension PaymentSheet {
let paymentHandlerCompletion: (STPPaymentHandlerActionStatus, NSObject?, NSError?) -> Void =
{
(status, _, error) in

if let paymentSheetAuthenticationContext = authenticationContext as? PaymentSheetAuthenticationContext {
// reset
paymentSheetAuthenticationContext.linkPaymentDetails = nil
}

switch status {
case .canceled:
completion(.canceled)
Expand Down Expand Up @@ -133,30 +127,31 @@ extension PaymentSheet {

case .link(let linkAccount, let confirmOption):
let confirmWithPaymentDetails: (ConsumerPaymentDetails) -> Void = { paymentDetails in
if let paymentSheetAuthenticationContext = authenticationContext as? PaymentSheetAuthenticationContext {
paymentSheetAuthenticationContext.linkPaymentDetails = (linkAccount, paymentDetails)

switch intent {
case .paymentIntent(let paymentIntent):
let paymentIntentParams = STPPaymentIntentParams(clientSecret: paymentIntent.clientSecret)
paymentIntentParams.paymentMethodParams = STPPaymentMethodParams(type: .link)
paymentIntentParams.returnURL = configuration.returnURL
paymentHandler.confirmPayment(paymentIntentParams,
with: authenticationContext,
completion: paymentHandlerCompletion)

case .setupIntent(let setupIntent):
let setupIntentParams = STPSetupIntentConfirmParams(clientSecret: setupIntent.clientSecret)
setupIntentParams.paymentMethodParams = STPPaymentMethodParams(type: .link)
setupIntentParams.returnURL = configuration.returnURL
paymentHandler.confirmSetupIntent(
setupIntentParams,
with: authenticationContext,
completion: paymentHandlerCompletion)
}
} else {
assertionFailure("Link only available if authenticationContest is PaymentSheetAuthenticationContext")
completion(.failed(error: NSError.stp_genericConnectionError()))
guard let paymentMethodParams = linkAccount.makePaymentMethodParams(from: paymentDetails) else {
let error = PaymentSheetError.unknown(debugDescription: "Paying with Link without valid session")
completion(.failed(error: error))
return
}

switch intent {
case .paymentIntent(let paymentIntent):
let paymentIntentParams = STPPaymentIntentParams(clientSecret: paymentIntent.clientSecret)
paymentIntentParams.paymentMethodParams = paymentMethodParams
paymentIntentParams.returnURL = configuration.returnURL
paymentHandler.confirmPayment(
paymentIntentParams,
with: authenticationContext,
completion: paymentHandlerCompletion
)
case .setupIntent(let setupIntent):
let setupIntentParams = STPSetupIntentConfirmParams(clientSecret: setupIntent.clientSecret)
setupIntentParams.paymentMethodParams = paymentMethodParams
setupIntentParams.returnURL = configuration.returnURL
paymentHandler.confirmSetupIntent(
setupIntentParams,
with: authenticationContext,
completion: paymentHandlerCompletion
)
}
}

Expand Down Expand Up @@ -366,6 +361,4 @@ extension PaymentSheet {
protocol PaymentSheetAuthenticationContext: STPAuthenticationContext {
func present(_ threeDS2ChallengeViewController: UIViewController, completion: @escaping () -> Void)
func dismiss(_ threeDS2ChallengeViewController: UIViewController)

var linkPaymentDetails: (PaymentSheetLinkAccount, ConsumerPaymentDetails)? { get set }
}
25 changes: 3 additions & 22 deletions Stripe/PaymentSheetFlowController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -312,24 +312,8 @@ extension PaymentSheet {
completion(.failed(error: error))
return
}

let linkPaymentDetails: (PaymentSheetLinkAccount, ConsumerPaymentDetails)? = {
switch paymentOption {
case .applePay, .saved, .new:
return nil
case .link(let account, let option):
switch option {

case .forNewAccount, .withPaymentMethodParams:
return nil
case .withPaymentDetails(paymentDetails: let paymentDetails):
return (account, paymentDetails)
}
}
}()

let authenticationContext = AuthenticationContext(presentingViewController: presentingViewController,
linkPaymentDetails: linkPaymentDetails)

let authenticationContext = AuthenticationContext(presentingViewController: presentingViewController)

PaymentSheet.confirm(
configuration: configuration,
Expand Down Expand Up @@ -427,13 +411,10 @@ class AuthenticationContext: NSObject, PaymentSheetAuthenticationContext {
threeDS2ChallengeViewController.dismiss(animated: true, completion: nil)
}

var linkPaymentDetails: (PaymentSheetLinkAccount, ConsumerPaymentDetails)?

let presentingViewController: UIViewController

init(presentingViewController: UIViewController, linkPaymentDetails: (PaymentSheetLinkAccount, ConsumerPaymentDetails)?) {
init(presentingViewController: UIViewController) {
self.presentingViewController = presentingViewController
self.linkPaymentDetails = linkPaymentDetails
super.init()
}
func authenticationPresentingViewController() -> UIViewController {
Expand Down
63 changes: 32 additions & 31 deletions Stripe/PaymentSheetLinkAccount.swift
Original file line number Diff line number Diff line change
Expand Up @@ -219,37 +219,6 @@ class PaymentSheetLinkAccount: PaymentSheetLinkAccountInfoProtocol {
consumerSession.createPaymentDetails(linkedAccountId: linkedAccountId, completion: completion)
}

func completeLinkPayment(for paymentIntent: STPPaymentIntent,
with paymentDetails: ConsumerPaymentDetails,
completion: @escaping STPPaymentIntentCompletionBlock) {
guard let consumerSession = currentSession else {
assertionFailure()
completion(nil, PaymentSheetError.unknown(debugDescription: "Paying with Link without valid session"))
return
}

consumerSession.completePayment(
with: apiClient,
for: paymentIntent,
paymentDetails: paymentDetails,
completion: completion
)
}

func completeLinkSetup(for setupIntent: STPSetupIntent,
with paymentDetails: ConsumerPaymentDetails,
completion: @escaping STPSetupIntentCompletionBlock) {
guard let consumerSession = currentSession else {
assertionFailure()
completion(nil, PaymentSheetError.unknown(debugDescription: "Paying with Link without valid session"))
return
}

consumerSession.completeSetup(for: setupIntent,
paymentDetails: paymentDetails,
completion: completion)
}

func listPaymentDetails(completion: @escaping ([ConsumerPaymentDetails]?, Error?) -> Void) {
guard let consumerSession = currentSession else {
assertionFailure()
Expand Down Expand Up @@ -329,3 +298,35 @@ class PaymentSheetLinkAccount: PaymentSheetLinkAccountInfoProtocol {
}

}

// MARK: - Payment method params

extension PaymentSheetLinkAccount {

/// Converts a `ConsumerPaymentDetails` into a `STPPaymentMethodParams` object, injecting
/// the required Link credentials.
///
/// Returns `nil` if not authenticated/logged in.
///
/// - Parameter paymentDetails: Payment details
/// - Returns: Payment method params for paying with Link.
func makePaymentMethodParams(from paymentDetails: ConsumerPaymentDetails) -> STPPaymentMethodParams? {
guard let currentSession = currentSession else {
assertionFailure("Cannot make payment method params without an active session.")
return nil
}

let params = STPPaymentMethodParams(type: .link)
params.link?.paymentDetailsId = paymentDetails.stripeID
params.link?.credentials = ["consumer_session_client_secret": currentSession.clientSecret]

if let cvc = paymentDetails.cvc {
params.link?.additionalAPIParameters["card"] = [
"cvc": cvc
]
}

return params
}

}
54 changes: 0 additions & 54 deletions Stripe/STPAPIClient+Link.swift
Original file line number Diff line number Diff line change
Expand Up @@ -292,60 +292,6 @@ extension STPAPIClient {
}
}

func completePayment(for paymentIntentID: String,
paymentIntentClientSecret: String,
consumerSessionClientSecret: String,
paymentDetailsID: String,
cvc: String?,
completion: @escaping STPPaymentIntentCompletionBlock) {
let endpoint: String = "consumers/payment_intents/\(paymentIntentID)/complete"

var parameters: [String: Any] = [
"credentials": ["consumer_session_client_secret": consumerSessionClientSecret],
"client_secret": paymentIntentClientSecret,
"payment_details_id": paymentDetailsID
]

if let cvc = cvc {
parameters["payment_method_options"] = ["card": ["cvc": cvc]]
}

APIRequest<STPPaymentIntent>.post(
with: self,
endpoint: endpoint,
parameters: parameters
) { paymentIntent, _, error in
completion(paymentIntent, error)
}
}

func completeSetup(for setupIntentID: String,
setupIntentClientSecret: String,
consumerSessionClientSecret: String,
paymentDetailsID: String,
cvc: String?,
completion: @escaping STPSetupIntentCompletionBlock) {
let endpoint: String = "consumers/setup_intents/\(setupIntentID)/complete"

var parameters: [String: Any] = [
"credentials": ["consumer_session_client_secret": consumerSessionClientSecret],
"client_secret": setupIntentClientSecret,
"payment_details_id": paymentDetailsID
]

if let cvc = cvc {
parameters["payment_method_options"] = ["card": ["cvc": cvc]]
}

APIRequest<STPSetupIntent>.post(
with: self,
endpoint: endpoint,
parameters: parameters
) { setupIntent, _, error in
completion(setupIntent, error)
}
}

func logout(
consumerSessionClientSecret: String,
cookieStore: LinkCookieStore,
Expand Down
11 changes: 0 additions & 11 deletions Stripe/STPIntentAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,6 @@ import Foundation
/// Contains instructions for authenticating a payment by redirecting your customer to the WeChat Pay App.
case weChatPayRedirectToApp

/// The payment intent requires authorization with Payment Sheet.
case linkAuthenticateAccount

/// The action type is Boleto payment. We provide `STPPaymentHandler` to display the Boleto voucher.
case boletoDisplayDetails

Expand All @@ -71,8 +68,6 @@ import Foundation
self = .boletoDisplayDetails
case "blik_authorize":
self = .BLIKAuthorize
case "link_authenticate_account":
self = .linkAuthenticateAccount
default:
self = .unknown
}
Expand All @@ -97,8 +92,6 @@ import Foundation
return "wechat_pay_redirect_to_ios_app"
case .boletoDisplayDetails:
return "boleto_display_details"
case .linkAuthenticateAccount:
return "link_authenticate_account"
case .unknown:
break
}
Expand Down Expand Up @@ -176,8 +169,6 @@ public class STPIntentAction: NSObject {
}
case .BLIKAuthorize:
break // no additional details
case .linkAuthenticateAccount:
break // no additional details
case .unknown:
// unrecognized type, just show the original dictionary for debugging help
props.append("allResponseFields = \(allResponseFields)")
Expand Down Expand Up @@ -271,8 +262,6 @@ extension STPIntentAction: STPAPIResponseDecodable {
}
case .BLIKAuthorize:
break // no additional details
case .linkAuthenticateAccount:
break // no additional details
}

return STPIntentAction(
Expand Down
Loading

0 comments on commit 7b2126b

Please sign in to comment.