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.1
  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

  1. The customer picks an operator, then enters an Ivorian number from that operator (Wave accepts 07, 05 and 01). initialPhoneNumber pre-fills the field when it belongs to the operator; otherwise the operator prefix is shown.
  2. For Orange Money, the customer also enters the 4-digit code received after dialling #144*82#.
  3. On "PAYER", getTransactionId is called, then the cashout is sent. For Wave, the returned paymentUrl opens in the Wave app (or browser) through url_launcher.
  4. The screen waits for the transaction status. On success, a toast confirms the payment, onTransactionCompleted receives the id returned by getTransactionId, and the screen closes with the result. Cancelled, failed or timed-out payments close the screen with success: 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 with payWithDuticotac(isInAppPurchase: true). The offer reference / productReference must be the store product id. The package only starts the purchase with buyConsumable: it resolves with success: true and data: null as soon as the store accepts the request. Listen to InAppPurchase.instance.purchaseStream in your app to verify, deliver and completePurchase.
  • 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.cash is 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 INTERNET permission.
  • 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).

Libraries

duticotac_flutter