kmobile/lib/app.dart
2025-06-02 10:29:32 +05:30

407 lines
12 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/security/secure_storage.dart';
import 'config/themes.dart';
import 'config/routes.dart';
import 'di/injection.dart';
import 'features/auth/controllers/auth_cubit.dart';
import 'features/card/screens/card_management_screen.dart';
import 'features/auth/screens/login_screen.dart';
import 'features/service/screens/service_screen.dart';
import 'features/dashboard/screens/dashboard_screen.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'features/auth/screens/mpin_screen.dart';
import 'package:local_auth/local_auth.dart';
class KMobile extends StatefulWidget {
const KMobile({super.key});
@override
State<KMobile> createState() => _KMobileState();
}
class _KMobileState extends State<KMobile> {
bool _showSplash = false;
@override
void initState() {
super.initState();
// Simulate a splash screen delay
Future.delayed(const Duration(seconds: 2), () {
setState(() {
_showSplash = false;
});
});
}
@override
Widget build(BuildContext context) {
// Set status bar color
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
),
);
if (_showSplash) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: SplashScreen(),
);
}
return MultiBlocProvider(
providers: [
BlocProvider<AuthCubit>(create: (_) => getIt<AuthCubit>()),
],
child: MaterialApp(
title: 'kMobile',
debugShowCheckedModeBanner: false,
theme: AppThemes.lightTheme,
darkTheme: AppThemes.darkTheme,
themeMode: ThemeMode.system, // Use system theme by default
onGenerateRoute: AppRoutes.generateRoute,
initialRoute: AppRoutes.splash,
home: const AuthGate(),
),
);
}
}
class AuthGate extends StatefulWidget {
const AuthGate({super.key});
@override
State<AuthGate> createState() => _AuthGateState();
}
class _AuthGateState extends State<AuthGate> {
bool _checking = true;
bool _isLoggedIn = false;
bool _hasMPin = false;
bool _biometricEnabled = false;
bool _biometricTried = false;
@override
void initState() {
super.initState();
_checkAuth();
}
Future<void> _checkAuth() async {
final storage = getIt<SecureStorage>();
final accessToken = await storage.read('access_token');
final accessTokenExpiry = await storage.read('token_expiry');
final mpin = await storage.read('mpin');
final biometric = await storage.read('biometric_enabled');
setState(() {
_isLoggedIn = accessToken != null &&
accessTokenExpiry != null &&
DateTime.parse(accessTokenExpiry).isAfter(DateTime.now());
_hasMPin = mpin != null;
_biometricEnabled = biometric == 'true';
_checking = false;
});
}
Future<bool> _tryBiometric() async {
if (_biometricTried) return false;
_biometricTried = true;
final localAuth = LocalAuthentication();
final canCheck = await localAuth.canCheckBiometrics;
if (!canCheck) return false;
try {
final didAuth = await localAuth.authenticate(
localizedReason: 'Authenticate to access kMobile',
options: const AuthenticationOptions(
stickyAuth: true,
biometricOnly: true,
),
);
return didAuth;
} catch (_) {
return false;
}
}
@override
Widget build(BuildContext context) {
if (_checking) {
return const SplashScreen();
}
if (_isLoggedIn) {
if (_hasMPin) {
if (_biometricEnabled) {
return FutureBuilder<bool>(
future: _tryBiometric(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SplashScreen();
}
if (snapshot.data == true) {
// Authenticated with biometrics, go to dashboard
return const NavigationScaffold();
}
// If not authenticated or user dismissed, show mPIN screen
return MPinScreen(
mode: MPinMode.enter,
onCompleted: (_) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => const NavigationScaffold()),
);
},
);
},
);
} else {
return MPinScreen(
mode: MPinMode.enter,
onCompleted: (_) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
);
},
);
}
} else {
return MPinScreen(
mode: MPinMode.set,
onCompleted: (_) async {
final storage = getIt<SecureStorage>();
final localAuth = LocalAuthentication();
// 1) Prompt user to optin for biometric
final optIn = await showDialog<bool>(
context: context,
barrierDismissible: false, // force choice
builder: (ctx) => AlertDialog(
title: const Text('Enable Fingerprint Login?'),
content: const Text(
'Would you like to enable fingerprint authentication for faster login?',
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('No'),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('Yes'),
),
],
),
);
// 2) If opted in, perform biometric auth
if (optIn == true) {
final canCheck = await localAuth.canCheckBiometrics;
bool didAuth = false;
if (canCheck) {
didAuth = await localAuth.authenticate(
localizedReason: 'Authenticate to enable fingerprint login',
options: const AuthenticationOptions(
stickyAuth: true,
biometricOnly: true,
),
);
}
await storage.write(
'biometric_enabled', didAuth ? 'true' : 'false');
} else {
await storage.write('biometric_enabled', 'false');
}
// 3) Finally go to your main scaffold
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
);
},
);
}
}
return const LoginScreen();
}
}
class NavigationScaffold extends StatefulWidget {
const NavigationScaffold({super.key});
@override
State<NavigationScaffold> createState() => _NavigationScaffoldState();
}
class _NavigationScaffoldState extends State<NavigationScaffold> {
final PageController _pageController = PageController();
int _selectedIndex = 0;
final List<Widget> _pages = [
const DashboardScreen(),
const CardManagementScreen(),
const ServiceScreen(),
];
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
_pageController.jumpToPage(index);
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (!didPop) {
final shouldExit = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Exit App'),
content: const Text('Do you really want to exit?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('No'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Yes'),
),
],
),
);
if (shouldExit == true) {
if (Platform.isAndroid) {
SystemNavigator.pop();
}
exit(0);
}
}
},
child: Scaffold(
body: PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
children: _pages,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
type: BottomNavigationBarType.fixed,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_filled),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Symbols.credit_card),
label: 'Card',
),
BottomNavigationBarItem(
icon: Icon(Symbols.concierge),
label: 'Services',
),
],
onTap: _onItemTapped,
backgroundColor: const Color(0xFFE0F7FA), // Light blue background
selectedItemColor: Colors.blue[800],
unselectedItemColor: Colors.black54,
),
),
);
}
}
class SplashScreen extends StatelessWidget {
const SplashScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 20),
Text('Loading...',
style: Theme.of(context).textTheme.headlineMedium),
],
),
),
);
}
}
// Add this widget at the end of the file
class BiometricPromptScreen extends StatelessWidget {
final VoidCallback onCompleted;
const BiometricPromptScreen({super.key, required this.onCompleted});
Future<void> _handleBiometric(BuildContext context) async {
final localAuth = LocalAuthentication();
final canCheck = await localAuth.canCheckBiometrics;
if (!canCheck) {
onCompleted();
return;
}
final didAuth = await localAuth.authenticate(
localizedReason: 'Enable fingerprint authentication for quick login?',
options: const AuthenticationOptions(
stickyAuth: true,
biometricOnly: true,
),
);
final storage = getIt<SecureStorage>();
if (didAuth) {
await storage.write('biometric_enabled', 'true');
} else {
await storage.write('biometric_enabled', 'false');
}
onCompleted();
}
@override
Widget build(BuildContext context) {
Future.microtask(() => _showDialog(context));
return const SplashScreen();
}
Future<void> _showDialog(BuildContext context) async {
final result = await showDialog<bool>(
context: context,
barrierDismissible: true,
builder: (ctx) => AlertDialog(
title: const Text('Enable Fingerprint Login?'),
content: const Text(
'Would you like to enable fingerprint authentication for faster login?'),
actions: [
TextButton(
onPressed: () {
Navigator.of(ctx).pop(false);
},
child: const Text('No'),
),
TextButton(
onPressed: () {
Navigator.of(ctx).pop(true);
},
child: const Text('Yes'),
),
],
),
);
if (result == true) {
await _handleBiometric(context);
} else {
final storage = getIt<SecureStorage>();
await storage.write('biometric_enabled', 'false');
onCompleted();
}
}
}