khaime_flutter

Khaime Checkout SDK for Flutter. Accept payments globally with a single widget.

The Flutter counterpart to @khaime/react. Same token, same callbacks, same "your app never branches on gateways" contract.

Installation

dependencies:
  khaime_flutter: ^0.1.0

Then run the platform setup below — Stripe's native PaymentSheet needs it.

Quick start

import 'package:khaime_flutter/khaime_flutter.dart';

KhaimeCheckout(
  token: token,
  onSuccess: (result) {
    debugPrint('Payment successful: ${result.reference ?? result.paymentIntentId}');
    Navigator.of(context).pushReplacementNamed('/success');
  },
  onError: (error) => debugPrint('Payment failed: ${error.message}'),
  onClose: () => debugPrint('Checkout dismissed'),
)

How it works

  1. Your backend calls the Khaime API to create a payment intent.
  2. Khaime returns a token containing gateway configuration.
  3. Pass the token to KhaimeCheckout.
  4. The widget renders the correct payment UI automatically.
Your Backend                    Khaime API
     │                              │
     │  POST /payment/intent        │
     │  { amount, currency, ... }   │
     │ ─────────────────────────────►
     │                              │
     │     { token: "eyJ..." }      │
     │ ◄─────────────────────────────
     │                              │
     ▼
KhaimeCheckout(token: token)

Create the intent on your server. The Khaime secret key must never ship inside the app bundle.

Supported payment gateways

Currency Gateway UI on Flutter
USD, EUR, GBP, CAD Stripe Native PaymentSheet (flutter_stripe)
NGN, ZAR, KES Paystack Full-screen in-app WebView
GHS StartButton Redirect to the system browser

Routing comes from the token's payment_gateway field, so Khaime can move a merchant between gateways without an app release.

Differences from the React SDK

  • Stripe presents a native modal PaymentSheet instead of an inline PaymentElement. That is the supported mobile surface and it is what enables Apple Pay and Google Pay.
  • Paystack opens a full-screen route rather than a browser popup. The hosted authorization_url is used when present; a token carrying only a public_key falls back to Paystack's inline script inside the WebView.
  • Redirect gateways hand off to the system browser. The app cannot observe the result — confirm these payments from your backend via webhook.

Platform setup

Android

android/app/build.gradle:

android {
    compileSdk 34
    defaultConfig {
        minSdk 21   // required by flutter_stripe
    }
}

flutter_stripe requires the host activity to extend FlutterFragmentActivity. In MainActivity.kt:

import io.flutter.embedding.android.FlutterFragmentActivity

class MainActivity : FlutterFragmentActivity()

Use an AppCompat theme in android/app/src/main/res/values/styles.xml:

<style name="LaunchTheme" parent="Theme.AppCompat.DayNight.NoActionBar">

android/app/src/main/AndroidManifest.xml needs internet access, plus a <queries> entry so url_launcher can resolve a browser on Android 11+:

<uses-permission android:name="android.permission.INTERNET" />

<queries>
  <intent>
    <action android:name="android.intent.action.VIEW" />
    <data android:scheme="https" />
  </intent>
</queries>

iOS

Set the platform to iOS 13 or newer in ios/Podfile:

platform :ios, '13.0'

Then cd ios && pod install.

For Apple Pay, add the Apple Pay capability in Xcode and pass your merchant identifier through Stripe's own configuration.

Return URL (required for redirect payment methods)

If your PaymentIntent allows a method that leaves the app — Link, Cash App Pay, iDEAL, Klarna — Stripe needs a URL to come back to. Without one the PaymentSheet fails to present and the widget sits on "Processing...".

Register a custom scheme in ios/Runner/Info.plist:

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLName</key>
    <string>com.example.myapp</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>myapp</string>
    </array>
  </dict>
</array>

On Android, add the matching intent filter to the .MainActivity activity in AndroidManifest.xml:

<intent-filter>
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="myapp" />
</intent-filter>

Then pass it:

KhaimeCheckout(
  token: token,
  returnUrl: 'myapp://stripe-redirect',
)

Card-only PaymentIntents work without this. Check your intent's payment_method_types — if it is anything beyond card, set a return URL.

