Skip to content

// project

PaymentsLab

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

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 runs — and step-by-step visualizes — real payment flows across a 66-gateway catalog behind a single PaymentGateway abstraction, backed by a Ktor server that does the order creation, signature verification and webhook reconciliation a real integration requires — and, beyond one-shot pay-in, models five money-movement rails.

Kotlin MultiplatformCompose MultiplatformKtorAndroidiOSRoom
GitHub Mileway (sibling KMP app) Field notes on this work 40 modules · 66 gateways · 5 rails

// guided tour

Two minutes, narrated

A storyboarded walkthrough of the real app — tap the speaker for the voiceover, or read along with the captions.

PaymentsLab — narrated product tour

0

Gradle modules (15 local + 25 composed)

0

gateways cataloged

0

money-movement rails

0

PaymentGateway contract

// multiplatform

One codebase, every surface

The real screens (and, where it's live, the running build) per platform — not a mockup.

Android screen

// deep dive

How it works

The one idea worth stealing

A client-side Success is a hint, never proof. Only the server — after signature verification and webhook reconciliation — decides the true state. 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.

40 modules, 66 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 — 15 local modules plus 25 composed from kmp-toolkit (19 of them standalone provider gateway modules). The in-app catalog spans 66 registered gateways: 7 native-SDK integrations, 47 hosted-webview gateways behind one archetype, 8 mobile-money flows and 4 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 — 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
  • 40 Gradle modules (15 + 25 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

// gallery

Screens (28)

Swipe or use the arrows · tap a screen to enlarge

See how everything connects →