Skip to content

Commit

Permalink
Cancel error handling (#297)
Browse files Browse the repository at this point in the history
* http performRequest returns NetworkingClientErrors

* CardClient helper function for threeDSCancel, demo app cancel, demo app minor fixes

* PayPalClient cancel helper functions and demo app changes

* Steven PR feedback: move static helper functions to error enums

* Rename CardClientError -> CardError, PayPalWebCheckoutError -> PayPalError

* Steven PR feedback: return CoreSDKError in merchant completion handler

* CHANGELOG and analytics typo and fix wrong code in graphql error
  • Loading branch information
KunJeongPark authored Nov 13, 2024
1 parent b13a3c4 commit 319894d
Show file tree
Hide file tree
Showing 17 changed files with 300 additions and 199 deletions.
14 changes: 9 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,25 @@
* Remove `PayPalWebCheckoutDelegate` and `PayPalVaultDelegate`
* Remove `start(request:)` method that uses delegate callbacks
* Remove `vault(vaultRequest:)` method that uses delegate callbacks
* Add `start(request:completion(PayPalWebCheckoutResult?, Error?) -> Void)` to `PayPalWebCheckoutClient`
* Add `vault(vaultRequest:completion(PayPalVaultResult?, Error?) -> Void)` to `PayPalWebCheckoutClient`
* Add `start(request:completion(PayPalWebCheckoutResult?, CoreSDKError?) -> Void)` to `PayPalWebCheckoutClient`
* Add `vault(vaultRequest:completion(PayPalVaultResult?, CoreSDKError?) -> Void)` to `PayPalWebCheckoutClient`
* Add `start(request:) async throws -> PayPalCheckoutResult`
* Add `vault(vaultRequest:) async throws -> PayPalVaultResult`
* Add `.checkoutCanceled` and `.vaultCanceled` to `PayPalWebCheckoutClientError`
* Rename `PayPalWebCheckoutClientError` to `PayPalError`
* Add public static functions `isCheckoutCanceled(Error)` and `isVaultCanceled(Error)` to `PayPalError` to distinguish cancellation errors in PayPal flows.
* CardPayments
* Replace delegate pattern with completion handlers and Swift concurrency
* Remove `CardDelegate` and `CardVaultDelegate`
* Remove `approveOrder(request:)` method that uses delegate callbacks
* Remove `vault(vaultRequest:)` method that uses delegate callbacks
* Add `approveOrder(request:completion:(CardResult?, Error?) -> Void)` to `CardClient`
* Add `vault(request:completion:(CardVaultResult?, Error?) -> Void)` to `CardClient`
* Add `approveOrder(request:completion:(CardResult?, CoreSDKError?) -> Void)` to `CardClient`
* Add `vault(request:completion:(CardVaultResult?, CoreSDKError?) -> Void)` to `CardClient`
* Add `approveOrder(request:) async throws -> CardResult`
* Add `vault(vaultRequest:) async throws -> CardVaultResult`
* Add `.threeDSecureCanceled` to `CardClientError`
* Add `.threeDSecureCanceled` to `CardClientError`
* Rename `PayPalClientError` to `PayPalError`
* Add public static function `isThreeDSecureCanceled(Error)` to `CardClientError` to distinguish cancellation error from threeDSecure verification
* PayPalWebPayments
* Deprecate `PayPalVaultRequest(url:setupTokenID:)`
* Add `PayPalVaultRequest(setupTokenID:)`
Expand Down
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 PayPalError.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 CardError.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 CardError.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
if let error {
DispatchQueue.main.async {
self.state.paypalVaultTokenResponse = .error(message: error.localizedDescription)
if PayPalError.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
16 changes: 8 additions & 8 deletions PayPal.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
80B8B2FC2A8EBBFD00AB60CD /* VaultPaymentTokensAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80B8B2FB2A8EBBFD00AB60CD /* VaultPaymentTokensAPI.swift */; };
80B96AAE2A980F6B00C62916 /* MockTrackingEventsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 802EFBD72A9685DF00AB709D /* MockTrackingEventsAPI.swift */; };
80DB2F762980795D00CFB86A /* CorePaymentsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80DB2F752980795D00CFB86A /* CorePaymentsError.swift */; };
80DCC59E2719DB6F00EC7C5A /* CardClientError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80DCC59D2719DB6F00EC7C5A /* CardClientError.swift */; };
80DCC59E2719DB6F00EC7C5A /* CardError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80DCC59D2719DB6F00EC7C5A /* CardError.swift */; };
80E237DF2A84434B00FF18CA /* HTTPRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E237DE2A84434B00FF18CA /* HTTPRequest.swift */; };
80E2FDBE2A83528B0045593D /* CheckoutOrdersAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E2FDBD2A83528B0045593D /* CheckoutOrdersAPI.swift */; };
80E2FDC12A83535A0045593D /* TrackingEventsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E2FDBF2A8353550045593D /* TrackingEventsAPI.swift */; };
Expand Down Expand Up @@ -104,7 +104,7 @@
BCFAC71F27ED04C800C3AF00 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEDB7FE32788AB8E00CEA554 /* Coordinator.swift */; };
BE4F785327EB656400FF4C0E /* Environment+PayPalWebCheckout.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE4F784327EB629100FF4C0E /* Environment+PayPalWebCheckout.swift */; };
BE4F785427EB656400FF4C0E /* PayPalWebCheckoutClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE4F784627EB629100FF4C0E /* PayPalWebCheckoutClient.swift */; };
BE4F785527EB656400FF4C0E /* PayPalWebCheckoutClientError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE4F784427EB629100FF4C0E /* PayPalWebCheckoutClientError.swift */; };
BE4F785527EB656400FF4C0E /* PayPalError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE4F784427EB629100FF4C0E /* PayPalError.swift */; };
BE4F785727EB656400FF4C0E /* PayPalWebCheckoutRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE4F784527EB629100FF4C0E /* PayPalWebCheckoutRequest.swift */; };
BE4F785827EB656400FF4C0E /* PayPalWebCheckoutResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE4F784727EB629100FF4C0E /* PayPalWebCheckoutResult.swift */; };
BEA100E726EF9EDA0036A6A5 /* NetworkingClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEA100E626EF9EDA0036A6A5 /* NetworkingClient.swift */; };
Expand Down Expand Up @@ -221,7 +221,7 @@
80B8B2FB2A8EBBFD00AB60CD /* VaultPaymentTokensAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultPaymentTokensAPI.swift; sourceTree = "<group>"; };
80B9F85126B8750000D67843 /* CorePayments.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CorePayments.framework; sourceTree = BUILT_PRODUCTS_DIR; };
80DB2F752980795D00CFB86A /* CorePaymentsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CorePaymentsError.swift; sourceTree = "<group>"; };
80DCC59D2719DB6F00EC7C5A /* CardClientError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardClientError.swift; sourceTree = "<group>"; };
80DCC59D2719DB6F00EC7C5A /* CardError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardError.swift; sourceTree = "<group>"; };
80E237DE2A84434B00FF18CA /* HTTPRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPRequest.swift; sourceTree = "<group>"; };
80E2FDBD2A83528B0045593D /* CheckoutOrdersAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckoutOrdersAPI.swift; sourceTree = "<group>"; };
80E2FDBF2A8353550045593D /* TrackingEventsAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackingEventsAPI.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -265,7 +265,7 @@
BE00B7AA2743FD9F00758C63 /* PaymentButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentButton.swift; sourceTree = "<group>"; };
BE00B7AC27444FE900758C63 /* PayPalButton_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalButton_Tests.swift; sourceTree = "<group>"; };
BE4F784327EB629100FF4C0E /* Environment+PayPalWebCheckout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Environment+PayPalWebCheckout.swift"; sourceTree = "<group>"; };
BE4F784427EB629100FF4C0E /* PayPalWebCheckoutClientError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PayPalWebCheckoutClientError.swift; sourceTree = "<group>"; };
BE4F784427EB629100FF4C0E /* PayPalError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PayPalError.swift; sourceTree = "<group>"; };
BE4F784527EB629100FF4C0E /* PayPalWebCheckoutRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PayPalWebCheckoutRequest.swift; sourceTree = "<group>"; };
BE4F784627EB629100FF4C0E /* PayPalWebCheckoutClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PayPalWebCheckoutClient.swift; sourceTree = "<group>"; };
BE4F784727EB629100FF4C0E /* PayPalWebCheckoutResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PayPalWebCheckoutResult.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -535,7 +535,7 @@
isa = PBXGroup;
children = (
06CE009A26F3D5A40000CC46 /* CardClient.swift */,
80DCC59D2719DB6F00EC7C5A /* CardClientError.swift */,
80DCC59D2719DB6F00EC7C5A /* CardError.swift */,
80DBC9D829C336D500462539 /* APIRequests */,
065A4DBD26FCDA270007014A /* Models */,
3BE738622B9A482800598F05 /* PrivacyInfo.xcprivacy */,
Expand Down Expand Up @@ -639,7 +639,7 @@
children = (
BE4F784327EB629100FF4C0E /* Environment+PayPalWebCheckout.swift */,
BE4F784627EB629100FF4C0E /* PayPalWebCheckoutClient.swift */,
BE4F784427EB629100FF4C0E /* PayPalWebCheckoutClientError.swift */,
BE4F784427EB629100FF4C0E /* PayPalError.swift */,
BE4F784527EB629100FF4C0E /* PayPalWebCheckoutRequest.swift */,
BE4F784727EB629100FF4C0E /* PayPalWebCheckoutResult.swift */,
CB51FF7127FCB947001A97F5 /* PayPalWebCheckoutFundingSource.swift */,
Expand Down Expand Up @@ -1209,7 +1209,7 @@
80E8DAE126B8784600FAFC3F /* Card.swift in Sources */,
3B22E8B82A841AEA00962E34 /* CardVaultResult.swift in Sources */,
CBC16DD529E99B4600307117 /* PaymentSource.swift in Sources */,
80DCC59E2719DB6F00EC7C5A /* CardClientError.swift in Sources */,
80DCC59E2719DB6F00EC7C5A /* CardError.swift in Sources */,
3BDB34942A80CE6E008100D7 /* CardVaultRequest.swift in Sources */,
80B8B2FC2A8EBBFD00AB60CD /* VaultPaymentTokensAPI.swift in Sources */,
CB4BE27D2847AF6F00EA2DD1 /* SCA.swift in Sources */,
Expand Down Expand Up @@ -1241,7 +1241,7 @@
3B3C511E2B2395B5009125FE /* PayPalVaultResult.swift in Sources */,
BE4F785327EB656400FF4C0E /* Environment+PayPalWebCheckout.swift in Sources */,
BE4F785427EB656400FF4C0E /* PayPalWebCheckoutClient.swift in Sources */,
BE4F785527EB656400FF4C0E /* PayPalWebCheckoutClientError.swift in Sources */,
BE4F785527EB656400FF4C0E /* PayPalError.swift in Sources */,
BE4F785727EB656400FF4C0E /* PayPalWebCheckoutRequest.swift in Sources */,
BE4F785827EB656400FF4C0E /* PayPalWebCheckoutResult.swift in Sources */,
CB51FF7227FCB947001A97F5 /* PayPalWebCheckoutFundingSource.swift in Sources */,
Expand Down
Loading

0 comments on commit 319894d

Please sign in to comment.