feat: Add connectivity handling and offline screen, update MPIN to 4-digit support

This commit is contained in:
Vaibhav Surve 2025-07-24 17:35:59 +05:30
parent 9e4c0378c6
commit 982c81f849
7 changed files with 222 additions and 67 deletions

View File

@ -6,5 +6,6 @@
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
</manifest> </manifest>

View File

@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

View File

@ -1,9 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:marco/helpers/services/app_initializer.dart'; import 'package:marco/helpers/services/app_initializer.dart';
import 'package:marco/view/my_app.dart'; import 'package:marco/view/my_app.dart';
import 'package:provider/provider.dart';
import 'package:marco/helpers/theme/app_notifier.dart'; import 'package:marco/helpers/theme/app_notifier.dart';
import 'package:marco/helpers/services/app_logger.dart'; import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/view/layouts/offline_screen.dart';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -18,11 +21,12 @@ Future<void> main() async {
runApp( runApp(
ChangeNotifierProvider<AppNotifier>( ChangeNotifierProvider<AppNotifier>(
create: (_) => AppNotifier(), create: (_) => AppNotifier(),
child: const MyApp(), child: const MainWrapper(),
), ),
); );
} catch (e, stacktrace) { } catch (e, stacktrace) {
logSafe('App failed to initialize.', logSafe(
'App failed to initialize.',
level: LogLevel.error, level: LogLevel.error,
error: e, error: e,
stackTrace: stacktrace, stackTrace: stacktrace,
@ -42,3 +46,55 @@ Future<void> main() async {
); );
} }
} }
/// This widget listens to connectivity changes and switches between
/// `MyApp` and `OfflineScreen` automatically.
class MainWrapper extends StatefulWidget {
const MainWrapper({super.key});
@override
State<MainWrapper> createState() => _MainWrapperState();
}
class _MainWrapperState extends State<MainWrapper> {
// Use a List to store connectivity status as the API now returns a list
List<ConnectivityResult> _connectivityStatus = [ConnectivityResult.none];
final Connectivity _connectivity = Connectivity();
@override
void initState() {
super.initState();
_initializeConnectivity();
// Listen for changes, the callback now provides a List<ConnectivityResult>
_connectivity.onConnectivityChanged.listen((List<ConnectivityResult> results) {
setState(() {
_connectivityStatus = results;
});
});
}
Future<void> _initializeConnectivity() async {
// checkConnectivity() now returns a List<ConnectivityResult>
final result = await _connectivity.checkConnectivity();
setState(() {
_connectivityStatus = result;
});
}
@override
Widget build(BuildContext context) {
// Check if any of the connectivity results indicate no internet
final bool isOffline = _connectivityStatus.contains(ConnectivityResult.none);
// Show OfflineScreen if no internet
if (isOffline) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: OfflineScreen(),
);
}
// Show main app if online
return const MyApp();
}
}

View File

@ -168,10 +168,10 @@ class _MPINAuthScreenState extends State<MPINAuthScreen>
const SizedBox(height: 10), const SizedBox(height: 10),
MyText( MyText(
isChangeMpin isChangeMpin
? 'Set a new 6-digit MPIN for your account.' ? 'Set a new 4-digit MPIN for your account.'
: (isNewUser : (isNewUser
? 'Set your 6-digit MPIN for quick login.' ? 'Set your 4-digit MPIN for quick login.'
: 'Enter your 6-digit MPIN to continue.'), : 'Enter your 4-digit MPIN to continue.'),
fontSize: 14, fontSize: 14,
color: Colors.black54, color: Colors.black54,
textAlign: TextAlign.center, textAlign: TextAlign.center,

View File

@ -6,7 +6,6 @@ import 'package:marco/controller/project_controller.dart';
import 'package:marco/helpers/services/storage/local_storage.dart'; import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/utils/my_shadow.dart'; import 'package:marco/helpers/utils/my_shadow.dart';
// import 'package:marco/helpers/widgets/my_button.dart';
import 'package:marco/helpers/widgets/my_card.dart'; import 'package:marco/helpers/widgets/my_card.dart';
import 'package:marco/helpers/widgets/my_container.dart'; import 'package:marco/helpers/widgets/my_container.dart';
import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_spacing.dart';
@ -53,11 +52,10 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Layout( return Layout(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(10),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
MySpacing.height(12),
_buildDashboardStats(), _buildDashboardStats(),
MySpacing.height(24), MySpacing.height(24),
GetBuilder<ProjectController>( GetBuilder<ProjectController>(
@ -80,64 +78,6 @@ class _DashboardScreenState extends State<DashboardScreen> with UIMixin {
); );
}, },
), ),
// if (!hasMpin) ...[
// MyCard(
// borderRadiusAll: 12,
// paddingAll: 16,
// shadow: MyShadow(elevation: 2),
// color: Colors.red.withOpacity(0.05),
// child: Row(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// const Icon(Icons.warning_amber_rounded,
// color: Colors.redAccent, size: 28),
// MySpacing.width(12),
// Expanded(
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// MyText.bodyMedium(
// "MPIN Not Generated",
// color: Colors.redAccent,
// fontWeight: 700,
// ),
// MySpacing.height(4),
// MyText.bodySmall(
// "To secure your account, please generate your MPIN now.",
// color: contentTheme.onBackground.withOpacity(0.8),
// ),
// MySpacing.height(10),
// Align(
// alignment: Alignment.center,
// child: MyButton.rounded(
// onPressed: () {
// Get.toNamed("/auth/mpin-auth");
// },
// backgroundColor: contentTheme.brandRed,
// padding: const EdgeInsets.symmetric(
// horizontal: 20, vertical: 10),
// child: Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// const Icon(Icons.lock_outline,
// size: 18, color: Colors.white),
// MySpacing.width(8),
// MyText.bodyMedium(
// "Generate MPIN",
// color: Colors.white,
// fontWeight: 600,
// ),
// ],
// ),
// ),
// ),
// ],
// ),
// ),
// ],
// ),
// ),
// ],
], ],
), ),
), ),

