Skip to content

Quick Start

Before reading this document, make sure you have completed the Integration Guide and are familiar with the APIs for creating a checkout.

The PayKKa iOS SDK makes it easy to natively embed multiple payment methods in your iOS app. Simply pass a sessionId to the SDK to open the checkout in your app, accept payments from users, and customize and handle the relevant payment callbacks. You can find the latest SDK download in iOS SDK Release History and Changelog.

Step 1: Add the SDK to Your Project

After downloading the SDK, you will get a .zip file with a directory structure similar to the following:

.
├── Libs
│   ├── AlipayPlusClient.xcframework
│   ├── CardinalMobile.xcframework
│   ├── PayKKaCheckoutPayments.xcframework
│   └── ...
└── PayKKaCheckoutApp-iOS
    ├── OcCheckoutDemo
    ├── PayKKaCheckoutApp-iOS.xcworkspace
    └── SwiftCheckoutDemo

PayKKaCheckoutApp-iOS contains example apps written in Objective-C and Swift. Developers can use the example code to learn how to use the PayKKa SDK APIs.

PayKKaCheckoutPayments.xcframework in the root-level Libs folder is the core PayKKa SDK and is required for all integration methods. To add it, select Project Navigator <Your Project Name>TARGETS <Your TARGET>Build PhasesLink Binary With Libraries+Add Files… in Xcode, and then select Libs/PayKKaCheckoutPayments.xcframework.

Under TARGETS <Your TARGET>GeneralFrameworks, Libraries, and Embedded Content, make sure all added .xcframework files are set to Embed & Sign, as shown below:

Xcode Frameworks, Libraries, and Embedded Content configuration

The Libs folder also contains the dependencies required by each payment method. In addition to the core SDK, add the corresponding .xcframework files for your chosen integration method:

Integration MethodPayment MethodRecommended XCFrameworks
Drop-InAll payment methodsAll .xcframework files in the Libs folder
ComponentCard paymentCardinalMobile.xcframework, StripeCore.xcframework, Stripe3DS2.xcframework, StripePayments.xcframework
ComponentApple PayCardinalMobile.xcframework, StripeCore.xcframework, Stripe3DS2.xcframework, StripePayments.xcframework
ComponentAlipay+AlipayPlusClient.xcframework
ComponentWeChat PayWechatOpenSDK.xcframework

The SDK supports iOS 14.0 and later.

Step 2: Obtain AppCode

During initialization, the SDK verifies that the host app's Bundle Identifier, the signing Team ID, and the information registered in the PayKKa database match. This confirms that SDK calls come from an app packaged through the merchant's official channel. Therefore, you need to provide the following information to your PayKKa integration contact:

  • The app's Bundle Identifier
  • The Team ID of the Apple Developer account used to sign the app

You can find the Bundle Identifier in Xcode under TARGETS <Your TARGET>Signing & CapabilitiesSigningBundle Identifier. You can find the Team ID in your Apple Developer Account.

Your PayKKa integration contact will generate a PayKKaAppCode based on the information above. Then add the following configuration to your app project's Info.plist:

Info.plist
<key>paykka_appcode</key>
<string>{YourPayKKaAppCode}</string>
<key>paykka_mch_teamid</key>
<string>{YourAppleTeamID}</string>
  • paykka_appcode: Enter the PayKKaAppCode generated for you by PayKKa.
  • paykka_mch_teamid: Enter the Team ID of the Apple Developer account used to sign the app.
Configure AppCode and Team ID in Info.plist

Step 3: Integrate and Use

After adding the SDK and configuring AppCode, you can use the PayKKa Checkout SDK in your app. The SDK provides two main integration methods: Drop-In, an embedded checkout that provides a complete payment form with multiple payment methods, and Component, a component-based checkout that contains only one payment method.

The key Swift and Objective-C code is provided below. For the complete code, refer to the PayKKaCheckoutApp-iOS Xcode project in the SDK download package.

Define Your Merchant Configuration

First define the SDK environment, merchant information, and the configuration required by each payment method. You can omit configuration for payment methods you do not use.

import Foundation
import PayKKaCheckoutPayments

