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

Cancel error handling #297

Merged
merged 7 commits into from
Nov 13, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,25 @@ struct PayPalWebButtonsView: View {
Text("Pay Later").tag(PayPalWebCheckoutFundingSource.paylater)
}
.pickerStyle(SegmentedPickerStyle())

switch selectedFundingSource {
case .paypalCredit:
PayPalCreditButton.Representable(color: .black, size: .full) {
payPalWebViewModel.paymentButtonTapped(funding: .paypalCredit)
}
case .paylater:
PayPalPayLaterButton.Representable(color: .silver, edges: .softEdges, size: .full) {
payPalWebViewModel.paymentButtonTapped(funding: .paylater)
ZStack {
switch selectedFundingSource {
case .paypalCredit:
PayPalCreditButton.Representable(color: .black, size: .full) {
payPalWebViewModel.paymentButtonTapped(funding: .paypalCredit)
}
case .paylater:
PayPalPayLaterButton.Representable(color: .silver, edges: .softEdges, size: .full) {
payPalWebViewModel.paymentButtonTapped(funding: .paylater)
}
case .paypal:
PayPalButton.Representable(color: .blue, size: .full) {
payPalWebViewModel.paymentButtonTapped(funding: .paypal)
}
}
case .paypal:
PayPalButton.Representable(color: .blue, size: .full) {
payPalWebViewModel.paymentButtonTapped(funding: .paypal)
if payPalWebViewModel.state == .loading &&
payPalWebViewModel.checkoutResult == nil &&
payPalWebViewModel.orderID != nil {
CircularProgressView()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ struct PayPalWebCreateOrderView: View {
}
}
.buttonStyle(RoundedBlueButtonStyle())
if payPalWebViewModel.state == .loading && payPalWebViewModel.checkoutResult == nil {
if payPalWebViewModel.state == .loading && payPalWebViewModel.checkoutResult == nil && payPalWebViewModel.orderID == nil {
CircularProgressView()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class PayPalWebViewModel: ObservableObject {
func paymentButtonTapped(funding: PayPalWebCheckoutFundingSource) {
Task {
do {
self.updateState(.loading)
payPalWebCheckoutClient = try await getPayPalClient()
guard let payPalWebCheckoutClient else {
print("Error initializing PayPalWebCheckoutClient")
Expand All @@ -72,7 +73,12 @@ class PayPalWebViewModel: ObservableObject {
let payPalRequest = PayPalWebCheckoutRequest(orderID: orderID, fundingSource: funding)
payPalWebCheckoutClient.start(request: payPalRequest) { result, error in
if let error {
self.updateState(.error(message: error.localizedDescription))
if PayPalWebCheckoutClient.isCheckoutCanceled(error) {
print("Canceled")
self.updateState(.idle)
} else {
self.updateState(.error(message: error.localizedDescription))
}
} else {
self.updateState(.success)
self.checkoutResult = result
Expand Down Expand Up @@ -137,22 +143,4 @@ class PayPalWebViewModel: ObservableObject {
self.state = state
}
}

// MARK: - PayPalWeb Checkout Delegate

func payPal(
_ payPalClient: PayPalWebCheckoutClient,
didFinishWithResult result: PayPalWebCheckoutResult
) {
updateState(.success)
checkoutResult = result
}

func payPal(_ payPalClient: PayPalWebCheckoutClient, didFinishWithError error: CoreSDKError) {
updateState(.error(message: error.localizedDescription))
}

func payPalDidCancel(_ payPalClient: PayPalWebCheckoutClient) {
print("PayPal Checkout Canceled")
}
}
46 changes: 11 additions & 35 deletions Demo/Demo/ViewModels/CardPaymentViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,13 @@ class CardPaymentViewModel: ObservableObject {
let cardRequest = CardRequest(orderID: orderID, card: card, sca: sca)
cardClient?.approveOrder(request: cardRequest) { result, error in
if let error {
self.setUpdateSetupTokenFailureResult(vaultError: error)
if CardClient.isThreeDSecureCanceled(error) {
self.setApprovalCancelResult()
} else {
self.setApprovalFailureResult(vaultError: error)
}
} else if let result {
self.approveResultSuccessResult(
self.setApprovalSuccessResult(
approveResult: CardPaymentState.CardResult(
id: result.orderID,
status: result.status,
Expand All @@ -132,57 +136,29 @@ class CardPaymentViewModel: ObservableObject {
}
}
} catch {
setUpdateSetupTokenFailureResult(vaultError: error)
setApprovalFailureResult(vaultError: error)
print("failed in checkout with card. \(error.localizedDescription)")
}
}

func approveResultSuccessResult(approveResult: CardPaymentState.CardResult) {
func setApprovalSuccessResult(approveResult: CardPaymentState.CardResult) {
DispatchQueue.main.async {
self.state.approveResultResponse = .loaded(
approveResult
)
}
}

func setUpdateSetupTokenFailureResult(vaultError: Error) {
func setApprovalFailureResult(vaultError: Error) {
DispatchQueue.main.async {
self.state.approveResultResponse = .error(message: vaultError.localizedDescription)
}
}

// MARK: - Card Delegate

func card(_ cardClient: CardPayments.CardClient, didFinishWithResult result: CardPayments.CardResult) {
approveResultSuccessResult(
approveResult: CardPaymentState.CardResult(
id: result.orderID,
status: result.status,
didAttemptThreeDSecureAuthentication: result.didAttemptThreeDSecureAuthentication
)
)
}

func card(_ cardClient: CardPayments.CardClient, didFinishWithError error: CorePayments.CoreSDKError) {
print("Error here")
DispatchQueue.main.async {
self.state.approveResultResponse = .error(message: error.localizedDescription)
}
}

func cardDidCancel(_ cardClient: CardPayments.CardClient) {
print("Card Payment Canceled")
func setApprovalCancelResult() {
print("Canceled")
DispatchQueue.main.async {
self.state.approveResultResponse = .idle
self.state.approveResult = nil
}
}

func cardThreeDSecureWillLaunch(_ cardClient: CardPayments.CardClient) {
print("About to launch 3DS")
}

func cardThreeDSecureDidFinish(_ cardClient: CardPayments.CardClient) {
print("Finished 3DS")
}
}
7 changes: 6 additions & 1 deletion Demo/Demo/ViewModels/CardVaultViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ class CardVaultViewModel: VaultViewModel {
)
)
} else if let vaultError {
self.state.updateSetupTokenResponse = .error(message: vaultError.localizedDescription)
if CardClient.isThreeDSecureCanceled(vaultError) {
print("Canceled")
self.state.updateSetupTokenResponse = .idle
} else {
self.state.updateSetupTokenResponse = .error(message: vaultError.localizedDescription)
}
}
}
}
Expand Down
11 changes: 9 additions & 2 deletions Demo/Demo/ViewModels/PayPalVaultViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,15 @@ class PayPalVaultViewModel: VaultViewModel {
let vaultRequest = PayPalVaultRequest(setupTokenID: setupTokenID)
paypalClient.vault(vaultRequest) { result, error in
KunJeongPark marked this conversation as resolved.
Show resolved Hide resolved
if let error {
DispatchQueue.main.async {
self.state.paypalVaultTokenResponse = .error(message: error.localizedDescription)
if PayPalWebCheckoutClient.isVaultCanceled(error) {
DispatchQueue.main.async {
print("Canceled")
self.state.paypalVaultTokenResponse = .idle
}
} else {
DispatchQueue.main.async {
self.state.paypalVaultTokenResponse = .error(message: error.localizedDescription)
}
}
} else if let result {
DispatchQueue.main.async {
Expand Down
8 changes: 8 additions & 0 deletions Sources/CardPayments/CardClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,14 @@ public class CardClient: NSObject {
}
}

// Helper function that allows handling of 3DS cancel errors separately without having to cast the error to CoreSDKError and checking code and domain properties.
public static func isThreeDSecureCanceled(_ error: Error) -> Bool {
guard let error = error as? CoreSDKError else {
return false
}
return error.domain == CardClientError.domain && error.code == CardClientError.threeDSecureCanceled.code
}

private func startThreeDSecureChallenge(
url: URL,
orderId: String,
Expand Down
12 changes: 10 additions & 2 deletions Sources/CorePayments/Networking/HTTP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,16 @@ class HTTP {
httpRequest.headers.forEach { key, value in
urlRequest.addValue(value, forHTTPHeaderField: key.rawValue)
}

let (data, response) = try await urlSession.performRequest(with: urlRequest)

let (data, response): (Data, URLResponse)
do {
(data, response) = try await urlSession.performRequest(with: urlRequest)
} catch _ as URLError {
throw NetworkingClientError.urlSessionError
} catch {
throw NetworkingClientError.unknownError
}

guard let response = response as? HTTPURLResponse else {
throw NetworkingClientError.invalidURLResponseError
}
Expand Down
12 changes: 5 additions & 7 deletions Sources/CorePayments/Networking/NetworkingClientError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,11 @@ enum NetworkingClientError {
errorDescription: "An unknown error occured. Contact developer.paypal.com/support."
)

static let urlSessionError: (String) -> CoreSDKError = { description in
CoreSDKError(
code: Code.urlSessionError.rawValue,
domain: domain,
errorDescription: description
)
}
static let urlSessionError = CoreSDKError(
code: Code.urlSessionError.rawValue,
domain: domain,
errorDescription: "An error occured during network call. Contact developer.paypal.com/support."
)

static let jsonDecodingError: (String) -> CoreSDKError = { description in
CoreSDKError(
Expand Down
16 changes: 16 additions & 0 deletions Sources/PayPalWebPayments/PayPalWebCheckoutClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,22 @@ public class PayPalWebCheckoutClient: NSObject {
}
}

// Helper function that allows handling of PayPal checkout cancel errors separately without having to cast the error to CoreSDKError and checking code and domain properties.
public static func isCheckoutCanceled(_ error: Error) -> Bool {
guard let error = error as? CoreSDKError else {
return false
}
return error.domain == PayPalWebCheckoutClientError.domain && error.code == PayPalWebCheckoutClientError.checkoutCanceled.code
}

// Helper function that allows handling of PayPal vault cancel errors separately without having to cast the error to CoreSDKError and checking code and domain properties.
public static func isVaultCanceled(_ error: Error) -> Bool {
guard let error = error as? CoreSDKError else {
return false
}
return error.domain == PayPalWebCheckoutClientError.domain && error.code == PayPalWebCheckoutClientError.vaultCanceled.code
}

private func getQueryStringParameter(url: String, param: String) -> String? {
guard let url = URLComponents(string: url) else { return nil }
return url.queryItems?.first { $0.name == param }?.value
Expand Down
25 changes: 25 additions & 0 deletions UnitTests/CardPaymentsTests/CardClient_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import AuthenticationServices
@testable import TestShared

// swiftlint:disable type_body_length
// swiftlint:disable file_length
class CardClient_Tests: XCTestCase {

// MARK: - Helper Properties
Expand Down Expand Up @@ -397,4 +398,28 @@ class CardClient_Tests: XCTestCase {

waitForExpectations(timeout: 2, handler: nil)
}

// MARK: - Helper function test

func testApproveOrder_withThreeDSecure_userCancelsBrowser_returns_isThreeDSecureCanceledTrue() {
mockCheckoutOrdersAPI.stubConfirmResponse = FakeConfirmPaymentResponse.withValid3DSURL
mockWebAuthSession.cannedErrorResponse = ASWebAuthenticationSessionError(
.canceledLogin,
userInfo: ["Description": "Mock cancellation error description."]
)

let expectation = expectation(description: "approveOrder() completed")

sut.approveOrder(request: cardRequest) { result, error in
XCTAssertNil(result)
if let error = error {
XCTAssertTrue(CardClient.isThreeDSecureCanceled(error))
} else {
XCTFail("Expected error due to user cancellation")
}
expectation.fulfill()
}

waitForExpectations(timeout: 2, handler: nil)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,29 @@ class PayPalClient_Tests: XCTestCase {
waitForExpectations(timeout: 10)
}

func testVault_whenWebSession_cancelled_returnsIsVaultCanceledTrue() {

mockWebAuthenticationSession.cannedErrorResponse = ASWebAuthenticationSessionError(
.canceledLogin,
userInfo: ["Description": "Mock cancellation error description."]
)

let expectation = expectation(description: "vault(url:) completed")

let vaultRequest = PayPalVaultRequest(setupTokenID: "fakeTokenID")
payPalClient.vault(vaultRequest) { result, error in
if let error {
XCTAssertNil(result)
XCTAssertTrue(PayPalWebCheckoutClient.isVaultCanceled(error))
} else {
XCTFail("Expected error from PayPal vault cancellation")
}
expectation.fulfill()
}

waitForExpectations(timeout: 10)
}

func testVault_whenWebSession_returnsDefaultError() {

let expectedError = CoreSDKError(
Expand Down Expand Up @@ -170,6 +193,33 @@ class PayPalClient_Tests: XCTestCase {
waitForExpectations(timeout: 2, handler: nil)
}

func testStart_whenWebSession_cancelled_returnsIsCheckoutCanceledTrue() {

let request = PayPalWebCheckoutRequest(orderID: "1234")

mockWebAuthenticationSession.cannedErrorResponse = ASWebAuthenticationSessionError(
_bridgedNSError: NSError(
domain: ASWebAuthenticationSessionError.errorDomain,
code: ASWebAuthenticationSessionError.canceledLogin.rawValue,
userInfo: ["Description": "Mock cancellation error description."]
)
)

let expectation = self.expectation(description: "Call back invoked with error")
payPalClient.start(request: request) { result, error in
XCTAssertNil(result)
if let error {
XCTAssertTrue(PayPalWebCheckoutClient.isCheckoutCanceled(error))
} else {
XCTFail("Expected error from PayPal checkout cancellation.")
}
expectation.fulfill()
}

waitForExpectations(timeout: 2, handler: nil)
}


func testStart_whenWebAuthenticationSessions_returnsWebSessionError() {
let request = PayPalWebCheckoutRequest(orderID: "1234")

Expand Down
8 changes: 6 additions & 2 deletions UnitTests/PaymentsCoreTests/HTTP_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,12 @@ class HTTP_Tests: XCTestCase {
do {
_ = try await sut.performRequest(fakeHTTPRequest)
XCTFail("Request succeeded. Expected error.")
} catch let error {
XCTAssertTrue(serverError === (error as AnyObject))
} catch let error as CoreSDKError {
XCTAssertEqual(error.domain, NetworkingClientError.domain)
XCTAssertEqual(error.code, NetworkingClientError.Code.urlSessionError.rawValue)
XCTAssertEqual(error.localizedDescription, "An error occured during network call. Contact developer.paypal.com/support.")
} catch {
XCTFail("Unexpected error type")
}
}

Expand Down
Loading