welloo_sdk 0.0.118 copy "welloo_sdk: ^0.0.118" to clipboard
welloo_sdk: ^0.0.118 copied to clipboard

Package de transaction Welloo

example/lib/main.dart

import 'dart:async';

import 'package:example/auth_service.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:welloo_sdk/welloo_sdk.dart';

// 1. DÉCLARATION GLOBALE DE LA CLÉ DE NAVIGATION
// Cela permet d'accéder au navigateur depuis n'importe où (même hors du contexte)
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 2. INITIALISATION DU SDK (une seule fois, au démarrage).
  // Choisit l'environnement ; AuthService suivra `WellooSdk.config.baseUrl`.
  WellooSdk.initialize(WellooConfig.sandbox());

  runApp(const MainApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Welloo SDK - Nouveau Système',
      // 3. INJECTION DE LA CLÉ DANS MATERIAL APP
      navigatorKey: navigatorKey,
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorSchemeSeed: Colors.blue,
        useMaterial3: true,
      ),
      home: const DemoWello(),
    );
  }
}

class DemoWello extends StatefulWidget {
  const DemoWello({super.key});

  @override
  State<DemoWello> createState() => _DemoWelloState();
}

class _DemoWelloState extends State<DemoWello> {
  bool _isLoggedIn = false;
  bool _isLoading = false; // Pour afficher un spinner
  bool _obscurePin = true; // Masque le code PIN par défaut
  final TextEditingController _phoneController = TextEditingController();
  final TextEditingController _pineController = TextEditingController();
  final AuthService _authService = AuthService();

  // Getters pour sécuriser l'accès aux variables d'env
  String _accessToken = "";
  String _refreshToken = '';

  StreamSubscription<Uri>? _linkSubscription;

  // Focus pour animer les champs à la sélection
  final FocusNode _phoneFocus = FocusNode();
  final FocusNode _pinFocus = FocusNode();

  @override
  void initState() {
    super.initState();

    _phoneController.text = "0707860944";
    _pineController.text = "1945";

    _phoneFocus.addListener(() => setState(() {}));
    _pinFocus.addListener(() => setState(() {}));
  }

  @override
  void dispose() {
    _linkSubscription?.cancel();
    _phoneFocus.dispose();
    _pinFocus.dispose();
    super.dispose();
  }

  void _handleLogin() async {
    if (_isLoading) return; // évite la double-soumission
    FocusScope.of(context).unfocus();
    setState(() => _isLoading = true);

    try {
      // Demande la permission contacts AVANT de lancer le SDK (non bloquant).
      await Permission.contacts.request();

      final result = await _authService.login(
        _phoneController.text,
        _pineController.text,
      );

      if (!mounted) return;
      setState(() {
        _accessToken = result['access_token'] as String;
        _refreshToken = result['refresh_token'] as String;
        _isLoggedIn = true;
      });
    } on LoginException catch (e) {
      _showError(e.message);
    } catch (_) {
      _showError("Une erreur inattendue est survenue.");
    } finally {
      if (mounted) setState(() => _isLoading = false);
    }
  }

  void _showError(String message) {
    ScaffoldMessenger.of(context)
        .showSnackBar(SnackBar(content: Text(message)));
  }

  // --- LOGIQUE DEEP LINK ---

  // --- HANDLERS DU SDK ---

  Future<void> _handleDepositNew() async {
    Navigator.of(context).push(MaterialPageRoute(
        builder: (_) => WellooDeposit(
              accessToken: _accessToken,
              refreshToken: _refreshToken,

              // ✅ Callback Succès
              onSuccess: (result) {
                debugPrint("🎉 SUCCÈS PAIEMENT");
                debugPrint("-------------------");
                debugPrint("Ref  : ${result.transactionId}");
                debugPrint("Info : ${result.description}");
              },

              // ❌ Callback Erreur (Refus, Annulation, Technique)
              onError: (error) {
                debugPrint("⚠️ ÉCHEC PAIEMENT");
                debugPrint("------------------");
                debugPrint("Cause : ${error.description}");
              },
            )));
  }

