Hosted Checkout in Flutter
The malipopay_checkout package opens your hosted checkout full screen inside your Flutter app (card and mobile money) and returns how it ended.
Card numbers go straight from the payer to the bank's hosted fields. They never pass through your app or your servers, so your app stays out of PCI scope. The bank's 3-D Secure challenge opens inside the same screen and comes back to the checkout when it is done.
Before you start
-
In the Malipopay dashboard open Settings > Checkout, create a checkout and copy its id. See Create a checkout.
-
If the checkout only accepts signed links, sign them on your server. See Signed links.
-
Android: your app needs the internet permission in
android/app/src/main/AndroidManifest.xml:<uses-permission android:name="android.permission.INTERNET"/>
Install
dependencies:
malipopay_checkout: ^0.1.0
Open the checkout
import 'package:malipopay_checkout/malipopay_checkout.dart';
final result = await MalipopayCheckout.open(
context,
integration: 'YOUR_CHECKOUT_ID',
amount: 25000,
order: 'ORDER-42',
);
switch (result.status) {
case CheckoutStatus.successful:
// Confirm on your server before you deliver (see below).
break;
case CheckoutStatus.cancelled:
// The payer closed the checkout or went back.
break;
default:
// pending, partial or failed: check on your server.
break;
}
result.reference is the Malipopay payment reference and result.receiptUrl the receipt when it was paid.
Always confirm on your server
The result is what the payer's phone saw. Before you deliver, confirm the payment on your server with the payment.confirmed webhook or GET /api/v1/payment/verify/{reference}. The webhook carries your order as customerReference.
One order, one payment
Pass your own order id as order. Opening the checkout again for the same order continues the same payment instead of charging twice. Opening it for the same order with a different amount is refused.
Options
| Argument | What it does |
|---|---|
integration | Your checkout id (required). |
amount | What to charge, in TZS. Leave it out to let the payer enter it, if your checkout allows that. A checkout with a fixed amount ignores it. |
order | Your order id. |
description | A line shown under the amount. |
payer | CheckoutPayer(name:, phone:, email:), so the page can skip asking. |
language | 'en' or 'sw'. The payer can switch on the page. |
signed | SignedLink(sig:, exp:) from your server. |
returnUrl | Only used on a signed link, and only if it is on one of your checkout's website addresses. |
environment | CheckoutEnvironment.production (default) or CheckoutEnvironment.uat. |
baseUrl | Override the checkout host (for example a proxy). |
title | The app bar title. Payment by default. |
To place the checkout inside your own layout instead of a full-screen route, use MalipopayCheckoutView(uri: checkoutUri(...), onResult: ...).
Signed links
When Only accept signed links is on for your checkout, the amount and order cannot be changed on the phone. Your server computes the signature exactly as described in Signed links:
HMAC-SHA256(signingSecret, "{checkoutId}|{amount}|{currency}|{order}|{returnUrl}|{exp}")
Empty values stay empty between the bars, and exp is a Unix time in seconds. Send sig and exp to the app and pass them as signed: SignedLink(sig: sig, exp: exp).
Anyone can read a secret out of an app bundle. Sign on your server and send only sig and exp to the app.
How it works
The package opens https://app.malipopay.co.tz/c/{checkoutId}/embed (or the UAT address) in a WebView and listens on a JavaScript channel. It believes the page's messages only while the WebView is on the checkout's own address, so no other page, such as the bank's 3-D Secure page, can end the checkout.
Testing
Use environment: CheckoutEnvironment.uat with a checkout created on the UAT dashboard (demo.malipopay.co.tz). Card payments there use the test cards in Testing Card Payments, and no money moves.