diff --git a/lib/core/utils/version_check_service.dart b/lib/core/utils/version_check_service.dart new file mode 100644 index 0000000..8dea48b --- /dev/null +++ b/lib/core/utils/version_check_service.dart @@ -0,0 +1,67 @@ +import 'package:flutter/foundation.dart'; +import 'dart:io' show Platform; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +class VersionCheckService { + final _supabase = Supabase.instance.client; + + /// Controlla se l'app corrente deve essere bloccata o aggiornata. + /// Ritorna il link di download se l'aggiornamento è obbligatorio, altrimenti null. + Future checkForceUpdate() async { + try { + // 1. Determiniamo la piattaforma corrente + String platformKey = 'web'; + if (!kIsWeb) { + if (Platform.isAndroid) platformKey = 'android'; + if (Platform.isWindows) platformKey = 'windows'; + } + + // 2. Recuperiamo la configurazione minima da Supabase + final data = await _supabase + .from('app_config') + .select() + .eq('platform', platformKey) + .maybeSingle(); + + if (data == null) return null; + + final String minVersion = data['min_version']; + final String downloadUrl = data['download_url']; + + // 3. Recuperiamo la versione attuale dell'app dal pubspec.yaml + final packageInfo = await PackageInfo.fromPlatform(); + final String currentVersion = packageInfo.version; + + // 4. Confronto matematico semantico (es. 1.2.3 vs 1.1.9) + if (_isVersionLower(currentVersion, minVersion)) { + return downloadUrl; // Aggiornamento obbligatorio richiesto! + } + + return null; + } catch (e) { + debugPrint('Errore controllo versione: $e'); + return null; // In caso di errore non blocchiamo l'utente + } + } + + bool _isVersionLower(String current, String min) { + List currentParts = current + .split('.') + .map((e) => int.tryParse(e) ?? 0) + .toList(); + List minParts = min + .split('.') + .map((e) => int.tryParse(e) ?? 0) + .toList(); + + for (int i = 0; i < 3; i++) { + int currentPart = currentParts.length > i ? currentParts[i] : 0; + int minPart = minParts.length > i ? minParts[i] : 0; + + if (currentPart < minPart) return true; + if (currentPart > minPart) return false; + } + return false; + } +} diff --git a/lib/features/operations/ui/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen.dart index 3fe122e..90a7a8c 100644 --- a/lib/features/operations/ui/operation_form_screen.dart +++ b/lib/features/operations/ui/operation_form_screen.dart @@ -6,7 +6,6 @@ import 'package:flux/features/operations/blocs/operation_form_cubit.dart'; import 'package:flux/features/operations/models/operation_model.dart'; import 'package:flux/core/widgets/shared_forms/customer_section.dart'; import 'package:flux/features/operations/ui/widgets/details_section.dart'; -import 'package:flux/core/widgets/shared_forms/shared_files_section.dart'; // <- Cambiato ad un file unico per coerenza col ticket class OperationFormScreen extends StatefulWidget { final String? operationId; diff --git a/lib/main.dart b/lib/main.dart index bab8741..5f95c4f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flux/core/utils/version_check_service.dart'; import 'package:flux/features/attachments/data/attachments_repository.dart'; import 'package:flux/features/auth/bloc/auth_cubit.dart'; import 'package:flux/features/company/data/company_repository.dart'; @@ -38,6 +39,7 @@ import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; import 'package:flux/features/master_data/store/data/store_repository.dart'; import 'package:flux/features/settings/settings.dart'; import 'package:flutter_web_plugins/url_strategy.dart'; +import 'package:url_launcher/url_launcher.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -159,19 +161,14 @@ class _FluxAppState extends State { @override Widget build(BuildContext context) { - // Il BlocConsumer unisce Listener e Builder in un colpo solo! return BlocConsumer( - // --- PARTE LISTENER (Il colpo di clacson in background) --- listenWhen: (previous, current) => previous.status != SessionStatus.authenticated && current.status == SessionStatus.authenticated, listener: (context, state) { - // BAM! L'utente è dentro. Pre-carichiamo i Cubit leggeri. context.read().loadStores(); context.read().loadAllStaff(); }, - - // --- PARTE BUILDER (La UI che viene disegnata a schermo) --- builder: (context, sessionState) { if (sessionState.status == SessionStatus.initial) { return _buildLoadingScreen(); @@ -189,6 +186,11 @@ class _FluxAppState extends State { localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, locale: const Locale('it'), + + // 🥷 ECCO LA MAGIA: Avvolgiamo tutta l'app nel nostro checker! + builder: (context, child) { + return GlobalUpdateChecker(child: child!); + }, ); }, ); @@ -222,3 +224,146 @@ class _FluxAppState extends State { ); } } + +// --- IL WIDGET GUARDIANO DEGLI AGGIORNAMENTI --- +class GlobalUpdateChecker extends StatefulWidget { + final Widget child; + const GlobalUpdateChecker({super.key, required this.child}); + + @override + State createState() => _GlobalUpdateCheckerState(); +} + +class _GlobalUpdateCheckerState extends State { + bool _mustUpdate = false; + String? _updateUrl; + + @override + void initState() { + super.initState(); + _checkVersionAndBlock(); + } + + Future _checkVersionAndBlock() async { + final updateUrl = await VersionCheckService().checkForceUpdate(); + + if (updateUrl != null && mounted) { + // Invece di aprire un dialog, cambiamo lo stato e attiviamo lo "Scudo" + setState(() { + _mustUpdate = true; + _updateUrl = updateUrl; + }); + } + } + + @override + Widget build(BuildContext context) { + // 1. Se l'app è aggiornata, mostriamo solo l'app normale + if (!_mustUpdate) return widget.child; + + // 2. Se l'app è vecchia, sovrapponiamo il blocco con uno Stack + return Stack( + children: [ + // L'app sotto continua ad esistere, ma è inaccessibile + widget.child, + + // IL BLOCCO INVALICABILE SOPRA A TUTTO + Positioned.fill( + child: Container( + color: Colors.black.withValues(alpha: 0.85), // Sfondo oscurante + child: Center( + // Usiamo Material per ereditare correttamente temi, font e colori + child: Material( + color: Colors.transparent, + child: Container( + margin: const EdgeInsets.all(24), + padding: const EdgeInsets.all(24), + constraints: const BoxConstraints(maxWidth: 400), + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: Colors.black54, + blurRadius: 20, + offset: Offset(0, 10), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + kIsWeb ? Icons.cached : Icons.system_update, + color: Colors.orange, + size: 32, + ), + const SizedBox(width: 12), + Text( + kIsWeb + ? "Aggiornamento" + : "Aggiornamento Obbligatorio", + style: Theme.of(context).textTheme.titleLarge + ?.copyWith(fontWeight: FontWeight.bold), + ), + ], + ), + const SizedBox(height: 16), + Text( + kIsWeb + ? "È stata rilasciata una nuova versione dell'applicazione. Ricarica la pagina per continuare." + : "Per continuare ad utilizzare l'applicazione è necessario scaricare e installare l'ultimo aggiornamento.", + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16), + ), + const SizedBox(height: 24), + if (kIsWeb) + FilledButton.icon( + icon: const Icon(Icons.refresh), + label: const Text("RICARICA ORA"), + style: FilledButton.styleFrom( + minimumSize: const Size(double.infinity, 50), + ), + onPressed: () async { + // Trick cross-platform per fare il reload + await launchUrl( + Uri.parse(Uri.base.toString()), + webOnlyWindowName: '_self', + ); + }, + ) + else + FilledButton.icon( + icon: const Icon(Icons.download), + + label: const Text("SCARICA AGGIORNAMENTO"), + style: FilledButton.styleFrom( + minimumSize: const Size(double.infinity, 50), + backgroundColor: Colors.blue, + ), + onPressed: () async { + if (_updateUrl != null) { + final url = Uri.parse(_updateUrl!); + if (await canLaunchUrl(url)) { + await launchUrl( + url, + mode: LaunchMode.externalApplication, + ); + } + } + }, + ), + ], + ), + ), + ), + ), + ), + ), + ], + ); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 3b1fe6d..9d0506c 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import app_links import file_picker import file_selector_macos +import package_info_plus import pdfx import printing import shared_preferences_foundation @@ -17,6 +18,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PdfxPlugin.register(with: registry.registrar(forPlugin: "PdfxPlugin")) PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 6be7d58..05e45e7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -645,6 +645,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20" + url: "https://pub.dev" + source: hosted + version: "9.0.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" path: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0f58d70..4ee35c5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flux description: "Gestione attività negozio di telefonia" publish_to: 'none' -version: 0.1.0+1 +version: 1.0.0+1 environment: sdk: ^3.11.3 @@ -37,6 +37,7 @@ dependencies: printing: ^5.14.3 font_awesome_flutter: ^11.0.0 flutter_launcher_icons: ^0.14.4 + package_info_plus: ^9.0.1 dev_dependencies: flutter_test: