bceao_pispi_app 0.0.5 copy "bceao_pispi_app: ^0.0.5" to clipboard
bceao_pispi_app: ^0.0.5 copied to clipboard

SDK Flutter permettant d'intégrer PI-SPI (transactions, demande d'annulation, retour de fond, demande de paiement, QR Code, gestion des alias, gestion des abonnements, notifications, profil, compte) d [...]

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:bceao_pispi_app/bceao_pispi_app.dart';

/// Application hôte factice : simule une app participant déjà authentifiée,
/// qui pousse l'écran du SDK PI-SPI par-dessus la sienne.
///
/// **L'essentiel à lire pour intégrer le SDK dans une vraie app est la
/// méthode [FakeHostHomePage._openPiSpiSdk] ci-dessous** : c'est le seul
/// point d'entrée du SDK, tout le reste de ce fichier (widgets `_App*`,
/// `_StepUpAuthPage`, `_PinKeypad`) n'est que du décor pour cette démo.
///
/// Tourne en `demoMode: true` par défaut (aucune requête réelle, données
/// mockées depuis `assets/json/`) pour pouvoir visualiser le SDK sans
/// backend. Repassez `demoMode` à `false` et fournissez une vraie
/// `apiBaseUrl` + un vrai token pour tester contre un backend PI-SPI réel.
void main() {
  runApp(const FakeHostApp());
}

class FakeHostApp extends StatelessWidget {
  const FakeHostApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Fake Host App',
      debugShowCheckedModeBanner: false,
      // Le SDK a besoin de son propre delegate de traductions pour que
      // les textes de ses écrans s'affichent (fr/en/pt).
      localizationsDelegates: const [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
        AppLocalizations.delegate,
      ],
      supportedLocales: AppLocalizations.supportedLocales,
      home: const FakeHostHomePage(),
    );
  }
}

/// Accueil de l'app hôte factice : une simple grille de trois scénarios de
/// démo, chacun ouvrant le SDK dans une configuration différente. Chaque
/// tuile de la grille ouvre en plus le SDK avec son propre thème de
/// démarrage (voir [_AppTileSpec.theme] et [PiSpiAppConfig.defaultTheme]).
///
/// - **Compte unique** → [_openPiSpiSdk] avec un seul [PiSpiCompte] : le SDK
///   saute l'écran de sélection de compte et le carrousel de l'accueil.
/// - **Comptes multiples** → [_openPiSpiSdk] avec plusieurs [PiSpiCompte] :
///   écran de sélection à l'ouverture, puis carrousel sur l'accueil du SDK.
/// - **Simuler un push** → [_openPiSpiSdkAndSimulatePush] : ouvre le SDK
///   normalement, puis simule 3s plus tard la réception d'un message push
///   (voir [_simulateIncomingPush] et `PiSpiPush.handleIncoming`).
class FakeHostHomePage extends StatelessWidget {
  const FakeHostHomePage({super.key});

  // Mode démo : aucune requête réelle n'est faite via l'API mockée
  // (MockInterceptor), donc ni l'URL ni le token n'ont besoin d'être
  // valides. Passez `demoMode: false` plus bas pour tester contre un vrai
  // backend PI-SPI : il faudra alors renseigner ici une vraie apiBaseUrl et
  // fournir un vrai token d'accès (voir accessToken dans _openPiSpiSdk).
  static const _apiBaseUrl = 'https://sandbox.pi-bceao.com/api';
  static const _fakeAccessToken = 'DEMO_MODE_TOKEN';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('App PSP')),
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(20),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // Chaque tuile de cette section ouvre le SDK avec un seul
              // compte (pas de carrousel, pas d'écran de sélection) et son
              // propre thème de démarrage (voir _AppTileSpec.theme et
              // PiSpiAppConfig.defaultTheme).
              _AppSection(
                title: 'Le client a un seul compte',
                onTapTile: (theme) => _openPiSpiSdk(context, theme: theme),
                tiles: const [
                  _AppTileSpec(
                    'assets/pi_marron.png',
                    Color(0xFFF5F0E8),
                  ),
                  _AppTileSpec(
                    'assets/pi_jaune.png',
                    Color(0xFF151413),
                    theme: Themes.dark,
                  ),
                  _AppTileSpec(
                    'assets/pi_jaune.png',
                    Color(0xFFF5F0E8),
                    theme: Themes.yellow,
                  ),  
                  _AppTileSpec(
                    'assets/pi_marron.png',
                    Color(0xFF2E7D32),
                    theme: Themes.green,
                  ),
                  _AppTileSpec(
                    'assets/pi_jaune.png',
                    Color(0xFF1565C0),
                    theme: Themes.blue,
                  ),
                  _AppTileSpec(
                    'assets/pi_marron.png',
                    Color(0xFFC62828),
                    theme: Themes.red,
                  ),
                ],
              ),
              const SizedBox(height: 28),
              // Chaque tuile de cette section ouvre le SDK avec plusieurs
              // comptes (carrousel + écran de sélection à l'ouverture) et
              // son propre thème de démarrage.
              _AppSection(
                title: 'Le client a plusieurs comptes',
                onTapTile: (theme) => _openPiSpiSdk(
                  context,
                  multiCompte: true,
                  theme: theme,
                ),
                tiles: const [
                  _AppTileSpec(
                    'assets/logo_maron_jaune.png',
                    Color(0xFFF5F0E8),
                    wide: true,
                  ),
                  _AppTileSpec(
                    'assets/logo_blanc.png',
                    Color(0xFF151413),
                    theme: Themes.dark,
                    wide: true,
                  ),
                  _AppTileSpec(
                    'assets/logo_maron_jaune.png',
                    Color(0xFFF5F0E8),
                    theme: Themes.light,
                    wide: true,
                  ),
                  _AppTileSpec(
                    'assets/logo_jaune_maron.png',
                    Color(0xFFF5F0E8),
                    theme: Themes.yellow,
                    wide: true,
                  ),
                  _AppTileSpec(
                    'assets/logo_maron_blanc.png',
                    Color(0xFF2E7D32),
                    theme: Themes.green,
                    wide: true,
                  ),
                  _AppTileSpec(
                    'assets/logo_jaune_blanc.png',
                    Color(0xFF1565C0),
                    theme: Themes.blue,
                    wide: true,
                  ),
                  _AppTileSpec(
                    'assets/logo_blanc.png',
                    Color(0xFFC62828),
                    theme: Themes.red,
                    wide: true,
                  ),
                ],
              ),
              const SizedBox(height: 28),
              // Chaque tuile de cette section ouvre le SDK (avec son propre
              // thème de démarrage) puis simule, trois secondes plus tard,
              // la réception d'un push (voir PiSpiPush.handleIncoming).
              _AppSection(
                title: 'Simuler un push (transfert reçu)',
                onTapTile: (theme) =>
                    _openPiSpiSdkAndSimulatePush(context, theme: theme),
                tiles: const [
                  _AppTileSpec(
                    'assets/logo_jaune_blanc.png',
                    Color(0xFF1565C0),
                    theme: Themes.blue,
                    wide: true,
                  ),
                  _AppTileSpec(
                    'assets/logo_blanc.png',
                    Color(0xFFC62828),
                    theme: Themes.red,
                    wide: true,
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }

  /// Point d'entrée du SDK : c'est cette fonction (et uniquement elle) que
  /// vous devez adapter pour l'intégrer dans une vraie app.
  ///
  /// Elle fait deux choses :
  /// 1. Construit un [PiSpiSdkConfig] à partir des informations que l'app
  ///    hôte possède déjà à ce stade (l'utilisateur est censé être déjà
  ///    connecté chez le participant *avant* d'ouvrir le SDK — le SDK ne
  ///    gère lui-même aucune authentification, voir le README). Ici, tout
  ///    est codé en dur pour la démo ; dans une vraie app, remplacez ces
  ///    valeurs par celles de votre session utilisateur/backend réels
  ///    (profil du client, comptes bancaires, token d'accès en cours...).
  /// 2. Pousse [PiSpiSdkEntry] comme une route normale de l'app hôte — le
  ///    SDK n'a pas son propre `MaterialApp`, il s'affiche simplement
  ///    par-dessus celui de l'hôte.
  ///
  /// [multiCompte] et [theme] ne servent qu'à faire varier la démo (voir
  /// [FakeHostHomePage]) : [multiCompte] bascule entre un
  /// [PiSpiUserIdentity.comptes] à un seul élément ou à plusieurs, pour
  /// illustrer les deux comportements du SDK (voir la section « Comptes
  /// multiples » du README) ; [theme] fixe le thème de démarrage passé à
  /// [PiSpiAppConfig.defaultTheme], différent pour chaque tuile de
  /// l'accueil (voir [_AppTileSpec.theme]).
  void _openPiSpiSdk(
    BuildContext context, {
    bool multiCompte = false,
    Themes? theme,
  }) {
    final config = PiSpiSdkConfig(
      // Identité du client déjà connecté côté hôte : voir le tableau
      // PiSpiUserIdentity du README pour le détail de chaque champ. Dans une
      // vraie app, ces valeurs viennent de votre propre backend/session, pas
      // de constantes en dur comme ici.
      user: PiSpiUserIdentity(
        firstName: 'Jean',
        lastName: 'Doe',
        fullName: 'Jean Doe',
        categorie: 'P',
        nationalite: 'SN',
        paysResidence: 'CI',
        genre: '1',
        // Pays de domiciliation du compte : doit être un code pays UEMOA
        // (voir PiSpiUserIdentity.supportedPaysCodes).
        pays: 'SN',
        // comptes ne doit jamais être null ni vide. Avec un seul élément
        // (cas "compte unique"), le SDK saute l'écran de sélection et le
        // carrousel de l'accueil et utilise ce compte directement.
        comptes: multiCompte
            ? const [
                PiSpiCompte(
                  numero: 'SNB0000000000000000001',
                  libelle: 'Courant',
                  solde: 9240320,
                ),
                PiSpiCompte(
                  numero: 'SNB0000000000000000002',
                  libelle: 'Chèque',
                  solde: 240000,
                ),
                PiSpiCompte(
                  numero: 'SNB0000000000000000003',
                  libelle: 'Épargne',
                  solde: 32240320,
                ),
              ]
            : const [
                PiSpiCompte(
                  numero: 'SNB0000000000000000001',
                  libelle: 'Courant',
                  solde: 9240320,
                ),
              ],
      ),
      // Connexion au backend PI-SPI : URL, authentification, mode démo.
      api: PiSpiApiConfig(
        apiBaseUrl: _apiBaseUrl,
        // Token d'accès (Bearer) valide au moment de l'ouverture du SDK :
        // c'est lui que le SDK ajoute à chaque requête HTTP faite vers
        // [apiBaseUrl] (voir l'openapi du SDK, section Authentification).
        // C'est le même token que celui utilisé par l'app hôte elle-même
        // pour s'authentifier auprès de son propre backend — le SDK n'en
        // obtient jamais un lui-même, il ne fait que le transmettre.
        accessToken: _fakeAccessToken,
        // Appelé si une requête échoue en 401 ou 403 et que le sdk détecte
        // que accessToken a expiré : l'hôte rafraîchirait normalement son
        // token ici et le retournerait. En démo, on renvoie toujours le
        // même token factice (jamais rejeté par le mock).
        onAccessTokenExpired: () async => _fakeAccessToken,
        // Si le SDK ne parvient plus à obtenir de token valide (401
        // persistant), on referme son écran et on revient à l'accueil.
        onSessionExpired: () async {
          final navigator = Navigator.of(context);
          if (navigator.canPop()) navigator.pop();
          if (context.mounted) {
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(content: Text('Session PI-SPI expirée')),
            );
          }
        },
        // Mode démo : aucune requête réelle n'est faite via l'API mockée
        // Si demoMode est definie à true tout ce qui est appelle api dans le sdk sera mocké
        // parconte s'il est definie à false les appelles api iront jusqu'au backend
        demoMode: true,
      ),
      // Identité et préférences de l'application hôte : participant PSP,
      // langue, thème.
      appConfig: PiSpiAppConfig(
        // Code membre PSP du participant auprès de la BCEAO — le même pour
        // tous les utilisateurs de cette app, à récupérer auprès de la
        // BCEAO lors de votre intégration (pas une donnée utilisateur).
        pspCodeMembre: 'SNB000',
        // Le SDK n'a pas son propre sélecteur de langue : c'est l'hôte qui
        // décide (et qui gère son propre paramétrage de langue, s'il en a
        // un).
        // le français est utilisé par defaut
        defaultLocale: const Locale('fr'),
        // Optionnel : appliqué et persisté comme thème par défaut à chaque
        // ouverture (écrase même un choix fait par l'utilisateur dans les
        // paramètres du SDK lors d'une session précédente). Ici, chaque
        // tuile de l'accueil factice appelle _openPiSpiSdk avec un thème
        // différent (voir _AppTileSpec.theme), pour illustrer ce
        // comportement.
        defaultTheme: theme,
      ),
      // Politique de step-up avant chaque action sensible.
      identification: PiSpiIdentificationConfig(
        confirmationMethodLabel: 'external',
        // Appelé par le SDK avant chaque action sensible (confirmation
        // d'une transaction, suppression d'un alias/compte). Dans une
        // vraie app, remplacez ce dialogue factice par un vrai écran
        // PIN/biométrie (via `local_auth` par exemple) : le SDK ne voit
        // jamais le secret, seulement le booléen retourné ici.
        requireStepUpIdentification: () async {
          if (!context.mounted) return false;
          final confirmed = await Navigator.of(context).push<bool>(
            MaterialPageRoute(
              fullscreenDialog: true,
              builder: (_) => const _StepUpAuthPage(),
            ),
          );
          return confirmed ?? false;
        },
      ),
      // Journalisation et remontée d'erreurs Flutter.
      log: PiSpiLogConfig(
        logLevel: "debug",
        // Appelé pour chaque erreur Flutter interceptée pendant que le SDK
        // est affiché. Dans une vraie app, transmettez `details` à votre
        // propre outil de crash reporting (Crashlytics, Sentry...) ; ici on
        // se contente de l'afficher dans les logs de la console.
        onFlutterError: (details) {
          debugPrint(
            '[Host] Erreur Flutter remontée par le SDK : '
            '${details.exceptionAsString()}',
          );
        },
      ),
      // Fermeture volontaire (bouton retour/fermeture, compte clôturé...) :
      // contrairement à onSessionExpired, ce n'est pas une erreur, donc pas
      // de message "session expirée" ici — on referme juste l'écran.
      onClose: () async {
        final navigator = Navigator.of(context);
        if (navigator.canPop()) navigator.pop();
      },
    );

    Navigator.of(context).push(MaterialPageRoute(
      builder: (_) => PiSpiSdkEntry(config: config),
    ));
  }
  
  void _openPiSpiSdkAndSimulatePush(
    BuildContext context, {
    Themes? theme ,
  }) async {
    // ouvrir d'abord le sdk
    _openPiSpiSdk(context, theme: theme);
    // attendre trois secondes (le temps que l'écran du SDK soit affiché)
    // avant de simuler la réception du push
    await Future.delayed(const Duration(seconds: 3));
    if (context.mounted) _simulateIncomingPush(context);
  }

  /// Illustre PiSpiPush.handleIncoming : dans une vraie app, ce serait
  /// appelé depuis le handler de message push de l'hôte (ex.
  /// FirebaseMessaging.onMessage). Si l'écran du SDK est ouvert, ça
  /// déclenche le même toast/rafraîchissement qu'un vrai push.
  void _simulateIncomingPush(BuildContext context) {
    PiSpiPush.handleIncoming(
      {
        'idObject': 'ESNB00120240105153554CancelpNvhUk9e',
        'type': 'TRANSFERT_RECU',
        'montant': '1500'
      },
      title: 'Transfert reçu',
      body: 'Vous avez reçu 1 500 F CFA de Aïda Diop',
    );
  }
}

/// Description d'une tuile de la grille : image + couleur de fond, en
/// version carrée (icône seule) ou large (logo complet avec wordmark), et
/// le thème avec lequel cette tuile ouvre le SDK (voir
/// [PiSpiAppConfig.defaultTheme]).
class _AppTileSpec {
  const _AppTileSpec(
    this.asset,
    this.background,{
    this.wide = false,
    this.theme
  });

  final String asset;
  final Color background;
  final Themes? theme;
  final bool wide;
}

/// Section titrée de la grille (ex. "Compte unique") : toutes ses tuiles
/// déclenchent la même action au tap, mais chacune avec son propre thème
/// (voir [_AppTileSpec.theme]).
class _AppSection extends StatelessWidget {
  const _AppSection({
    required this.title,
    required this.tiles,
    required this.onTapTile,
  });

  final String title;
  final List<_AppTileSpec> tiles;
  final void Function(Themes? theme) onTapTile;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(title, style: Theme.of(context).textTheme.titleMedium),
        const SizedBox(height: 12),
        Wrap(
          spacing: 14,
          runSpacing: 14,
          children: [
            for (final tile in tiles)
              _AppIconTile(
                spec: tile,
                onTap: () => onTapTile(tile.theme),
              ),
          ],
        ),
      ],
    );
  }
}

/// Une tuile façon icône d'app : fond coloré arrondi + image centrée,
/// libellé "PI-SPI" en dessous.
class _AppIconTile extends StatelessWidget {
  const _AppIconTile({required this.spec, required this.onTap});

  final _AppTileSpec spec;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    final double width = spec.wide ? 140 : 72;
    const double height = 72;

    return InkWell(
      borderRadius: BorderRadius.circular(18),
      onTap: onTap,
      child: Container(
        width: width,
        height: height,
        padding: EdgeInsets.symmetric(
          horizontal: spec.wide ? 12 : 14,
          vertical: spec.wide ? 20 : 14,
        ),
        decoration: BoxDecoration(
          color: spec.background,
          borderRadius: BorderRadius.circular(18),
        ),
        child: Image.asset(spec.asset, fit: BoxFit.contain),
      ),
    );
  }
}


/// Page d'authentification factice affichée avant une action sensible.
/// Simule un scan biométrique (auto-validé après une courte animation)
/// avec un repli sur code PIN. Dans une vraie app, remplacez ce widget par
/// un vrai flux `local_auth` / saisie de code sécurisée : le SDK ne voit
/// jamais le secret, seulement le booléen retourné par cette page.
class _StepUpAuthPage extends StatefulWidget {
  const _StepUpAuthPage();

  @override
  State<_StepUpAuthPage> createState() => _StepUpAuthPageState();
}

class _StepUpAuthPageState extends State<_StepUpAuthPage>
    with SingleTickerProviderStateMixin {
  static const int _pinLength = 4;

  bool _showPinFallback = false;
  bool _scanning = false;
  final List<int> _pin = [];

  void _startBiometricScan() async {
    setState(() => _scanning = true);
    // Simule le temps d'un scan biométrique réel.
    await Future.delayed(const Duration(milliseconds: 1400));
    if (mounted) Navigator.of(context).pop(true);
  }

  void _onDigitPressed(int digit) {
    if (_pin.length >= _pinLength) return;
    setState(() => _pin.add(digit));
    if (_pin.length == _pinLength) {
      // Simulation : n'importe quel code à 4 chiffres est accepté.
      Future.delayed(const Duration(milliseconds: 200), () {
        if (mounted) Navigator.of(context).pop(true);
      });
    }
  }

  void _onBackspace() {
    if (_pin.isEmpty) return;
    setState(() => _pin.removeLast());
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return Scaffold(
      backgroundColor: theme.colorScheme.surface,
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 24),
          child: Column(
            children: [
              Align(
                alignment: Alignment.topLeft,
                child: IconButton(
                  icon: const Icon(Icons.close),
                  onPressed: () => Navigator.of(context).pop(false),
                ),
              ),
              const Spacer(),
              Text(
                'Vérification requise',
                style: theme.textTheme.headlineSmall
                    ?.copyWith(fontWeight: FontWeight.w600),
              ),
              const SizedBox(height: 8),
              Text(
                _showPinFallback
                    ? 'Saisissez votre code PIN'
                    : 'Confirmez avec votre empreinte ou Face ID',
                style: theme.textTheme.bodyMedium
                    ?.copyWith(color: theme.colorScheme.outline),
                textAlign: TextAlign.center,
              ),
              const SizedBox(height: 40),
              if (!_showPinFallback) ...[
                GestureDetector(
                  onTap: _scanning ? null : _startBiometricScan,
                  child: AnimatedContainer(
                    duration: const Duration(milliseconds: 300),
                    width: 120,
                    height: 120,
                    decoration: BoxDecoration(
                      shape: BoxShape.circle,
                      color: _scanning
                          ? theme.colorScheme.primary.withOpacity(0.15)
                          : theme.colorScheme.primaryContainer,
                      border: Border.all(
                        color: theme.colorScheme.primary,
                        width: 2,
                      ),
                    ),
                    child: _scanning
                        ? const Padding(
                            padding: EdgeInsets.all(28),
                            child: CircularProgressIndicator(strokeWidth: 3),
                          )
                        : Icon(
                            Icons.fingerprint,
                            size: 64,
                            color: theme.colorScheme.primary,
                          ),
                  ),
                ),
                const SizedBox(height: 24),
                Text(
                  _scanning ? 'Scan en cours…' : 'Touchez pour scanner',
                  style: theme.textTheme.bodySmall,
                ),
                const SizedBox(height: 32),
                TextButton(
                  onPressed: () => setState(() => _showPinFallback = true),
                  child: const Text('Utiliser le code PIN à la place'),
                ),
              ] else ...[
                // Indicateurs de saisie du code PIN.
                Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: List.generate(_pinLength, (i) {
                    final filled = i < _pin.length;
                    return Container(
                      margin: const EdgeInsets.symmetric(horizontal: 8),
                      width: 16,
                      height: 16,
                      decoration: BoxDecoration(
                        shape: BoxShape.circle,
                        color: filled
                            ? theme.colorScheme.primary
                            : theme.colorScheme.outlineVariant,
                      ),
                    );
                  }),
                ),
                const SizedBox(height: 32),
                _PinKeypad(
                  onDigit: _onDigitPressed,
                  onBackspace: _onBackspace,
                ),
              ],
              const Spacer(flex: 2),
            ],
          ),
        ),
      ),
    );
  }
}

