bceao_pispi_app 0.0.3 copy "bceao_pispi_app: ^0.0.3" to clipboard
bceao_pispi_app: ^0.0.3 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
0
points
0
downloads

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)
View/report issues

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