diff --git a/lib/view/mandatory_update_screen.dart b/lib/view/mandatory_update_screen.dart new file mode 100644 index 0000000..e3125b3 --- /dev/null +++ b/lib/view/mandatory_update_screen.dart @@ -0,0 +1,291 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:flutter_in_store_app_version_checker/flutter_in_store_app_version_checker.dart'; +import 'package:on_field_work/helpers/services/app_logger.dart'; +import 'package:on_field_work/helpers/utils/mixins/ui_mixin.dart'; +import 'package:on_field_work/images.dart'; +import 'package:on_field_work/helpers/widgets/wave_background.dart'; + +class MandatoryUpdateScreen extends StatefulWidget { + final String newVersion; + final InStoreAppVersionCheckerResult? updateResult; + + const MandatoryUpdateScreen({ + super.key, + required this.newVersion, + this.updateResult, + }); + + @override + State createState() => _MandatoryUpdateScreenState(); +} + +class _MandatoryUpdateScreenState extends State + with SingleTickerProviderStateMixin, UIMixin { + late AnimationController _controller; + late Animation _logoAnimation; + + static const double _kMaxContentWidth = 480.0; + + Color get _primaryColor => contentTheme.primary; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 800), + ); + _logoAnimation = CurvedAnimation( + parent: _controller, + curve: Curves.elasticOut, + ); + _controller.forward(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + Future _launchStoreUrl() async { + final url = widget.updateResult?.appURL; + if (url != null && url.isNotEmpty) { + final uri = Uri.parse(url); + try { + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } else { + logSafe("Could not launch store URL: $url"); + } + } catch (e, stack) { + logSafe( + "Error launching store URL: $url", + error: e, + stackTrace: stack, + level: LogLevel.error, + ); + } + } + } + + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.ltr, + child: Scaffold( + body: Stack( + children: [ + RedWaveBackground(brandRed: _primaryColor), + + SafeArea( + child: Center( + child: ConstrainedBox( + constraints: + const BoxConstraints(maxWidth: _kMaxContentWidth), + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 32), + + ScaleTransition( + scale: _logoAnimation, + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 8), + ), + ], + ), + padding: const EdgeInsets.all(20), + child: Image.asset(Images.logoDark), + ), + ), + + const SizedBox(height: 32), + + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 5), + ), + ], + ), + padding: const EdgeInsets.all(32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Title + Text( + "Update Required", + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .headlineMedium + ?.copyWith( + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 12), + + // Subtitle/Message + Text( + "A mandatory update (version ${widget.newVersion}) is available to continue using the application. Please update now for uninterrupted access.", + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith( + color: Colors.black54, + height: 1.4, + ), + ), + + const SizedBox(height: 32), + + // Prominent Action Button + ElevatedButton.icon( + onPressed: _launchStoreUrl, + icon: const Icon( + Icons.system_update_alt, + color: Colors.white, + ), + label: Text( + "UPDATE NOW", + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + fontWeight: FontWeight.w700, + color: Colors.white, + fontSize: 16, + ), + ), + style: ElevatedButton.styleFrom( + padding: + const EdgeInsets.symmetric(vertical: 18), + backgroundColor: _primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + elevation: 5, + ), + ), + + const SizedBox(height: 32), + + // Why Update Section + Text( + "Why updating is important:", + textAlign: TextAlign.start, + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + const SizedBox(height: 12), + + const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BulletPoint( + text: + "Access new features and improvements"), + BulletPoint( + text: + "Fix critical bugs and security issues"), + BulletPoint( + text: + "Ensure smooth app performance and stability"), + BulletPoint( + text: + "Stay compatible with latest operating system and services"), + ], + ), + + const SizedBox(height: 12), + + Text( + "Thank you for keeping your app up to date!", + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: Colors.black45, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ); + } +} + +class BulletPoint extends StatelessWidget { + final String text; + final Color bulletColor; + + const BulletPoint({ + super.key, + required this.text, + this.bulletColor = const Color(0xFF555555), + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(right: 8, top: 4), + child: Icon( + Icons.circle, + size: 6, + color: bulletColor, + ), + ), + Expanded( + child: Text( + text, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.black87, + height: 1.4, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/view/my_app.dart b/lib/view/my_app.dart index 76c1796..89da1ea 100644 --- a/lib/view/my_app.dart +++ b/lib/view/my_app.dart @@ -3,8 +3,9 @@ import 'package:get/get.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:provider/provider.dart'; +import 'package:flutter_in_store_app_version_checker/flutter_in_store_app_version_checker.dart'; + import 'package:on_field_work/helpers/services/app_logger.dart'; -import 'package:on_field_work/helpers/extensions/app_localization_delegate.dart'; import 'package:on_field_work/helpers/services/localizations/language.dart'; import 'package:on_field_work/helpers/services/navigation_services.dart'; import 'package:on_field_work/helpers/services/storage/local_storage.dart'; @@ -13,84 +14,120 @@ import 'package:on_field_work/helpers/theme/theme_customizer.dart'; import 'package:on_field_work/helpers/theme/app_notifier.dart'; import 'package:on_field_work/routes.dart'; -class MyApp extends StatelessWidget { - final bool isOffline; +import 'package:on_field_work/view/mandatory_update_screen.dart'; +class MyApp extends StatefulWidget { + final bool isOffline; const MyApp({super.key, required this.isOffline}); + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + bool _needsUpdate = false; + String? _newVersion; + InStoreAppVersionCheckerResult? _updateResult; + + final InStoreAppVersionChecker _checker = InStoreAppVersionChecker( + androidStore: AndroidStore.googlePlayStore, + ); + + @override + void initState() { + super.initState(); + _checkVersion(); + } + + /// ------------------------- + /// Version Check + /// ------------------------- + Future _checkVersion() async { + try { + final result = await _checker.checkUpdate(); + _updateResult = result; + logSafe("Version Check initiated..."); + logSafe("Current App Version: ${_checker.currentVersion}"); + logSafe("Result canUpdate: ${result.canUpdate}"); + logSafe("Result newVersion: ${result.newVersion}"); + logSafe("Result appURL: ${result.appURL}"); + if (result.canUpdate) { + setState(() { + _needsUpdate = true; + _newVersion = result.newVersion ?? ""; + }); + + logSafe("New version available → $_newVersion"); + } + + if (result.errorMessage != null) { + logSafe("VersionChecker Error: ${result.errorMessage}"); + } + } catch (e, stack) { + logSafe( + "Version check exception", + error: e, + stackTrace: stack, + level: LogLevel.error, + ); + } + } + + /// ------------------------- + /// Initial Route Logic + /// ------------------------- Future _getInitialRoute() async { try { final token = LocalStorage.getJwtToken(); + if (token == null || token.isEmpty) { - logSafe("User not logged in. Routing to /auth/login-option"); return "/auth/login-option"; } - final bool hasMpin = LocalStorage.getIsMpin(); - if (hasMpin) { + if (LocalStorage.getIsMpin()) { await LocalStorage.setBool("mpin_verified", false); - logSafe("Routing to /auth/mpin-auth"); return "/auth/mpin-auth"; } - logSafe("No MPIN. Routing to /dashboard"); return "/dashboard"; - } catch (e, stacktrace) { - logSafe("Error determining initial route", - level: LogLevel.error, error: e, stackTrace: stacktrace); + } catch (e, stack) { + logSafe( + "Initial route ERROR", + error: e, + stackTrace: stack, + level: LogLevel.error, + ); return "/auth/login-option"; } } - // ✨ REVISED: Helper Widget to show a full-screen, well-designed offline status - Widget _buildConnectivityOverlay(BuildContext context) { - // If not offline, return an empty widget. - if (!isOffline) return const SizedBox.shrink(); - - // Otherwise, return a full-screen overlay. + /// ------------------------- + /// Offline Overlay (Blocking) + /// ------------------------- + Widget _buildOfflineOverlay() { return Directionality( textDirection: AppTheme.textDirection, child: Scaffold( - backgroundColor: - Colors.grey.shade100, // Light background for the offline state + backgroundColor: Colors.grey.shade200, body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.cloud_off, - color: Colors.red.shade700, // Prominent color - size: 100, - ), - const SizedBox(height: 24), + Icon(Icons.cloud_off, size: 100, color: Colors.red.shade600), + const SizedBox(height: 20), const Text( "You Are Offline", - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - color: Colors.black87, - ), + style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold), ), - const SizedBox(height: 8), + const SizedBox(height: 10), const Padding( - padding: EdgeInsets.symmetric(horizontal: 40.0), + padding: EdgeInsets.symmetric(horizontal: 32), child: Text( "Please check your internet connection and try again.", textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - color: Colors.black54, - ), + style: TextStyle(fontSize: 16, color: Colors.black54), ), ), - const SizedBox(height: 32), - // Optional: Add a button for the user to potentially refresh/retry - // ElevatedButton( - // onPressed: () { - // // Add logic to re-check connectivity or navigate (if possible) - // }, - // child: const Text("RETRY"), - // ), ], ), ), @@ -98,21 +135,28 @@ class MyApp extends StatelessWidget { ); } + /// ------------------------- + /// Build + /// ------------------------- @override Widget build(BuildContext context) { + if (_needsUpdate && !widget.isOffline) { + // 4. USE THE NEW WIDGET HERE + return MandatoryUpdateScreen( + newVersion: _newVersion ?? "", + updateResult: _updateResult, + ); + } + + if (widget.isOffline) { + return _buildOfflineOverlay(); + } + return Consumer( builder: (_, notifier, __) { return FutureBuilder( future: _getInitialRoute(), builder: (context, snapshot) { - if (snapshot.hasError) { - logSafe("FutureBuilder snapshot error", - level: LogLevel.error, error: snapshot.error); - return const MaterialApp( - home: Center(child: Text("Error determining route")), - ); - } - if (!snapshot.hasData) { return const MaterialApp( home: Center(child: CircularProgressIndicator()), @@ -127,29 +171,19 @@ class MyApp extends StatelessWidget { navigatorKey: NavigationService.navigatorKey, initialRoute: snapshot.data!, getPages: getPageRoute(), - builder: (context, child) { - NavigationService.registerContext(context); - - // 💡 REVISED: Use a Stack to place the offline overlay ON TOP of the app content. - // This allows the full-screen view to cover everything, including the main app content. - return Stack( - children: [ - Directionality( - textDirection: AppTheme.textDirection, - child: child ?? const SizedBox(), - ), - // 2. The full-screen connectivity overlay, only visible when offline - _buildConnectivityOverlay(context), - ], - ); - }, - localizationsDelegates: [ - AppLocalizationsDelegate(context), + supportedLocales: Language.getLocales(), + localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], - supportedLocales: Language.getLocales(), + builder: (context, child) { + NavigationService.registerContext(context); + return Directionality( + textDirection: AppTheme.textDirection, + child: child ?? const SizedBox(), + ); + }, ); }, ); diff --git a/pubspec.yaml b/pubspec.yaml index 67d24dd..9288b1f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -56,7 +56,7 @@ dependencies: syncfusion_flutter_calendar: ^31.2.18 syncfusion_flutter_maps: ^31.2.18 http: ^1.6.0 - geolocator: ^14.0.2 + geolocator: ^14.0.1 permission_handler: ^12.0.1 image: ^4.0.17 image_picker: ^1.0.7 @@ -87,6 +87,7 @@ dependencies: share_plus: ^12.0.1 timeline_tile: ^2.0.0 encrypt: ^5.0.3 + flutter_in_store_app_version_checker: ^1.10.0 dev_dependencies: flutter_test: