Skip to content

1. Overview

PaymentGatewaySDK is a Kotlin Multiplatform Mobile (KMM) module that provides merchant applications with a ready-made, branded payment experience for card transactions processed through iFortePay's BRICC channel.

The same Kotlin business logic—including request building, validation, NFC EMV card reading, and API communication—runs on both Android and iOS, while the UI is rendered natively through Compose Multiplatform.

Supported Capabilities

The SDK supports four payment-related capabilities:

  • Card
    • Manual entry of credit/debit card details with real-time validation and card-network detection.
  • NFC
    • Contactless EMV card reading (Tap-to-Pay) that automatically fills the card form from a physical card.
  • Tenor
    • Installment plans (0, 3, 6, or 12 months).

SDK Design Summary

ComponentDescription
Shared ModuleKotlin Multiplatform (commonMain + androidMain + iosMain)
UICompose Multiplatform (Jetpack Compose on Android, Skia-rendered on iOS)
ArchitectureMVVM + Clean Architecture (Presentation / Domain / Data)
Serializationkotlinx.serialization
Distribution.aar (Android), .xcframework (iOS)

2. Tech Stack

The SDK is built entirely in Kotlin and shares the same business logic across both target platforms.

Only the thinnest possible platform layer—the HTTP engine and the NFC reader implementation—is platform-specific.

Technology Stack

LayerTechnologyNotes
LanguageKotlin Multiplatform (KMM)commonMain shared by Android & iOS
UICompose MultiplatformJetpack Compose API, native rendering on both platforms
ArchitectureMVVM + Clean ArchitecturePresentation / Domain / Data layers
Serializationkotlinx.serialization@Serializable data classes, JSON encode/decode
NetworkingKtor ClientOkHttp engine (Android), Darwin engine (iOS)
NFC — AndroidAndroid NfcAdapter + IsoDepEMV APDU exchange over ISO 7816-4
NFC — iOSCoreNFC (NFCTagReaderSession)NFCISO7816Tag, EMV APDU exchange
LoggingNapierCross-platform logging, tagged "NFC" for card-read diagnostics
Base64io.ktor.util.encodeBase64() / decodeBase64String()Credential token encode/decode

Why Compose Multiplatform Instead of Separate Native UIs?

A single PaymentScreen composable renders identically on Android and iOS, so card-form validation, NFC sheet behavior, and dialogs only need to be implemented and tested once.

Platform conventions are still respected:

  • Android exposes a Composable.
  • iOS exposes a UIViewController.

This matches each platform's native integration pattern while keeping the business logic shared.

Key Architectural Decisions

expect / actual for Platform Boundaries

Only two contracts require platform-specific implementations:

  • getHttpEngine()

    • Returns OkHttp on Android.
    • Returns Darwin on iOS.
  • getNFCManager()

    • Wraps Android's NfcAdapter on Android.
    • Wraps CoreNFC's NFCTagReaderSession on iOS.

Single Result Contract

Both Android and iOS return the same PaymentSdkResult structure to the host application, regardless of any internal response-code differences returned by the API.

No Merchant Backend Dependency at Runtime

The SDK communicates directly with the iFortePay API.

The merchant backend is only responsible for:

  • Issuing merchant credentials.
  • Receiving the asynchronous webhook callback.

3. Clean Architecture Layers

The shared module follows a conventional three-layer Clean Architecture split.

Dependencies only point inward:

  • Presentation depends on Domain
  • Domain depends on nothing
  • Data implements the contracts defined by Domain

Architecture Layers

LayerResponsibilityKey Types
PresentationRenders the UI, owns UI state, and reacts to user inputPaymentScreen, PaymentViewModel, CardState
DomainBusiness rules, request/response models, validation, and NFC decodingPaymentRequest, PaymentResponse, CardDetails, CardTypeDetector, TLVParser
DataCommunicates with external systems such as HTTP services and NFC hardwarePaymentGateway, getHttpEngine(), getNFCManager()

Presentation Layer in Detail

PaymentViewModel is the single source of truth for the payment screen.

It exposes:

  • One mutable UI state object (CardState)
  • One result callback (PaymentSdkResult)

Every user interaction—including typing card information, tapping the NFC button, and submitting a payment—is handled by the ViewModel before delegating to the Domain and Data layers.

Main Responsibilities

  • updateCardNumber()
  • updateHolder()
  • onExpiryDateChange()
  • onCvvChange()

Updates field-level UI state with live formatting.

  • validateAll()

Runs all field validators before allowing payment submission.

  • openNfc()
  • closeNfc()
  • autofillFromNfc()

Manages the NFC lifecycle and automatically fills card information after a successful scan.

  • processPayment()
  • onPayClicked()

Builds the final payment request and invokes the payment gateway.

  • onSuccessDone()
  • onFailedDone()

Creates a PaymentSdkResult and invokes the host application's callback.

4. Main Payment Flow

This sequence describes the complete lifecycle of a payment transaction, regardless of whether the card information is entered manually or populated through NFC, and regardless of the target platform.

Main Flow

  1. The host application launches PaymentScreen.
  2. PaymentViewModel is created with:
    • Base64 token
    • Payment request JSON
    • Result callback
  3. The user enters card details or scans an NFC card.
  4. The user taps Pay.
  5. PaymentViewModel validates all inputs.
  6. If validation succeeds, processPayment() is executed.
  7. PaymentGateway sends a request to the iFortePay API.
  8. The API returns the payment response.
  9. The SDK converts the response into PaymentSdkResult.
  10. The host application receives the callback and continues its own business flow.