  // Future<void> _handleTransfer() async {
  //   Navigator.of(context).push(
  //     MaterialPageRoute(
  //       builder: (_) => WellooTransfer(
  //         accessToken: _accessToken,
  //         refreshToken: _refreshToken,
  //         // ✅ Callback Succès
  //         onSuccess: (result) {
  //           debugPrint("Info : ${result.description}");
  //         },
  //         // ❌ Callback Erreur
  //         onError: (error) {
  //           debugPrint("Error : ${error.description}");
  //         },
  //       ),
  //     ),
  //   );
  // }

  Future<void> _handleTransferNew() async {
    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (_) => WellooTransfer(
          accessToken: _accessToken,
          refreshToken: _refreshToken,
          // ✅ Callback Succès
          onSuccess: (result) {
            debugPrint("Info : ${result.description}");
          },
          // ❌ Callback Erreur
          onError: (error) {
            debugPrint("Error : ${error.description}");
          },
        ),
      ),
    );
  }

  Future<void> _handleScanner() async {
    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (_) => WellooScanQr(
          accessToken: _accessToken,
          refreshToken: _refreshToken,
          // ✅ Callback Succès
          onSuccess: (result) {
            debugPrint("✅ Succès : ${result.description}");
          },
          // ❌ Callback Erreur
          onError: (error) {
            debugPrint("❌ Erreur : ${error.description}");
          },
        ),
      ),
    );
  }

  // --- UI ---

  // Palette de marque (locale à l'exemple)
  static const _brand = Color(0xFF5B4DF0);
  static const _brandDark = Color(0xFF3D2EC4);
  static const _ink = Color(0xFF1A1A2E);
  static const _inkMuted = Color(0xFF8A8AA3);

  @override
  Widget build(BuildContext context) {
    if (!_isLoggedIn) return _buildLoginScreen();
    return Scaffold(
      appBar: AppBar(
        title: const Text('Welloo SDK Demo'),
        centerTitle: true,
      ),
      backgroundColor: Colors.grey[100],
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(20.0),
          child: _buildMainMenu(),
        ),
      ),
    );
  }

  // Écran de connexion modernisé — hero « aurora » + carte flottante
  Widget _buildLoginScreen() {
    final media = MediaQuery.of(context);
    return Scaffold(
      backgroundColor: const Color(0xFFF4F4FB),
      body: SingleChildScrollView(
        child: ConstrainedBox(
          constraints: BoxConstraints(minHeight: media.size.height),
          child: Column(
            children: [
              // ── Hero aurora (dégradé + halos décoratifs) ──────────────
              SizedBox(
                height: media.size.height * 0.42,
                width: double.infinity,
                child: ClipRRect(
                  borderRadius: const BorderRadius.vertical(
                      bottom: Radius.circular(36)),
                  child: Stack(
                    fit: StackFit.expand,
                    children: [
                      // Fond dégradé
                      const DecoratedBox(
                        decoration: BoxDecoration(
                          gradient: LinearGradient(
                            begin: Alignment.topLeft,
                            end: Alignment.bottomRight,
                            colors: [
                              Color(0xFF7C6BFF),
                              Color(0xFF5B4DF0),
                              _brandDark,
                            ],
                          ),
                        ),
                      ),
                      // Halos lumineux pour la profondeur
                      Positioned(
                        top: -60,
                        right: -40,
                        child: _glowBlob(180, 0.18),
                      ),
                      Positioned(
                        bottom: -50,
                        left: -50,
                        child: _glowBlob(160, 0.12),
                      ),
                      // Contenu du hero
                      SafeArea(
                        bottom: false,
                        child: Padding(
                          padding: const EdgeInsets.fromLTRB(24, 24, 24, 48),
                          child: Column(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: [
                              Container(
                                width: 70,
                                height: 70,
                                decoration: BoxDecoration(
                                  color: Colors.white.withValues(alpha: 0.16),
                                  borderRadius: BorderRadius.circular(20),
                                  border: Border.all(
                                      color: Colors.white
                                          .withValues(alpha: 0.28)),
                                ),
                                child: const Icon(
                                    Icons.account_balance_wallet_rounded,
                                    color: Colors.white,
                                    size: 34),
                              ),
                              const SizedBox(height: 18),
                              const Text(
                                'Bienvenue 👋',
                                style: TextStyle(
                                  fontSize: 28,
                                  fontWeight: FontWeight.w800,
                                  color: Colors.white,
                                  letterSpacing: -0.5,
                                ),
                              ),
                              const SizedBox(height: 6),
                              Text(
                                'Connectez-vous pour accéder à la démo',
                                textAlign: TextAlign.center,
                                style: TextStyle(
                                  fontSize: 14.5,
                                  height: 1.4,
                                  color:
                                      Colors.white.withValues(alpha: 0.85),
                                ),
                              ),
                            ],
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
              // ── Carte flottante qui chevauche le hero ─────────────────
              Transform.translate(
                offset: const Offset(0, -36),
                child: Container(
                  margin: const EdgeInsets.symmetric(horizontal: 20),
                  constraints: const BoxConstraints(maxWidth: 440),
                  padding: const EdgeInsets.fromLTRB(22, 26, 22, 26),
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(26),
                    boxShadow: [
                      BoxShadow(
                        color: _brandDark.withValues(alpha: 0.12),
                        blurRadius: 32,
                        offset: const Offset(0, 16),
                      ),
                    ],
                  ),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: [
                      _buildLabel('NUMÉRO DE TÉLÉPHONE'),
                      const SizedBox(height: 8),
                      _buildField(
                        controller: _phoneController,
                        focusNode: _phoneFocus,
                        hint: '07 00 00 00 00',
                        icon: Icons.phone_rounded,
                        keyboardType: TextInputType.phone,
                      ),
                      const SizedBox(height: 18),
                      _buildLabel('CODE PIN'),
                      const SizedBox(height: 8),
                      _buildField(
                        controller: _pineController,
                        focusNode: _pinFocus,
                        hint: '••••',
                        icon: Icons.lock_rounded,
                        keyboardType: TextInputType.number,
                        obscure: _obscurePin,
                        trailing: IconButton(
                          onPressed: () =>
                              setState(() => _obscurePin = !_obscurePin),
                          icon: Icon(
                            _obscurePin
                                ? Icons.visibility_off_rounded
                                : Icons.visibility_rounded,
                            color: _inkMuted,
                            size: 20,
                          ),
                        ),
                        onSubmitted: (_) => _handleLogin(),
                      ),
                      const SizedBox(height: 26),
                      _buildSubmitButton(),
                    ],
                  ),
                ),
              ),
              const SizedBox(height: 4),
              _buildEnvBadge(),
              SizedBox(height: media.padding.bottom + 16),
            ],
          ),
        ),
      ),
    );
  }

  /// Halo lumineux flou pour la profondeur du hero.
  Widget _glowBlob(double size, double opacity) {
    return Container(
      width: size,
      height: size,
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        gradient: RadialGradient(
          colors: [
            Colors.white.withValues(alpha: opacity),
            Colors.white.withValues(alpha: 0),
          ],
        ),
      ),
    );
  }

  Widget _buildLabel(String text) {
    return Padding(
      padding: const EdgeInsets.only(left: 4),
      child: Text(
        text,
        style: const TextStyle(
          fontSize: 11,
          fontWeight: FontWeight.w700,
          color: _inkMuted,
          letterSpacing: 0.8,
        ),
      ),
    );
  }

  Widget _buildField({
    required TextEditingController controller,
    required String hint,
    required IconData icon,
    required TextInputType keyboardType,
    FocusNode? focusNode,
    bool obscure = false,
    Widget? trailing,
    ValueChanged<String>? onSubmitted,
  }) {
    final focused = focusNode?.hasFocus ?? false;
    return AnimatedContainer(
      duration: const Duration(milliseconds: 180),
      curve: Curves.easeOut,
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
        border: Border.all(
          color: focused ? _brand : const Color(0xFFE9E9F2),
          width: focused ? 1.6 : 1,
        ),
        boxShadow: [
          BoxShadow(
            color: focused
                ? _brand.withValues(alpha: 0.16)
                : const Color(0x0A1A1A2E),
            blurRadius: focused ? 20 : 16,
            offset: const Offset(0, 6),
          ),
        ],
      ),
      child: Row(
        children: [
          const SizedBox(width: 12),
          AnimatedContainer(
            duration: const Duration(milliseconds: 180),
            width: 38,
            height: 38,
            decoration: BoxDecoration(
              color: _brand.withValues(alpha: focused ? 0.16 : 0.08),
              borderRadius: BorderRadius.circular(11),
            ),
            child: Icon(icon, color: _brand, size: 20),
          ),
          Expanded(
            child: TextField(
              controller: controller,
              focusNode: focusNode,
              keyboardType: keyboardType,
              obscureText: obscure,
              textInputAction: onSubmitted != null
                  ? TextInputAction.done
                  : TextInputAction.next,
              onSubmitted: onSubmitted,
              style: const TextStyle(
                fontSize: 16,
                fontWeight: FontWeight.w600,
                color: _ink,
              ),
              decoration: InputDecoration(
                hintText: hint,
                hintStyle: const TextStyle(
                  color: _inkMuted,
                  fontWeight: FontWeight.w500,
                ),
                border: InputBorder.none,
                contentPadding:
                    const EdgeInsets.symmetric(horizontal: 12, vertical: 18),
              ),
            ),
          ),
          if (trailing != null) trailing,
          const SizedBox(width: 6),
        ],
      ),
    );
  }

  Widget _buildSubmitButton() {
    return GestureDetector(
      onTap: _isLoading ? null : _handleLogin,
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 200),
        height: 56,
        decoration: BoxDecoration(
          gradient: const LinearGradient(
            colors: [Color(0xFF6D5BF8), _brandDark],
          ),
          borderRadius: BorderRadius.circular(16),
          boxShadow: [
            BoxShadow(
              color: _brand.withValues(alpha: _isLoading ? 0.0 : 0.4),
              blurRadius: 20,
              offset: const Offset(0, 10),
            ),
          ],
        ),
        alignment: Alignment.center,
        child: _isLoading
            ? const SizedBox(
                height: 24,
                width: 24,
                child: CircularProgressIndicator(
                  strokeWidth: 2.5,
                  color: Colors.white,
                ),
              )
            : const Text(
                'Se connecter',
                style: TextStyle(
                  fontSize: 17,
                  fontWeight: FontWeight.w700,
                  color: Colors.white,
                  letterSpacing: 0.2,
                ),
              ),
      ),
    );
  }

  Widget _buildEnvBadge() {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
      decoration: BoxDecoration(
        color: _brand.withValues(alpha: 0.07),
        borderRadius: BorderRadius.circular(20),
      ),
      child: const Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(Icons.lock_outline_rounded, size: 14, color: _brand),
          SizedBox(width: 6),
          Text(
            'Environnement Sandbox',
            style: TextStyle(
              fontSize: 12,
              fontWeight: FontWeight.w600,
              color: _brand,
            ),
          ),
        ],
      ),
    );
  }

  // Votre menu actuel déplacé dans une fonction
  Widget _buildMainMenu() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        _buildMenuButton(
          icon: Icons.download,
          label: "Initier un Dépôt (v2)",
          onPressed: _handleDepositNew,
          color: Colors.green,
        ),
        const SizedBox(height: 20),
        _buildMenuButton(
          icon: Icons.send,
          label: "Initier un Transfert (V2)",
          onPressed: _handleTransferNew,
          color: Colors.blue,
        ),
        const SizedBox(height: 20),
        _buildMenuButton(
          icon: Icons.qr_code_scanner,
          label: "Scanner QR Code",
          onPressed: _handleScanner,
          color: Colors.purple,
        ),
      ],
    );
  }

  // Widget helper pour éviter la répétition de code
  Widget _buildMenuButton({
    required IconData icon,
    required String label,
    required VoidCallback onPressed,
    required Color color,
  }) {
    return SizedBox(
      width: double.infinity,
      height: 60,
      child: ElevatedButton.icon(
        onPressed: onPressed,
        icon: Icon(icon),
        label: Text(label, style: const TextStyle(fontSize: 16)),
        style: ElevatedButton.styleFrom(
          backgroundColor: color,
          foregroundColor: Colors.white,
          elevation: 2,
          shape:
              RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
        ),
      ),
    );
  }
}