iosswiftuiapplepaypasskituiviewrepresentable

Implementing Apple Pay with SwiftUI


This is my first time working with PassKit and with SwiftUI on a big project. I'm trying to implement Apple Pay SwiftUI and since there isn't a native way to do so yet, I tried wrapped the PKPaymentAuthorizationViewController in UIViewControllerRepresentable, but I'm not sure if I'm doing it properly.

The view displays properly and seems to work when clicking on it to pay. I control showing the window by binding the view to an isPresentingApplePay bool (see below). The issues happen when the window should be dismissed. Tapping on the cancel button does not dismiss the view; sometimes it doesn't even call the paymentAuthorizationViewControllerDidFinish delegate function. The same thing happens after submitting the payment. Sometimes the didFinish delegate gets called but the view is not dismissed. I tried passing the binding variable isPresentingApplePay and setting it to false from the didFinish but it doesn't do anything. The only way to get the view to disappear is to tap on any part outside of the apple pay window.

Does anyone know what I'm doing wrong? Is there anything I'm totally missing?

I get the apple pay window to show properly when pressing a button, by binding the view under an if statement

Here is my PKPaymentAuthorizationViewController wrapper:

import Foundation
import PassKit
import SwiftUI

struct ApplePayController: UIViewControllerRepresentable {
    @Environment(\.presentationMode) var presentationMode
    @EnvironmentObject var userData: UserData
    @Binding var purchase: Purchase
    @Binding var isPresenting: Bool

    let items: [PKPaymentSummaryItem]

    func updateUIViewController(_ uiViewController: PKPaymentAuthorizationViewController, context: Context) {

    }

    typealias UIViewControllerType = PKPaymentAuthorizationViewController


    func makeUIViewController(context: Context) ->  PKPaymentAuthorizationViewController {
        let applePayManager = ApplePayManager(items: items)
        let apm = applePayManager.paymentViewController()!
        apm.delegate = context.coordinator
        return apm
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, PKPaymentAuthorizationViewControllerDelegate  {
        var parent: ApplePayController

        init(_ parent: ApplePayController) {
            self.parent = parent
        }

        func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) {
            controller.dismiss(animated: true) {
                    self.parent.isPresenting = false
                }
        }

        func paymentAuthorizationViewController(_ controller: PKPaymentAuthorizationViewController, didAuthorizePayment payment: PKPayment, handler completion: @escaping (PKPaymentAuthorizationResult) -> Void) {
            print("did authorize payment")

        }

        func paymentAuthorizationViewControllerWillAuthorizePayment(_ controller: PKPaymentAuthorizationViewController) {
            print("Will authorize payment")
        }
    }

    class ApplePayManager: NSObject {
        let currencyCode: String
        let countryCode: String
        let merchantID: String
        let paymentNetworks: [PKPaymentNetwork]
        let items: [PKPaymentSummaryItem]

        init(items: [PKPaymentSummaryItem],
               currencyCode: String = "USD",
               countryCode: String = "US",
               merchantID: String = "xxx.merchant.xxx",
               paymentNetworks: [PKPaymentNetwork] = [PKPaymentNetwork.amex, PKPaymentNetwork.masterCard, PKPaymentNetwork.visa]) {
            self.items = items
            self.currencyCode = currencyCode
            self.countryCode = countryCode
            self.merchantID = merchantID
            self.paymentNetworks = paymentNetworks
        }

        func paymentViewController() -> PKPaymentAuthorizationViewController? {
            if PKPaymentAuthorizationViewController.canMakePayments(usingNetworks: paymentNetworks) {
                let request = PKPaymentRequest()
                request.currencyCode = self.currencyCode
                request.countryCode = self.countryCode
                request.supportedNetworks = paymentNetworks
                request.merchantIdentifier = self.merchantID
                request.paymentSummaryItems = items
                request.merchantCapabilities = [.capabilityCredit, .capabilityDebit]
                return PKPaymentAuthorizationViewController(paymentRequest: request)
            }
            return nil
        }
    }
}

and this is how I show it in my UIView:

if isPresentingApplePay {
    ApplePayController(purchase: self.$currentOrder.purchase, isPresenting: $isPresentingApplePay, items: self.createOrder(with: self.currentOrder.purchase)).environmentObject(self.userData)
}

Solution

  • I have faced similar problem then I have solved by using another approach not interfacing UIViewControllerRepresentable. Instead I created a separate class for handling Apple Pay like following

    import PassKit
    
    typealias PaymentCompletionHandler = (Bool) -> Void
    
    class PaymentHandler: NSObject {
    
    static let supportedNetworks: [PKPaymentNetwork] = [
        .amex,
        .masterCard,
        .visa
    ]
    
    var paymentController: PKPaymentAuthorizationController?
    var paymentSummaryItems = [PKPaymentSummaryItem]()
    var paymentStatus = PKPaymentAuthorizationStatus.failure
    var completionHandler: PaymentCompletionHandler?
    
    func startPayment(completion: @escaping PaymentCompletionHandler) {
    
        let amount = PKPaymentSummaryItem(label: "Amount", amount: NSDecimalNumber(string: "8.88"), type: .final)
        let tax = PKPaymentSummaryItem(label: "Tax", amount: NSDecimalNumber(string: "1.12"), type: .final)
        let total = PKPaymentSummaryItem(label: "ToTal", amount: NSDecimalNumber(string: "10.00"), type: .pending)
    
        paymentSummaryItems = [amount, tax, total];
        completionHandler = completion
    
        // Create our payment request
        let paymentRequest = PKPaymentRequest()
        paymentRequest.paymentSummaryItems = paymentSummaryItems
        paymentRequest.merchantIdentifier = "merchant.com.YOURDOMAIN.YOURAPPNAME"
        paymentRequest.merchantCapabilities = .capability3DS
        paymentRequest.countryCode = "US"
        paymentRequest.currencyCode = "USD"
        paymentRequest.requiredShippingContactFields = [.phoneNumber, .emailAddress]
        paymentRequest.supportedNetworks = PaymentHandler.supportedNetworks
    
        // Display our payment request
        paymentController = PKPaymentAuthorizationController(paymentRequest: paymentRequest)
        paymentController?.delegate = self
        paymentController?.present(completion: { (presented: Bool) in
            if presented {
                NSLog("Presented payment controller")
            } else {
                NSLog("Failed to present payment controller")
                self.completionHandler!(false)
             }
         })
      }
    }
    
    /*
        PKPaymentAuthorizationControllerDelegate conformance.
    */
    extension PaymentHandler: PKPaymentAuthorizationControllerDelegate {
    
    func paymentAuthorizationController(_ controller: PKPaymentAuthorizationController, didAuthorizePayment payment: PKPayment, completion: @escaping (PKPaymentAuthorizationStatus) -> Void) {
    
        // Perform some very basic validation on the provided contact information
        if payment.shippingContact?.emailAddress == nil || payment.shippingContact?.phoneNumber == nil {
            paymentStatus = .failure
        } else {
            // Here you would send the payment token to your server or payment provider to process
            // Once processed, return an appropriate status in the completion handler (success, failure, etc)
            paymentStatus = .success
        }
    
        completion(paymentStatus)
    }
    
    func paymentAuthorizationControllerDidFinish(_ controller: PKPaymentAuthorizationController) {
        controller.dismiss {
            DispatchQueue.main.async {
                if self.paymentStatus == .success {
                    self.completionHandler!(true)
                } else {
                    self.completionHandler!(false)
                }
            }
        }
    }
    
    }
    

    And used the following Button code for Apple Pay popup.

    struct ContentView: View {
    
    let paymentHandler = PaymentHandler()
    
    var body: some View {
        Button(action: {
                self.paymentHandler.startPayment { (success) in
                    if success {
                        print("Success")
                    } else {
                        print("Failed")
                    }
                }
            }, label: {
                Text("PAY WITH  APPLE")
                .font(Font.custom("HelveticaNeue-Bold", size: 16))
                .padding(10)
                .foregroundColor(.white)
        }
    }
    
    }
    

    Finally, It works fine