struct AppConf {
    static let EXAMPLE_CHECKOUT_SESSION_ID: String = "{YOUR_EXAMPLE_CHECKOUT_SESSION_ID}"
    static let ENVIRONMENT: PayKKaEnv = .SANDBOX
    static let CONFIGURATION: PayKKaConf = {
        PayKKaConf(dict: [
            KEY_GATEWAY_MERCHANT_ID: "{YOUR_GATEWAY_MERCHANT_ID}",
            KEY_CLIENT_KEY: "{YOUR_CLIENT_KEY}",
            KEY_APPLE_PAY_MERCHANT_ID: "{YOUR_APPLE_PAY_MERCHANT_ID}",
            KEY_ALIPAY_PLUS_ACQUIRER_ID: "{YOUR_ALIPAY_PARTICIPANT_ID}",
            KEY_WECHAT_PAY_APP_ID: "{YOUR_WECHATPAY_APP_ID}",
            KEY_WECHAT_PAY_APP_UNIVERSAL_LINK: "{YOUR_APP_UNIVERSAL_LINK}",
        ])
    }()
}

Configuration details:

  • KEY_GATEWAY_MERCHANT_ID: Required. Your PayKKa merchant ID.
  • KEY_CLIENT_KEY: Required. Your client key.
  • KEY_APPLE_PAY_MERCHANT_ID: When integrating Apple Pay, enter your Apple Pay Merchant ID.
  • KEY_ALIPAY_PLUS_ACQUIRER_ID: When integrating Alipay+, enter your Participant ID from the Alipay+ Developer Center.
  • KEY_WECHAT_PAY_APP_ID: When integrating WeChat Pay, enter your WeChat Open Platform App ID.
  • KEY_WECHAT_PAY_APP_UNIVERSAL_LINK: When integrating WeChat Pay, enter your Universal Link.

The EXAMPLE_CHECKOUT_SESSION_ID in the examples is for demonstration only. In an actual integration, the merchant backend should call the PayKKa API to create a Checkout Session for the corresponding mode and then pass the returned Session ID to the app. Drop-In and Component use different Session types; do not mix them.

Define the Payment Result Callback

All payment methods return payment results, errors, and user cancellation events through PKPaymentResultCallback. The common callback code below is reused by the payment methods that follow.

import Foundation
import PayKKaCheckoutPayments

class CheckoutCoordinator: NSObject, ObservableObject, PKPaymentResultCallback {
    @Published var isLoading = false
    @Published var isPayEnabled = true
    @Published var presentedError: CheckoutPresentedError?

    let sessionID: String
    private var onFinished: ((PKPaymentResult) -> Void)?
    private var hasFinished = false

    init(sessionID: String, onFinished: @escaping (PKPaymentResult) -> Void) {
        self.sessionID = sessionID
        self.onFinished = onFinished
        super.init()
    }

    func pay() {}

    func onResult(_ paymentResult: PKPaymentResult) {
        runOnMain { [weak self] in
            guard let self, !self.hasFinished else { return }
            guard paymentResult.status == .success || paymentResult.status == .expired else {
                self.isLoading = false
                return
            }
            self.hasFinished = true
            self.isLoading = false
            self.onFinished?(paymentResult)
        }
    }

    func onError(_ throwable: Any) {
        runOnMain { [weak self] in
            self?.isLoading = false
            self?.presentedError = CheckoutPresentedError(message: Self.errorMessage(from: throwable))
        }
    }

    func onUserCanceled(_ paymentSource: PKPaymentBase?, extraData: [AnyHashable: Any]?) {
        runOnMain { [weak self] in
            self?.isLoading = false
        }
    }

    func tearDown() {
        onFinished = nil
    }

    func runOnMain(_ action: @escaping () -> Void) {
        if Thread.isMainThread {
            action()
        } else {
            DispatchQueue.main.async(execute: action)
        }
    }

    private static func errorMessage(from throwable: Any) -> String {
        if let error = throwable as? NSError {
            return error.localizedDescription
        }
        return String(describing: throwable)
    }
}

The example treats only successful payments and expired Checkout Sessions as terminal page states. When an error occurs or the user cancels the payment, the example stops the loading state and remains on the current page. You can display a message or allow the user to retry the payment according to your business needs.

Option 1: Integrate in Drop-In Mode

Drop-In mode provides a complete checkout with multiple payment methods and is simpler to integrate. After the merchant backend calls the PayKKa API to initialize a Drop-In Checkout Session ID, the app passes the Session ID to PKPaymentBottomSheet to display the payment form and guide the user through payment.

