441 lines
13 KiB
Dart
441 lines
13 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 './l10n/app_localizations.dart';
|
|
import 'package:kmobile/features/auth/controllers/theme_cubit.dart';
|
|
import 'package:kmobile/features/auth/controllers/theme_state.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/splash_screen.dart';
|
|
import 'features/auth/screens/login_screen.dart';
|
|
import 'features/service/screens/service_screen.dart';
|
|
import 'features/dashboard/screens/dashboard_screen.dart';
|
|
import 'features/auth/screens/mpin_screen.dart';
|
|
import 'package:local_auth/local_auth.dart';
|
|
import 'package:shared_preferences/shared_preferences.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 = true;
|
|
Locale? _locale;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
loadPreferences();
|
|
Future.delayed(const Duration(seconds: 3), () {
|
|
setState(() {
|
|
showSplash = false;
|
|
});
|
|
});
|
|
}
|
|
|
|
Future<void> loadPreferences() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
// Load Locale
|
|
final String? langCode = prefs.getString('locale');
|
|
if (langCode != null) {
|
|
setState(() {
|
|
_locale = Locale(langCode);
|
|
});
|
|
}
|
|
}
|
|
|
|
void setLocale(Locale locale) {
|
|
setState(() {
|
|
_locale = locale;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
SystemChrome.setSystemUIOverlayStyle(
|
|
const SystemUiOverlayStyle(
|
|
statusBarColor: Colors.transparent,
|
|
statusBarIconBrightness: Brightness.dark,
|
|
),
|
|
);
|
|
|
|
return MultiBlocProvider(
|
|
providers: [
|
|
BlocProvider<AuthCubit>(create: (_) => getIt<AuthCubit>()),
|
|
BlocProvider<ThemeCubit>(create: (_) => ThemeCubit()),
|
|
],
|
|
child: BlocBuilder<ThemeCubit, ThemeState>(
|
|
builder: (context, themeState) {
|
|
return MaterialApp(
|
|
debugShowCheckedModeBanner: false,
|
|
locale: _locale ?? const Locale('en'),
|
|
supportedLocales: const [
|
|
Locale('en'),
|
|
Locale('hi'),
|
|
],
|
|
localizationsDelegates: const [
|
|
AppLocalizations.delegate,
|
|
GlobalMaterialLocalizations.delegate,
|
|
GlobalWidgetsLocalizations.delegate,
|
|
GlobalCupertinoLocalizations.delegate,
|
|
],
|
|
title: 'kMobile',
|
|
theme: themeState.getThemeData(),
|
|
//darkTheme: themeState.getThemeData(),
|
|
themeMode: ThemeMode.light,
|
|
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 _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;
|
|
String localizedReason = "";
|
|
if (mounted) {
|
|
localizedReason = AppLocalizations.of(context).authenticateToAccess;
|
|
}
|
|
try {
|
|
final didAuth = await localAuth.authenticate(
|
|
localizedReason: localizedReason,
|
|
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) {
|
|
return const NavigationScaffold(); // Authenticated
|
|
}
|
|
|
|
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;
|
|
String authEnable = "";
|
|
if (context.mounted) {
|
|
authEnable = AppLocalizations.of(context).authenticateToEnable;
|
|
}
|
|
|
|
if (canCheck) {
|
|
didAuth = await localAuth.authenticate(
|
|
localizedReason: authEnable,
|
|
options: const AuthenticationOptions(
|
|
stickyAuth: true,
|
|
biometricOnly: true,
|
|
),
|
|
);
|
|
await storage.write(
|
|
'biometric_enabled', didAuth ? 'true' : 'false');
|
|
} else {
|
|
await storage.write('biometric_enabled', 'false');
|
|
}
|
|
}
|
|
|
|
if (context.mounted) {
|
|
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(),
|
|
];
|
|
|
|
@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: Theme.of(context)
|
|
.scaffoldBackgroundColor, // Light blue background
|
|
selectedItemColor: Theme.of(context).primaryColor,
|
|
unselectedItemColor: Colors.black54,
|
|
onTap: (index) {
|
|
setState(() {
|
|
_selectedIndex = index;
|
|
});
|
|
_pageController.jumpToPage(index);
|
|
},
|
|
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,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
String localizedReason = "";
|
|
if (context.mounted) {
|
|
localizedReason = AppLocalizations.of(context).enableFingerprintQuick;
|
|
}
|
|
final didAuth = await localAuth.authenticate(
|
|
localizedReason: localizedReason,
|
|
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 (!context.mounted) {
|
|
return;
|
|
}
|
|
if (result == true) {
|
|
await _handleBiometric(context);
|
|
} else {
|
|
final storage = getIt<SecureStorage>();
|
|
await storage.write('biometric_enabled', 'false');
|
|
onCompleted();
|
|
}
|
|
}
|
|
}
|