welloo_sdk 0.0.121
welloo_sdk: ^0.0.121 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)),
),
),
);
}
}