Duticotac Flutter
Ready-made payment screens for Duticotac: mobile money in Côte d'Ivoire (Orange Money, MTN, Moov, Wave) and in-app purchases through the App Store / Google Play.
The package shows the method choice, the operator list and the phone number
form, sends the cashout, waits for the transaction to settle, and returns an
ApiResponseModel<TransactionModel>. It re-exports the whole
duticotac_api_flutter API and PhoneNumber, so one import is enough.
Features
- Mobile money: Orange Money CI, MTN CI, Moov CI and Wave CI. The phone
number is checked against the operator's prefix (07, 05, 01), Orange Money
asks for the code obtained with
#144*82#, and Wave opens its payment page in the Wave app or browser. - In-app purchase: starts a consumable purchase with
in_app_purchase. - Offers (
payAnOffer): loads an offer configured in the AppLite dashboard and uses its price and payment methods. - Direct payment (
payWithDuticotac): any amount, with the methods you pick.
Installation
dependencies:
duticotac_flutter: ^0.3.0
flutter_easyloading: ^4.0.2 # the loading and error toasts need EasyLoading.init()
hive_ce_flutter: ^2.3.4 # for Hive.initFlutter()
Requires Dart ^3.13.0 and Flutter >=3.47.0.
Setup
Initialise Hive and the Duticotac adapters, open a box for the offer cache, and
install EasyLoading in your MaterialApp: the package reports progress and
errors through it, and nothing shows without EasyLoading.init().
import 'package:duticotac_flutter/duticotac_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:hive_ce_flutter/hive_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
await initDuticotacHiveAdapters();
await Hive.openBox('duticotac_imp_db');
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
builder: EasyLoading.init(),
home: const HomeScreen(),
);
}
}
Pay an offer
payAnOffer loads the offer reference (from the Hive box when it is cached,
from the API otherwise), then proposes the payment methods activated in the
app's Duticotac settings:
- only mobile money → the operator list opens directly;
- only in-app purchase and/or credit card → that flow starts directly;
- both → a sheet asks the customer to choose.
final ApiResponseModel<TransactionModel> result = await payAnOffer(
context,
apiKey: 'YOUR_API_KEY',
reference: 'OFFER_REFERENCE',
localDB: Hive.box('duticotac_imp_db'),
// A unique id per payment attempt, e.g. Uuid().v4().
getTransactionId: () async => uuid.v4(),
// Optional
initialPhoneNumber: PhoneNumber.parse('+2250707070707'),
name: 'Client Name',
email: 'client@example.com',
kolaboReference: 'KOLABO_REF',
onTransactionCompleted: (transactionId) {
// Called after a successful mobile money payment.
},
);
if (result.success) {
// result.data is the settled transaction (null for an in-app purchase).
} else {
// result.error explains why, see "Results and errors".
}
If the offer has no price (price == 0), pass amount, otherwise the call
fails with "Cette offre n'a pas de prix : indiquez un montant".
title and description change the text of the method choice sheet
(default: "Payer
Direct payment
payWithDuticotac charges amount for productReference, with the methods
listed in selectedProviders (by default the four mobile money operators and
the credit card).
final result = await payWithDuticotac(
context,
apiKey: 'YOUR_API_KEY',
amount: 1000,
productReference: 'YOUR_PRODUCT_REF',
getTransactionId: () async => uuid.v4(),
// Optional
title: 'Payer la commande',
description: 'Choisissez un moyen de paiement',
name: 'Client Name',
email: 'client@example.com',
initialPhoneNumber: PhoneNumber.parse('+2250707070707'),
kolaboReference: 'KOLABO_REF',
selectedProviders: [
PaymentProvider.orangeMoneyCI,
PaymentProvider.mtnCI,
PaymentProvider.waveCI,
],
);
The return type is Future<dynamic>: an ApiResponseModel<TransactionModel>
once a method has run, or null when the customer closes the method choice
sheet. payWithDuticotac has no onTransactionCompleted parameter; read the
returned transaction instead.
To sell through the stores, include PaymentProvider.creditCard and pass
isInAppPurchase: true: the "Carte de Crédit" choice then starts an in-app
purchase of the store product productReference.
How the mobile money flow works
- The customer picks an operator, then enters an Ivorian number from that
operator (Wave accepts 07, 05 and 01).
initialPhoneNumberpre-fills the field when it belongs to the operator; otherwise the operator prefix is shown. - For Orange Money, the customer also enters the 4-digit code received after
dialling
#144*82#. - On "PAYER",
getTransactionIdis called, then the cashout is sent. For Wave, the returnedpaymentUrlopens in the Wave app (or browser) throughurl_launcher. - The screen waits for the transaction status. On success, a toast confirms
the payment,
onTransactionCompletedreceives the id returned bygetTransactionId, and the screen closes with the result. Cancelled, failed or timed-out payments close the screen withsuccess: false.
When name or email is omitted, placeholder values are sent to the API.
In-app purchase and credit card
- In-app purchase runs for an offer whose settings include
IAP, or withpayWithDuticotac(isInAppPurchase: true). The offer reference /productReferencemust be the store product id. The package only starts the purchase withbuyConsumable: it resolves withsuccess: trueanddata: nullas soon as the store accepts the request. Listen toInAppPurchase.instance.purchaseStreamin your app to verify, deliver andcompletePurchase. - Credit card outside in-app purchase is not implemented yet: the
"Carte de Crédit" choice opens a placeholder screen whose button does
nothing. The "Espèces" (cash) choice, shown when
PaymentProvider.cashis selected, does nothing either.
Results and errors
Every flow resolves to an ApiResponseModel<TransactionModel> with success,
data and error (except the null case of payWithDuticotac above). The
package does not throw for a payment failure; it shows an EasyLoading error
toast and returns success: false.
| Situation | error |
|---|---|
| A sheet is closed, or the mobile money screen is left, without paying | show-modal-bottom-sheet-error |
| The transaction is cancelled, fails or times out | the code from duticotac_api_flutter: payment-cancelled, payment-failed, polling-timeout |
payAnOffer or the in-app purchase fails with a known error |
its French message, e.g. "Produit non trouvé" |
payAnOffer or the in-app purchase fails otherwise (network…) |
unknown-error |
On the mobile money screen, a missing number, a missing Orange Money code or a rejected cashout keeps the screen open and shows the message as a toast, so the customer can correct it and retry.
Platform setup
- Android: the
INTERNETpermission. - In-app purchase: create the products in App Store Connect and the Google Play Console, see in_app_purchase.
- Wave: the payment page opens with url_launcher in external mode.
Dependencies note
phone_form_field stays on ^10.0.18. Version 11 builds its fields with the
standalone material_ui package; those widgets don't recognise the Material
ancestor provided by package:flutter/material.dart, which this package and
apps created with Flutter 3.47 use, and the phone field crashes with "No
Material widget found".
Contributing
flutter pub get
flutter analyze
flutter test
Tests never reach the network: API calls get the 400 that flutter_test answers
to every request, and provider logos are served a blank image
(test/support/fake_image_http.dart).