import SwiftUI
import PayKKaCheckoutPayments

final class DropInCheckoutCoordinator: CheckoutCoordinator {
    private var paymentBottomSheet: PKPaymentBottomSheet?

    override func pay() {
        guard !isLoading, paymentBottomSheet == nil else { return }
        isLoading = true

        let sheet = PKPaymentBottomSheet(checkoutSessionId: sessionID) { [weak self] _ in
            self?.runOnMain {
                self?.isLoading = false
                self?.paymentBottomSheet = nil
            }
        }
        sheet.onPayCallback = self
        sheet.showMerchantName()
        paymentBottomSheet = sheet
        sheet.show(fromPresenter: nil, completion: nil)
    }

    override func tearDown() {
        paymentBottomSheet?.hide()
        paymentBottomSheet = nil
        super.tearDown()
    }
}

struct DropInCheckoutConfirmOrder: View {
    @StateObject private var coordinator: DropInCheckoutCoordinator

    init(sessionID: String, onFinished: @escaping (PKPaymentResult) -> Void) {
        _coordinator = StateObject(wrappedValue: DropInCheckoutCoordinator(sessionID: sessionID, onFinished: onFinished))
    }

    var body: some View {
        BaseCheckoutConfirmOrder(error: $coordinator.presentedError) {
            EmptyView()
        } paymentArea: {
            CheckoutPayButton(
                title: "Pay Now ¥1118.83",
                isLoading: coordinator.isLoading,
                isEnabled: coordinator.isPayEnabled,
                action: coordinator.pay
            )
        }
        .onDisappear(perform: coordinator.tearDown)
    }
}

Calling showMerchantName() displays the merchant name in the form. Omit this call if you do not need to display it. When leaving the page or no longer using the form, call hide() and release the PKPaymentBottomSheet instance.

Option 2: Integrate in Component Mode

Component mode provides a component for a single payment method. You can embed the required component at a specific location in your app for greater customization. After the merchant backend calls the PayKKa API to initialize a Component Checkout Session ID, the app passes the Session ID to the corresponding payment component to accept payments.

Card Payment

The card payment component uses PKCardPaymentForm to display the card information entry form and controls the payment button state through form validation callbacks.

Card payment form examples

⒈ Embed the UIKit card payment form in SwiftUI.

import SwiftUI
import PayKKaCheckoutPayments

struct CardPaymentFormView: UIViewRepresentable {
    let form: PKCardPaymentForm

    func makeUIView(context: Context) -> PKCardPaymentForm { form }
    func updateUIView(_ uiView: PKCardPaymentForm, context: Context) {}
}

⒉ Initialize the form and PKBankCardPayment, monitor the form validation state, and initiate the payment.

import SwiftUI
import PayKKaCheckoutPayments

final class BankCardPayCheckoutCoordinator: CheckoutCoordinator {
    let form = PKCardPaymentForm()
    private var payment: PKBankCardPayment?
    private var isFormInitialized = false

    override init(sessionID: String, onFinished: @escaping (PKPaymentResult) -> Void) {
        super.init(sessionID: sessionID, onFinished: onFinished)
        isLoading = true
        isPayEnabled = false
        form.isEnabled = false

        payment = PKBankCardPayment(
            pkCardPaymentForm: form,
            checkoutSessionId: sessionID
        ) { [weak self] error, _ in
            guard let self else { return }
            if let error {
                self.form.setError(error)
                self.isLoading = false
                self.isPayEnabled = false
                self.presentedError = CheckoutPresentedError(message: error.localizedDescription)
                return
            }

            self.isFormInitialized = true
            self.form.isEnabled = true
            self.isLoading = false
        }
        payment?.paymentResultCallback = self

        form.setEditingChangedCallback({ [weak self] in
            guard let self, self.isFormInitialized else { return }
            self.isPayEnabled = true
        }, onFieldInputInvalidCallback: { [weak self] _ in
            self?.isPayEnabled = false
        })
    }

    override func pay() {
        guard isFormInitialized, isPayEnabled, !isLoading, let payment else { return }
        isLoading = true
        payment.setError(nil)
        payment.perform(NSSelectorFromString("requestPaymentWithCheckoutSessionId:"), with: sessionID)
    }

    override func tearDown() {
        payment?.paymentResultCallback = nil
        payment = nil
        super.tearDown()
    }
}