What Changes Between Platforms?

Android

  • PaymentGatewayScreen is exposed as a @Composable.
  • Embedded directly inside the host application's NavHost.
  • No additional Activity is launched.

iOS

  • MainViewController(token, json, onResult) returns a UIViewController.
  • The controller is presented modally by the host application.

Shared Logic

The following implementation is identical on both Android and iOS:

  • PaymentViewModel construction
  • Input validation
  • Networking
  • PaymentSdkResult callback generation

5. Authentication

Every request sent to the iFortePay API is authenticated using a Basic Authorization Token generated from three merchant-specific credentials.

The token should be generated once by the host application (preferably by the merchant backend rather than directly on the device), then passed into the SDK together with the serialized payment request.

Token Format

The authorization token is a Base64 encoding of three colon-separated values.

text
credential = "{merchantId}:{secretUnbound}:{hashKey}"

token = Base64Encode(credential)

Kotlin (Shared / Android)

kotlin
val credential = "$merchantId:$secretUnbound:$hashKey"

val token = credential
    .toByteArray()
    .encodeBase64()

Swift (iOS)

swift
let credential = "\(merchantId):\(secretUnbound):\(hashKey)"

let token = Data(credential.utf8)
    .base64EncodedString()

Token Parsing Inside the SDK

When the SDK is initialized, PaymentViewModel decodes the Base64 token back into its original credential components.

kotlin
val decodedString = credToken.decodeBase64String()

val parts = decodedString.split(":")

val merchantId = parts.getOrNull(0) ?: ""
val secretUnbound = parts.getOrNull(1) ?: ""
val hashKey = parts.getOrNull(2) ?: ""

The decoded values are subsequently used to construct the authenticated request sent to the iFortePay API.

6. NFC Payment

NFC EMV card reading allows users to tap a physical contactless card instead of manually entering card details.

The SDK communicates directly with EMV cards using ISO 7816-4 APDU commands and decodes the relevant TLV (Tag-Length-Value) records internally. No additional card-reader SDK is required.

Platform Implementation Notes

AspectAndroidiOS
Reader APINfcAdapter.enableReaderMode() + IsoDepNFCTagReaderSession + NFCISO7816Tag
Context RequiredActivity passed into openNfc(context)None — Unit is passed; CoreNFC manages its own UI
Reader UISDK-provided "Tap your card" bottom sheetNative iOS NFC scanning sheet
APDU ExchangeIsoDep.transceive(ByteArray)NFCISO7816Tag.sendCommand(apdu:)
Result DeliveryKotlin Flow (nfcManager.tags)Same shared Flow; platform differences are hidden inside androidMain / iosMain

Why NFCTagReaderSession, Not NFCNDEFReaderSession?

NFCNDEFReaderSession is designed for NFC tags and stickers that contain small NDEF payloads—not EMV payment cards.

EMV payment cards require direct ISO 7816-4 APDU command/response communication, which is only supported through:

  • NFCTagReaderSession
  • NFCISO7816Tag

This is the implementation used by the SDK on iOS.

7. Callback / Webhook

Besides the synchronous response returned directly from the payment API, iFortePay also sends an asynchronous webhook to the merchant's callbackUrl after the transaction reaches its final status.

The webhook is the authoritative source of truth for payment status.

Important

Merchant backends should not rely solely on the SDK callback for reconciliation.

Callback Flow

StepDescription
1iFortePay sends the callback payload to callbackUrl.
2The callback service verifies the HMAC signature contained in the payload.
3If the signature is invalid, return HTTP 403 and stop processing.
4If valid, forward the payload to the merchant backend. The backend updates the order status and returns HTTP 200.
5If the merchant backend does not return HTTP 200, iFortePay retries after 5 seconds, 30 seconds, and 5 minutes (maximum 3 attempts).

Example Callback Payload

json
{
  "event": "payment.success",
  "data": {
    "transactionId": "TXN-001",
    "amount": 10000,
    "paymentStatus": "SUCCESS"
  },
  "signature": "a1b2c3..."
}

Implementation Note

The SDK never receives or processes webhook callbacks.

Webhooks are delivered server-to-server, directly from iFortePay to the merchant backend, regardless of whether the mobile application is still open.


8. Error Handling & State Machine

PaymentViewModel models the payment lifecycle as an implicit state machine using fields inside CardState.

Examples include:

  • isLoading
  • showSuccessDialog
  • showErrorDialog
  • errorMessage

State Summary

StateTriggerUser-visible Result
IdleInitial state or after reset()Payment form is displayed and editable
ValidatingonPayClicked()Field validation is performed. Validation errors are shown if any exist.
LoadingprocessPayment() is executingLoading indicator is displayed and user input is disabled.
SuccessresponseCode == "00"Success dialog is displayed with the payment redirect link.
ServerValidationErrorHTTP 400Error dialog displaying validation errors returned by the server.
AuthErrorHTTP 401Token is invalid. The host application should regenerate the authentication token and retry.
ServerErrorHTTP 5xxRetry is recommended (up to three attempts).
NetworkErrorIOException or timeout caught in try/catchGeneric network error message is shown and retry is recommended.

iFortepay API Documentation