duticotac_flutter 0.3.1 copy "duticotac_flutter: ^0.3.1" to clipboard
duticotac_flutter: ^0.3.1 copied to clipboard

Payment using Duticotac.

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