Flutter 3.27+ with UIScene lifecycle (iOS)

Recent Flutter iOS templates adopt the UIScene lifecycle: Info.plist carries a UIApplicationSceneManifest and a SceneDelegate. Under UIScene the window belongs to the scene, so UIApplication.shared.delegate.window is nil.

flutter_stripe still presents the PaymentSheet from there — Flutter logs Plugin StripeIosPlugin uses deprecated application lifecycle events at startup. The sheet is created and Stripe reports it as shown, but it lands on a window that is not on screen. Nothing is visible, nothing is dismissed, and presentPaymentSheet never returns, so checkout sits on "Processing..." forever.

Until flutter_stripe supports UIScene, remove the manifest from ios/Runner/Info.plist to fall back to the application lifecycle:

/usr/libexec/PlistBuddy -c "Delete :UIApplicationSceneManifest" ios/Runner/Info.plist

Check UIMainStoryboardFile is set to Main afterwards, and do a full rebuild — plist changes do not survive hot restart. Flutter still supports the application lifecycle, but it will not forever, so track the plugin's UIScene support before relying on this long term.

Running the example

cd example
flutter run   # developer mode: paste a token by hand

With a backend that exposes POST /checkout returning { "token": "..." }:

flutter run --dart-define=MERCHANT_BACKEND_URL=https://your-store.example.com

The example walks the real shape of an integration — catalogue, buyer details, a token minted server-side, then KhaimeCheckout — and falls back to a pasted token when no backend is configured. To mint tokens for that fallback:

dart run tool/mint_test_token.dart --gateway stripe
dart run tool/mint_test_token.dart --gateway stripe \
  --publishable-key pk_test_xxx --client-secret pi_xxx_secret_yyy

The first exercises routing and UI only; the second completes a real test payment.

API

KhaimeCheckout

Parameter Type Default Description
token String required Payment token from the Khaime API
onSuccess void Function(PaymentResult)? Called on successful payment
onError void Function(PaymentError)? Called on payment error
onClose VoidCallback? Called when checkout is dismissed
onReady VoidCallback? Called when checkout is ready for input
productName String? token value Overrides the product name
productImage String? token value Overrides the product image
showOrderSummary bool true Show or hide the order summary card
submitButtonText String? Custom button label
returnUrl String? URL the gateway returns to after payment
autoPresent bool false Open Stripe's PaymentSheet without a tap
appearance KhaimeAppearance defaults Colour and shape overrides
invalidTokenBuilder WidgetBuilder? error text UI shown for an undecodable token

PaymentResult

Field Type Description
success bool Whether the payment succeeded
gateway PaymentGateway The gateway that processed it
paymentIntentId String? Stripe PaymentIntent id
reference String? Paystack transaction reference
error PaymentError? Failure detail when unsuccessful

KhaimeAppearance

KhaimeCheckout(
  token: token,
  appearance: const KhaimeAppearance(
    primaryColor: Color(0xFF6C2BD9),
    surfaceColor: Color(0xFFF4F4F5),
    borderRadius: 12,
    buttonBorderRadius: 999,
  ),
)

Unset colours fall back to the active gateway's brand colour.

Advanced use

The gateway widgets are exported for callers that decode the token themselves:

final data = decodePaymentToken(token);
if (data?.paymentGateway == PaymentGateway.paystack) {
  return PaystackPayment(data: data!, onSuccess: handle);
}

decodePaymentToken only decodes the payload and checks exp. The signature is verified server-side by the Khaime API, so a token that decodes locally is not proof of anything — always confirm payments from your backend.

Verifying payments

Client callbacks tell you what the payer saw, not what settled. Treat onSuccess as a UI signal and confirm the payment from your backend against the Khaime webhook before fulfilling an order. For redirect gateways the webhook is the only signal you get.

License

MIT

For AI agents

llms.txt in this repository is a condensed integration reference: the rules that prevent the common mistakes, the platform setup, the error-to-fix table, and the backend calls that produce a token — including where the published docs are wrong.

Libraries

khaime_flutter
Khaime Checkout SDK for Flutter.