Skip to content

// project

PaymentsLab-KMP

An Integration Lab for the Android payments ecosystem: every gateway behind one abstraction, with a live look at what actually happens on each transaction.

The brief

Payments is the hardest integration surface on Android: every gateway ships a different SDK, most of them are Activity-callback-era, the client can lie about the outcome, and the interesting logic (signatures, webhooks, idempotency, recovery) lives on the server. PaymentsLab-KMP runs real payment flows across a 70-gateway catalog behind a single PaymentGateway abstraction, and visualizes them step by step. A Ktor server does the order creation, signature verification and webhook reconciliation a real integration requires. Beyond one-shot pay-in it models five money-movement rails.

What shipped

  • 48-module registry (17 local + 31 composed) spans 70 cataloged payment gateways.
  • Five money-movement rails plus split payments, all idempotency-keyed and MOCK_MODE-honest.
Kotlin MultiplatformCompose MultiplatformKtorAndroidiOSRoom
GitHub Doori (sibling KMP app) Install it 48 modules · 70 gateways · 5 rails
built on
kmp-build-logic
kmp-toolkit
written up in

no public push in the last 20 events

// case study

The short version

The problem

Every payment gateway ships a different, mostly Activity-callback-era SDK, and the client can lie about the outcome: the logic that actually decides whether money moved (signatures, webhooks, idempotency, recovery) has to live somewhere the client can't fake it.

The decision

Put a Ktor server between the client and the truth: order creation, signature verification and webhook reconciliation happen there, a Room journal is written before the SDK even launches so a process death mid-payment is always recoverable, and a redaction layer keeps every secret and PII out of logs and screens.

The result

70 cataloged gateways now sit behind one PaymentGateway contract, with a client-side Success read only as a hint until the server confirms it. The same discipline now extends to five money-movement rails beyond one-shot checkout, every one of them MOCK_MODE-honest until real sandbox keys are set.

70 gateways cataloged

Watch the two-minute tour ↓
Evidence: 70 cataloged gateways now sit behind one PaymentGateway contract, with a client-side Success read only as a hint until the server confirms it. The same discipline now extends to five money-movement rails beyond one-shot checkout, every one of them MOCK_MODE-honest until real sandbox keys are set.
Go deeper

Analyst view: PaymentsLab-KMP draws on 6 stack entries and links to 4 outside sources, and shares 2 shared foundations with 3 other builds.

// guided tour

The product, narrated

A recorded walkthrough of the app with sample content. Tap the speaker for the voiceover, or read the captions. In-app figures describe the recorded demo, not live production activity.

PaymentsLab-KMP — narrated product tour

// shipping

Where you can actually get it

Every line below is checkable. Add the repository in an F-Droid client and the app installs, signed with the same key its listing pins.

  • F-DroidLive in a self-hosted repository. 33 MB, signed, tagged NonFreeDep and NonFreeNet because integrating real gateway SDKs is the whole point of the app. open
  • GitHub ReleasesSigned APK per tag. open

// pipeline

The build that put it there

Read live from GitHub Actions and the F-Droid index. Not a badge: the actual runs, what shipped, and the certificate you can check the download against.

reading the pipeline

48

Gradle modules (17 local + 31 composed)

70

gateways cataloged

5

money-movement rails

1

PaymentGateway contract

// multiplatform

One codebase, every surface

Recorded app screens and, where available, the running build. Sample content inside a screen is separate from the source-linked project metrics.

Android screen (1 of 5)

// design notes

How it works

The one idea worth stealing

A client-side Success is a hint, never proof. Only the server decides the true state, after signature verification and webhook reconciliation. A server that owns price and truth, a client that always confirms before trusting, a journal written to Room before the SDK launches so a process death mid-payment is always recoverable, and a redaction layer so no secret or PII ever renders or logs.

48 modules, 70 gateways

