715 lines
21 KiB
Dart
715 lines
21 KiB
Dart
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 'package:flutter_localizations/flutter_localizations.dart';
|
||
import 'package:flutter_gen/gen_l10n/app_localizations.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/welcome_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();
|
||
|
||
static void setLocale(BuildContext context, Locale newLocale) {
|
||
final _KMobileState? state =
|
||
context.findAncestorStateOfType<_KMobileState>();
|
||
state?.setLocale(newLocale);
|
||
}
|
||
}
|
||
|
||
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;
|
||
});
|
||
});
|
||
}
|
||
|
||
Locale? _locale;
|
||
|
||
void setLocale(Locale locale) {
|
||
setState(() {
|
||
_locale = locale;
|
||
});
|
||
}
|
||
|
||
/*
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
// Set status bar color
|
||
SystemChrome.setSystemUIOverlayStyle(
|
||
const SystemUiOverlayStyle(
|
||
statusBarColor: Colors.transparent,
|
||
statusBarIconBrightness: Brightness.dark,
|
||
),
|
||
);
|
||
|
||
if (_showSplash) {
|
||
return MaterialApp(
|
||
debugShowCheckedModeBanner: false,
|
||
locale: _locale,
|
||
supportedLocales: const [
|
||
Locale('en'),
|
||
Locale('hi'),
|
||
],
|
||
localizationsDelegates: const [
|
||
AppLocalizations.delegate,
|
||
GlobalMaterialLocalizations.delegate,
|
||
GlobalWidgetsLocalizations.delegate,
|
||
GlobalCupertinoLocalizations.delegate,
|
||
],
|
||
home: const 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(),
|
||
),
|
||
);
|
||
}
|
||
*/
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
// Set status bar color
|
||
SystemChrome.setSystemUIOverlayStyle(
|
||
const SystemUiOverlayStyle(
|
||
statusBarColor: Colors.transparent,
|
||
statusBarIconBrightness: Brightness.dark,
|
||
),
|
||
);
|
||
|
||
return MultiBlocProvider(
|
||
providers: [
|
||
BlocProvider<AuthCubit>(create: (_) => getIt<AuthCubit>()),
|
||
],
|
||
child: MaterialApp(
|
||
debugShowCheckedModeBanner: false,
|
||
locale: _locale, // Use your existing locale variable
|
||
supportedLocales: const [
|
||
Locale('en'),
|
||
Locale('hi'),
|
||
],
|
||
localizationsDelegates: const [
|
||
AppLocalizations.delegate,
|
||
GlobalMaterialLocalizations.delegate,
|
||
GlobalWidgetsLocalizations.delegate,
|
||
GlobalCupertinoLocalizations.delegate,
|
||
],
|
||
title: 'kMobile',
|
||
theme: AppThemes.lightTheme,
|
||
// darkTheme: AppThemes.darkTheme,
|
||
themeMode: ThemeMode.system, // Use system theme by default
|
||
onGenerateRoute: AppRoutes.generateRoute,
|
||
initialRoute: AppRoutes.splash,
|
||
home: _showSplash ? const SplashScreen() : 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 _showWelcome = true;
|
||
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: AppLocalizations.of(context).authenticateToAccess,
|
||
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 opt‐in 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();
|
||
}
|
||
}*/
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
if (_checking) {
|
||
return const SplashScreen();
|
||
}
|
||
// ✅ Step 1: Show welcome screen first, only once
|
||
if (_showWelcome) {
|
||
return WelcomeScreen(
|
||
onContinue: () {
|
||
setState(() {
|
||
_showWelcome = false;
|
||
});
|
||
},
|
||
);
|
||
}
|
||
|
||
// ✅ Step 2: Check login status
|
||
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) {
|
||
return const NavigationScaffold(); // Authenticated
|
||
}
|
||
|
||
// ❌ Biometric failed → 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 {
|
||
// No MPIN set → show MPIN set screen + biometric dialog
|
||
return MPinScreen(
|
||
mode: MPinMode.set,
|
||
onCompleted: (_) async {
|
||
final storage = getIt<SecureStorage>();
|
||
final localAuth = LocalAuthentication();
|
||
|
||
final optIn = await showDialog<bool>(
|
||
context: context,
|
||
barrierDismissible: false,
|
||
builder: (ctx) => AlertDialog(
|
||
title:
|
||
Text(AppLocalizations.of(context).enableFingerprintLogin),
|
||
content:
|
||
Text(AppLocalizations.of(context).enableFingerprintMessage),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(ctx).pop(false),
|
||
child: Text(AppLocalizations.of(context).no),
|
||
),
|
||
TextButton(
|
||
onPressed: () => Navigator.of(ctx).pop(true),
|
||
child: Text(AppLocalizations.of(context).yes),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
|
||
if (optIn == true) {
|
||
final canCheck = await localAuth.canCheckBiometrics;
|
||
bool didAuth = false;
|
||
|
||
if (canCheck) {
|
||
didAuth = await localAuth.authenticate(
|
||
localizedReason:
|
||
AppLocalizations.of(context).authenticateToEnable,
|
||
options: const AuthenticationOptions(
|
||
stickyAuth: true,
|
||
biometricOnly: true,
|
||
),
|
||
);
|
||
await storage.write(
|
||
'biometric_enabled', didAuth ? 'true' : 'false');
|
||
} else {
|
||
await storage.write('biometric_enabled', 'false');
|
||
}
|
||
}
|
||
|
||
Navigator.of(context).pushReplacement(
|
||
MaterialPageRoute(
|
||
builder: (_) => const NavigationScaffold(),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
// ✅ Step 3: If not logged in, show login screen
|
||
return const LoginScreen();
|
||
}
|
||
}
|
||
|
||
/*@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) {
|
||
return const NavigationScaffold(); // Authenticated
|
||
}
|
||
|
||
// Failed or dismissed biometric → Show MPIN
|
||
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();
|
||
final optin = await showDialog<bool>(
|
||
context: context,
|
||
barrierDismissible: false,
|
||
builder: (ctx) => AlertDialog(
|
||
title:
|
||
Text(AppLocalizations.of(context).enableFingerprintLogin),
|
||
content: Text(
|
||
AppLocalizations.of(context).enableFingerprintMessage,
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(ctx).pop(false),
|
||
child: Text(AppLocalizations.of(context).no),
|
||
),
|
||
TextButton(
|
||
onPressed: () => Navigator.of(ctx).pop(true),
|
||
child: Text(AppLocalizations.of(context).yes),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
|
||
if (optin == true) {
|
||
final canCheck = await localAuth.canCheckBiometrics;
|
||
bool didAuth = false;
|
||
|
||
if (canCheck) {
|
||
didAuth = await localAuth.authenticate(
|
||
localizedReason:
|
||
AppLocalizations.of(context).authenticateToEnable,
|
||
options: const AuthenticationOptions(
|
||
stickyAuth: true,
|
||
biometricOnly: true,
|
||
),
|
||
);
|
||
}
|
||
|
||
await storage.write(
|
||
'biometric_enabled',
|
||
didAuth ? 'true' : 'false',
|
||
);
|
||
} else {
|
||
await storage.write('biometric_enabled', 'false');
|
||
}
|
||
|
||
Navigator.of(context).pushReplacement(
|
||
MaterialPageRoute(
|
||
builder: (_) => const NavigationScaffold(),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
// 🔻 Show Welcome screen before login if not logged in
|
||
if (_showWelcome) {
|
||
return WelcomeScreen(
|
||
onContinue: () {
|
||
setState(() {
|
||
_showWelcome = false;
|
||
});
|
||
},
|
||
);
|
||
}
|
||
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: Text(AppLocalizations.of(context).exitApp),
|
||
content: Text(AppLocalizations.of(context).exitConfirmation),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(context).pop(false),
|
||
child: Text(AppLocalizations.of(context).no),
|
||
),
|
||
TextButton(
|
||
onPressed: () => Navigator.of(context).pop(true),
|
||
child: Text(AppLocalizations.of(context).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,
|
||
backgroundColor: const Color(0xFFE0F7FA), // Light blue background
|
||
selectedItemColor: Colors.blue[800],
|
||
unselectedItemColor: Colors.black54,
|
||
onTap: _onItemTapped,
|
||
items: [
|
||
BottomNavigationBarItem(
|
||
icon: const Icon(Icons.home_filled),
|
||
label: AppLocalizations.of(context).home,
|
||
),
|
||
BottomNavigationBarItem(
|
||
icon: const Icon(Icons.credit_card),
|
||
label: AppLocalizations.of(context).card,
|
||
),
|
||
BottomNavigationBarItem(
|
||
icon: const Icon(Icons.miscellaneous_services),
|
||
label: AppLocalizations.of(context).services,
|
||
),
|
||
]),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
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(AppLocalizations.of(context).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: AppLocalizations.of(context).enableFingerprintQuick,
|
||
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: Text(AppLocalizations.of(context).enableFingerprintLogin),
|
||
content: Text(AppLocalizations.of(context).enableFingerprintMessage),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () {
|
||
Navigator.of(ctx).pop(false);
|
||
},
|
||
child: Text(AppLocalizations.of(context).no),
|
||
),
|
||
TextButton(
|
||
onPressed: () {
|
||
Navigator.of(ctx).pop(true);
|
||
},
|
||
child: Text(AppLocalizations.of(context).yes),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
if (result == true) {
|
||
await _handleBiometric(context);
|
||
} else {
|
||
final storage = getIt<SecureStorage>();
|
||
await storage.write('biometric_enabled', 'false');
|
||
onCompleted();
|
||
}
|
||
}
|
||
}
|