Appearance
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, or12months).
- Installment plans (
SDK Design Summary
| Component | Description |
|---|---|
| Shared Module | Kotlin Multiplatform (commonMain + androidMain + iosMain) |
| UI | Compose Multiplatform (Jetpack Compose on Android, Skia-rendered on iOS) |
| Architecture | MVVM + Clean Architecture (Presentation / Domain / Data) |
| Serialization | kotlinx.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
| Layer | Technology | Notes |
|---|---|---|
| Language | Kotlin Multiplatform (KMM) | commonMain shared by Android & iOS |
| UI | Compose Multiplatform | Jetpack Compose API, native rendering on both platforms |
| Architecture | MVVM + Clean Architecture | Presentation / Domain / Data layers |
| Serialization | kotlinx.serialization | @Serializable data classes, JSON encode/decode |
| Networking | Ktor Client | OkHttp engine (Android), Darwin engine (iOS) |
| NFC — Android | Android NfcAdapter + IsoDep | EMV APDU exchange over ISO 7816-4 |
| NFC — iOS | CoreNFC (NFCTagReaderSession) | NFCISO7816Tag, EMV APDU exchange |
| Logging | Napier | Cross-platform logging, tagged "NFC" for card-read diagnostics |
| Base64 | io.ktor.util.encodeBase64() / decodeBase64String() | Credential token encode/decode |
Why Compose Multiplatform Instead of Separate Native UIs?
A single
PaymentScreencomposable 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
NfcAdapteron Android. - Wraps CoreNFC's
NFCTagReaderSessionon iOS.
- Wraps Android's
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
| Layer | Responsibility | Key Types |
|---|---|---|
| Presentation | Renders the UI, owns UI state, and reacts to user input | PaymentScreen, PaymentViewModel, CardState |
| Domain | Business rules, request/response models, validation, and NFC decoding | PaymentRequest, PaymentResponse, CardDetails, CardTypeDetector, TLVParser |
| Data | Communicates with external systems such as HTTP services and NFC hardware | PaymentGateway, 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
- The host application launches
PaymentScreen. PaymentViewModelis created with:- Base64 token
- Payment request JSON
- Result callback
- The user enters card details or scans an NFC card.
- The user taps Pay.
PaymentViewModelvalidates all inputs.- If validation succeeds,
processPayment()is executed. PaymentGatewaysends a request to the iFortePay API.- The API returns the payment response.
- The SDK converts the response into
PaymentSdkResult. - The host application receives the callback and continues its own business flow.
What Changes Between Platforms?
Android
PaymentGatewayScreenis exposed as a@Composable.- Embedded directly inside the host application's
NavHost.- No additional
Activityis launched.iOS
MainViewController(token, json, onResult)returns aUIViewController.- 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
PaymentSdkResultcallback 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
| Aspect | Android | iOS |
|---|---|---|
| Reader API | NfcAdapter.enableReaderMode() + IsoDep | NFCTagReaderSession + NFCISO7816Tag |
| Context Required | Activity passed into openNfc(context) | None — Unit is passed; CoreNFC manages its own UI |
| Reader UI | SDK-provided "Tap your card" bottom sheet | Native iOS NFC scanning sheet |
| APDU Exchange | IsoDep.transceive(ByteArray) | NFCISO7816Tag.sendCommand(apdu:) |
| Result Delivery | Kotlin 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:
NFCTagReaderSessionNFCISO7816Tag
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
| Step | Description |
|---|---|
| 1 | iFortePay sends the callback payload to callbackUrl. |
| 2 | The callback service verifies the HMAC signature contained in the payload. |
| 3 | If the signature is invalid, return HTTP 403 and stop processing. |
| 4 | If valid, forward the payload to the merchant backend. The backend updates the order status and returns HTTP 200. |
| 5 | If 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:
isLoadingshowSuccessDialogshowErrorDialogerrorMessage
State Summary
| State | Trigger | User-visible Result |
|---|---|---|
| Idle | Initial state or after reset() | Payment form is displayed and editable |
| Validating | onPayClicked() | Field validation is performed. Validation errors are shown if any exist. |
| Loading | processPayment() is executing | Loading indicator is displayed and user input is disabled. |
| Success | responseCode == "00" | Success dialog is displayed with the payment redirect link. |
| ServerValidationError | HTTP 400 | Error dialog displaying validation errors returned by the server. |
| AuthError | HTTP 401 | Token is invalid. The host application should regenerate the authentication token and retry. |
| ServerError | HTTP 5xx | Retry is recommended (up to three attempts). |
| NetworkError | IOException or timeout caught in try/catch | Generic network error message is shown and retry is recommended. |