One Gradle module per native-SDK provider is contributed into a registry via Koin's getAll<PaymentGateway>(), so adding gateway N+1 touches no existing code. There are 17 local modules plus 31 composed from kmp-toolkit (19 of kmp-toolkit's 20 standalone provider gateway modules). The in-app catalog spans 70 registered gateways: 15 native-SDK integrations, an internal wallet ledger, 44 hosted-webview gateways behind one archetype, 7 mobile-money flows and 3 catalog-only / KYC-gated entries, each with its own status badge and region.

Five money-movement rails + split payments

Beyond one-shot checkout the server models payouts (/payouts: money out to a beneficiary), mandates & subscriptions (/mandates + scheduled debits and cancel), a card vault (/vault: tokenize once, charge later by id), marketplace Connect onboarding (/connect: sub-merchant KYC + split payouts) and an internal double-entry wallet ledger (/wallet: seed / debit / refund against a real running balance), plus split payments, a two-leg orchestration that compensates if one leg fails. Ten provider modules ride these rails (Paystack, Flutterwave, Paytm, Xendit, M-Pesa, Peach, NMI, Stripe Connect, plus wallet and a record-only cash gateway), every one MOCK_MODE-honest until real sandbox keys are set.

One contract, real SDKs

Razorpay, Cashfree, Stripe (+ Google Pay), Square, Omise and a raw UPI intent flow all implement the same tiny PaymentGateway interface. The Activity-callback SDKs are bridged into suspending coroutines by a PaymentHost that never leaks an Activity upward. A generic hosted-webview archetype covers the whole class of gateways with no native SDK behind the same contract. Env-backed credentials auto-degrade from SANDBOX_READY to MOCK_MODE honestly instead of silently pretending to work.

Pure, replayable state machine

The lifecycle is a pure (State, Event) → Effects reducer, zero coroutines/DI/IO, with the orchestrator just executing its effects. A payment's path is a recorded event log that replays byte-for-byte identically, the auditing property money movement wants. The MVI base comes from my own kmp-toolkit library, shared with other apps.

VAPT-grade security

core:security holds real Android Keystore AES-256-GCM at-rest encryption, FLAG_SECURE + recursive tapjacking protection, device-integrity checks (root, emulator, debugger, Frida/Xposed hook detection, SSL-pinning-bypass detection), and a certificate-pinning config, with detection kept deliberately separate from enforcement policy.

// architecture

How it's built

Gateway registry: adding provider N+1 touches no existing code

Client Success is a hint: the server decides truth

Five rails beyond one-shot pay-in

// under the hood

Tech stack

Architecture

  • Kotlin Multiplatform
  • Compose Multiplatform
  • 48 Gradle modules (17 local + 31 composed)
  • Koin registry (getAll)
  • kmp-toolkit (shared MVI base)

Backend & rails

  • Ktor server
  • HMAC-SHA256 signatures
  • Webhook reconciliation
  • Payouts · mandates · vault · connect · wallet ledger

Data & Security

  • Room (process-death journal)
  • Android Keystore AES-256-GCM
  • Certificate pinning
  • Device-integrity checks

Build & quality

  • kmp-build-logic convention plugins
  • Roborazzi screenshot tests
  • ktlint
  • detekt
  • GitHub Actions CI

// gateways

Compare two providers

Pick two of PaymentsLab-KMP's 75 cataloged gateways to compare region, archetype and shipped status.

Region
Global
Archetype
Hosted webview
Status
MOCK_MODE
Region
Ethiopia
Archetype
Hosted webview
Status
MOCK_MODE

One contract, five money-movement rails

// directions

Same screen, 3 treatments

Pick a treatment to see it whole. Where one ships a dark counterpart, drag the divider to compare the two faces of it — same layout, same content, only the light changes.

checkout lifecycle — order summary, light
order summarysingle mode — no dark counterpart

// gallery

Screens (28)

Swipe or use the arrows · tap a screen to enlarge

See how everything connects →