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

Supports EU URL for tokenization, add cancelled ApplePay state, supports server connection issues completion #41

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
29 changes: 14 additions & 15 deletions ContainerApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,14 +139,13 @@ struct ContentView: View {
applePayInfo.currencyCode = "USD"

/// Starting the Apple Pay flow
self.paymentHandler.startApplePayment(with: applePayInfo) { (success, token, billingInfo) in
paymentHandler.startApplePayment(with: applePayInfo) { result in

if success {
switch result {
case .success(let pkPaymentData):
/// Token object 'PKPaymentToken' returned by Apple Pay
guard let token = token else { return }

/// Billing info
guard let billingInfo = billingInfo else { return }
let token = pkPaymentData.0
let billingInfo = pkPaymentData.1

/// Decode ApplePaymentData from Token
let decoder = JSONDecoder()
Expand All @@ -172,14 +171,14 @@ struct ContentView: View {
let applePaymentMethod = REApplePaymentMethod(paymentMethod: REApplePaymentMethodBody(displayName: displayName, network: network, type: "\(type)"))

// Creating Billing Info
let billingData = REBillingInfo(firstName: billingInfo.name?.givenName ?? String(),
lastName: billingInfo.name?.familyName ?? String(),
address1: billingInfo.postalAddress?.street ?? String(),
let billingData = REBillingInfo(firstName: billingInfo?.name?.givenName ?? String(),
lastName: billingInfo?.name?.familyName ?? String(),
address1: billingInfo?.postalAddress?.street ?? String(),
address2: "",
country: billingInfo.postalAddress?.country ?? String(),
city: billingInfo.postalAddress?.city ?? String(),
state: billingInfo.postalAddress?.state ?? String(),
postalCode: billingInfo.postalAddress?.postalCode ?? String(),
country: billingInfo?.postalAddress?.country ?? String(),
city: billingInfo?.postalAddress?.city ?? String(),
state: billingInfo?.postalAddress?.state ?? String(),
postalCode: billingInfo?.postalAddress?.postalCode ?? String(),
taxIdentifier: "",
taxIdentifierType: "")

Expand All @@ -200,8 +199,8 @@ struct ContentView: View {
completion(tokenId ?? "")
}

} else {
print("Apple Payment Failed")
case .failure(let paymentError):
print("Apple Payment Failed: ", paymentError)
completion("")
}
}
Expand Down
85 changes: 51 additions & 34 deletions RecurlySDK-iOS/Helpers/REApplePaymentHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@

import PassKit

public enum REApplePaymentError: Error {
case failedToPresentController
case paymentAuthorization
case cancelled
}

// Callback for payment status
public typealias PaymentCompletionHandler = (Bool, PKPaymentToken?, PKContact?) -> Void
public typealias PaymentCompletionHandler = (Result<(PKPaymentToken, PKContact?), REApplePaymentError>) -> Void

// Primary Apple Payment class to handle all logic about ApplePay Button
public class REApplePaymentHandler: NSObject {
Expand All @@ -22,15 +28,16 @@ public class REApplePaymentHandler: NSObject {
]

var paymentController: PKPaymentAuthorizationController?
var requiredContactFields = Set<PKContactField>()
// Reference to REApplePayItem
var paymentSummaryItems = [PKPaymentSummaryItem]()
// Current status of the transaction
var paymentStatus = PKPaymentAuthorizationStatus.failure
var paymentStatus: PKPaymentAuthorizationStatus?
// ApplePay token
var currentToken: PKPaymentToken?
// Billing info
var currentBillingInfo: PKContact?
var completionHandler: PaymentCompletionHandler?
var completionHandler: PaymentCompletionHandler!

// TDD
var isPaymentControllerPresented = false
Expand All @@ -43,6 +50,7 @@ public class REApplePaymentHandler: NSObject {
*/
public func startApplePayment(with applePayInfo: REApplePayInfo, completion: @escaping PaymentCompletionHandler) {

requiredContactFields = applePayInfo.requiredContactFields
paymentSummaryItems = applePayInfo.paymentSummaryItems()
completionHandler = completion

Expand All @@ -57,19 +65,20 @@ public class REApplePaymentHandler: NSObject {
private func presentPaymentRequest(with paymentRequest: PKPaymentRequest) {
paymentController = PKPaymentAuthorizationController(paymentRequest: paymentRequest)
paymentController?.delegate = self
paymentController?.present(completion: { (presented: Bool) in
paymentController?.present { [weak self] presented in
guard let self = self else { return }
if presented {
NSLog("Presented payment controller")
self.isPaymentControllerPresented = true

if self.isTesting {
self.completionHandler!(true, nil, nil)
self.completionHandler(.success((PKPaymentToken(), nil)))
}
} else {
NSLog("Failed to present payment controller")
self.completionHandler!(false, nil, nil)
self.completionHandler(.failure(.failedToPresentController))
}
})
}
}

// Create the payment request
Expand Down Expand Up @@ -97,7 +106,7 @@ public class REApplePaymentHandler: NSObject {

// This method is intented to use from XCTest
public func paymentControllerIsPresented() -> Bool {
self.isPaymentControllerPresented
isPaymentControllerPresented
}

}
Expand All @@ -109,41 +118,49 @@ extension REApplePaymentHandler: PKPaymentAuthorizationControllerDelegate {

public func paymentAuthorizationController(_ controller: PKPaymentAuthorizationController, didAuthorizePayment payment: PKPayment, handler completion: @escaping (PKPaymentAuthorizationResult) -> Void) {

var errors = [Error]()
paymentStatus = .failure

// Perform some very basic validation on the provided contact information
if payment.shippingContact?.postalAddress == nil {
let emailError = PKPaymentRequest.paymentContactInvalidError(withContactField: .postalAddress, localizedDescription: "An error with postal address occurred")
errors.append(emailError)
} else if payment.shippingContact?.phoneNumber == nil {
let phoneError = PKPaymentRequest.paymentContactInvalidError(withContactField: .phoneNumber, localizedDescription: "An error with phone number occurred")
errors.append(phoneError)
} else if payment.shippingContact?.name == nil {
let nameError = PKPaymentRequest.paymentContactInvalidError(withContactField: .name, localizedDescription: "An error with Name occurred")
errors.append(nameError)
} else {
// Here you would send the payment token to your server or payment provider to process
currentToken = payment.token

// Set current billing info
currentBillingInfo = payment.shippingContact

// Once processed, return an appropriate status in the completion handler (success, failure, etc)
paymentStatus = .success
if requiredContactFields.contains(.name), payment.shippingContact?.name == nil {
let error = PKPaymentRequest.paymentContactInvalidError(withContactField: .name, localizedDescription: "An error with Name occurred")
completion(PKPaymentAuthorizationResult(status: .failure, errors: [error]))
return
}

if requiredContactFields.contains(.phoneNumber), payment.shippingContact?.phoneNumber == nil {
let error = PKPaymentRequest.paymentContactInvalidError(withContactField: .phoneNumber, localizedDescription: "An error with phone number occurred")
completion(PKPaymentAuthorizationResult(status: .failure, errors: [error]))
return
}

if requiredContactFields.contains(.postalAddress), payment.shippingContact?.postalAddress == nil {
let error = PKPaymentRequest.paymentContactInvalidError(withContactField: .postalAddress, localizedDescription: "An error with postal address occurred")
completion(PKPaymentAuthorizationResult(status: .failure, errors: [error]))
return
}

completion(PKPaymentAuthorizationResult(status: paymentStatus, errors: errors))
// Here you would send the payment token to your server or payment provider to process
currentToken = payment.token
// Set current billing info
currentBillingInfo = payment.shippingContact
// Once processed, return an appropriate status in the completion handler (success, failure, etc)
paymentStatus = .success

completion(PKPaymentAuthorizationResult(status: paymentStatus!, errors: nil))
}

// Responsible for dismissing and releasing the controller
public func paymentAuthorizationControllerDidFinish(_ controller: PKPaymentAuthorizationController) {
controller.dismiss {
DispatchQueue.main.async {
if self.paymentStatus == .success {
self.completionHandler!(true, self.currentToken, self.currentBillingInfo)
controller.dismiss { [weak self] in
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if let paymentStatus = self.paymentStatus {
if paymentStatus == .success {
self.completionHandler(.success((self.currentToken!, self.currentBillingInfo)))
} else {
self.completionHandler(.failure(.paymentAuthorization))
}
} else {
self.completionHandler!(false, nil, nil)
self.completionHandler(.failure(.cancelled))
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion RecurlySDK-iOS/Networking/NetworkEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,12 @@ class NetworkEngine {
func sendRequest<T:Codable>(responseModel: T.Type, request: URLRequest, completionHandler: @escaping (Result<T, REBaseErrorResponse>) -> ()) {

URLSession.shared.dataTask(with: request as URLRequest, completionHandler: { data, response, error in
guard error == nil else {
if let error = error {
completionHandler(.failure(REBaseErrorResponse(error: RETokenError(code: "sdk-internal \(error._code)", message: error.localizedDescription))))
return
}
guard let data = data else {
completionHandler(.failure(REBaseErrorResponse(error: RETokenError(code: "sdk-internal", message: "No data"))))
return
}

Expand Down
8 changes: 7 additions & 1 deletion RecurlySDK-iOS/Networking/TokenizationAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,15 @@ extension TokenizationAPI: BaseRequest {
}

var baseURL: String {
let defaultURL = "api.recurly.com/js/v1"
let defaultURLEU = "api.eu.recurly.com/js/v1"

switch self {
default:
return "api.recurly.com/js/v1"
if REConfiguration.shared.apiPublicKey.starts(with: "fra-") {
return defaultURLEU
}
return defaultURL
}
}

Expand Down
8 changes: 6 additions & 2 deletions RecurlySDK-iOSTests/RecurlySDK-iOSTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,12 @@ class RecurlySDK_iOSTests: XCTestCase {
applePayInfo.currencyCode = "USD"

let tokenResponseExpectation = expectation(description: "ApplePayTokenResponse")
paymentHandler.startApplePayment(with: applePayInfo) { (success, token, nil) in
XCTAssertTrue(success, "Apple Pay is not ready")
paymentHandler.startApplePayment(with: applePayInfo) { result in
/// Wrap workaround for: https://github.com/apple/swift/issues/43104
func assertNoThrow() {
XCTAssertNoThrow(try result.get(), "Apple Pay is not ready")
}
assertNoThrow()
tokenResponseExpectation.fulfill()
}
wait(for: [tokenResponseExpectation], timeout: 3.0)
Expand Down