struct BankCardPayConfirmOrder: View {
    @StateObject private var coordinator: BankCardPayCheckoutCoordinator

    init(sessionID: String, onFinished: @escaping (PKPaymentResult) -> Void) {
        _coordinator = StateObject(wrappedValue: BankCardPayCheckoutCoordinator(sessionID: sessionID, onFinished: onFinished))
    }

    var body: some View {
        BaseCheckoutConfirmOrder(error: $coordinator.presentedError) {
            CardPaymentFormView(form: coordinator.form)
                .disabled(!coordinator.form.isEnabled)
        } paymentArea: {
            CheckoutPayButton(
                title: "Card Payment ¥1118.83",
                isLoading: coordinator.isLoading,
                isEnabled: coordinator.isPayEnabled,
                action: coordinator.pay
            )
        }
        .onDisappear(perform: coordinator.tearDown)
    }
}

Apple Pay

Apple payment form examples

Before integrating Apple Pay, add the Apple Pay Capability under TARGETS <Your TARGET>Signing & Capabilities in Xcode and select your Merchant ID. This Merchant ID must match the KEY_APPLE_PAY_MERCHANT_ID configuration.

⒈ Embed the Apple Pay form in SwiftUI.

struct ApplePaymentFormView: UIViewRepresentable {
    let form: PKApplePaymentForm

    func makeUIView(context: Context) -> PKApplePaymentForm {
        form.accessibilityIdentifier = "checkout-pay-button"
        return form
    }

    func updateUIView(_ uiView: PKApplePaymentForm, context: Context) {}
}

⒉ Initialize PKApplePayment, bind the payment button, and initiate the payment.

import PassKit
import SwiftUI
import PayKKaCheckoutPayments

final class ApplePayCheckoutCoordinator: CheckoutCoordinator, PKPaymentInterceptor {
    let form = PKApplePaymentForm()
    private let payment: PKApplePayment

    override init(sessionID: String, onFinished: @escaping (PKPaymentResult) -> Void) {
        payment = PKApplePayment(checkoutSessionId: sessionID)
        super.init(sessionID: sessionID, onFinished: onFinished)
        form.isHidden = false
        form.pkApplePaymentButton.addTarget(self, action: #selector(payButtonTapped), for: .touchUpInside)
        payment.form = form
        payment.paymentResultCallback = self
        payment.interceptor = self
    }

    @objc private func payButtonTapped() {
        pay()
    }

    override func pay() {
        guard !isLoading else { return }
        isLoading = true
        form.setLoading(true, animated: true)
        payment.setError(nil)
        payment.request()
    }

    func beforeRequestPayment(_ paymentSource: Any) {
        guard let payment = paymentSource as? PKApplePayment,
              let request = payment.paymentRequest else { return }
        let items = [
            PKPaymentSummaryItem(label: "Goods 1", amount: .zero),
            PKPaymentSummaryItem(label: "Goods 2", amount: .zero)
        ]
        request.paymentSummaryItems = items + request.paymentSummaryItems
    }

    override func onResult(_ paymentResult: PKPaymentResult) {
        stopLoading()
        super.onResult(paymentResult)
    }

    override func onError(_ throwable: Any) {
        stopLoading()
        super.onError(throwable)
    }

    override func onUserCanceled(_ paymentSource: PKPaymentBase?, extraData: [AnyHashable: Any]?) {
        stopLoading()
        super.onUserCanceled(paymentSource, extraData: extraData)
    }

    override func tearDown() {
        payment.paymentResultCallback = nil
        payment.interceptor = nil
        payment.form = nil
        form.pkApplePaymentButton.removeTarget(self, action: #selector(payButtonTapped), for: .touchUpInside)
        super.tearDown()
    }

    private func stopLoading() {
        runOnMain { [weak self] in
            self?.isLoading = false
            self?.form.setLoading(false, animated: true)
        }
    }
}

The example uses PKPaymentInterceptor to add product items before the payment summary generated by the SDK. Goods 1, Goods 2, and the zero amounts in the example are only used to demonstrate the API. In an actual project, you must use product names and amounts that match the order. Do not copy merchant.com.paykka.apptest directly from the demo; configure your own Merchant ID in the Apple Pay Capability.

WeChat Pay

WeChat payment form examples

When integrating WeChat Pay, in addition to the payment component code, you must configure the URL Scheme, Universal Link, and payment return handling.

⒈ Embed the WeChat Pay form in SwiftUI and initialize the payment component.

struct WeChatPaymentFormView: UIViewRepresentable {
    let form: PKWeChatPaymentForm

    func makeUIView(context: Context) -> PKWeChatPaymentForm { form }
    func updateUIView(_ uiView: PKWeChatPaymentForm, context: Context) {}
}

⒉ Handle URL Scheme and Universal Link returns in the SwiftUI app lifecycle.

import SwiftUI
import PayKKaCheckoutPayments
import AlipayPlusClient

@main
struct SwiftCheckoutDemoApp: App {
    @StateObject var router = NavigationRouter()

    init() {
        PayKKa.Init(AppConf.CONFIGURATION, AppConf.ENVIRONMENT)
    }

    var body: some Scene {
        WindowGroup {
            Index()
                .environmentObject(router)
                .onOpenURL { url in
                    let client = AlipayPlusClient.shared()
                    if client.canProcessOrder(withPaymentResult: url) {
                        client.processOrder(withPaymentResult: url)
                    }
                    _ = WeChatPayKKaHandler.handleOpen(url)
                }
                .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
                    _ = WeChatPayKKaHandler.handleUniversalLink(activity)
                }
        }
    }
}

Register your WeChat App ID in Info.plist and allow queries for the URL Schemes used by the WeChat client:

Info.plist
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLName</key>
        <string>wechat</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>{YOUR_WECHATPAY_APP_ID}</string>
        </array>
    </dict>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
    <string>weixin</string>
    <string>weixinULAPI</string>
</array>

Then add the Associated Domains Capability under Signing & Capabilities in Xcode and configure your domain:

YourApp.entitlements
<key>com.apple.developer.associated-domains</key>
<array>
    <string>applinks:{YOUR_ASSOCIATED_DOMAIN}</string>
</array>

Make sure the following configurations match:

