import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:geosector_app/core/services/app_info_service.dart'; import 'package:geosector_app/core/services/hive_service.dart'; import 'package:geosector_app/core/services/location_service.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'dart:async'; import 'dart:math' as math; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:package_info_plus/package_info_plus.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:shared_preferences/shared_preferences.dart'; // Import conditionnel pour le web import 'package:universal_html/html.dart' as html; class SplashPage extends StatefulWidget { /// Action à effectuer après l'initialisation (login ou register) final String? action; /// Type de login/register (user ou admin) - ignoré pour register final String? type; const SplashPage({super.key, this.action, this.type}); @override State createState() => _SplashPageState(); } // Class pour dessiner les petits points blancs sur le fond class DotsPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.white.withValues(alpha: 0.5) ..style = PaintingStyle.fill; final random = math.Random(42); // Seed fixe pour consistance final numberOfDots = (size.width * size.height) ~/ 1500; for (int i = 0; i < numberOfDots; i++) { final x = random.nextDouble() * size.width; final y = random.nextDouble() * size.height; final radius = 1.0 + random.nextDouble() * 2.0; canvas.drawCircle(Offset(x, y), radius, paint); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } class _SplashPageState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _scaleAnimation; bool _isInitializing = true; String _statusMessage = "Initialisation..."; double _progress = 0.0; bool _showButtons = false; String _appVersion = ''; bool _showLocationError = false; String? _locationErrorMessage; bool _isCleaningCache = false; Future _getAppVersion() async { try { final packageInfo = await PackageInfo.fromPlatform(); if (mounted) { setState(() { _appVersion = packageInfo.version; }); } } catch (e) { debugPrint('Erreur lors de la récupération de la version: $e'); if (mounted) { setState(() { _appVersion = AppInfoService.fullVersion.split(' ').last; }); } } } /// Effectue un nettoyage sélectif du cache /// Préserve la box pending_requests et les données critiques Future _performSelectiveCleanup({bool manual = false}) async { debugPrint('🧹 === DÉBUT DU NETTOYAGE DU CACHE === 🧹'); debugPrint('📌 Type: ${manual ? "MANUEL" : "AUTOMATIQUE"}'); debugPrint('📱 Platform: ${kIsWeb ? "WEB" : "MOBILE"}'); debugPrint('📦 Version actuelle: $_appVersion'); try { if (mounted) { setState(() { _isCleaningCache = true; _statusMessage = "Nettoyage du cache en cours..."; _progress = 0.1; }); } // Étape 1: Nettoyer le Service Worker (Web uniquement) if (kIsWeb) { debugPrint('🔄 Nettoyage du Service Worker...'); try { // Désenregistrer tous les service workers final registrations = await html.window.navigator.serviceWorker?.getRegistrations(); if (registrations != null) { for (final registration in registrations) { await registration.unregister(); debugPrint('✅ Service Worker désenregistré'); } } // Nettoyer les caches du navigateur if (html.window.caches != null) { final cacheNames = await html.window.caches!.keys(); for (final cacheName in cacheNames) { await html.window.caches!.delete(cacheName); debugPrint('✅ Cache "$cacheName" supprimé'); } } } catch (e) { debugPrint('⚠️ Erreur lors du nettoyage Service Worker: $e'); } } if (mounted) { setState(() { _statusMessage = "Fermeture des bases de données..."; _progress = 0.3; }); } // Étape 2: Sauvegarder les données de pending_requests debugPrint('💾 Sauvegarde des requêtes en attente...'); List? pendingRequests; try { if (Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) { final pendingBox = Hive.box(AppKeys.pendingRequestsBoxName); pendingRequests = pendingBox.values.toList(); debugPrint('📊 ${pendingRequests.length} requêtes en attente sauvegardées'); await pendingBox.close(); } } catch (e) { debugPrint('⚠️ Erreur lors de la sauvegarde des requêtes: $e'); } if (mounted) { setState(() { _statusMessage = "Nettoyage des données locales..."; _progress = 0.5; }); } // Étape 3: Lister toutes les boxes à nettoyer (SAUF pending_requests) final boxesToClean = [ AppKeys.userBoxName, AppKeys.operationsBoxName, AppKeys.passagesBoxName, AppKeys.sectorsBoxName, AppKeys.membresBoxName, AppKeys.amicaleBoxName, AppKeys.clientsBoxName, AppKeys.userSectorBoxName, AppKeys.settingsBoxName, AppKeys.chatRoomsBoxName, AppKeys.chatMessagesBoxName, ]; // Étape 4: Fermer et supprimer les boxes debugPrint('🗑️ Nettoyage des boxes Hive...'); for (final boxName in boxesToClean) { try { if (Hive.isBoxOpen(boxName)) { await Hive.box(boxName).close(); debugPrint('📦 Box "$boxName" fermée'); } await Hive.deleteBoxFromDisk(boxName); debugPrint('✅ Box "$boxName" supprimée'); } catch (e) { debugPrint('⚠️ Erreur lors du nettoyage de "$boxName": $e'); } } if (mounted) { setState(() { _statusMessage = "Réinitialisation de Hive..."; _progress = 0.7; }); } // Étape 5: Réinitialiser Hive proprement debugPrint('🔄 Réinitialisation de Hive...'); await Hive.close(); await Future.delayed(const Duration(milliseconds: 500)); await Hive.initFlutter(); // Étape 6: Restaurer les requêtes en attente if (pendingRequests != null && pendingRequests.isNotEmpty) { debugPrint('♻️ Restauration des requêtes en attente...'); final pendingBox = await Hive.openBox(AppKeys.pendingRequestsBoxName); for (final request in pendingRequests) { await pendingBox.add(request); } debugPrint('✅ ${pendingRequests.length} requêtes restaurées'); } if (mounted) { setState(() { _statusMessage = "Nettoyage terminé !"; _progress = 1.0; }); } // Étape 7: Sauvegarder la nouvelle version if (!manual && kIsWeb) { final prefs = await SharedPreferences.getInstance(); await prefs.setString('app_version', _appVersion); debugPrint('💾 Version $_appVersion sauvegardée'); } debugPrint('🎉 === NETTOYAGE TERMINÉ AVEC SUCCÈS === 🎉'); // Petit délai pour voir le message de succès await Future.delayed(const Duration(milliseconds: 500)); if (mounted) { setState(() { _isCleaningCache = false; _progress = 0.0; }); } } catch (e) { debugPrint('❌ ERREUR CRITIQUE lors du nettoyage: $e'); if (mounted) { setState(() { _isCleaningCache = false; _statusMessage = "Erreur lors du nettoyage"; _progress = 0.0; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur lors du nettoyage: $e'), backgroundColor: Colors.red, duration: const Duration(seconds: 5), ), ); } } } /// Vérifie si une nouvelle version est disponible et nettoie si nécessaire Future _checkVersionAndCleanIfNeeded() async { if (!kIsWeb) { debugPrint('📱 Plateforme mobile - pas de nettoyage automatique'); return; } try { final prefs = await SharedPreferences.getInstance(); final lastVersion = prefs.getString('app_version') ?? ''; debugPrint('🔍 Vérification de version:'); debugPrint(' Version stockée: $lastVersion'); debugPrint(' Version actuelle: $_appVersion'); // Si changement de version détecté if (lastVersion.isNotEmpty && lastVersion != _appVersion) { debugPrint('🆕 NOUVELLE VERSION DÉTECTÉE !'); debugPrint(' Migration de $lastVersion vers $_appVersion'); if (mounted) { setState(() { _statusMessage = "Nouvelle version détectée, mise à jour..."; }); } // Effectuer le nettoyage automatique await _performSelectiveCleanup(manual: false); } else if (lastVersion.isEmpty) { // Première installation debugPrint('🎉 Première installation détectée'); await prefs.setString('app_version', _appVersion); } else { debugPrint('✅ Même version - pas de nettoyage nécessaire'); } } catch (e) { debugPrint('⚠️ Erreur lors de la vérification de version: $e'); } } @override void initState() { super.initState(); // Animation controller _animationController = AnimationController( vsync: this, duration: const Duration(seconds: 5), ); _scaleAnimation = Tween( begin: 4.0, end: 1.0, ).animate( CurvedAnimation( parent: _animationController, curve: Curves.easeOutBack, ), ); _animationController.forward(); _getAppVersion(); _startInitialization(); } @override void dispose() { _animationController.dispose(); super.dispose(); } void _startInitialization() async { try { debugPrint('🚀 Début de l\'initialisation complète de l\'application...'); // Étape 0: Vérifier et nettoyer si nouvelle version (Web uniquement) await _checkVersionAndCleanIfNeeded(); // Étape 1: Vérification des permissions GPS (obligatoire) - 0 à 10% if (!kIsWeb) { if (mounted) { setState(() { _statusMessage = "Vérification des autorisations GPS..."; _progress = 0.05; }); } await Future.delayed(const Duration(milliseconds: 200)); final hasPermission = await LocationService.checkAndRequestPermission(); final errorMessage = await LocationService.getLocationErrorMessage(); if (!hasPermission) { // Si les permissions ne sont pas accordées, on arrête tout debugPrint('❌ Permissions GPS refusées'); if (mounted) { setState(() { _showLocationError = true; _locationErrorMessage = errorMessage ?? "L'application nécessite l'accès à votre position pour fonctionner correctement."; _isInitializing = false; _progress = 0.0; }); } return; // On arrête l'initialisation ici } if (mounted) { setState(() { _statusMessage = "Autorisations GPS accordées..."; _progress = 0.10; }); } } // Étape 1: Préparation - 10 à 15% if (mounted) { setState(() { _statusMessage = "Démarrage de l'application..."; _progress = 0.12; }); } await Future.delayed(const Duration(milliseconds: 200)); // Petit délai pour voir le début if (mounted) { setState(() { _statusMessage = "Chargement des composants..."; _progress = 0.15; }); } // Étape 2: Initialisation Hive - 15 à 60% (étape la plus longue) await HiveService.instance.initializeAndResetHive(); if (mounted) { setState(() { _statusMessage = "Configuration du stockage..."; _progress = 0.45; }); } await Future.delayed(const Duration(milliseconds: 300)); // Simulation du temps de traitement if (mounted) { setState(() { _statusMessage = "Préparation des données..."; _progress = 0.60; }); } // Étape 3: Ouverture des Box - 60 à 80% await HiveService.instance.ensureBoxesAreOpen(); // Gérer la box pending_requests séparément pour préserver les données try { debugPrint('📦 Gestion de la box pending_requests...'); if (!Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) { // Importer PendingRequest si nécessaire final pendingRequestBox = await Hive.openBox(AppKeys.pendingRequestsBoxName); final pendingCount = pendingRequestBox.length; if (pendingCount > 0) { debugPrint('⏳ $pendingCount requêtes en attente trouvées dans la box'); } else { debugPrint('✅ Box pending_requests ouverte (vide)'); } } else { debugPrint('✅ Box pending_requests déjà ouverte'); } } catch (e) { debugPrint('⚠️ Erreur lors de l\'ouverture de la box pending_requests: $e'); // On continue quand même, ce n'est pas critique pour le démarrage } if (mounted) { setState(() { _statusMessage = "Vérification du système..."; _progress = 0.80; }); } // Étape 4: Vérification finale - 80 à 95% final allBoxesOpen = HiveService.instance.areAllBoxesOpen(); if (!allBoxesOpen) { final diagnostic = HiveService.instance.getDiagnostic(); debugPrint('❌ Diagnostic des Box: $diagnostic'); throw Exception('Une erreur est survenue lors de l\'initialisation'); } if (mounted) { setState(() { _statusMessage = "Finalisation du chargement..."; _progress = 0.95; }); } await Future.delayed(const Duration(milliseconds: 300)); // Petit délai pour finaliser // Étape 5: Finalisation - 95 à 100% if (mounted) { setState(() { _statusMessage = "Application prête !"; _progress = 1.0; }); // Marquer dans settings que l'initialisation complète de Hive a été effectuée try { if (Hive.isBoxOpen(AppKeys.settingsBoxName)) { final settingsBox = Hive.box(AppKeys.settingsBoxName); await settingsBox.put('hive_initialized', true); await settingsBox.put('hive_initialized_at', DateTime.now().toIso8601String()); debugPrint('✅ Clé hive_initialized définie à true dans settings'); } } catch (e) { debugPrint('⚠️ Impossible de définir la clé hive_initialized: $e'); } // Attendre un court instant pour que l'utilisateur voie "Application prête !" await Future.delayed(const Duration(milliseconds: 400)); setState(() { _isInitializing = false; }); // Redirection automatique si des paramètres sont fournis if (widget.action != null) { await _handleAutoRedirect(); } else { setState(() { _showButtons = true; }); } } debugPrint('✅ Initialisation complète de l\'application terminée avec succès'); } catch (e) { debugPrint('❌ Erreur lors de l\'initialisation: $e'); if (mounted) { setState(() { _statusMessage = "Erreur de chargement - Veuillez redémarrer l'application"; _progress = 1.0; _isInitializing = false; _showButtons = true; }); } } } /// Gère la redirection automatique après l'initialisation Future _handleAutoRedirect() async { // Petit délai pour voir le message "Application prête !" await Future.delayed(const Duration(milliseconds: 300)); if (!mounted) return; final action = widget.action?.toLowerCase(); final type = widget.type?.toLowerCase(); debugPrint('🔄 Redirection automatique: action=$action, type=$type'); // Afficher un message de redirection avant de naviguer setState(() { _statusMessage = action == 'login' ? "Redirection vers la connexion..." : action == 'register' ? "Redirection vers l'inscription..." : "Redirection..."; }); await Future.delayed(const Duration(milliseconds: 200)); if (!context.mounted) return; switch (action) { case 'login': if (type == 'admin') { context.go('/login/admin'); } else { // Par défaut, rediriger vers user si type non spécifié ou invalid context.go('/login/user'); } break; case 'register': // Pour register, le type n'est pas pris en compte context.go('/register'); break; default: // Si action non reconnue, afficher les boutons normalement setState(() { _showButtons = true; }); break; } } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( body: Stack( children: [ // Fond dégradé avec petits points blancs AnimatedContainer( duration: const Duration(milliseconds: 500), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.white, Colors.blue.shade300], ), ), child: CustomPaint( painter: DotsPainter(), child: const SizedBox(width: double.infinity, height: double.infinity), ), ), // Contenu principal SafeArea( child: Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 40), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Spacer(flex: 2), // Logo avec animation AnimatedBuilder( animation: _scaleAnimation, builder: (context, child) { return Transform.scale( scale: _scaleAnimation.value, child: child, ); }, child: Image.asset( 'assets/images/logo-geosector-1024.png', height: 180, ), ), const SizedBox(height: 24), // Titre AnimatedOpacity( opacity: _isInitializing ? 0.9 : 1.0, duration: const Duration(milliseconds: 500), child: Text( 'Geosector', style: theme.textTheme.headlineLarge?.copyWith( color: theme.colorScheme.primary, fontWeight: FontWeight.bold, letterSpacing: 1.2, ), ), ), const SizedBox(height: 16), // Sous-titre AnimatedOpacity( opacity: _isInitializing ? 0.8 : 1.0, duration: const Duration(milliseconds: 500), child: Text( 'Une application puissante et intuitive de gestion de vos distributions de calendriers', textAlign: TextAlign.center, style: theme.textTheme.bodyLarge?.copyWith( color: theme.colorScheme.onSurface.withValues(alpha: 0.7), fontWeight: FontWeight.w500, ), ), ), const Spacer(flex: 1), // Indicateur de chargement if ((_isInitializing || _isCleaningCache) && !_showLocationError) ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 40), child: Column( children: [ // Barre de progression avec animation Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( color: theme.colorScheme.primary.withValues(alpha: 0.2), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(10), child: TweenAnimationBuilder( duration: const Duration(milliseconds: 800), curve: Curves.easeInOut, tween: Tween(begin: 0.0, end: _progress), builder: (context, value, child) { return LinearProgressIndicator( value: value, backgroundColor: Colors.grey.withValues(alpha: 0.15), valueColor: AlwaysStoppedAnimation( theme.colorScheme.primary, ), minHeight: 12, ); }, ), ), ), const SizedBox(height: 8), // Pourcentage Text( '${(_progress * 100).round()}%', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.primary, fontWeight: FontWeight.w600, ), ), ], ), ), const SizedBox(height: 16), // Message de statut avec animation AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: Text( _statusMessage, key: ValueKey(_statusMessage), style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurface.withValues(alpha: 0.7), fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, ), ), ], // Erreur de localisation if (_showLocationError) ...[ Container( padding: const EdgeInsets.all(16), margin: const EdgeInsets.symmetric(horizontal: 20), decoration: BoxDecoration( color: Colors.red.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.red.shade200), ), child: Column( children: [ Icon( Icons.location_off, size: 48, color: Colors.red.shade700, ), const SizedBox(height: 16), Text( 'Autorisations GPS requises', style: theme.textTheme.titleLarge?.copyWith( color: Colors.red.shade700, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Text( _locationErrorMessage ?? "L'application nécessite l'accès à votre position pour fonctionner correctement.", textAlign: TextAlign.center, style: theme.textTheme.bodyMedium?.copyWith( color: Colors.red.shade700, ), ), const SizedBox(height: 24), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // Bouton Réessayer ElevatedButton.icon( onPressed: () { setState(() { _showLocationError = false; _isInitializing = true; }); _startInitialization(); }, icon: const Icon(Icons.refresh), label: const Text('Réessayer'), style: ElevatedButton.styleFrom( backgroundColor: Colors.green, foregroundColor: Colors.white, ), ), const SizedBox(width: 16), // Bouton Paramètres OutlinedButton.icon( onPressed: () async { if (_locationErrorMessage?.contains('définitivement') ?? false) { await LocationService.openAppSettings(); } else { await LocationService.openLocationSettings(); } }, icon: const Icon(Icons.settings), label: const Text('Paramètres'), style: OutlinedButton.styleFrom( foregroundColor: Colors.red.shade700, side: BorderSide(color: Colors.red.shade700), ), ), ], ), ], ), ), ], // Boutons (reste identique) if (_showButtons) ...[ // Bouton Connexion Utilisateur AnimatedOpacity( opacity: _showButtons ? 1.0 : 0.0, duration: const Duration(milliseconds: 500), child: ElevatedButton( onPressed: () { context.go('/login/user'); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.green, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: 40, vertical: 16, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30), ), elevation: 2, ), child: const Text( 'Connexion Utilisateur', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ), ), const SizedBox(height: 16), // Bouton Connexion Administrateur AnimatedOpacity( opacity: _showButtons ? 1.0 : 0.0, duration: const Duration(milliseconds: 500), child: ElevatedButton( onPressed: () { context.go('/login/admin'); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: 40, vertical: 16, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30), ), elevation: 2, ), child: const Text( 'Connexion Administrateur', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ), ), const SizedBox(height: 32), // Bouton d'inscription AnimatedOpacity( opacity: _showButtons ? 1.0 : 0.0, duration: const Duration(milliseconds: 500), child: ElevatedButton( onPressed: () { context.go('/register'); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: 40, vertical: 16, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30), ), elevation: 2, ), child: const Text( 'Pas encore inscrit ?', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ), ), const SizedBox(height: 16), // Lien vers le site web AnimatedOpacity( opacity: _showButtons ? 1.0 : 0.0, duration: const Duration(milliseconds: 500), child: TextButton.icon( onPressed: () { String webUrl = 'https://geosector.fr'; if (kIsWeb) { final host = Uri.base.host; if (host.startsWith('dapp.')) { webUrl = 'https://dev.geosector.fr'; } else if (host.startsWith('rapp.')) { webUrl = 'https://rec.geosector.fr'; } else if (host.startsWith('app.')) { webUrl = 'https://geosector.fr'; } } launchUrl( Uri.parse(webUrl), mode: LaunchMode.externalApplication, ); }, icon: Icon( Icons.language, size: 18, color: theme.colorScheme.primary, ), label: Text( 'Site web Geosector', style: TextStyle( color: theme.colorScheme.primary, fontWeight: FontWeight.w500, ), ), ), ), const SizedBox(height: 8), // Bouton de nettoyage du cache (en noir) AnimatedOpacity( opacity: _showButtons ? 1.0 : 0.0, duration: const Duration(milliseconds: 500), child: TextButton.icon( onPressed: _isCleaningCache ? null : () async { // Confirmation avant nettoyage final confirm = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Nettoyer le cache ?'), content: const Text( 'Cette action va :\n' '• Supprimer toutes les données locales\n' '• Préserver les requêtes en attente\n' '• Forcer le rechargement de l\'application\n\n' 'Continuer ?' ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Annuler'), ), ElevatedButton( onPressed: () => Navigator.of(context).pop(true), style: ElevatedButton.styleFrom( backgroundColor: Colors.orange, ), child: const Text('Nettoyer'), ), ], ), ); if (confirm == true) { debugPrint('👤 Utilisateur a demandé un nettoyage manuel'); await _performSelectiveCleanup(manual: true); // Après le nettoyage, relancer l'initialisation _startInitialization(); } }, icon: Icon( Icons.cleaning_services, size: 18, color: _isCleaningCache ? Colors.grey : Colors.black87, ), label: Text( _isCleaningCache ? 'Nettoyage...' : 'Nettoyer le cache', style: TextStyle( color: _isCleaningCache ? Colors.grey : Colors.black87, fontWeight: FontWeight.w500, ), ), ), ), ], const Spacer(flex: 1), ], ), ), ), ), // Badge de version if (_appVersion.isNotEmpty) Positioned( bottom: 16, right: 16, child: AnimatedOpacity( opacity: _showButtons ? 0.7 : 0.5, duration: const Duration(milliseconds: 500), child: Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: theme.colorScheme.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), border: Border.all( color: theme.colorScheme.primary, width: 1, ), ), child: Text( 'v$_appVersion', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.primary, fontSize: 10, fontWeight: FontWeight.w500, ), ), ), ), ), ], ), ); } }