View File

@ -0,0 +1,156 @@
import 'package:flutter/material.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/images.dart';
class OfflineScreen extends StatefulWidget {
const OfflineScreen({super.key});
@override
State<OfflineScreen> createState() => _OfflineScreenState();
}
class _OfflineScreenState extends State<OfflineScreen>
with SingleTickerProviderStateMixin, UIMixin {
late AnimationController _controller;
late Animation<double> _logoAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_logoAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeOutBack,
);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
_RedWaveBackground(brandRed: contentTheme.brandRed),
SafeArea(
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ScaleTransition(
scale: _logoAnimation,
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: const [
BoxShadow(
color: Colors.black12,
blurRadius: 10,
offset: Offset(0, 10),
),
],
),
padding: const EdgeInsets.all(20),
child: Image.asset(Images.logoDark),
),
),
// Increased spacing here
const SizedBox(height: 120),
const Icon(Icons.wifi_off,
size: 100, color: Colors.redAccent),
const SizedBox(height: 20),
const Text(
"No Internet Connection",
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 15),
const Text(
"It seems you're currently offline. Please check your network settings or Wi-Fi connection.",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, color: Colors.grey),
),
],
),
),
),
),
],
),
);
}
}
class _RedWaveBackground extends StatelessWidget {
final Color brandRed;
const _RedWaveBackground({required this.brandRed});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _WavePainter(brandRed),
size: Size.infinite,
);
}
}
class _WavePainter extends CustomPainter {
final Color brandRed;
_WavePainter(this.brandRed);
@override
void paint(Canvas canvas, Size size) {
final paint1 = Paint()
..shader = LinearGradient(
colors: [brandRed, const Color.fromARGB(255, 97, 22, 22)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
final path1 = Path()
..moveTo(0, size.height * 0.2)
..quadraticBezierTo(size.width * 0.25, size.height * 0.05,
size.width * 0.5, size.height * 0.15)
..quadraticBezierTo(
size.width * 0.75, size.height * 0.25, size.width, size.height * 0.1)
..lineTo(size.width, 0)
..lineTo(0, 0)
..close();
canvas.drawPath(path1, paint1);
final paint2 = Paint()..color = Colors.redAccent.withOpacity(0.15);
final path2 = Path()
..moveTo(0, size.height * 0.25)
..quadraticBezierTo(
size.width * 0.4, size.height * 0.1, size.width, size.height * 0.2)
..lineTo(size.width, 0)
..lineTo(0, 0)
..close();
canvas.drawPath(path2, paint2);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}

View File

@ -77,6 +77,7 @@ dependencies:
html_editor_enhanced: ^2.7.0 html_editor_enhanced: ^2.7.0
flutter_quill_delta_from_html: ^1.5.2 flutter_quill_delta_from_html: ^1.5.2
quill_delta: ^3.0.0-nullsafety.2 quill_delta: ^3.0.0-nullsafety.2
connectivity_plus: ^6.1.4
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter