# Northstar iOS - Cards API Proposal ## Summary **Problem**: A merchant accepting cards (without 3DS) is required to implement 3 non-applicable methods: `cardDidCancel`, `cardThreeDSecureWillLaunch`, and `cardThreeDSecureDidFinish` methods. This is confusing as these methods will never be called for card-only integrations, yet are required for their app to compile. **Solution**: * Have `approveOrder()` return a callback of (`CardResult`, `Error`). * Move 3DS-related methods to an *optional* `ThreeDSecureChallengePresentationDelegate`. * Allows merchants to hook into UI related events regarding the 3DS WebView presentation and dismissal. **Rationale**: * It is common in Apple's APIs to use delegate methods for process-oriented tasks, whereas closures are more common for result oriented tasks ([reference](https://stablekernel.com/article/use-blocks-closures-delegates-callbacks/)). * Less lines of code for card integration. * Avoid confusing `NOT APPLICABLE`, yet required function conformance. ## Card Integration ### Current ```swift class ViewController: UIViewController { ... func launchCardFlow() { let config = CoreConfig(accessToken: "ACCESS_TOKEN", environment: .sandbox) let cardClient = CardClient(config: config) let card = Card(...) let cardRequest = CardRequest(orderID: "ORDER_ID", card: card) cardClient.delegate = self cardClient.approveOrder(request: cardRequest, context: self) } } extension ViewController: CardDelegate { func card(_ cardClient: CardClient, didFinishWithResult result: CardResult) { print(result) authorizeOrder(result) // complete payment flow } func card(_ cardClient: CardClient, didFinishWithError error: CoreSDKError) { print(error.localizedDescription) } func cardDidCancel(_ cardClient: CardClient) { // NOT APPLICABLE } func cardThreeDSecureWillLaunch(_ cardClient: CardClient) { // NOT APPLICABLE } func cardThreeDSecureDidFinish(_ cardClient: CardClient) { // NOT APPLICABLE } } ``` ### Proposed ```swift class ViewController: UIViewController { ... func launchCardFlow() async { let config = CoreConfig(accessToken: "ACCESS_TOKEN", environment: .sandbox) let cardClient = CardClient(config: config) let card = Card(...) let cardRequest = CardRequest(orderID: "ORDER_ID", card: card) result, error = await approveOrder(request: cardRequest) if error { // handle error } else { // authorize or capture } } } ``` ## 3DS ### Proposed ```swift class ViewController: UIViewController { ... let config = CoreConfig(accessToken: "ACCESS_TOKEN", environment: .sandbox) func launchCardFlow() async { let cardClient = CardClient(config: config) let card = Card(...) let cardRequest = CardRequest(orderID: "ORDER_ID", card: card) // Comment to disable 3DS let threeDSecureRequest = ThreeDSecureRequest(...) cardRequest.threeDSecureRequest = threeDSecureRequest cardClient.threeDSecureChallengeDelegate = self result, error = await approveOrder(request: cardRequest) if error { // handle error } else { // authorize or capture } } } extension ViewController: ThreeDSecureChallengePresentationDelegate { func threeDSecureDidCancel(_ cardClient: CardClient) { // show error onscreen } func threeDSecureWillLaunch(_ cardClient: CardClient) { // show spinner } func threeDSecureDidFinish(_ cardClient: CardClient) { // clean-up UI } } ``` ### Current ```swift class ViewController: UIViewController { ... func launchCardFlow() { let config = CoreConfig(accessToken: "ACCESS_TOKEN", environment: .sandbox) let cardClient = CardClient(config: config) let card = Card(...) let cardRequest = CardRequest(orderID: "ORDER_ID", card: card) let threeDSecureRequest = ThreeDSecureRequest(...) cardRequest.threeDSecureRequest = threeDSecureRequest cardClient.delegate = self cardClient.approveOrder(request: cardRequest, context: self) } } extension ViewController: CardDelegate { func card(_ cardClient: CardClient, didFinishWithResult result: CardResult) { print(result) authorizeOrder(result) // complete payment flow } func card(_ cardClient: CardClient, didFinishWithError error: CoreSDKError) { print(error.localizedDescription) } func cardDidCancel(_ cardClient: CardClient) { print("cancelled") } func cardThreeDSecureWillLaunch(_ cardClient: CardClient) { print("will launch") } func cardThreeDSecureDidFinish(_ cardClient: CardClient) { print("did finish") } } ``` ## PayPal (Web) ### Current ```swift class ViewController: UIViewController { ... func launchPayPalFlow() { let config = CoreConfig(accessToken: "ACCESS_TOKEN", environment: .sandbox) let payPalClient = PayPalWebCheckoutClient(config: config) let payPalRequest = PayPalWebCheckoutRequest(orderID: "ORDER_ID") payPalClient.delegate = self payPalClient.start(request: payPalRequest, context: self) } } extension ViewController: PayPalWebCheckoutDelegate { func payPal(_ payPalClient: PayPalWebCheckoutClient, didFinishWithResult result: PayPalWebCheckoutResult) { print(result) authorizeOrder(result) // complete payment flow } func payPal(_ payPalClient: PayPalWebCheckoutClient, didFinishWithError error: CoreSDKError) { print(error.localizedDescription) } func payPalDidCancel(_ payPalClient: PayPalWebCheckoutClient) { print("cancelled") } } ``` ## PayPal (Native) ```swift class ViewController: UIViewController { ... func launchPayPalFlow() { let config = CoreConfig(accessToken: "ACCESS_TOKEN", environment: .sandbox) let payPalClient = PayPalClient(config: config) payPalClient.delegate = self payPalClient.start { createOrderAction in createOrderAction.set(orderId: orderID) } } } extension ViewController: PayPalWebCheckoutDelegate { func paypal(_ payPalClient: PayPalClient, didFinishWithResult approvalResult: Approval) { print(result) authorizeOrder(result) // complete payment flow } func paypal(_ payPalClient: PayPalClient, didFinishWithError error: CoreSDKError) { print(error.localizedDescription) } func paypalDidCancel(_ payPalClient: PayPalClient) { print("cancelled") } func paypalWillStart(_ payPalClient: PayPalClient) { // the paypal pay sheet is about to appear. Handle loading views, spinners, etc. } func paypalDidShippingAddressChange( _ payPalClient: PayPalClient, shippingChange: ShippingChange, shippingChangeAction: ShippingChangeAction ) { // called when the user decides to change the address or the shipping method of the order. } }