Implementing Apple Pay with SwiftUI
Asked Answered
O

4

14

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)
}
Opportune answered 19/3, 2020 at 23:5 Comment(5)
Did you ever find a solution to this? I'm trying to work it out myself...Jordanjordana
I have not yet. I'm tackling this this week tho and I'll post back anything I findOpportune
@Opportune Hi, did you figure out a native SwiftUI way?Klipspringer
For a demonstration of how to present an Apple Pay button in your SwiftUI app, please download the Fruta sample code project. In particular, check out the file Fruta > Shared > Orders > PaymentButton.swift. - posted by Apple Documentation Engineer.Enucleate
The Fruta app only wraps the UI button, but doesn't illustrated presenting/dismissing the authorization view or state management.Backset
K
16

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

Kelt answered 12/5, 2020 at 9:38 Comment(3)
paymentController?.present(completion: doesn't seem to work, how is it possible that you don't need to pass the VC to present?Emilieemiline
@DepartamentoB PKPaymentAuthorizationController is not a ViewController, so it works.Whitebook
Thankyou so much, this was driving me mental!!Bookman
P
2

The accepted answer by Taif is very good on the PaymentHandler side.

But to use the actual Apple Pay button, it sounds like you were running into the exact same problem I had! Basically, you have to put your UIViewRepresentable in a ButtonStyle, and then style a SwiftUI button with it. I'm not sure why this is true, but here's the code as it should work:

import SwiftUI
import UIKit
import PassKit

struct PaymentButton: View {
    let paymentHandler = PaymentHandler() //As defined by Taif

    var body: some View {
        Button(action: {
            // Using the code from Tarif!
            self.paymentHandler.startPayment { success in
                if success {
                    print("Success")
                } else {
                    print("Failed")
                }
            }
        }, label: { EmptyView() } )
            .buttonStyle(PaymentButtonStyle())
    }
}

struct PaymentButtonStyle: ButtonStyle {
    func makeBody(configuration: Self.Configuration) -> some View {
        return PaymentButtonHelper()
    }
}  
    
struct PaymentButtonHelper: View {
    var body: some View {
        PaymentButtonRepresentable()
            .frame(minWidth: 100, maxWidth: 400)
            .frame(height: 60)
            .frame(maxWidth: .infinity)
    }
}

extension PaymentButtonHelper {
    struct PaymentButtonRepresentable: UIViewRepresentable {
    
    var button: PKPaymentButton {
        let button = PKPaymentButton(paymentButtonType: .buy, paymentButtonStyle: .black) /*customize here*/
        button.cornerRadius = 4.0 /* also customize here */
        return button
    }
     
    func makeUIView(context: Context) -> PKPaymentButton {
        return button
    }
    func updateUIView(_ uiView: PKPaymentButton, context: Context) { }
}

Then to use it, you would say

struct ContentView: View {
    var body: some View {
        PaymentButton()
    }
}
Printing answered 20/10, 2020 at 22:23 Comment(0)
E
0

A version that works for presenting the PKPaymentAuthorizationViewController can be found here: How to properly present PKPaymentAuthorizationViewController in a SwiftUI only project?

The wrapper I'm using looks as follows:

import PassKit
import SwiftUI

struct ApplePayWrapper: UIViewControllerRepresentable {
    typealias UIViewControllerType = PKPaymentAuthorizationViewController

    let request: PKPaymentRequest

    func makeUIViewController(context: Context) -> PKPaymentAuthorizationViewController {
        let applePayController = PKPaymentAuthorizationViewController(paymentRequest: request)
        return applePayController!
    }

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

I'm using a ViewModel to act as a delegate, you need to add this to the fields of the struct after let request: PKPaymentRequest and then set the delegate when you create the PKPaymentAuthorizationViewController

Emilieemiline answered 2/7, 2020 at 8:53 Comment(0)
B
0

You don't even need to use UIViewControllerRepresentable or UIKit at all in SwiftUI. Use PKPaymentAuthorizationController, NOT the view controller version:

let request = PKPaymentRequest()
request.paymentSummaryItems = ...

let controller = PKPaymentAuthorizationController(paymentRequest: request)
controller.delegate = self

controller.present { [weak self] presented in
   // Apple Pay presented from scene window
}

https://developer.apple.com/forums/thread/649662?answerId=641228022#641228022

Backset answered 16/10, 2020 at 13:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.