Skip to content

iOS

Step 1 Embed the framework

Drag PaymentGatewaySDK.xcframework into your Xcode project and add it under Frameworks, Libraries, and Embedded Content with Embed & Sign.

Step 2 Build the PaymentRequest and generate the token

Swift mirrors the Kotlin model closely, with two important type differences (see callout below):

swift
let paymentRequest = PaymentRequest(
    externalId: "1Ktru19Cp7",
    orderId: "ONxeTcEBE6",
    currency: "IDR",
    source: "payment_page",
    paymentMethod: "card",
    paymentChannel: "BRIQC",
    paymentMode: "CLOSE",
    paymentDetails: PaymentDetails(
        amount: KotlinLong(value: Int64(amountValue)), // Long -> KotlinLong wrapper
        isCustomerPayingFee: false,
        transactionDescription: "Order #1234",
        expiredTime: ""
    ),
    itemDetails: [
        ItemDetails(
            itemId: "SKU-1",
            name: "Shirt",
            amount: Int32(unitPrice), // Int -> Int32, no wrapper needed
            qty: 1,
            description: ""
        )
    ],
    // ...customerDetails, billingAddress, shippingAddress,
    // cardDetails, returnUrl, callbackUrl, paymentOptions
)

let credential = "\(merchantId):\(secretUnbound):\(hashKey)"
let token = Data(credential.utf8).base64EncodedString()
let json = JsonHelper().toJson(request: paymentRequest)

Kotlin → Swift numeric type mapping

  • Kotlin Long is exposed in Swift as the boxed class KotlinLong — never cast a Swift Int32 directly to it.

  • Always wrap using:

    swift
    KotlinLong(value: Int64(yourValue))
  • Kotlin Int is exposed in Swift as the native Int32 — no wrapper needed.

    swift
    Int32(yourValue)

Mixing these up (e.g. Int32(value) as KotlinLong) compiles inconsistently across toolchains and fails at runtime with a cast exception.

Step 3 Launch the SDK screen

swift
let vc = MainViewControllerKt.MainViewController(
    token: token,
    json: json
) { result in
    if result.isSuccess {
        print("Success, link: \(result.link ?? "-")")
    } else {
        print("Failed: \(result.responseMessage ?? "-")")
    }
}

vc.modalPresentationStyle = .fullScreen

DispatchQueue.main.async {
    presenter.present(vc, animated: true)
}

Always present on the main thread, from a stable presenter

  • Compose Multiplatform enforces that setContent runs on the main thread. Presenting from a background queue raises an IllegalStateException inside Compose's threading check.
  • Never present immediately after dismissing a SwiftUI .sheet in the same call — the presenting view controller may not yet be back in the window hierarchy.
  • Wait for the sheet's dismissal to complete (for example, via onChange(of:) on the @State binding) before presenting the SDK's view controller.
  • See Annex C for the complete pattern used in the reference app.

Wiring the NFC Scan Button (Both Platforms)

PaymentScreen exposes onOpenNfc and onCloseNfc callbacks that the host composable/view must forward into PaymentViewModel, since the context required to start a scan is platform-specific.

AndroidiOS
Context value passedA resolved ActivityUnit (CoreNFC needs no UIKit context)
openNfc callviewModel.openNfc(activity)viewModel.openNfc(Unit)
closeNfc callviewModel.closeNfc(activity)viewModel.closeNfc(Unit)

iOS wiring example

Matching the MainViewController pattern shown earlier:

swift
PaymentScreen(
    viewModel: viewModel,
    onOpenNfc: {
        viewModel.openNfc(Unit)
    },
    onCloseNfc: {
        viewModel.closeNfc(Unit)
    }
)

iFortepay API Documentation