# 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.
}
}