api integration
This commit is contained in:
426
lib/app.dart
426
lib/app.dart
@@ -1,21 +1,42 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:kmobile/features/customer_info/screens/customer_info_screen.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/auth/controllers/auth_state.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 StatelessWidget {
|
||||
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
|
||||
@@ -26,90 +47,184 @@ class KMobile extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
|
||||
if (_showSplash) {
|
||||
return const MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: SplashScreen(),
|
||||
);
|
||||
}
|
||||
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<AuthCubit>(
|
||||
create: (_) => getIt<AuthCubit>(),
|
||||
),
|
||||
// Add other Bloc/Cubit providers here
|
||||
BlocProvider<AuthCubit>(create: (_) => getIt<AuthCubit>()),
|
||||
],
|
||||
child: MaterialApp(
|
||||
title: 'Banking App',
|
||||
title: 'kMobile',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: AppThemes.lightTheme,
|
||||
darkTheme: AppThemes.darkTheme,
|
||||
themeMode: ThemeMode.system, // Use system theme by default
|
||||
onGenerateRoute: AppRoutes.generateRoute,
|
||||
initialRoute: AppRoutes.splash,
|
||||
builder: (context, child) {
|
||||
return MediaQuery(
|
||||
// Prevent font scaling to maintain design consistency
|
||||
data: MediaQuery.of(context)
|
||||
.copyWith(textScaler: const TextScaler.linear(1.0)),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
home: BlocBuilder<AuthCubit, AuthState>(
|
||||
builder: (context, state) {
|
||||
// Handle different authentication states
|
||||
if (state is AuthInitial || state is AuthLoading) {
|
||||
return const SplashScreen();
|
||||
}
|
||||
|
||||
if(state is ShowBiometricPermission){
|
||||
return const BiometricPermissionScreen();
|
||||
}
|
||||
|
||||
if (state is Authenticated) {
|
||||
return const NavigationScaffold();
|
||||
}
|
||||
|
||||
return const LoginScreen();
|
||||
},
|
||||
),
|
||||
home: const AuthGate(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Simple splash screens component
|
||||
class SplashScreen extends StatelessWidget {
|
||||
const SplashScreen();
|
||||
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) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Replace with your bank logo
|
||||
Image.asset(
|
||||
'assets/images/logo.png',
|
||||
width: 150,
|
||||
height: 150,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return const Icon(
|
||||
Icons.account_balance,
|
||||
size: 100,
|
||||
color: Colors.blue,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'SecureBank',
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const CircularProgressIndicator(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,10 +240,9 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
|
||||
int _selectedIndex = 0;
|
||||
|
||||
final List<Widget> _pages = [
|
||||
DashboardScreen(),
|
||||
CardManagementScreen(),
|
||||
ServiceScreen(),
|
||||
CustomerInfoScreen()
|
||||
const DashboardScreen(),
|
||||
const CardManagementScreen(),
|
||||
const ServiceScreen(),
|
||||
];
|
||||
|
||||
void _onItemTapped(int index) {
|
||||
@@ -140,59 +254,82 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return 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,
|
||||
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 BiometricPermissionScreen extends StatelessWidget {
|
||||
const BiometricPermissionScreen({super.key});
|
||||
class SplashScreen extends StatelessWidget {
|
||||
const SplashScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cubit = context.read<AuthCubit>();
|
||||
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('Would you like to enable biometric authentication?'),
|
||||
ElevatedButton(
|
||||
onPressed: () => cubit.handleBiometricChoice(true),
|
||||
child: const Text('Yes'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => cubit.handleBiometricChoice(false),
|
||||
child: const Text('No, thanks'),
|
||||
),
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: 20),
|
||||
Text('Loading...',
|
||||
style: Theme.of(context).textTheme.headlineMedium),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -200,5 +337,70 @@ class BiometricPermissionScreen extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user