  • The URL Scheme in Info.plist, KEY_WECHAT_PAY_APP_ID, and the WeChat Open Platform App ID must match.
  • The domain in Associated Domains, the Universal Link configured in the WeChat Open Platform, and KEY_WECHAT_PAY_APP_UNIVERSAL_LINK must match.

Alipay+

Alipay+ payment form examples

When integrating Alipay+, you must add AlipayPlusClient.xcframework, configure the payment return URL Scheme, and pass the system return URL to AlipayPlusClient for handling.

⒈ Embed the Alipay+ form in SwiftUI and initialize the payment component.

struct AlipayPlusPaymentFormView: UIViewRepresentable {
    let form: PKAlipayPlusPaymentForm

    func makeUIView(context: Context) -> PKAlipayPlusPaymentForm { form }
    func updateUIView(_ uiView: PKAlipayPlusPaymentForm, context: Context) {}
}

⒉ Handle the Alipay+ return in the SwiftUI app lifecycle.

.onOpenURL { url in
    let client = AlipayPlusClient.shared()
    if client.canProcessOrder(withPaymentResult: url) {
        client.processOrder(withPaymentResult: url)
    }
    _ = WeChatPayKKaHandler.handleOpen(url)
}

Register the return URL Scheme for Alipay+ in Info.plist:

Info.plist
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLName</key>
        <string>alixpay</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>{YOUR_ALIPAY_PLUS_URL_SCHEME}</string>
        </array>
    </dict>
</array>

paykka-sw-checkout-demo and paykka-oc-checkout-demo in the example app are demonstration values only. Replace them with your own URL Scheme.

Step 4: Test Payments

You can test payments in the PayKKa sandbox environment (SANDBOX) and switch to the corresponding production environment before releasing your app.

// You can specify the payment environment in the Init method
PayKKa.Init(AppConf.CONFIGURATION, AppConf.ENVIRONMENT)

/// SANDBOX (default)
PayKKa.useEnv(.SANDBOX)
/// Switch to the EU production environment
PayKKa.useEnv(.PROD_EU)
/// Switch to the HK production environment
PayKKa.useEnv(.PROD_HK)

Apple Pay sandbox testing requires a physical device compatible with Apple Pay, a sandbox test account, and test cards. Alipay+ and WeChat Pay involve switching to external apps, so use a physical device to verify the URL Scheme and Universal Link return flows.

Notes

Apple Pay

1. How do I use test cards for payment testing?

See: Payment Method - Apple Pay.