api integration
This commit is contained in:
@@ -12,7 +12,7 @@ class AuthService {
|
||||
Future<AuthToken> login(AuthCredentials credentials) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
'/login',
|
||||
'/api/auth/login',
|
||||
data: credentials.toJson(),
|
||||
);
|
||||
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:kmobile/data/models/user.dart';
|
||||
@@ -6,10 +8,11 @@ class UserService {
|
||||
final Dio _dio;
|
||||
UserService(this._dio);
|
||||
|
||||
Future<List<User>> getUserDetails(String customerNo) async {
|
||||
Future<List<User>> getUserDetails() async {
|
||||
try {
|
||||
final response = await _dio.get('/customer/details');
|
||||
final response = await _dio.get('/api/customer/details');
|
||||
if (response.statusCode == 200) {
|
||||
log('Response: ${response.data}');
|
||||
return (response.data as List)
|
||||
.map((user) => User.fromJson(user))
|
||||
.toList();
|
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:kmobile/features/customer_info/screens/customer_info_screen.dart';
|
||||
import 'package:kmobile/features/auth/screens/mpin_screen.dart';
|
||||
import '../app.dart';
|
||||
import '../features/auth/screens/login_screen.dart';
|
||||
@@ -26,7 +25,6 @@ class AppRoutes {
|
||||
static const String accounts = '/accounts';
|
||||
static const String transactions = '/transactions';
|
||||
static const String payments = '/payments';
|
||||
static const String customer_info = '/customer-info';
|
||||
|
||||
// Route generator
|
||||
static Route<dynamic> generateRoute(RouteSettings settings) {
|
||||
@@ -37,8 +35,8 @@ class AppRoutes {
|
||||
return MaterialPageRoute(builder: (_) => const LoginScreen());
|
||||
|
||||
case mPin:
|
||||
return MaterialPageRoute(builder: (_) => const MPinScreen());
|
||||
|
||||
return MaterialPageRoute(builder: (_) => const MPinScreen(mode: MPinMode.enter,));
|
||||
|
||||
case register:
|
||||
// Placeholder - create the RegisterScreen class and uncomment
|
||||
// return MaterialPageRoute(builder: (_) => const RegisterScreen());
|
||||
@@ -54,9 +52,6 @@ class AppRoutes {
|
||||
|
||||
case dashboard:
|
||||
return MaterialPageRoute(builder: (_) => const DashboardScreen());
|
||||
|
||||
case customer_info:
|
||||
return MaterialPageRoute(builder: (_) => const CustomerInfoScreen());
|
||||
|
||||
case accounts:
|
||||
// Placeholder - create the AccountsScreen class and uncomment
|
||||
|
@@ -11,8 +11,12 @@ class NetworkException extends AppException {
|
||||
NetworkException(super.message);
|
||||
}
|
||||
|
||||
class AuthException extends AppException {
|
||||
AuthException(super.message);
|
||||
class AuthException implements Exception{
|
||||
final String message;
|
||||
AuthException(this.message);
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
class UnexpectedException extends AppException {
|
||||
|
28
lib/data/models/transaction.dart
Normal file
28
lib/data/models/transaction.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
class Transaction {
|
||||
final String? id;
|
||||
final String? name;
|
||||
final String? date;
|
||||
final int? amount;
|
||||
final String? type;
|
||||
|
||||
Transaction({this.id, this.name, this.date, this.amount, this.type});
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'date': date,
|
||||
'amount': amount,
|
||||
'type': type,
|
||||
};
|
||||
}
|
||||
|
||||
factory Transaction.fromJson(Map<String, dynamic> json) {
|
||||
return Transaction(
|
||||
id: json['id'] as String?,
|
||||
name: json['name'] as String?,
|
||||
date: json['date'] as String?,
|
||||
amount: json['amount'] as int?,
|
||||
type: json['type'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,25 +1,30 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:kmobile/data/models/transaction.dart';
|
||||
|
||||
class User extends Equatable {
|
||||
|
||||
final String accountNo;
|
||||
final String accountType;
|
||||
final String bookingNumber;
|
||||
final String branchId;
|
||||
final String currency;
|
||||
final String? accountNo;
|
||||
final String? accountType;
|
||||
final String? bookingNumber;
|
||||
final String? branchId;
|
||||
final String? currency;
|
||||
final String? productType;
|
||||
final String? approvedAmount;
|
||||
final String availableBalance;
|
||||
final String currentBalance;
|
||||
final String name;
|
||||
final String mobileNo;
|
||||
final String address;
|
||||
final String picode;
|
||||
|
||||
final String? availableBalance;
|
||||
final String? currentBalance;
|
||||
final String? name;
|
||||
final String? mobileNo;
|
||||
final String? address;
|
||||
final String? picode;
|
||||
final int? activeAccounts;
|
||||
final String? cifNumber;
|
||||
final String? primaryId;
|
||||
final String? dateOfBirth;
|
||||
final List<Transaction>? transactions;
|
||||
|
||||
const User({
|
||||
required this.accountNo,
|
||||
required this.accountType,
|
||||
required this.bookingNumber,
|
||||
this.bookingNumber,
|
||||
required this.branchId,
|
||||
required this.currency,
|
||||
this.productType,
|
||||
@@ -30,14 +35,42 @@ class User extends Equatable {
|
||||
required this.mobileNo,
|
||||
required this.address,
|
||||
required this.picode,
|
||||
this.transactions,
|
||||
this.activeAccounts,
|
||||
this.cifNumber,
|
||||
this.primaryId,
|
||||
this.dateOfBirth,
|
||||
});
|
||||
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'stAccountNo': accountNo,
|
||||
'stAccountType': accountType,
|
||||
'stBookingNumber': bookingNumber,
|
||||
'stBranchId': branchId,
|
||||
'stCurrency': currency,
|
||||
'stProductType': productType,
|
||||
'stApprovedAmount': approvedAmount,
|
||||
'stAvailableBalance': availableBalance,
|
||||
'stCurrentBalance': currentBalance,
|
||||
'custname': name,
|
||||
'mobileno': mobileNo,
|
||||
'custaddress': address,
|
||||
'picode': picode,
|
||||
'transactions': transactions?.map((tx) => tx.toJson()).toList(),
|
||||
'activeAccounts': activeAccounts,
|
||||
'cifNumber': cifNumber,
|
||||
'primaryId': primaryId,
|
||||
'dateOfBirth': dateOfBirth,
|
||||
};
|
||||
}
|
||||
|
||||
factory User.fromJson(Map<String, dynamic> json) {
|
||||
return User(
|
||||
accountNo: json['stAccountNo'],
|
||||
accountType: json['stAccountType'],
|
||||
bookingNumber: json['stBookingNumber'],
|
||||
branchId: json['stBranchId'],
|
||||
branchId: json['stBranchNo'],
|
||||
currency: json['stCurrency'],
|
||||
productType: json['stProductType'],
|
||||
approvedAmount: json['stApprovedAmount'],
|
||||
@@ -47,9 +80,34 @@ class User extends Equatable {
|
||||
mobileNo: json['mobileno'],
|
||||
address: json['custaddress'],
|
||||
picode: json['picode'],
|
||||
cifNumber: json['cifNumber'],
|
||||
primaryId: json['id'],
|
||||
activeAccounts: json['activeAccounts'] as int?,
|
||||
dateOfBirth: json['custdob'],
|
||||
transactions: (json['transactions'] as List<dynamic>?)
|
||||
?.map((tx) => Transaction.fromJson(tx))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
List<Object?> get props => [accountNo, accountType, bookingNumber, branchId, currency, productType, approvedAmount, availableBalance, currentBalance, name, mobileNo, address, picode];
|
||||
List<Object?> get props => [
|
||||
accountNo,
|
||||
accountType,
|
||||
bookingNumber,
|
||||
branchId,
|
||||
currency,
|
||||
productType,
|
||||
approvedAmount,
|
||||
availableBalance,
|
||||
currentBalance,
|
||||
name,
|
||||
mobileNo,
|
||||
address,
|
||||
picode,
|
||||
transactions,
|
||||
activeAccounts,
|
||||
cifNumber,
|
||||
primaryId,
|
||||
];
|
||||
}
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import 'package:kmobile/api/services/user_service.dart';
|
||||
|
||||
import '../../api/services/auth_service.dart';
|
||||
import '../../features/auth/models/auth_token.dart';
|
||||
import '../../features/auth/models/auth_credentials.dart';
|
||||
@@ -6,28 +8,25 @@ import '../../security/secure_storage.dart';
|
||||
|
||||
class AuthRepository {
|
||||
final AuthService _authService;
|
||||
final UserService _userService;
|
||||
final SecureStorage _secureStorage;
|
||||
|
||||
static const _accessTokenKey = 'access_token';
|
||||
static const _refreshTokenKey = 'refresh_token';
|
||||
static const _tokenExpiryKey = 'token_expiry';
|
||||
static const _userKey = 'user_data';
|
||||
|
||||
AuthRepository(this._authService, this._secureStorage);
|
||||
|
||||
Future<User> login(String username, String password) async {
|
||||
|
||||
AuthRepository(this._authService, this._userService, this._secureStorage);
|
||||
|
||||
Future<List<User>> login(String customerNo, String password) async {
|
||||
// Create credentials and call service
|
||||
final credentials = AuthCredentials(username: username, password: password);
|
||||
final credentials = AuthCredentials(customerNo: customerNo, password: password);
|
||||
final authToken = await _authService.login(credentials);
|
||||
|
||||
// Save token securely
|
||||
await _saveAuthToken(authToken);
|
||||
|
||||
// Get and save user profile
|
||||
final user = await _authService.getUserProfile();
|
||||
await _saveUserData(user);
|
||||
|
||||
return user;
|
||||
final users = await _userService.getUserDetails();
|
||||
return users;
|
||||
}
|
||||
|
||||
Future<bool> isLoggedIn() async {
|
||||
@@ -35,83 +34,29 @@ class AuthRepository {
|
||||
return token != null && !token.isExpired;
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
final token = await _getAuthToken();
|
||||
if (token != null) {
|
||||
try {
|
||||
await _authService.logout(token.refreshToken);
|
||||
} finally {
|
||||
// Clear stored data regardless of logout API success
|
||||
await _clearAuthData();
|
||||
}
|
||||
}
|
||||
}
|
||||
Future<User?> getCurrentUser() async {
|
||||
final userJson = await _secureStorage.read(_userKey);
|
||||
if (userJson != null) {
|
||||
return User.fromJson(userJson);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<String?> getAccessToken() async {
|
||||
final token = await _getAuthToken();
|
||||
if (token == null) return null;
|
||||
|
||||
// If token expired, try to refresh it
|
||||
if (token.isExpired) {
|
||||
final newToken = await _refreshToken(token.refreshToken);
|
||||
if (newToken != null) {
|
||||
return newToken.accessToken;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return token.accessToken;
|
||||
}
|
||||
|
||||
// Private helper methods
|
||||
Future<void> _saveAuthToken(AuthToken token) async {
|
||||
await _secureStorage.write(_accessTokenKey, token.accessToken);
|
||||
await _secureStorage.write(_refreshTokenKey, token.refreshToken);
|
||||
await _secureStorage.write(_tokenExpiryKey, token.expiresAt.toIso8601String());
|
||||
}
|
||||
|
||||
Future<AuthToken?> _getAuthToken() async {
|
||||
final accessToken = await _secureStorage.read(_accessTokenKey);
|
||||
final refreshToken = await _secureStorage.read(_refreshTokenKey);
|
||||
final expiryString = await _secureStorage.read(_tokenExpiryKey);
|
||||
|
||||
if (accessToken != null && refreshToken != null && expiryString != null) {
|
||||
|
||||
if (accessToken != null && expiryString != null) {
|
||||
return AuthToken(
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken,
|
||||
expiresAt: DateTime.parse(expiryString),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _saveUserData(User user) async {
|
||||
await _secureStorage.write(_userKey, user);
|
||||
}
|
||||
|
||||
Future<void> _clearAuthData() async {
|
||||
await _secureStorage.delete(_accessTokenKey);
|
||||
await _secureStorage.delete(_refreshTokenKey);
|
||||
await _secureStorage.delete(_tokenExpiryKey);
|
||||
await _secureStorage.delete(_userKey);
|
||||
}
|
||||
|
||||
Future<AuthToken?> _refreshToken(String refreshToken) async {
|
||||
try {
|
||||
final newToken = await _authService.refreshToken(refreshToken);
|
||||
await _saveAuthToken(newToken);
|
||||
return newToken;
|
||||
} catch (e) {
|
||||
// If refresh fails, clear auth data
|
||||
await _clearAuthData();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:kmobile/api/services/user_service.dart';
|
||||
import '../api/services/auth_service.dart';
|
||||
import '../api/interceptors/auth_interceptor.dart';
|
||||
import '../data/repositories/auth_repository.dart';
|
||||
@@ -11,31 +12,35 @@ final getIt = GetIt.instance;
|
||||
Future<void> setupDependencies() async {
|
||||
// Register Dio client
|
||||
getIt.registerSingleton<Dio>(_createDioClient());
|
||||
|
||||
|
||||
// Register secure storage
|
||||
getIt.registerSingleton<SecureStorage>(SecureStorage());
|
||||
|
||||
|
||||
// Register user service if needed
|
||||
getIt.registerSingleton<UserService>(UserService(getIt<Dio>()));
|
||||
|
||||
// Register services
|
||||
getIt.registerSingleton<AuthService>(AuthService(getIt<Dio>()));
|
||||
|
||||
|
||||
// Register repositories
|
||||
getIt.registerSingleton<AuthRepository>(
|
||||
AuthRepository(getIt<AuthService>(), getIt<SecureStorage>()),
|
||||
AuthRepository(
|
||||
getIt<AuthService>(), getIt<UserService>(), getIt<SecureStorage>()),
|
||||
);
|
||||
|
||||
|
||||
// Add auth interceptor after repository is available
|
||||
getIt<Dio>().interceptors.add(
|
||||
AuthInterceptor(getIt<AuthRepository>(), getIt<Dio>()),
|
||||
);
|
||||
|
||||
AuthInterceptor(getIt<AuthRepository>(), getIt<Dio>()),
|
||||
);
|
||||
|
||||
// Register controllers/cubits
|
||||
getIt.registerFactory<AuthCubit>(() => AuthCubit(getIt<AuthRepository>()));
|
||||
getIt.registerFactory<AuthCubit>(() => AuthCubit(getIt<AuthRepository>(), getIt<UserService>()));
|
||||
}
|
||||
|
||||
Dio _createDioClient() {
|
||||
final dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: 'http://localhost:3000',
|
||||
baseUrl: 'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080',
|
||||
connectTimeout: const Duration(seconds: 5),
|
||||
receiveTimeout: const Duration(seconds: 3),
|
||||
headers: {
|
||||
@@ -44,7 +49,7 @@ Dio _createDioClient() {
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
// Add logging interceptor for development
|
||||
dio.interceptors.add(LogInterceptor(
|
||||
request: true,
|
||||
@@ -54,6 +59,6 @@ Dio _createDioClient() {
|
||||
responseBody: true,
|
||||
error: true,
|
||||
));
|
||||
|
||||
|
||||
return dio;
|
||||
}
|
||||
}
|
||||
|
@@ -1,49 +1,71 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:kmobile/data/models/user.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
|
||||
class AccountInfoScreen extends StatefulWidget {
|
||||
const AccountInfoScreen({super.key});
|
||||
final User user;
|
||||
|
||||
const AccountInfoScreen({super.key, required this.user});
|
||||
|
||||
@override
|
||||
State<AccountInfoScreen> createState() => _AccountInfoScreen();
|
||||
}
|
||||
|
||||
class _AccountInfoScreen extends State<AccountInfoScreen>{
|
||||
class _AccountInfoScreen extends State<AccountInfoScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final user = widget.user;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(icon: const Icon(Symbols.arrow_back_ios_new),
|
||||
leading: IconButton(
|
||||
icon: const Icon(Symbols.arrow_back_ios_new),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},),
|
||||
title: const Text('Account Info', style: TextStyle(color: Colors.black,
|
||||
fontWeight: FontWeight.w500),),
|
||||
},
|
||||
),
|
||||
title: const Text(
|
||||
'Account Info',
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
||||
),
|
||||
centerTitle: false,
|
||||
actions: const [
|
||||
actions: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: 10.0),
|
||||
padding: const EdgeInsets.only(right: 10.0),
|
||||
child: CircleAvatar(
|
||||
backgroundImage: AssetImage('assets/images/avatar.jpg'), // Replace with your image
|
||||
backgroundColor: Colors.grey[200],
|
||||
radius: 20,
|
||||
child: SvgPicture.asset(
|
||||
'assets/images/avatar_male.svg',
|
||||
width: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
children: const [
|
||||
InfoRow(title: 'Account Number', value: '700127638009871'),
|
||||
// InfoRow(title: 'Nominee Customer No', value: '700127638009871'),
|
||||
InfoRow(title: 'SMS Service', value: 'Active'),
|
||||
InfoRow(title: 'Missed Call Service', value: 'Active'),
|
||||
InfoRow(title: 'Customer Number', value: '9000875272000212'),
|
||||
InfoRow(title: 'Product Name', value: 'SAVINGS-PERSONAL'),
|
||||
InfoRow(title: 'Account Opening Date', value: '12-09-2012'),
|
||||
InfoRow(title: 'Account Status', value: 'OPEN'),
|
||||
InfoRow(title: 'Available Balance', value: '12,000 CR'),
|
||||
InfoRow(title: 'Interest Rate', value: '12.00'),
|
||||
children: [
|
||||
InfoRow(title: 'Account Number', value: user.accountNo ?? 'N/A'),
|
||||
// InfoRow(title: 'Nominee Customer No', value: user.nomineeCustomerNo),
|
||||
// InfoRow(title: 'SMS Service', value: user.smsService),
|
||||
// InfoRow(title: 'Missed Call Service', value: user.missedCallService),
|
||||
InfoRow(title: 'Customer Number', value: user.cifNumber ?? 'N/A'),
|
||||
InfoRow(title: 'Product Name', value: user.productType ?? 'N/A'),
|
||||
// InfoRow(title: 'Account Opening Date', value: user.accountOpeningDate ?? 'N/A'),
|
||||
const InfoRow(title: 'Account Status', value: 'OPEN'),
|
||||
InfoRow(
|
||||
title: 'Available Balance',
|
||||
value: user.availableBalance ?? 'N/A'),
|
||||
InfoRow(
|
||||
title: 'Current Balance', value: user.currentBalance ?? 'N/A'),
|
||||
|
||||
user.approvedAmount != null
|
||||
? InfoRow(
|
||||
title: 'Approved Amount', value: user.approvedAmount ?? 'N/A')
|
||||
: const SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -58,19 +80,30 @@ class InfoRow extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 14)),
|
||||
const SizedBox(height: 4),
|
||||
Text(value, style: const TextStyle(fontSize: 14)),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,13 +1,14 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:kmobile/api/services/user_service.dart';
|
||||
import 'package:kmobile/core/errors/exceptions.dart';
|
||||
import '../../../data/repositories/auth_repository.dart';
|
||||
import 'auth_state.dart';
|
||||
|
||||
class AuthCubit extends Cubit<AuthState> {
|
||||
final AuthRepository _authRepository;
|
||||
final UserService _userService;
|
||||
|
||||
AuthCubit(this._authRepository) : super(AuthInitial()) {
|
||||
AuthCubit(this._authRepository, this._userService) : super(AuthInitial()) {
|
||||
checkAuthStatus();
|
||||
}
|
||||
|
||||
@@ -16,12 +17,8 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
try {
|
||||
final isLoggedIn = await _authRepository.isLoggedIn();
|
||||
if (isLoggedIn) {
|
||||
final user = await _authRepository.getCurrentUser();
|
||||
if (user != null) {
|
||||
emit(Authenticated(user));
|
||||
} else {
|
||||
emit(Unauthenticated());
|
||||
}
|
||||
final users = await _userService.getUserDetails();
|
||||
emit(Authenticated(users));
|
||||
} else {
|
||||
emit(Unauthenticated());
|
||||
}
|
||||
@@ -30,79 +27,24 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> login(String username, String password) async {
|
||||
Future<void> refreshUserData() async {
|
||||
try {
|
||||
// emit(AuthLoading());
|
||||
final users = await _userService.getUserDetails();
|
||||
emit(Authenticated(users));
|
||||
} catch (e) {
|
||||
emit(AuthError('Failed to refresh user data: ${e.toString()}'));
|
||||
// Optionally, re-emit the previous state or handle as needed
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> login(String customerNo, String password) async {
|
||||
emit(AuthLoading());
|
||||
try {
|
||||
final user = await _authRepository.login(username, password);
|
||||
emit(Authenticated(user));
|
||||
final users = await _authRepository.login(customerNo, password);
|
||||
emit(Authenticated(users));
|
||||
} catch (e) {
|
||||
emit(AuthError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
emit(AuthLoading());
|
||||
try {
|
||||
await _authRepository.logout();
|
||||
emit(Unauthenticated());
|
||||
} catch (e) {
|
||||
emit(AuthError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> checkFirstLaunch() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final isFirstLaunch = prefs.getBool('isFirstLaunch') ?? true;
|
||||
|
||||
if (isFirstLaunch) {
|
||||
emit(ShowBiometricPermission());
|
||||
} else {
|
||||
// Continue to authentication logic (e.g., check token)
|
||||
emit(AuthLoading()); // or Unauthenticated/Authenticated
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleBiometricChoice(bool enabled) async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('biometric_opt_in', enabled);
|
||||
await prefs.setBool('isFirstLaunch', false);
|
||||
|
||||
// Then continue to auth logic or home
|
||||
if (enabled) {
|
||||
authenticateBiometric(); // implement biometric logic
|
||||
} else {
|
||||
emit(Unauthenticated());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> authenticateBiometric() async {
|
||||
final LocalAuthentication auth = LocalAuthentication();
|
||||
|
||||
try {
|
||||
final isAvailable = await auth.canCheckBiometrics;
|
||||
final isDeviceSupported = await auth.isDeviceSupported();
|
||||
|
||||
if (isAvailable && isDeviceSupported) {
|
||||
final authenticated = await auth.authenticate(
|
||||
localizedReason: 'Touch the fingerprint sensor',
|
||||
options: const AuthenticationOptions(
|
||||
biometricOnly: true,
|
||||
stickyAuth: true,
|
||||
),
|
||||
);
|
||||
|
||||
if (authenticated) {
|
||||
// Continue to normal auth logic (e.g., auto login)
|
||||
emit(AuthLoading());
|
||||
await checkAuthStatus(); // Your existing method to verify token/session
|
||||
} else {
|
||||
emit(Unauthenticated());
|
||||
}
|
||||
} else {
|
||||
emit(Unauthenticated());
|
||||
}
|
||||
} catch (e) {
|
||||
emit(Unauthenticated());
|
||||
emit(AuthError(e is AuthException ? e.message : e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -10,15 +10,13 @@ class AuthInitial extends AuthState {}
|
||||
|
||||
class AuthLoading extends AuthState {}
|
||||
|
||||
class ShowBiometricPermission extends AuthState {}
|
||||
|
||||
class Authenticated extends AuthState {
|
||||
final User user;
|
||||
final List<User> users;
|
||||
|
||||
Authenticated(this.user);
|
||||
Authenticated(this.users);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [user];
|
||||
List<Object?> get props => [users];
|
||||
}
|
||||
|
||||
class Unauthenticated extends AuthState {}
|
||||
|
@@ -1,11 +1,11 @@
|
||||
class AuthCredentials {
|
||||
final String username;
|
||||
final String customerNo;
|
||||
final String password;
|
||||
|
||||
AuthCredentials({required this.username, required this.password});
|
||||
AuthCredentials({required this.customerNo, required this.password});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'customer_no': username,
|
||||
'customerNo': customerNo,
|
||||
'password': password,
|
||||
};
|
||||
}
|
||||
|
@@ -1,26 +1,48 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class AuthToken extends Equatable {
|
||||
final String accessToken;
|
||||
final String refreshToken;
|
||||
final DateTime expiresAt;
|
||||
|
||||
const AuthToken({
|
||||
required this.accessToken,
|
||||
required this.refreshToken,
|
||||
required this.expiresAt,
|
||||
});
|
||||
|
||||
factory AuthToken.fromJson(Map<String, dynamic> json) {
|
||||
return AuthToken(
|
||||
accessToken: json['access_token'],
|
||||
refreshToken: json['refresh_token'],
|
||||
expiresAt: DateTime.parse(json['expires_at']),
|
||||
accessToken: json['token'],
|
||||
expiresAt: _decodeExpiryFromToken(json['token']),
|
||||
);
|
||||
}
|
||||
|
||||
static DateTime _decodeExpiryFromToken(String token) {
|
||||
try {
|
||||
final parts = token.split('.');
|
||||
if (parts.length != 3) {
|
||||
throw Exception('Invalid JWT');
|
||||
}
|
||||
final payload = parts[1];
|
||||
// Pad the payload if necessary
|
||||
String normalized = base64Url.normalize(payload);
|
||||
final payloadMap = json.decode(utf8.decode(base64Url.decode(normalized)));
|
||||
if (payloadMap is! Map<String, dynamic> || !payloadMap.containsKey('exp')) {
|
||||
throw Exception('Invalid payload');
|
||||
}
|
||||
final exp = payloadMap['exp'];
|
||||
return DateTime.fromMillisecondsSinceEpoch(exp * 1000);
|
||||
} catch (e) {
|
||||
// Fallback: 1 hour from now if decoding fails
|
||||
log(e.toString());
|
||||
return DateTime.now().add(const Duration(hours: 1));
|
||||
}
|
||||
}
|
||||
|
||||
bool get isExpired => DateTime.now().isAfter(expiresAt);
|
||||
|
||||
@override
|
||||
List<Object> get props => [accessToken, refreshToken, expiresAt];
|
||||
List<Object> get props => [accessToken, expiresAt];
|
||||
}
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:kmobile/di/injection.dart';
|
||||
import 'package:kmobile/features/auth/screens/mpin_screen.dart';
|
||||
import 'package:kmobile/security/secure_storage.dart';
|
||||
import '../../../app.dart';
|
||||
import '../controllers/auth_cubit.dart';
|
||||
import '../controllers/auth_state.dart';
|
||||
@@ -27,16 +28,12 @@ class LoginScreenState extends State<LoginScreen> {
|
||||
}
|
||||
|
||||
void _submitForm() {
|
||||
// if (_formKey.currentState!.validate()) {
|
||||
// context.read<AuthCubit>().login(
|
||||
// _customerNumberController.text.trim(),
|
||||
// _passwordController.text,
|
||||
// );
|
||||
// }
|
||||
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (context) => const MPinScreen()),
|
||||
);
|
||||
if (_formKey.currentState!.validate()) {
|
||||
context.read<AuthCubit>().login(
|
||||
_customerNumberController.text.trim(),
|
||||
_passwordController.text,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -44,11 +41,30 @@ class LoginScreenState extends State<LoginScreen> {
|
||||
return Scaffold(
|
||||
// appBar: AppBar(title: const Text('Login')),
|
||||
body: BlocConsumer<AuthCubit, AuthState>(
|
||||
listener: (context, state) {
|
||||
listener: (context, state) async {
|
||||
if (state is Authenticated) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (context) => const NavigationScaffold()),
|
||||
);
|
||||
final storage = getIt<SecureStorage>();
|
||||
final mpin = await storage.read('mpin');
|
||||
if (mpin == null) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MPinScreen(
|
||||
mode: MPinMode.set,
|
||||
onCompleted: (_) {
|
||||
Navigator.of(context, rootNavigator: true)
|
||||
.pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const NavigationScaffold()),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
|
||||
);
|
||||
}
|
||||
} else if (state is AuthError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.message)),
|
||||
@@ -62,18 +78,23 @@ class LoginScreenState extends State<LoginScreen> {
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
// crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Bank logo or app branding
|
||||
SvgPicture.asset('assets/images/kccb_logo.svg', width: 100, height: 100,),
|
||||
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: 16),
|
||||
// Title
|
||||
const Text(
|
||||
'KCCB',
|
||||
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.blue),
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
|
||||
|
||||
TextFormField(
|
||||
controller: _customerNumberController,
|
||||
decoration: const InputDecoration(
|
||||
@@ -99,8 +120,8 @@ class LoginScreenState extends State<LoginScreen> {
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
decoration: InputDecoration(
|
||||
@@ -118,8 +139,8 @@ class LoginScreenState extends State<LoginScreen> {
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscurePassword
|
||||
? Icons.visibility
|
||||
_obscurePassword
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
),
|
||||
onPressed: () {
|
||||
@@ -138,40 +159,29 @@ class LoginScreenState extends State<LoginScreen> {
|
||||
return null;
|
||||
},
|
||||
),
|
||||
|
||||
// Align(
|
||||
// alignment: Alignment.centerRight,
|
||||
// child: TextButton(
|
||||
// onPressed: () {
|
||||
// // Navigate to forgot password screen
|
||||
// },
|
||||
// child: const Text('Forgot Password?'),
|
||||
// ),
|
||||
// ),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
|
||||
SizedBox(
|
||||
width: 250,
|
||||
child: ElevatedButton(
|
||||
onPressed: state is AuthLoading ? null : _submitForm,
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: const StadiumBorder(),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.blueAccent,
|
||||
side: const BorderSide(color: Colors.black, width: 1),
|
||||
elevation: 0
|
||||
),
|
||||
shape: const StadiumBorder(),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.blueAccent,
|
||||
side: const BorderSide(color: Colors.black, width: 1),
|
||||
elevation: 0),
|
||||
child: state is AuthLoading
|
||||
? const CircularProgressIndicator()
|
||||
: const Text('Login', style: TextStyle(fontSize: 16),),
|
||||
: const Text(
|
||||
'Login',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// OR Divider
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Row(
|
||||
@@ -185,26 +195,22 @@ class LoginScreenState extends State<LoginScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 25),
|
||||
|
||||
// Register Button
|
||||
SizedBox(
|
||||
width: 250,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
// Handle register
|
||||
},
|
||||
//disable until registration is implemented
|
||||
onPressed: null,
|
||||
style: OutlinedButton.styleFrom(
|
||||
shape: const StadiumBorder(),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
backgroundColor: Colors.lightBlue[100],
|
||||
foregroundColor: Colors.black
|
||||
),
|
||||
shape: const StadiumBorder(),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
backgroundColor: Colors.lightBlue[100],
|
||||
foregroundColor: Colors.black),
|
||||
child: const Text('Register'),
|
||||
),
|
||||
),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -213,4 +219,4 @@ class LoginScreenState extends State<LoginScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,24 +1,72 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import '../../../app.dart';
|
||||
import 'package:kmobile/app.dart';
|
||||
import 'package:kmobile/di/injection.dart';
|
||||
import 'package:kmobile/security/secure_storage.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
|
||||
enum MPinMode { enter, set, confirm }
|
||||
|
||||
class MPinScreen extends StatefulWidget {
|
||||
const MPinScreen({super.key});
|
||||
final MPinMode mode;
|
||||
final String? initialPin;
|
||||
final void Function(String pin)? onCompleted;
|
||||
|
||||
const MPinScreen({
|
||||
super.key,
|
||||
required this.mode,
|
||||
this.initialPin,
|
||||
this.onCompleted,
|
||||
});
|
||||
|
||||
@override
|
||||
MPinScreenState createState() => MPinScreenState();
|
||||
State<MPinScreen> createState() => _MPinScreenState();
|
||||
}
|
||||
|
||||
class MPinScreenState extends State<MPinScreen> {
|
||||
class _MPinScreenState extends State<MPinScreen> {
|
||||
List<String> mPin = [];
|
||||
String? errorText;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.mode == MPinMode.enter) {
|
||||
_tryBiometricBeforePin();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _tryBiometricBeforePin() async {
|
||||
final storage = getIt<SecureStorage>();
|
||||
final enabled = await storage.read('biometric_enabled');
|
||||
log('biometric_enabled: $enabled');
|
||||
if (enabled) {
|
||||
final auth = LocalAuthentication();
|
||||
if (await auth.canCheckBiometrics) {
|
||||
final didAuth = await auth.authenticate(
|
||||
localizedReason: 'Authenticate to access kMobile',
|
||||
options: const AuthenticationOptions(biometricOnly: true),
|
||||
);
|
||||
if (didAuth && mounted) {
|
||||
// success → directly “complete” your flow
|
||||
widget.onCompleted?.call('');
|
||||
// or navigate yourself:
|
||||
// Navigator.of(context).pushReplacement(
|
||||
// MaterialPageRoute(builder: (_) => const NavigationScaffold()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addDigit(String digit) {
|
||||
if (mPin.length < 4) {
|
||||
setState(() {
|
||||
mPin.add(digit);
|
||||
errorText = null;
|
||||
});
|
||||
if (mPin.length == 4) {
|
||||
_handleComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,10 +74,63 @@ class MPinScreenState extends State<MPinScreen> {
|
||||
if (mPin.isNotEmpty) {
|
||||
setState(() {
|
||||
mPin.removeLast();
|
||||
errorText = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleComplete() async {
|
||||
final pin = mPin.join();
|
||||
final storage = SecureStorage();
|
||||
|
||||
switch (widget.mode) {
|
||||
case MPinMode.enter:
|
||||
final storedPin = await storage.read('mpin');
|
||||
log('storedPin: $storedPin');
|
||||
if (storedPin == int.tryParse(pin)) {
|
||||
widget.onCompleted?.call(pin);
|
||||
} else {
|
||||
setState(() {
|
||||
errorText = "Incorrect mPIN. Try again.";
|
||||
mPin.clear();
|
||||
});
|
||||
}
|
||||
break;
|
||||
case MPinMode.set:
|
||||
// propagate parent onCompleted into confirm step
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MPinScreen(
|
||||
mode: MPinMode.confirm,
|
||||
initialPin: pin,
|
||||
onCompleted: widget.onCompleted, // <-- use parent callback
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case MPinMode.confirm:
|
||||
if (widget.initialPin == pin) {
|
||||
// 1) persist the pin
|
||||
await storage.write('mpin', pin);
|
||||
|
||||
// 3) now clear the entire navigation stack and go to your main scaffold
|
||||
if (mounted) {
|
||||
Navigator.of(context, rootNavigator: true).pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setState(() {
|
||||
errorText = "Pins do not match. Try again.";
|
||||
mPin.clear();
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildMPinDots() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -63,24 +164,16 @@ class MPinScreenState extends State<MPinScreen> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
onTap: () {
|
||||
if (key == '<') {
|
||||
deleteDigit();
|
||||
} else if (key == 'Enter') {
|
||||
if (mPin.length == 4) {
|
||||
// String storedMpin = await SecureStorage().read("mpin");
|
||||
// if(!mounted) return;
|
||||
// if(storedMpin == mPin.join()) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const NavigationScaffold()),
|
||||
);
|
||||
// }
|
||||
_handleComplete();
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("Please enter 4 digits")),
|
||||
);
|
||||
setState(() {
|
||||
errorText = "Please enter 4 digits";
|
||||
});
|
||||
}
|
||||
} else if (key.isNotEmpty) {
|
||||
addDigit(key);
|
||||
@@ -94,15 +187,15 @@ class MPinScreenState extends State<MPinScreen> {
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: key == 'Enter' ? const Icon(Symbols.check) : Text(
|
||||
key == '<' ? '⌫' : key,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: key == 'Enter' ?
|
||||
FontWeight.normal : FontWeight.normal,
|
||||
color: key == 'Enter' ? Colors.blue : Colors.black,
|
||||
),
|
||||
),
|
||||
child: key == 'Enter'
|
||||
? const Icon(Icons.check)
|
||||
: Text(
|
||||
key == '<' ? '⌫' : key,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: key == 'Enter' ? Colors.blue : Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -112,34 +205,40 @@ class MPinScreenState extends State<MPinScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
String getTitle() {
|
||||
switch (widget.mode) {
|
||||
case MPinMode.enter:
|
||||
return "Enter your mPIN";
|
||||
case MPinMode.set:
|
||||
return "Set your new mPIN";
|
||||
case MPinMode.confirm:
|
||||
return "Confirm your mPIN";
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cubit = context.read<AuthCubit>();
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const Spacer(),
|
||||
// Logo
|
||||
const FlutterLogo(size: 100),
|
||||
Image.asset('assets/images/logo.png', height: 100),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
"Enter your mPIN",
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
|
||||
Text(
|
||||
getTitle(),
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
buildMPinDots(),
|
||||
if (errorText != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child:
|
||||
Text(errorText!, style: const TextStyle(color: Colors.red)),
|
||||
),
|
||||
const Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TextButton(onPressed: () {
|
||||
cubit.authenticateBiometric();
|
||||
}, child: const Text("Try another way")),
|
||||
TextButton(onPressed: () {}, child: const Text("Register?")),
|
||||
],
|
||||
),
|
||||
buildNumberPad(),
|
||||
const Spacer(),
|
||||
],
|
||||
@@ -147,4 +246,4 @@ class MPinScreenState extends State<MPinScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,144 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
|
||||
import 'package:kmobile/features/auth/screens/mpin_setup_confirm.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
|
||||
class MPinSetupScreen extends StatefulWidget {
|
||||
const MPinSetupScreen({super.key});
|
||||
|
||||
@override
|
||||
MPinSetupScreenState createState() => MPinSetupScreenState();
|
||||
}
|
||||
|
||||
class MPinSetupScreenState extends State<MPinSetupScreen> {
|
||||
List<String> mPin = [];
|
||||
|
||||
void addDigit(String digit) {
|
||||
if (mPin.length < 4) {
|
||||
setState(() {
|
||||
mPin.add(digit);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void deleteDigit() {
|
||||
if (mPin.isNotEmpty) {
|
||||
setState(() {
|
||||
mPin.removeLast();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildMPinDots() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(4, (index) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(8),
|
||||
width: 15,
|
||||
height: 15,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: index < mPin.length ? Colors.black : Colors.grey[400],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildNumberPad() {
|
||||
List<List<String>> keys = [
|
||||
['1', '2', '3'],
|
||||
['4', '5', '6'],
|
||||
['7', '8', '9'],
|
||||
['Enter', '0', '<']
|
||||
];
|
||||
|
||||
return Column(
|
||||
children: keys.map((row) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: row.map((key) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (key == '<') {
|
||||
deleteDigit();
|
||||
} else if (key == 'Enter') {
|
||||
if (mPin.length == 4) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => MPinSetupConfirmScreen(mPin: mPin,)),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("Please enter 4 digits")),
|
||||
);
|
||||
}
|
||||
} else if (key.isNotEmpty) {
|
||||
addDigit(key);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: 70,
|
||||
height: 70,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: key == 'Enter' ? const Icon(Symbols.check) : Text(
|
||||
key == '<' ? '⌫' : key,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: key == 'Enter' ?
|
||||
FontWeight.normal : FontWeight.normal,
|
||||
color: key == 'Enter' ? Colors.blue : Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cubit = context.read<AuthCubit>();
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const Spacer(),
|
||||
// Logo
|
||||
const FlutterLogo(size: 100),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
"Enter your mPIN",
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
buildMPinDots(),
|
||||
const Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TextButton(onPressed: () {
|
||||
cubit.authenticateBiometric();
|
||||
}, child: const Text("Try another way")),
|
||||
TextButton(onPressed: () {}, child: const Text("Register?")),
|
||||
],
|
||||
),
|
||||
buildNumberPad(),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,154 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
|
||||
import 'package:kmobile/security/secure_storage.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../../app.dart';
|
||||
|
||||
class MPinSetupConfirmScreen extends StatefulWidget {
|
||||
final List<String> mPin;
|
||||
const MPinSetupConfirmScreen({super.key, required this.mPin});
|
||||
|
||||
@override
|
||||
MPinSetupConfirmScreenState createState() => MPinSetupConfirmScreenState();
|
||||
}
|
||||
|
||||
class MPinSetupConfirmScreenState extends State<MPinSetupConfirmScreen> {
|
||||
List<String> mPin = [];
|
||||
|
||||
void addDigit(String digit) {
|
||||
if (mPin.length < 4) {
|
||||
setState(() {
|
||||
mPin.add(digit);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void deleteDigit() {
|
||||
if (mPin.isNotEmpty) {
|
||||
setState(() {
|
||||
mPin.removeLast();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildMPinDots() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(4, (index) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(8),
|
||||
width: 15,
|
||||
height: 15,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: index < mPin.length ? Colors.black : Colors.grey[400],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildNumberPad() {
|
||||
List<List<String>> keys = [
|
||||
['1', '2', '3'],
|
||||
['4', '5', '6'],
|
||||
['7', '8', '9'],
|
||||
['Enter', '0', '<']
|
||||
];
|
||||
|
||||
return Column(
|
||||
children: keys.map((row) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: row.map((key) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
if (key == '<') {
|
||||
deleteDigit();
|
||||
} else if (key == 'Enter') {
|
||||
if (mPin.length == 4 && mPin.join() == widget.mPin.join()) {
|
||||
|
||||
await SecureStorage().write("mpin", mPin.join());
|
||||
await SharedPreferences.getInstance()
|
||||
.then((prefs) => prefs.setBool('mpin_set', true));
|
||||
if(!mounted) return;
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const NavigationScaffold()),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("Please enter 4 digits")),
|
||||
);
|
||||
}
|
||||
} else if (key.isNotEmpty) {
|
||||
addDigit(key);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: 70,
|
||||
height: 70,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.grey[200],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: key == 'Enter' ? const Icon(Symbols.check) : Text(
|
||||
key == '<' ? '⌫' : key,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: key == 'Enter' ?
|
||||
FontWeight.normal : FontWeight.normal,
|
||||
color: key == 'Enter' ? Colors.blue : Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cubit = context.read<AuthCubit>();
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const Spacer(),
|
||||
// Logo
|
||||
const FlutterLogo(size: 100),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
"Enter your mPIN",
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
buildMPinDots(),
|
||||
const Spacer(),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TextButton(onPressed: () {
|
||||
cubit.authenticateBiometric();
|
||||
}, child: const Text("Try another way")),
|
||||
TextButton(onPressed: () {}, child: const Text("Register?")),
|
||||
],
|
||||
),
|
||||
buildNumberPad(),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,69 +1,109 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:kmobile/data/models/user.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
|
||||
class CustomerInfoScreen extends StatefulWidget {
|
||||
const CustomerInfoScreen({super.key});
|
||||
final User user;
|
||||
const CustomerInfoScreen({super.key, required this.user});
|
||||
|
||||
@override
|
||||
State<CustomerInfoScreen> createState() => _CustomerInfoScreen();
|
||||
State<CustomerInfoScreen> createState() => _CustomerInfoScreenState();
|
||||
}
|
||||
|
||||
class _CustomerInfoScreen extends State<CustomerInfoScreen>{
|
||||
class _CustomerInfoScreenState extends State<CustomerInfoScreen> {
|
||||
late final User user = widget.user;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(icon: const Icon(Symbols.arrow_back_ios_new),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},),
|
||||
title: const Text('kMobile', style: TextStyle(color: Colors.black,
|
||||
fontWeight: FontWeight.w500),),
|
||||
actions: const [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: 10.0),
|
||||
child: CircleAvatar(
|
||||
backgroundImage: AssetImage('assets/images/avatar.jpg'), // Replace with your image
|
||||
radius: 20,
|
||||
),
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Symbols.arrow_back_ios_new),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
body: const SingleChildScrollView(
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: SafeArea(
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 30),
|
||||
CircleAvatar(
|
||||
backgroundImage: AssetImage('assets/images/avatar.jpg'), // Replace with your image
|
||||
radius: 50,
|
||||
title: const Text(
|
||||
'kMobile',
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
||||
),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 10.0),
|
||||
child: CircleAvatar(
|
||||
backgroundColor: Colors.grey[200],
|
||||
radius: 20,
|
||||
child: SvgPicture.asset(
|
||||
'assets/images/avatar_male.svg',
|
||||
width: 100,
|
||||
height: 100,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 10.0),
|
||||
child: Text('Trina Bakshi', style: TextStyle(fontSize: 20,
|
||||
color: Colors.black, fontWeight: FontWeight.w500),),
|
||||
),
|
||||
|
||||
Text('CIF: 2553677487774', style: TextStyle(fontSize: 16, color: Colors.grey),),
|
||||
SizedBox(height: 30,),
|
||||
InfoField(label: 'Number of Active Accounts', value: '3'),
|
||||
InfoField(label: 'Mobile Number', value: '987XXXXX78'),
|
||||
InfoField(label: 'Date of Birth', value: '12-07-1984'),
|
||||
InfoField(label: 'Branch', value: 'Krishnapur'),
|
||||
InfoField(label: 'Aadhar Number', value: '7665 XXXX 1276'),
|
||||
InfoField(label: 'PAN Number', value: '700127638009871'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
);
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SafeArea(
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 30),
|
||||
CircleAvatar(
|
||||
backgroundColor: Colors.grey[200],
|
||||
radius: 50,
|
||||
child: SvgPicture.asset(
|
||||
'assets/images/avatar_male.svg',
|
||||
width: 150,
|
||||
height: 150,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 10.0),
|
||||
child: Text(
|
||||
user.name ?? '',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'CIF: ${user.cifNumber ?? 'N/A'}',
|
||||
style:
|
||||
const TextStyle(fontSize: 16, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
InfoField(
|
||||
label: 'Number of Active Accounts',
|
||||
value: user.activeAccounts?.toString() ?? '6'),
|
||||
InfoField(
|
||||
label: 'Mobile Number',
|
||||
value: user.mobileNo ?? 'N/A'),
|
||||
InfoField(
|
||||
label: 'Date of Birth',
|
||||
value: (user.dateOfBirth != null &&
|
||||
user.dateOfBirth!.length == 8)
|
||||
? '${user.dateOfBirth!.substring(0, 2)}-${user.dateOfBirth!.substring(2, 4)}-${user.dateOfBirth!.substring(4, 8)}'
|
||||
: 'N/A'), // Replace with DOB if available
|
||||
InfoField(label: 'Branch', value: user.branchId ?? 'N/A'),
|
||||
InfoField(
|
||||
label: 'Address',
|
||||
value: user.address ??
|
||||
'N/A'), // Replace with Aadhar if available
|
||||
InfoField(
|
||||
label: 'Primary Id',
|
||||
value: user.primaryId ??
|
||||
'N/A'), // Replace with PAN if available
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +111,7 @@ class InfoField extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
const InfoField({Key? key, required this.label, required this.value}) : super(key: key);
|
||||
const InfoField({super.key, required this.label, required this.value});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -101,4 +141,4 @@ class InfoField extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,14 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:kmobile/di/injection.dart';
|
||||
import 'package:kmobile/features/accounts/screens/account_info_screen.dart';
|
||||
import 'package:kmobile/features/accounts/screens/account_statement_screen.dart';
|
||||
import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
|
||||
import 'package:kmobile/features/auth/controllers/auth_state.dart';
|
||||
import 'package:kmobile/features/customer_info/screens/customer_info_screen.dart';
|
||||
import 'package:kmobile/features/beneficiaries/screens/manage_beneficiaries_screen.dart';
|
||||
import 'package:kmobile/features/dashboard/widgets/transaction_list_placeholder.dart';
|
||||
import 'package:kmobile/features/enquiry/screens/enquiry_screen.dart';
|
||||
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_beneficiary_screen.dart';
|
||||
import 'package:kmobile/features/quick_pay/screens/quick_pay_screen.dart';
|
||||
import 'package:kmobile/security/secure_storage.dart';
|
||||
import 'package:kmobile/src/preferences/preference.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
class DashboardScreen extends StatefulWidget {
|
||||
const DashboardScreen({super.key});
|
||||
@@ -18,275 +27,460 @@ class DashboardScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DashboardScreenState extends State<DashboardScreen> {
|
||||
// Mock data for transactions
|
||||
final List<Map<String, String>> transactions = [
|
||||
{
|
||||
'name': 'Raj Kumar',
|
||||
'amount': '₹1,000',
|
||||
'date': '09 March, 2025 16:04',
|
||||
'type': 'in'
|
||||
},
|
||||
{
|
||||
'name': 'Sunita Joshi',
|
||||
'amount': '₹1,45,000',
|
||||
'date': '07 March, 2025 16:04',
|
||||
'type': 'out'
|
||||
},
|
||||
{
|
||||
'name': 'Manoj Singh',
|
||||
'amount': '₹2,400',
|
||||
'date': '07 March, 2025 16:04',
|
||||
'type': 'in'
|
||||
},
|
||||
{
|
||||
'name': 'Raj Kumar',
|
||||
'amount': '₹11,500',
|
||||
'date': '09 March, 2025 16:04',
|
||||
'type': 'in'
|
||||
},
|
||||
{'name': 'Manoj Singh', 'amount': '₹1,000', 'date': '', 'type': 'in'},
|
||||
];
|
||||
|
||||
List<String> accountNumbers = [
|
||||
'0300015678903456',
|
||||
'0300015678903678',
|
||||
'0300015678903325',
|
||||
];
|
||||
|
||||
String selectedAccount = '0300015678903456';
|
||||
int selectedAccountIndex = 0;
|
||||
bool isVisible = false;
|
||||
bool isRefreshing = false;
|
||||
bool isBalanceLoading = false;
|
||||
bool _biometricPromptShown = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xfff5f9fc),
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color(0xfff5f9fc),
|
||||
automaticallyImplyLeading: false,
|
||||
title: Text(
|
||||
'kMobile',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
actions: [
|
||||
// IconButton(
|
||||
// icon: const Icon(Icons.notifications_outlined),
|
||||
// onPressed: () {
|
||||
// // Navigate to notifications
|
||||
// },
|
||||
// ),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 10.0),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const Preference()));
|
||||
},
|
||||
child: const CircleAvatar(
|
||||
backgroundImage: AssetImage('assets/images/avatar.jpg'),
|
||||
// Replace with your image
|
||||
radius: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Text(
|
||||
"Hi Trina Bakshi",
|
||||
style: GoogleFonts.sriracha().copyWith(fontSize: 20),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Future<void> _refreshAccountData(BuildContext context) async {
|
||||
setState(() {
|
||||
isRefreshing = true;
|
||||
});
|
||||
try {
|
||||
// Call your AuthCubit or repository to refresh user/accounts data
|
||||
await context.read<AuthCubit>().refreshUserData();
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Failed to refresh data')),
|
||||
);
|
||||
}
|
||||
setState(() {
|
||||
isRefreshing = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Account Info Card
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text("Account Number: ",
|
||||
style:
|
||||
TextStyle(color: Colors.white, fontSize: 12)),
|
||||
DropdownButton<String>(
|
||||
value: selectedAccount,
|
||||
dropdownColor: Theme.of(context).primaryColor,
|
||||
underline: const SizedBox(),
|
||||
icon: const Icon(Icons.keyboard_arrow_down),
|
||||
iconEnabledColor: Colors.white,
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontSize: 14),
|
||||
items: accountNumbers.map((String acc) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: acc,
|
||||
child: Text(acc,
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontSize: 14)),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (String? newValue) {
|
||||
setState(() {
|
||||
selectedAccount = newValue!;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
const Text("₹ ",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.w700)),
|
||||
Text(isVisible ? "1,23,456" : "*****",
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.w700)),
|
||||
const Spacer(),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
isVisible = !isVisible;
|
||||
});
|
||||
},
|
||||
child: Icon(
|
||||
isVisible
|
||||
? Symbols.visibility_lock
|
||||
: Symbols.visibility,
|
||||
color: Colors.white)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
const Text(
|
||||
'Quick Links',
|
||||
style: TextStyle(fontSize: 15),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Quick Links
|
||||
GridView.count(
|
||||
crossAxisCount: 4,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
_buildQuickLink(Symbols.id_card, "Customer \n Info", () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const CustomerInfoScreen()));
|
||||
}),
|
||||
_buildQuickLink(Symbols.currency_rupee, "Quick \n Pay", () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const QuickPayScreen()));
|
||||
}),
|
||||
_buildQuickLink(Symbols.send_money, "Fund \n Transfer", () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const FundTransferBeneficiaryScreen()));
|
||||
}),
|
||||
_buildQuickLink(Symbols.server_person, "Account \n Info", () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const AccountInfoScreen()));
|
||||
}),
|
||||
_buildQuickLink(Symbols.receipt_long, "Account \n Statement",
|
||||
() {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const AccountStatementScreen()));
|
||||
}),
|
||||
// _buildQuickLink(Symbols.checkbook, "Handle \n Cheque", () {
|
||||
// Navigator.push(
|
||||
// context,
|
||||
// MaterialPageRoute(
|
||||
// builder: (context) =>
|
||||
// const ChequeManagementScreen()));
|
||||
// }),
|
||||
_buildQuickLink(Icons.group, "Manage \n Beneficiary", () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const ManageBeneficiariesScreen()));
|
||||
}),
|
||||
_buildQuickLink(Symbols.support_agent, "Contact \n Us", () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const EnquiryScreen()));
|
||||
}),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Recent Transactions
|
||||
const Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text("Recent Transactions",
|
||||
style:
|
||||
TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
...transactions.map((tx) => ListTile(
|
||||
leading: Icon(
|
||||
tx['type'] == 'in'
|
||||
? Symbols.call_received
|
||||
: Symbols.call_made,
|
||||
color: tx['type'] == 'in' ? Colors.green : Colors.red),
|
||||
title: Text(tx['name']!),
|
||||
subtitle: Text(tx['date']!),
|
||||
trailing: Text(tx['amount']!),
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Widget _buildBalanceShimmer() {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: Colors.white.withOpacity(0.7),
|
||||
highlightColor: Colors.white.withOpacity(0.3),
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 32,
|
||||
color: Colors.white,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickLink(IconData icon, String label, VoidCallback onTap) {
|
||||
String getProcessedFirstName(String? name) {
|
||||
if (name == null || name.trim().isEmpty) return '';
|
||||
// Remove common titles
|
||||
final titles = [
|
||||
'mr.',
|
||||
'mrs.',
|
||||
'ms.',
|
||||
'miss',
|
||||
'dr.',
|
||||
'shri',
|
||||
'smt.',
|
||||
'kumari'
|
||||
];
|
||||
String processed = name.trim().toLowerCase();
|
||||
for (final title in titles) {
|
||||
if (processed.startsWith(title)) {
|
||||
processed = processed.replaceFirst(title, '').trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Take the first word (first name)
|
||||
final firstName = processed.split(' ').first;
|
||||
// Convert to title case
|
||||
if (firstName.isEmpty) return '';
|
||||
return firstName[0].toUpperCase() + firstName.substring(1);
|
||||
}
|
||||
|
||||
String getFullAccountType(String? accountType) {
|
||||
if (accountType == null || accountType.isEmpty) return 'N/A';
|
||||
// Convert to title case
|
||||
switch (accountType.toLowerCase()) {
|
||||
case 'sa':
|
||||
return 'Savings Account';
|
||||
case 'ln':
|
||||
return 'Loan Account';
|
||||
case 'td':
|
||||
return 'Term Deposit Account';
|
||||
case 'rd':
|
||||
return 'Recurring Deposit Account';
|
||||
default:
|
||||
return 'Unknown Account Type';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showBiometricOptInDialog() async {
|
||||
final storage = SecureStorage();
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => AlertDialog(
|
||||
title: const Text('Enable Biometric Authentication'),
|
||||
content: const Text('Use fingerprint/face ID for faster login?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await storage.write('biometric_prompt_shown', 'true');
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Later'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final auth = LocalAuthentication();
|
||||
final canCheck = await auth.canCheckBiometrics;
|
||||
bool ok = false;
|
||||
if (canCheck) {
|
||||
ok = await auth.authenticate(
|
||||
localizedReason: 'Scan to enable biometric login',
|
||||
);
|
||||
}
|
||||
if (ok) {
|
||||
await storage.write('biometric_enabled', 'true');
|
||||
}
|
||||
await storage.write('biometric_prompt_shown', 'true');
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Enable'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<AuthCubit, AuthState>(
|
||||
listener: (context, state) async {
|
||||
if (state is Authenticated && !_biometricPromptShown) {
|
||||
_biometricPromptShown = true;
|
||||
final storage = getIt<SecureStorage>();
|
||||
final already = await storage.read('biometric_prompt_shown');
|
||||
if (already == null) {
|
||||
_showBiometricOptInDialog();
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: const Color(0xfff5f9fc),
|
||||
appBar: AppBar(
|
||||
backgroundColor: const Color(0xfff5f9fc),
|
||||
automaticallyImplyLeading: false,
|
||||
title: Text(
|
||||
'kMobile',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 10.0),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const Preference()));
|
||||
},
|
||||
child: CircleAvatar(
|
||||
backgroundColor: Colors.grey[200],
|
||||
radius: 20,
|
||||
child: SvgPicture.asset(
|
||||
'assets/images/avatar_male.svg',
|
||||
width: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: BlocBuilder<AuthCubit, AuthState>(builder: (context, state) {
|
||||
if (state is AuthLoading || state is AuthInitial) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (state is Authenticated) {
|
||||
final users = state.users;
|
||||
final currAccount = users[selectedAccountIndex];
|
||||
final firstName = getProcessedFirstName(currAccount.name);
|
||||
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Text(
|
||||
"Hi! $firstName",
|
||||
style: GoogleFonts.montserrat().copyWith(
|
||||
fontSize: 25,
|
||||
color: Theme.of(context).primaryColorDark,
|
||||
fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Account Info Card
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 18, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).primaryColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Text("Account Number: ",
|
||||
style: TextStyle(
|
||||
color: Colors.white, fontSize: 12)),
|
||||
DropdownButton<int>(
|
||||
value: selectedAccountIndex,
|
||||
dropdownColor: Theme.of(context).primaryColor,
|
||||
underline: const SizedBox(),
|
||||
icon: const Icon(Icons.keyboard_arrow_down),
|
||||
iconEnabledColor: Colors.white,
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontSize: 14),
|
||||
items: List.generate(users.length, (index) {
|
||||
return DropdownMenuItem<int>(
|
||||
value: index,
|
||||
child: Text(
|
||||
users[index].accountNo ?? 'N/A',
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontSize: 14),
|
||||
),
|
||||
);
|
||||
}),
|
||||
onChanged: (int? newIndex) async {
|
||||
if (newIndex == null ||
|
||||
newIndex == selectedAccountIndex) {
|
||||
return;
|
||||
}
|
||||
if (isBalanceLoading) return;
|
||||
if (isVisible) {
|
||||
setState(() {
|
||||
isBalanceLoading = true;
|
||||
selectedAccountIndex = newIndex;
|
||||
});
|
||||
await Future.delayed(
|
||||
const Duration(seconds: 1));
|
||||
setState(() {
|
||||
isBalanceLoading = false;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
selectedAccountIndex = newIndex;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
icon: isRefreshing
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.refresh,
|
||||
color: Colors.white),
|
||||
onPressed: isRefreshing
|
||||
? null
|
||||
: () => _refreshAccountData(context),
|
||||
tooltip: 'Refresh',
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
getFullAccountType(currAccount.accountType),
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
const Text("₹ ",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.w700)),
|
||||
isRefreshing || isBalanceLoading
|
||||
? _buildBalanceShimmer()
|
||||
: Text(
|
||||
isVisible
|
||||
? currAccount.currentBalance ?? '0.00'
|
||||
: '********',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.w700)),
|
||||
const Spacer(),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
if (isBalanceLoading) return;
|
||||
if (!isVisible) {
|
||||
setState(() {
|
||||
isBalanceLoading = true;
|
||||
});
|
||||
await Future.delayed(
|
||||
const Duration(seconds: 1));
|
||||
setState(() {
|
||||
isVisible = true;
|
||||
isBalanceLoading = false;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
isVisible = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Icon(
|
||||
isVisible
|
||||
? Symbols.visibility_lock
|
||||
: Symbols.visibility,
|
||||
color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
const Text(
|
||||
'Quick Links',
|
||||
style: TextStyle(fontSize: 17),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Quick Links
|
||||
GridView.count(
|
||||
crossAxisCount: 4,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
_buildQuickLink(Symbols.id_card, "Customer \n Info",
|
||||
() {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CustomerInfoScreen(
|
||||
user: users[selectedAccountIndex],
|
||||
)));
|
||||
}),
|
||||
_buildQuickLink(Symbols.currency_rupee, "Quick \n Pay",
|
||||
() {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const QuickPayScreen()));
|
||||
}),
|
||||
_buildQuickLink(Symbols.send_money, "Fund \n Transfer",
|
||||
() {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const FundTransferBeneficiaryScreen()));
|
||||
}),
|
||||
_buildQuickLink(
|
||||
Symbols.server_person, "Account \n Info", () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AccountInfoScreen(
|
||||
user: users[selectedAccountIndex])));
|
||||
}),
|
||||
_buildQuickLink(
|
||||
Symbols.receipt_long, "Account \n Statement", () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const AccountStatementScreen()));
|
||||
}),
|
||||
_buildQuickLink(
|
||||
Symbols.checkbook, "Handle \n Cheque", () {},
|
||||
disable: true),
|
||||
_buildQuickLink(Icons.group, "Manage \n Beneficiary",
|
||||
() {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const ManageBeneficiariesScreen()));
|
||||
}),
|
||||
_buildQuickLink(Symbols.support_agent, "Contact \n Us",
|
||||
() {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const EnquiryScreen()));
|
||||
}),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
|
||||
// Recent Transactions
|
||||
const Text(
|
||||
'Recent Transactions',
|
||||
style: TextStyle(fontSize: 17),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (currAccount.transactions != null &&
|
||||
currAccount.transactions!.isNotEmpty)
|
||||
...currAccount.transactions!.map((tx) => ListTile(
|
||||
leading: Icon(
|
||||
tx.type == 'CR'
|
||||
? Symbols.call_received
|
||||
: Symbols.call_made,
|
||||
color: tx.type == 'CR'
|
||||
? Colors.green
|
||||
: Colors.red),
|
||||
title: Text(tx.name ?? ''),
|
||||
subtitle: Text(tx.date ?? ''),
|
||||
trailing: Text("₹${tx.amount}",
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
)),
|
||||
))
|
||||
else
|
||||
const EmptyTransactionsPlaceholder(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return const Center(child: Text("Something went wrong"));
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuickLink(IconData icon, String label, VoidCallback onTap,
|
||||
{bool disable = false}) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
onTap: disable ? null : onTap,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 30, color: Theme.of(context).primaryColor),
|
||||
Icon(icon,
|
||||
size: 30,
|
||||
color: disable ? Colors.grey : Theme.of(context).primaryColor),
|
||||
const SizedBox(height: 4),
|
||||
Text(label,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
style: const TextStyle(fontSize: 13)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EmptyTransactionsPlaceholder extends StatelessWidget {
|
||||
const EmptyTransactionsPlaceholder({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: List.generate(5, (index) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Placeholder for icon
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Placeholder for transaction details
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 120,
|
||||
height: 14,
|
||||
color: Colors.grey[350],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: 80,
|
||||
height: 10,
|
||||
color: Colors.grey[300],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Placeholder for amount
|
||||
Container(
|
||||
width: 60,
|
||||
height: 16,
|
||||
color: Colors.grey[350],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
@@ -3,7 +3,6 @@ import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:screenshot/screenshot.dart';
|
||||
import 'package:social_share/social_share.dart';
|
||||
|
||||
import '../../../app.dart';
|
||||
|
||||
@@ -27,10 +26,10 @@ class _TransactionSuccessScreen extends State<TransactionSuccessScreen> {
|
||||
final imagePath = File('${directory.path}/transaction_success.png');
|
||||
await imagePath.writeAsBytes(imageBytes);
|
||||
|
||||
SocialShare.shareOptions(
|
||||
"Transaction Successful",
|
||||
imagePath: imagePath.path,
|
||||
);
|
||||
// SocialShare.shareOptions(
|
||||
// "Transaction Successful",
|
||||
// imagePath: imagePath.path,
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user