/// Clavier numérique simple pour la saisie du code PIN factice.
class _PinKeypad extends StatelessWidget {
  const _PinKeypad({required this.onDigit, required this.onBackspace});

  final ValueChanged<int> onDigit;
  final VoidCallback onBackspace;

  @override
  Widget build(BuildContext context) {
    Widget buildKey(Widget child, VoidCallback? onTap) {
      return InkWell(
        borderRadius: BorderRadius.circular(36),
        onTap: onTap,
        child: SizedBox(
          width: 72,
          height: 72,
          child: Center(child: child),
        ),
      );
    }

    final rows = <List<Widget>>[
      for (var r = 0; r < 3; r++)
        [
          for (var c = 1; c <= 3; c++)
            buildKey(
              Text('${r * 3 + c}', style: const TextStyle(fontSize: 22)),
              () => onDigit(r * 3 + c),
            ),
        ],
      [
        const SizedBox(width: 72, height: 72),
        buildKey(const Text('0', style: TextStyle(fontSize: 22)),
            () => onDigit(0)),
        buildKey(const Icon(Icons.backspace_outlined), onBackspace),
      ],
    ];

    return Column(
      children: [
        for (final row in rows)
          Row(mainAxisAlignment: MainAxisAlignment.center, children: row),
      ],
    );
  }
}
0
likes
120
points
--
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

SDK Flutter permettant d'intégrer PI-SPI (transactions, demande d'annulation, retour de fond, demande de paiement, QR Code, gestion des alias, gestion des abonnements, notifications, profil, compte) dans une application mobile existante.

Repository (GitHub)

License

unknown (license)

Dependencies

audioplayers, calendar_date_picker2, dio, emoji_picker_flutter, encrypt, file_picker, flutter, flutter_bloc, flutter_client_sse, flutter_contacts, flutter_localizations, flutter_secure_storage, flutter_svg, geolocator, go_router, google_mlkit_barcode_scanning, hive_flutter, image_picker, intl, json_annotation, logger, mobile_scanner, path_provider, pdf, permission_handler, printing, qr, share_plus, shared_preferences, url_launcher, video_player, webview_flutter

More

Packages that depend on bceao_pispi_app