-
Notifications
You must be signed in to change notification settings - Fork 952
/
PaymentSheetFlowController.swift
385 lines (342 loc) · 17.3 KB
/
PaymentSheetFlowController.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
//
// PaymentSheetFlowController.swift
// StripePaymentSheet
//
// Created by Yuki Tokuhiro on 11/4/20.
// Copyright © 2020 Stripe, Inc. All rights reserved.
//
import Foundation
import UIKit
@_spi(STP) import StripeCore
@_spi(STP) import StripeUICore
@_spi(STP) import StripePayments
@_spi(STP) import StripePaymentsUI
typealias PaymentOption = PaymentSheet.PaymentOption
extension PaymentSheet {
/// Represents the ways a customer can pay in PaymentSheet
enum PaymentOption {
case applePay
case saved(paymentMethod: STPPaymentMethod)
case new(confirmParams: IntentConfirmParams)
case link(option: LinkConfirmOption)
var name: String {
switch self {
case .applePay:
return "applepay"
case .saved(paymentMethod: let paymentMethod):
return paymentMethod.type.displayName.lowercased()
case .new(confirmParams: let confirmParams):
return confirmParams.paymentMethodType.displayName.lowercased()
case .link(option: _):
return "link"
}
}
}
/// A class that presents the individual steps of a payment flow
@available(iOSApplicationExtension, unavailable)
@available(macCatalystApplicationExtension, unavailable)
public class FlowController {
// MARK: - Public properties
/// Contains details about a payment method that can be displayed to the customer
public struct PaymentOptionDisplayData {
/// 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
public let label: String
init(paymentOption: PaymentOption) {
image = paymentOption.makeIcon(updateImageHandler: nil)
switch paymentOption {
case .applePay:
label = String.Localized.apple_pay
case .saved(let paymentMethod):
label = paymentMethod.paymentSheetLabel
case .new(let confirmParams):
label = confirmParams.paymentSheetLabel
case .link(let option):
label = option.paymentSheetLabel
}
}
}
/// This contains all configurable properties of PaymentSheet
public let configuration: Configuration
/// Contains information about the customer's desired payment option.
/// You can use this to e.g. display the payment option in your UI.
public var paymentOption: PaymentOptionDisplayData? {
if let selectedPaymentOption = _paymentOption {
return PaymentOptionDisplayData(paymentOption: selectedPaymentOption)
}
return nil
}
// MARK: - Private properties
private var intent: Intent
private let savedPaymentMethods: [STPPaymentMethod]
lazy var paymentHandler: STPPaymentHandler = { STPPaymentHandler(apiClient: configuration.apiClient, formSpecPaymentHandler: PaymentSheetFormSpecPaymentHandler()) }()
private let isLinkEnabled: Bool
private lazy var paymentOptionsViewController: ChoosePaymentOptionViewController = {
let isApplePayEnabled = StripeAPI.deviceSupportsApplePay() && configuration.applePay != nil
let vc = ChoosePaymentOptionViewController(
intent: intent,
savedPaymentMethods: savedPaymentMethods,
configuration: configuration,
isApplePayEnabled: isApplePayEnabled,
isLinkEnabled: isLinkEnabled,
delegate: self
)
// Workaround to silence a warning in the Catalyst target
#if targetEnvironment(macCatalyst)
configuration.style.configure(vc)
#else
if #available(iOS 13.0, *) {
configuration.style.configure(vc)
}
#endif
return vc
}()
private var presentPaymentOptionsCompletion: (() -> ())? = nil
/// The desired, valid (ie passed client-side checks) payment option from the underlying payment options VC.
private var _paymentOption: PaymentOption? {
guard paymentOptionsViewController.error == nil else {
return nil
}
return paymentOptionsViewController.selectedPaymentOption
}
// MARK: - Initializer (Internal)
required init(
intent: Intent,
savedPaymentMethods: [STPPaymentMethod],
isLinkEnabled: Bool,
configuration: Configuration
) {
AnalyticsHelper.shared.generateSessionID()
STPAnalyticsClient.sharedClient.addClass(toProductUsageIfNecessary: PaymentSheet.FlowController.self)
STPAnalyticsClient.sharedClient.logPaymentSheetInitialized(isCustom: true, configuration: configuration)
self.intent = intent
self.savedPaymentMethods = savedPaymentMethods
self.isLinkEnabled = isLinkEnabled
self.configuration = configuration
}
// MARK: - Public methods
/// An asynchronous failable initializer for PaymentSheet.FlowController
/// This asynchronously loads the Customer's payment methods, their default payment method, and the PaymentIntent.
/// You can use the returned PaymentSheet.FlowController instance to e.g. update your UI with the Customer's default payment method
/// - Parameter paymentIntentClientSecret: The [client secret](https://stripe.com/docs/api/payment_intents/object#payment_intent_object-client_secret) of a Stripe PaymentIntent object
/// - Note: This can be used to complete a payment - don't log it, store it, or expose it to anyone other than the customer.
/// - Parameter configuration: Configuration for the PaymentSheet. e.g. your business name, Customer details, etc.
/// - Parameter completion: This is called with either a valid PaymentSheet.FlowController instance or an error if loading failed.
public static func create(
paymentIntentClientSecret: String,
configuration: PaymentSheet.Configuration,
completion: @escaping (Result<PaymentSheet.FlowController, Error>) -> Void
) {
create(clientSecret: .paymentIntent(clientSecret: paymentIntentClientSecret),
configuration: configuration,
completion: completion
)
}
/// An asynchronous failable initializer for PaymentSheet.FlowController
/// This asynchronously loads the Customer's payment methods, their default payment method, and the SetuptIntent.
/// You can use the returned PaymentSheet.FlowController instance to e.g. update your UI with the Customer's default payment method
/// - Parameter setupIntentClientSecret: The [client secret](https://stripe.com/docs/api/setup_intents/object#setup_intent_object-client_secret) of a Stripe SetupIntent object
/// - Parameter configuration: Configuration for the PaymentSheet. e.g. your business name, Customer details, etc.
/// - Parameter completion: This is called with either a valid PaymentSheet.FlowController instance or an error if loading failed.
public static func create(
setupIntentClientSecret: String,
configuration: PaymentSheet.Configuration,
completion: @escaping (Result<PaymentSheet.FlowController, Error>) -> Void
) {
create(clientSecret: .setupIntent(clientSecret: setupIntentClientSecret),
configuration: configuration,
completion: completion
)
}
static func create(
clientSecret: IntentClientSecret,
configuration: PaymentSheet.Configuration,
completion: @escaping (Result<PaymentSheet.FlowController, Error>) -> Void
) {
PaymentSheet.load(
clientSecret: clientSecret,
configuration: configuration
) { result in
switch result {
case .success(let intent, let paymentMethods, let isLinkEnabled):
// Verify that there are payment method types available for the intent and configuration.
let paymentMethodTypes = PaymentMethodType.filteredPaymentMethodTypes(
from: intent,
configuration: configuration)
guard !paymentMethodTypes.isEmpty else {
completion(.failure(PaymentSheetError.noPaymentMethodTypesAvailable))
return
}
let manualFlow = FlowController(
intent: intent,
savedPaymentMethods: paymentMethods,
isLinkEnabled: isLinkEnabled,
configuration: configuration)
// Synchronously pre-load image into cache
if let paymentOption = manualFlow.paymentOption {
_ = paymentOption.image
}
completion(.success(manualFlow))
case .failure(let error):
completion(.failure(error))
}
}
}
/// Presents a sheet where the customer chooses how to pay, either by selecting an existing payment method or adding a new one
/// Call this when your "Select a payment method" button is tapped
/// - Parameter presentingViewController: The view controller that presents the sheet.
/// - Parameter completion: This is called after the sheet is dismissed. Use the `paymentOption` property to get the customer's desired payment option.
public func presentPaymentOptions(
from presentingViewController: UIViewController,
completion: (() -> ())? = nil
) {
guard presentingViewController.presentedViewController == nil else {
assertionFailure("presentingViewController is already presenting a view controller")
completion?()
return
}
if let completion = completion {
presentPaymentOptionsCompletion = completion
}
let showPaymentOptions: () -> Void = { [weak self] in
guard let self = self else { return }
// Set the PaymentSheetViewController as the content of our bottom sheet
let bottomSheetVC = Self.makeBottomSheetViewController(
self.paymentOptionsViewController,
configuration: self.configuration,
didCancelNative3DS2: { [weak self] in
self?.paymentHandler.cancel3DS2ChallengeFlow()
}
)
presentingViewController.presentAsBottomSheet(bottomSheetVC, appearance: self.configuration.appearance)
}
if let linkAccount = LinkAccountContext.shared.account,
linkAccount.sessionState == .requiresVerification,
!linkAccount.hasStartedSMSVerification {
let verificationController = LinkVerificationController(linkAccount: linkAccount)
verificationController.present(from: presentingViewController) { [weak self] result in
switch result {
case .completed:
self?.paymentOptionsViewController.selectLink()
completion?()
case .canceled, .failed:
showPaymentOptions()
}
}
} else {
showPaymentOptions()
}
}
/// Completes the payment or setup.
/// - Parameter presentingViewController: The view controller used to present any view controllers required e.g. to authenticate the customer
/// - Parameter completion: Called with the result of the payment after any presented view controllers are dismissed
public func confirm(
from presentingViewController: UIViewController,
completion: @escaping (PaymentSheetResult) -> ()
) {
guard let paymentOption = _paymentOption else {
assertionFailure("`confirmPayment` should only be called when `paymentOption` is not nil")
let error = PaymentSheetError.unknown(debugDescription: "confirmPayment was called with a nil paymentOption")
completion(.failed(error: error))
return
}
let authenticationContext = AuthenticationContext(presentingViewController: presentingViewController, appearance: configuration.appearance)
PaymentSheet.confirm(
configuration: configuration,
authenticationContext: authenticationContext,
intent: intent,
paymentOption: paymentOption,
paymentHandler: paymentHandler
) { [intent, configuration] result in
STPAnalyticsClient.sharedClient.logPaymentSheetPayment(
isCustom: true,
paymentMethod: paymentOption.analyticsValue,
result: result,
linkEnabled: intent.supportsLink,
activeLinkSession: LinkAccountContext.shared.account?.sessionState == .verified,
paymentOption: paymentOption,
currency: intent.currency
)
if case .completed = result, case .link = paymentOption {
// Remember Link as default payment method for users who just created an account.
DefaultPaymentMethodStore.setDefaultPaymentMethod(.link, forCustomer: configuration.customer?.id)
}
completion(result)
}
}
// MARK: Internal helper methods
static func makeBottomSheetViewController(
_ contentViewController: BottomSheetContentViewController,
configuration: Configuration,
didCancelNative3DS2: (() -> ())? = nil
) -> BottomSheetViewController {
let sheet = BottomSheetViewController(
contentViewController: contentViewController,
appearance: configuration.appearance,
isTestMode: configuration.apiClient.isTestmode,
didCancelNative3DS2: didCancelNative3DS2 ?? { } // TODO(MOBILESDK-864): Refactor this out.
)
// Workaround to silence a warning in the Catalyst target
#if targetEnvironment(macCatalyst)
configuration.style.configure(sheet)
#else
if #available(iOS 13.0, *) {
configuration.style.configure(sheet)
}
#endif
return sheet
}
}
}
// MARK: - ChoosePaymentOptionViewControllerDelegate
@available(iOSApplicationExtension, unavailable)
@available(macCatalystApplicationExtension, unavailable)
extension PaymentSheet.FlowController: ChoosePaymentOptionViewControllerDelegate {
func choosePaymentOptionViewControllerShouldClose(
_ choosePaymentOptionViewController: ChoosePaymentOptionViewController
) {
choosePaymentOptionViewController.dismiss(animated: true) {
self.presentPaymentOptionsCompletion?()
}
}
func choosePaymentOptionViewControllerDidUpdateSelection(
_ choosePaymentOptionViewController: ChoosePaymentOptionViewController
) {
// no-op
}
}
// MARK: - STPAnalyticsProtocol
/// :nodoc:
@available(iOSApplicationExtension, unavailable)
@available(macCatalystApplicationExtension, unavailable)
@_spi(STP) extension PaymentSheet.FlowController: STPAnalyticsProtocol {
@_spi(STP) public static let stp_analyticsIdentifier: String = "PaymentSheet.FlowController"
}
// MARK: - PaymentSheetAuthenticationContext
/// A simple STPAuthenticationContext that wraps a UIViewController
/// For internal SDK use only
@objc(STP_Internal_AuthenticationContext)
class AuthenticationContext: NSObject, PaymentSheetAuthenticationContext {
func present(_ authenticationViewController: UIViewController, completion: @escaping () -> Void) {
presentingViewController.present(authenticationViewController, animated: true, completion: nil)
}
func presentPollingVCForAction(_ action: STPPaymentHandlerActionParams) {
let pollingVC = PollingViewController(currentAction: action,
appearance: self.appearance)
presentingViewController.present(pollingVC, animated: true, completion: nil)
}
func dismiss(_ authenticationViewController: UIViewController) {
authenticationViewController.dismiss(animated: true, completion: nil)
}
let presentingViewController: UIViewController
let appearance: PaymentSheet.Appearance
init(presentingViewController: UIViewController, appearance: PaymentSheet.Appearance) {
self.presentingViewController = presentingViewController
self.appearance = appearance
super.init()
}
func authenticationPresentingViewController() -> UIViewController {
return presentingViewController
}
}