From 8bb882fb1e29a9479dc29cedb27d8aed6b3e10e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awomir=20Kr=C3=B3l?= Date: Wed, 8 Nov 2023 15:43:43 +0100 Subject: [PATCH 1/4] Supports EU URL for tokenization --- RecurlySDK-iOS/Networking/TokenizationAPI.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/RecurlySDK-iOS/Networking/TokenizationAPI.swift b/RecurlySDK-iOS/Networking/TokenizationAPI.swift index 1e6fa28..78435ae 100644 --- a/RecurlySDK-iOS/Networking/TokenizationAPI.swift +++ b/RecurlySDK-iOS/Networking/TokenizationAPI.swift @@ -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 } } From 16b483573ff2261d8610891ca9f4af3c51f28a1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awomir=20Kr=C3=B3l?= Date: Mon, 27 Nov 2023 14:27:11 +0100 Subject: [PATCH 2/4] Add cancelled apple payment state, replace completion handler with Swift Result type --- ContainerApp/ContentView.swift | 15 ++++--- .../Helpers/REApplePaymentHandler.swift | 39 ++++++++++++------- RecurlySDK-iOSTests/RecurlySDK-iOSTests.swift | 8 +++- 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/ContainerApp/ContentView.swift b/ContainerApp/ContentView.swift index b876e53..07e53d3 100644 --- a/ContainerApp/ContentView.swift +++ b/ContainerApp/ContentView.swift @@ -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() @@ -200,8 +199,8 @@ struct ContentView: View { completion(tokenId ?? "") } - } else { - print("Apple Payment Failed") + case .failure(let paymentError): + print("Apple Payment Failed: ", paymentError) completion("") } } diff --git a/RecurlySDK-iOS/Helpers/REApplePaymentHandler.swift b/RecurlySDK-iOS/Helpers/REApplePaymentHandler.swift index 40d4aa8..582074a 100644 --- a/RecurlySDK-iOS/Helpers/REApplePaymentHandler.swift +++ b/RecurlySDK-iOS/Helpers/REApplePaymentHandler.swift @@ -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 { @@ -25,12 +31,12 @@ public class REApplePaymentHandler: NSObject { // 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 @@ -57,17 +63,18 @@ 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(completion: { [weak self] (presented: Bool) 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(), PKContact()))) } } else { NSLog("Failed to present payment controller") - self.completionHandler!(false, nil, nil) + self.completionHandler(.failure(.failedToPresentController)) } }) } @@ -97,7 +104,7 @@ public class REApplePaymentHandler: NSObject { // This method is intented to use from XCTest public func paymentControllerIsPresented() -> Bool { - self.isPaymentControllerPresented + isPaymentControllerPresented } } @@ -132,18 +139,22 @@ extension REApplePaymentHandler: PKPaymentAuthorizationControllerDelegate { // Once processed, return an appropriate status in the completion handler (success, failure, etc) paymentStatus = .success } - - completion(PKPaymentAuthorizationResult(status: paymentStatus, errors: errors)) + completion(PKPaymentAuthorizationResult(status: paymentStatus!, errors: errors)) } // 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)) } } } diff --git a/RecurlySDK-iOSTests/RecurlySDK-iOSTests.swift b/RecurlySDK-iOSTests/RecurlySDK-iOSTests.swift index 95d56f9..2783007 100644 --- a/RecurlySDK-iOSTests/RecurlySDK-iOSTests.swift +++ b/RecurlySDK-iOSTests/RecurlySDK-iOSTests.swift @@ -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) From bf8f1988e4a1f22f7b4c02b491825df9bbc0fea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awomir=20Kr=C3=B3l?= Date: Tue, 5 Dec 2023 17:15:56 +0100 Subject: [PATCH 3/4] Fix NetworkEngine to supports completion with connection errors (ex. internet connection issues) --- RecurlySDK-iOS/Networking/NetworkEngine.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/RecurlySDK-iOS/Networking/NetworkEngine.swift b/RecurlySDK-iOS/Networking/NetworkEngine.swift index b7b6605..3cdb54a 100644 --- a/RecurlySDK-iOS/Networking/NetworkEngine.swift +++ b/RecurlySDK-iOS/Networking/NetworkEngine.swift @@ -51,10 +51,12 @@ class NetworkEngine { func sendRequest(responseModel: T.Type, request: URLRequest, completionHandler: @escaping (Result) -> ()) { 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 } From d7543e1007af4e2c811b5b0d88bce5321783a770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awomir=20Kr=C3=B3l?= Date: Tue, 12 Dec 2023 14:29:55 +0100 Subject: [PATCH 4/4] Make contact fields optional for ApplePay --- ContainerApp/ContentView.swift | 14 ++--- .../Helpers/REApplePaymentHandler.swift | 58 ++++++++++--------- 2 files changed, 39 insertions(+), 33 deletions(-) diff --git a/ContainerApp/ContentView.swift b/ContainerApp/ContentView.swift index 07e53d3..64ea342 100644 --- a/ContainerApp/ContentView.swift +++ b/ContainerApp/ContentView.swift @@ -171,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: "") diff --git a/RecurlySDK-iOS/Helpers/REApplePaymentHandler.swift b/RecurlySDK-iOS/Helpers/REApplePaymentHandler.swift index 582074a..41805cf 100644 --- a/RecurlySDK-iOS/Helpers/REApplePaymentHandler.swift +++ b/RecurlySDK-iOS/Helpers/REApplePaymentHandler.swift @@ -14,7 +14,7 @@ public enum REApplePaymentError: Error { } // Callback for payment status -public typealias PaymentCompletionHandler = (Result<(PKPaymentToken, PKContact), REApplePaymentError>) -> Void +public typealias PaymentCompletionHandler = (Result<(PKPaymentToken, PKContact?), REApplePaymentError>) -> Void // Primary Apple Payment class to handle all logic about ApplePay Button public class REApplePaymentHandler: NSObject { @@ -28,6 +28,7 @@ public class REApplePaymentHandler: NSObject { ] var paymentController: PKPaymentAuthorizationController? + var requiredContactFields = Set() // Reference to REApplePayItem var paymentSummaryItems = [PKPaymentSummaryItem]() // Current status of the transaction @@ -49,6 +50,7 @@ public class REApplePaymentHandler: NSObject { */ public func startApplePayment(with applePayInfo: REApplePayInfo, completion: @escaping PaymentCompletionHandler) { + requiredContactFields = applePayInfo.requiredContactFields paymentSummaryItems = applePayInfo.paymentSummaryItems() completionHandler = completion @@ -63,20 +65,20 @@ public class REApplePaymentHandler: NSObject { private func presentPaymentRequest(with paymentRequest: PKPaymentRequest) { paymentController = PKPaymentAuthorizationController(paymentRequest: paymentRequest) paymentController?.delegate = self - paymentController?.present(completion: { [weak self] (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(.success((PKPaymentToken(), PKContact()))) + self.completionHandler(.success((PKPaymentToken(), nil))) } } else { NSLog("Failed to present payment controller") self.completionHandler(.failure(.failedToPresentController)) } - }) + } } // Create the payment request @@ -116,30 +118,34 @@ 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 } - completion(PKPaymentAuthorizationResult(status: paymentStatus!, errors: errors)) + + 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 + } + + // 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 @@ -149,7 +155,7 @@ extension REApplePaymentHandler: PKPaymentAuthorizationControllerDelegate { guard let self = self else { return } if let paymentStatus = self.paymentStatus { if paymentStatus == .success { - self.completionHandler(.success((self.currentToken!, self.currentBillingInfo!))) + self.completionHandler(.success((self.currentToken!, self.currentBillingInfo))) } else { self.completionHandler(.failure(.paymentAuthorization)) }