Reactive Programming – Leveraging RxSwift and Clean Architecture for Apple Pay.
In the ever-evolving landscape of iOS development, understanding and implementing payment solutions like Apple Pay is paramount. In the first part of this series, we delved into the intricacies of Apple Pay’s functionality in Swift. We explored its mechanisms, its integration into iOS applications, and the challenges developers face, particularly when handling payment-related interactions.
One significant challenge often encountered in such integrations is the reliance on traditional delegate patterns. While delegates have long been a staple in iOS development, they come with their limitations, especially in complex scenarios like payment processing. In this second part, we shift our focus to exploring the drawbacks of using delegates and propose a more robust alternative: the delegate proxy pattern.
Join us as we delve deeper into the intricacies of Apple Pay integration, uncover the limitations of delegates, and discover how embracing the delegate proxy pattern can enhance the efficiency, flexibility, and maintainability of our iOS applications.
RxSwift Delegate Proxy
To begin implementing DelegateProxy for PKPaymentAuthorizationViewControllerDelegate, we need to define a class that acts as the proxy. This involves implementing certain methods such as registerKnownImplementations, setCurrentDelegate, and currentDelegate.
Here’s a basic outline of how we can proceed:
// MARK: - PKPaymentAuthorizationViewControllerDelegateProxy
public class PKPaymentAuthorizationViewControllerDelegateProxy
: DelegateProxy<PKPaymentAuthorizationViewController, PKPaymentAuthorizationViewControllerDelegate>
, DelegateProxyType
, PKPaymentAuthorizationViewControllerDelegate {
// MARK: - DelegateProxyType methods
public static func registerKnownImplementations() {
register {
PKPaymentAuthorizationViewControllerDelegateProxy(paymentAuthorizationViewController: $0)
}
}
public static func currentDelegate(for object: PKPaymentAuthorizationViewController) -> PKPaymentAuthorizationViewControllerDelegate? {
object.delegate
}
public static func setCurrentDelegate(
_ delegate: PKPaymentAuthorizationViewControllerDelegate?,
to object: PKPaymentAuthorizationViewController
) {
object.delegate = delegate
}
// Implement delegate methods as necessary
}
This class serves as the DelegateProxy for PKPaymentAuthorizationViewControllerDelegate. It inherits from DelegateProxy
, specifies the parent object type (PKPaymentAuthorizationViewController
), and the delegate type (PKPaymentAuthorizationViewControllerDelegate
).
The registerKnownImplementations
method registers known implementations of the proxy, while setCurrentDelegate
and currentDelegate
methods manage setting and retrieving the current delegate, respectively.
Now, you can use this DelegateProxy to handle delegation for PKPaymentAuthorizationViewControllerDelegate in a reactive way using RxSwift. Long with creating the PKPaymentAuthorizationViewControllerDelegateProxy
, we need to extend PKPaymentAuthorizationViewController
to define a computed property of type ControlEvent<PKPaymentAuthorizationViewControllerDelegate>
. This property will use the DelegateProxy we’ve created to handle delegation in a reactive way. We have also a special property that will be very helpful later when we get the transactionState from our proxy delegate.
Here’s a basic outline of how we can proceed:
import PassKit
import RxSwift
import RxCocoa
extension Reactive where Base: PKPaymentAuthorizationViewController {
public var delegate: PKPaymentAuthorizationViewControllerDelegateProxy {
.proxy(for: base)
}
public var transactionState: Observable<ApplePayTransactionState> {
delegate.transactionStateObservable
}
}
Now, we have to implement Apple Pay delegates methods. To do we have define a state variable transactionStateObservable to handle the different states of our payment. We have created an enum type called ApplePayTransactionState. This is the definition of out the type :
public enum ApplePayTransactionState {
case cancelled
case confirmingPayment(
PKPayment,
(PKPaymentAuthorizationStatus) -> Void
)
case completed
}
Now we focus to implement the delegates
// MARK: - PKPaymentAuthorizationViewControllerDelegate methods
public func paymentAuthorizationViewController(
_ controller: PKPaymentAuthorizationViewController,
didAuthorizePayment payment: PKPayment,
completion: @escaping (PKPaymentAuthorizationStatus) -> Void
) {
authorizePaymentStatus = .authorized
transactionStateSubject.onNext(
.confirmingPayment(
payment,
completion
)
)
}
public func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) {
if case .authorized = authorizePaymentStatus {
transactionStateSubject.onNext(.completed)
} else {
transactionStateSubject.onNext(.cancelled)
}
}
I think that you remember the delegates that we have used in the first article. But something new now is that I use transactionStateSubject which is a private variable in my proxy and observed by the public variable transactionStateObservable
// MARK: - Public properties
public lazy var transactionStateObservable = transactionStateSubject.asObservable()
Also I added a private observable to handle the authorization from Apple Pay which is authorizePaymentStatus
This is the declaration of the two private variables
// MARK: - Private properties
private let transactionStateSubject = ReplaySubject<ApplePayTransactionState>.create(bufferSize: 1)
private var authorizePaymentStatus: ApplePayAuthorizePaymentStatus?
Using the delegate proxy
After declaring the PKPaymentAuthorizationViewControllerDelegateProxy, implementing the delegates methods reactively and extend PKPaymentAuthorizationViewController to support the PKPaymentAuthorizationViewControllerDelegateProxy as delegate, we should now show you how you can use that.
Our team philosophy, was to create an ApplePayAPI class that check if we can make a payment with Apple first and observe the transactionStateObservable. This logic is the same for all our payments methods like PayPal, credit cards…
We have create first a protocol that we named ApplePayAPIProtocol which contain only one method processPayment. We need at least the amount of the order and the currency code.
Here’s a basic outline of how we can proceed:
// sourcery: AutoMockable
public protocol ApplePayAPIProtocol {
var canMakePayments: Bool { get }
func processPayment(
_ amount: NSDecimalNumber,
currencyCode: String
) -> Observable<ApplePayTransactionState>
}
Now we implement our ApplePayAPIProtocol responsable of creating the ApplePayRequest, communicate with the proxy to retrieve the state object.
We check first if we can make ApplePay Payment (check if we have one card at least in the wallet).
import PassKit
import RxSwift
struct ApplePayAPI: ApplePayAPIProtocol {
// MARK: - ApplePayAPIProtocol methods
var canMakePayments: Bool {
PKPaymentAuthorizationViewController.canMakePayments(usingNetworks: configuration.supportedNetworks)
}
// MARK: - Private properties
private let configuration: ApplePayAPIConfiguration
// MARK: - Lifecycle
init(configuration: ApplePayAPIConfiguration) {
self.configuration = configuration
}
// MARK: - ApplePayAPIProtocol methods
}
canMakePayment is a static standard method used by the PKPaymentAuthorizationViewController to verify if we can make payment based on supported network wish is an array of PKPaymentNetwork. We have created a type that we called ApplePayAPIConfiguration that encapsulate all ApplePay configurations.
import PassKit
import Common
public struct ApplePayAPIConfiguration {
// MARK: - Public Properties
let summaryItemLabel: String
let merchantIdentifier: String
let supportedNetworks: [PKPaymentNetwork]
let merchantCapabilities: PKMerchantCapability
let countryCode: String
init(environment: Environment) {
self.summaryItemLabel = environment.applePaySummaryItemLabel
self.merchantIdentifier = environment.applePayMerchantIdentifier
self.supportedNetworks = environment.applePaySupportedNetworks
self.merchantCapabilities = environment.applePayMerchantCapabilities
self.countryCode = environment.appMarket
}
}
We use our common framework to inject the Environment attribute which contains specific configuration depending of the country.
Now, we can implement our processPayment method in order to get the transactionState from the delegates proxy. You will see that it’s really very simple 🙂
func processPayment(
_ amount: NSDecimalNumber,
currencyCode: String
) -> Observable<ApplePayTransactionState> {
let paymentRequest = createPaymentRequestAmount(
amount,
currencyCode: currencyCode
)
guard let authorizationViewController = createAuthorizationViewController(for: paymentRequest) else {
return .just(.cancelled)
}
let transactionStateObservable = authorizationViewController.rx.transactionState
.do { state in
switch state {
case .completed,
.cancelled:
UIApplication.topViewController()?.dismiss(animated: true)
default:
break
}
}
UIApplication.topViewController()?.present(
authorizationViewController,
animated: true,
completion: nil
)
return transactionStateObservable
}
To create the PKPaymentAuthorizationViewController we need the PKPaymentRequest using the amount and the currency and also our ApplePayAPIConfirguration , we retrieve this information from our use case (we will speak about that in the next article).
The authorizationViewController is a PKPaymentAuthorizationViewController in RxSwift with a special variable our famous transactionState that we have declare above when we have extend the PKPaymentAuthorizationViewController to be reactive.
For navigation purpose we need only to handle the two states completed and cancelled to dismiss the PKPaymentAuthorizationViewController. Maybe you think about the confirm state! no worry 🙂
case confirmingPayment(
PKPayment,
(PKPaymentAuthorizationStatus) -> Void
)
The third state is really very important later to validate the order or to cancel it. We return the three cases using the return transactionStateObservable statement. Of course, we need to open our authorizationViewController using this instruction.
UIApplication.topViewController()?.present(
authorizationViewController,
animated: true,
completion: nil
)
What’s else?
Now we have passed from a spaghetti one or two classes that handle delegates, navigation and business logic to at least five classes and struct encapsulated in Payment framework that we have created for that and this the structure
This is not sufficient, our business logic and how communicate with back end API is always in our viewModel. it’s time for clean architecture concept to come now to handle this issue. see you in the next part to speak about that.
References
https://github.com/ReactiveX/RxSwift/blob/main/RxCocoa/Common/DelegateProxyType.swift
https://blog.ippon.fr/2018/11/13/rxswift-how-to-make-your-favorite-delegate-based-apis-reactive/
https://samritchie.net/posts/rxswift-delegateproxy-with-required-methods/