This repository has been archived by the owner on Nov 16, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathBraintree.swift
357 lines (306 loc) · 14.8 KB
/
Braintree.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
import Foundation
#if os(iOS)
import PassKit
#endif
/// Braintree v.zero for iOS and OS X
public struct Braintree {
/// The current version of the library
public static let Version = "0.0.1"
/// The customer's raw payment method details for uploading to Braintree
///
/// - Card: Payment details for a credit or debit card
public enum TokenizationRequest {
/// A payment method's expiration date
public struct Expiration {
public let expirationDate : String
/// Initialize a TokenizationRequest based on an expiration date string
///
/// :param: expirationDate the human-friendly expiration date, formatted "MM/YY", "MM/YYYY" or YYYY-MM
///
/// :returns: An expiration
public init(expirationDate : String) {
self.expirationDate = expirationDate
}
/// Initialize a TokenizationRequest based on an expiration month and year
///
/// :param: expirationMonth expiration month 1-12
/// :param: expirationYear expiration year, such as 2014
///
/// :returns: An expiration
public init(expirationMonth : Int, expirationYear : Int) {
expirationDate = "\(expirationMonth)/\(expirationYear)"
}
}
/// A credit card tokenization request for credit cards, debit cards, etc.
///
/// :param: number A Luhn-10 valid card number
/// :param: expiration The card's expiration date
case Card(number : String, expiration : Expiration)
#if os(iOS)
/// An Apple Pay payment generated by PassKit and Touch ID
///
/// :see: PKPaymentAuthorizationViewController
///
/// :param: payment An Apple Pay payment containing encrypted card details
case ApplePay(payment : PKPayment)
#endif
internal func rawParameters() -> Dictionary<String, AnyObject> {
let creditCardParameters = { (number : String, expiration : Expiration) -> Dictionary<String, AnyObject> in
return [ "credit_card": [
"number": number,
"expiration_date": expiration.expirationDate,
"options": [ "validate": false ]
] ]
}
#if os(iOS)
switch self {
case let .Card(number: number, expiration: expiration):
return creditCardParameters(number, expiration)
case let .ApplePay(payment: payment):
let token = payment.token
return ["applePaymentToken": [
"paymentData": token.paymentData.base64EncodedStringWithOptions(nil),
"paymentInstrumentName": token.paymentInstrumentName,
"transactionIdentifier": token.transactionIdentifier,
"paymentNetwork": token.paymentNetwork ] ]
}
#else
switch self {
case let .Card(number: number, expiration: expiration):
return creditCardParameters(number, expiration)
}
#endif
}
internal var resource : String {
let cardResource = "v1/payment_methods/credit_cards"
#if os(iOS)
switch self {
case let .Card(number: String, expiration: Expiration):
return cardResource
case let .ApplePay(payment: payment):
return "v1/payment_methods/apple_payment_tokens"
}
#else
switch self {
case let .Card(number: String, expiration: Expiration):
return cardResource
}
#endif
}
internal var jsonResponseRoot : String {
#if os(iOS)
switch self {
case let .Card(number: number, expiration: expiration):
return "creditCards"
case let .ApplePay(payment: payment):
return "applePayCards"
}
#else
switch self {
case let .Card(number: number, expiration: expiration):
return "creditCards"
}
#endif
}
}
/// A response from Braintree's API
///
/// - PaymentMethodNonce: A payment method nonce has successfully created for the given payment method details
/// - RequestError: A payment method nonce could not be created because of the request
/// - BraintreeError: A payment method nonce could not be created because of Braintree
public enum TokenizationResponse {
case PaymentMethodNonce(nonce : String)
case RequestError(message : String, fieldErrors : JSON)
case BraintreeError(message : String)
}
// MARK: - Client
/// A client for interacting with Braintree's servers directly from your app
public class Client {
/// A function that obtains a Client Token from your server whenever this library asks for one
///
/// This function *must* invoke the completion callback, even if only to tell Braintree that
/// it is not possible to generate a Client Token at this time (by passing `nil`).
///
/// :note: A fresh client token should be generated and fetched each time this function is called.
public typealias ClientTokenProvider = ((String?) -> (Void)) -> Void
// MARK: - Public Interface
/// Initializes the Braintree Client
///
/// :param: clientTokenProvider A function that can provide a fresh Client Token on demand
///
/// :returns: A client that is ready to tokenize payment methods
public required init(clientTokenProvider : ClientTokenProvider) {
self.clientTokenProvider = clientTokenProvider
refreshConfiguration()
}
/// Tokenize a customer's raw payment details, generating a payment method nonce that
/// can safely be transmitted to your servers.
///
/// :param: details A tokenization request containing the raw payment details
/// :param: completion A closure that is called upon completion
public func tokenize(details : TokenizationRequest, completion : (TokenizationResponse) -> (Void)) {
withConfiguration() {
self.api.post(details.resource, parameters: details.rawParameters(), completion: { (response) in
switch response {
case let .UnexpectedError(description, error):
return completion(.BraintreeError(message: description))
case let .Error(error):
let e = error
let message = e.localizedDescription
return completion(.BraintreeError(message: message))
case let .Completion(JSON, response):
switch response.statusCode {
case 400..<500:
if let topLevelError = JSON["error"]["message"].asString {
return completion(.RequestError(message: topLevelError, fieldErrors: JSON))
}
return completion(.BraintreeError(message: "Tokenization Request Error"))
case 200..<300:
if let nonce = JSON[details.jsonResponseRoot][0]["nonce"].asString {
return completion(.PaymentMethodNonce(nonce: nonce))
}
return completion(.BraintreeError(message: "Invalid Response Format"))
case 100..<200:
fallthrough
case 500..<999:
fallthrough
default:
return completion(.BraintreeError(message: "Braintree Service Error"))
}
}
})
}
}
// MARK: Internal State
private let clientTokenProvider : ClientTokenProvider
private var configuration : Configuration? {
didSet {
if let configuration = configuration {
self.api.baseURL = configuration.clientApiBaseURL
self.api.authorizationFingerprint = configuration.authorizationFingerprint
for configurationHandler in withConfigurationQueue {
configurationHandler()
}
withConfigurationQueue.removeAll(keepCapacity: false)
}
}
}
private var withConfigurationQueue : [(Void) -> (Void)] = []
private let api : API = API()
// MARK: Internal Helpers
private func refreshConfiguration() {
self.clientTokenProvider() { [weak self] clientToken in
if let clientToken = clientToken {
if let parsedClientToken = ClientToken.parse(clientToken: clientToken) {
let version : Int? = parsedClientToken.version
let clientApiUrl : NSURL? = parsedClientToken.clientApiUrl
let authorizationFingerprint : String? = parsedClientToken.authorizationFingerprint
if version != 2 || clientApiUrl == nil || authorizationFingerprint == nil {
return println("Braintree: Invalid client token")
}
self?.configuration = Configuration(
clientApiBaseURL: clientApiUrl!,
authorizationFingerprint: authorizationFingerprint!
)
}
} else {
return println("Braintree: Client Token was not provided when requested from client token provider")
}
}
}
private func withConfiguration(completion : (Void) -> (Void)) {
if configuration != nil {
completion()
} else {
withConfigurationQueue.append(completion)
}
}
}
// MARK: - Types
class ClientToken : JSON {
class func parse(#clientToken: String) -> ClientToken? {
if let decodedClientToken = NSData(base64EncodedString: clientToken, options: nil) {
return ClientToken(data: decodedClientToken)
}
return nil
}
var version : Int? { return self["version"].asInt }
var clientApiUrl : NSURL? {
if let clientApiUrl = self["clientApiUrl"].asString {
return NSURL(string: clientApiUrl)
} else {
return nil
}
}
var authorizationFingerprint : String? { return self["authorizationFingerprint"].asString }
}
internal struct Configuration {
let clientApiBaseURL : NSURL
let authorizationFingerprint : String
}
// MARK: - API Client
internal class API {
enum Response {
case Completion(JSON : JSON, response : NSHTTPURLResponse)
case Error(error: NSError)
case UnexpectedError(description: String, error: NSError?)
}
private var baseURLComponents : NSURLComponents?
private var authorizationFingerprint : String?
private var session : NSURLSession {
let configuration = NSURLSessionConfiguration.ephemeralSessionConfiguration()
configuration.HTTPAdditionalHeaders = [
"User-Agent": "Braintree/Swift/\(Braintree.Version)",
"Accept": "application/json",
"Accept-Language": "en_US"
]
return NSURLSession(configuration: configuration, delegate: nil, delegateQueue: NSOperationQueue.mainQueue())
}
var baseURL : NSURL?
required init() {
}
func post(path : String, parameters : Dictionary<String, AnyObject>?, completion : (Response) -> (Void)) {
request("post", path: path, parameters: parameters, completion: completion)
}
private func request(method: String, path: String, parameters: [String:AnyObject]?, completion: (Response) -> (Void)) {
if let baseURL = baseURL {
if let pathComponents = NSURLComponents(URL: baseURL.URLByAppendingPathComponent(path), resolvingAgainstBaseURL: false) {
let legalURLCharactersToBeEscaped: CFStringRef = ":/?&=;+!@#$()',*"
let authorizationFingerprint = CFURLCreateStringByAddingPercentEscapes(
nil,
self.authorizationFingerprint,
nil,
legalURLCharactersToBeEscaped,
CFStringBuiltInEncodings.UTF8.rawValue
)
pathComponents.percentEncodedQuery = "authorizationFingerprint=\(authorizationFingerprint)"
let request = NSMutableURLRequest(URL: pathComponents.URL!)
request.HTTPMethod = method
if let parameters = parameters {
request.HTTPBody = JSON(parameters).toString().dataUsingEncoding(NSUTF8StringEncoding)
request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
}
print("[Braintree] API Request: ")
debugPrintln(request)
session.dataTaskWithRequest(request, { (data, response, error) -> Void in
let response = response as NSHTTPURLResponse!
let broxyId = response.allHeaderFields["X-BroxyId"] as String? ?? ""
println("[Braintree] API Response [\(broxyId)]: ")
debugPrintln(response)
if let error = error {
return completion(.Error(error: error))
}
if data.length == 0 || !startsWith(response.allHeaderFields["Content-Type"] as String, "application/json") {
return completion(.UnexpectedError(description: "Invalid API response", error: nil))
}
let responseJSON = JSON(data: data)
if let jsonError = responseJSON.asError {
return completion(.UnexpectedError(description: "Invalid JSON in response", error: jsonError))
}
return completion(.Completion(JSON: responseJSON, response: response))
}).resume()
}
}
}
}
}