Language Changes
This commit is contained in:
56
lib/api/interceptors/auth_interceptor.dart
Normal file
56
lib/api/interceptors/auth_interceptor.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../data/repositories/auth_repository.dart';
|
||||
|
||||
class AuthInterceptor extends Interceptor {
|
||||
final AuthRepository _authRepository;
|
||||
final Dio _dio;
|
||||
|
||||
AuthInterceptor(this._authRepository, this._dio);
|
||||
|
||||
@override
|
||||
Future<void> onRequest(
|
||||
RequestOptions options,
|
||||
RequestInterceptorHandler handler,
|
||||
) async {
|
||||
// Skip auth header for login and refresh endpoints
|
||||
if (options.path.contains('/login') ||
|
||||
options.path.contains('/auth/refresh')) {
|
||||
return handler.next(options);
|
||||
}
|
||||
|
||||
// Get token and add to request
|
||||
final token = await _authRepository.getAccessToken();
|
||||
if (token != null) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
return handler.next(options);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onError(
|
||||
DioException err,
|
||||
ErrorInterceptorHandler handler,
|
||||
) async {
|
||||
// Handle 401 errors by refreshing token and retrying
|
||||
if (err.response?.statusCode == 401) {
|
||||
// On 401, try to get a new token
|
||||
final token = await _authRepository.getAccessToken();
|
||||
|
||||
if (token != null) {
|
||||
// If we have a new token, retry the request
|
||||
final opts = err.requestOptions;
|
||||
opts.headers['Authorization'] = 'Bearer $token';
|
||||
|
||||
try {
|
||||
final response = await _dio.fetch(opts);
|
||||
return handler.resolve(response);
|
||||
} on DioException catch (e) {
|
||||
return handler.next(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return handler.next(err);
|
||||
}
|
||||
}
|
93
lib/api/services/auth_service.dart
Normal file
93
lib/api/services/auth_service.dart
Normal file
@@ -0,0 +1,93 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../features/auth/models/auth_token.dart';
|
||||
import '../../features/auth/models/auth_credentials.dart';
|
||||
import '../../core/errors/exceptions.dart';
|
||||
|
||||
|
||||
class AuthService {
|
||||
final Dio _dio;
|
||||
AuthService(this._dio);
|
||||
|
||||
Future<AuthToken> login(AuthCredentials credentials) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
'/api/auth/login',
|
||||
data: credentials.toJson(),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return AuthToken.fromJson(response.data);
|
||||
} else {
|
||||
throw AuthException('Login failed');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e.toString());
|
||||
}
|
||||
if (e.response?.statusCode == 401) {
|
||||
throw AuthException('Invalid credentials');
|
||||
}
|
||||
throw NetworkException('Network error during login');
|
||||
} catch (e) {
|
||||
throw UnexpectedException('Unexpected error during login: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<AuthToken> refreshToken(String refreshToken) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
'/auth/refresh',
|
||||
data: {'refresh_token': refreshToken},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return AuthToken.fromJson(response.data);
|
||||
} else {
|
||||
throw AuthException('Token refresh failed');
|
||||
}
|
||||
} catch (e) {
|
||||
throw AuthException('Failed to refresh token: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> checkTpin() async {
|
||||
try {
|
||||
final response = await _dio.get('/api/auth/tpin');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return response.data['tpinSet'] as bool;
|
||||
} else {
|
||||
throw AuthException('Failed to check TPIN status');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e.toString());
|
||||
}
|
||||
throw NetworkException('Network error during TPIN check');
|
||||
} catch (e) {
|
||||
throw UnexpectedException('Unexpected error during TPIN check: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setTpin(String tpin) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
'/api/auth/tpin',
|
||||
data: {'tpin': tpin},
|
||||
);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw AuthException('Failed to set TPIN');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e.toString());
|
||||
}
|
||||
throw NetworkException('Network error during TPIN setup');
|
||||
} catch (e) {
|
||||
throw UnexpectedException('Unexpected error during TPIN setup: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
43
lib/api/services/payment_service.dart
Normal file
43
lib/api/services/payment_service.dart
Normal file
@@ -0,0 +1,43 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:kmobile/data/models/payment_response.dart';
|
||||
import 'package:kmobile/data/models/transfer.dart';
|
||||
|
||||
class PaymentService {
|
||||
final Dio _dio;
|
||||
PaymentService(this._dio);
|
||||
|
||||
Future<PaymentResponse> processQuickPayWithinBank(Transfer transfer) async {
|
||||
try {
|
||||
await Future.delayed(const Duration(seconds: 3)); // Simulate delay
|
||||
final response =
|
||||
await _dio.post('/api/payment/transfer', data: transfer.toJson());
|
||||
|
||||
return PaymentResponse(
|
||||
isSuccess: true,
|
||||
date: DateTime.now(),
|
||||
creditedAccount: transfer.toAccount,
|
||||
amount: transfer.amount,
|
||||
currency: "INR",
|
||||
errorMessage: response.data['errorMessage'],
|
||||
errorCode: response.data['errorCode'],
|
||||
);
|
||||
} on DioException catch (e) {
|
||||
log('DioException: ${e.toString()}');
|
||||
if (e.response?.data != null) {
|
||||
return PaymentResponse(
|
||||
isSuccess: false,
|
||||
errorMessage: e.response?.data['error'] ?? 'Unknown error',
|
||||
errorCode: e.response?.data['status'] ?? 'UNKNOWN_ERROR',
|
||||
);
|
||||
}
|
||||
throw Exception(
|
||||
'Failed to process quick pay within bank: ${e.toString()}');
|
||||
} catch (e) {
|
||||
log('Unexpected error: ${e.toString()}');
|
||||
throw Exception(
|
||||
'Failed to process quick pay within bank: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
31
lib/api/services/user_service.dart
Normal file
31
lib/api/services/user_service.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:kmobile/data/models/user.dart';
|
||||
|
||||
class UserService {
|
||||
final Dio _dio;
|
||||
UserService(this._dio);
|
||||
|
||||
Future<List<User>> getUserDetails() async {
|
||||
try {
|
||||
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();
|
||||
} else {
|
||||
throw Exception('Failed to load customer details');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (kDebugMode) {
|
||||
print(e.toString());
|
||||
}
|
||||
throw Exception('Network error: ${e.message}');
|
||||
} catch (e) {
|
||||
throw Exception('Unexpected error: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
593
lib/app.dart
Normal file
593
lib/app.dart
Normal file
@@ -0,0 +1,593 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:kmobile/security/secure_storage.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'config/themes.dart';
|
||||
import 'config/routes.dart';
|
||||
import 'di/injection.dart';
|
||||
import 'features/auth/controllers/auth_cubit.dart';
|
||||
import 'features/card/screens/card_management_screen.dart';
|
||||
import 'features/auth/screens/welcome_screen.dart';
|
||||
import 'features/auth/screens/login_screen.dart';
|
||||
import 'features/service/screens/service_screen.dart';
|
||||
import 'features/dashboard/screens/dashboard_screen.dart';
|
||||
//import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'features/auth/screens/mpin_screen.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
|
||||
class KMobile extends StatefulWidget {
|
||||
const KMobile({super.key});
|
||||
|
||||
@override
|
||||
State<KMobile> createState() => _KMobileState();
|
||||
|
||||
static void setLocale(BuildContext context, Locale newLocale) {
|
||||
final _KMobileState? state =
|
||||
context.findAncestorStateOfType<_KMobileState>();
|
||||
state?.setLocale(newLocale);
|
||||
}
|
||||
}
|
||||
|
||||
class _KMobileState extends State<KMobile> {
|
||||
bool _showSplash = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Simulate a splash screen delay
|
||||
Future.delayed(const Duration(seconds: 2), () {
|
||||
setState(() {
|
||||
_showSplash = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Locale? _locale;
|
||||
|
||||
void setLocale(Locale locale) {
|
||||
setState(() {
|
||||
_locale = locale;
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Set status bar color
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
),
|
||||
);
|
||||
|
||||
if (_showSplash) {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
locale: _locale,
|
||||
supportedLocales: const [
|
||||
Locale('en'),
|
||||
Locale('hi'),
|
||||
],
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
home: const SplashScreen(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<AuthCubit>(create: (_) => getIt<AuthCubit>()),
|
||||
],
|
||||
child: MaterialApp(
|
||||
title: 'kMobile',
|
||||
// debugShowCheckedModeBanner: false,
|
||||
theme: AppThemes.lightTheme,
|
||||
// darkTheme: AppThemes.darkTheme,
|
||||
themeMode: ThemeMode.system, // Use system theme by default
|
||||
onGenerateRoute: AppRoutes.generateRoute,
|
||||
initialRoute: AppRoutes.splash,
|
||||
home: const AuthGate(),
|
||||
),
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Set status bar color
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
),
|
||||
);
|
||||
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<AuthCubit>(create: (_) => getIt<AuthCubit>()),
|
||||
],
|
||||
child: MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
locale: _locale, // Use your existing locale variable
|
||||
supportedLocales: const [
|
||||
Locale('en'),
|
||||
Locale('hi'),
|
||||
],
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
title: 'kMobile',
|
||||
theme: AppThemes.lightTheme,
|
||||
// darkTheme: AppThemes.darkTheme,
|
||||
themeMode: ThemeMode.system, // Use system theme by default
|
||||
onGenerateRoute: AppRoutes.generateRoute,
|
||||
initialRoute: AppRoutes.splash,
|
||||
home: _showSplash ? const SplashScreen() : const AuthGate(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthGate extends StatefulWidget {
|
||||
const AuthGate({super.key});
|
||||
|
||||
@override
|
||||
State<AuthGate> createState() => _AuthGateState();
|
||||
}
|
||||
|
||||
class _AuthGateState extends State<AuthGate> {
|
||||
bool _checking = true;
|
||||
bool _isLoggedIn = false;
|
||||
bool _showWelcome = true;
|
||||
bool _hasMPin = false;
|
||||
bool _biometricEnabled = false;
|
||||
bool _biometricTried = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_checkAuth();
|
||||
}
|
||||
|
||||
Future<void> _checkAuth() async {
|
||||
final storage = getIt<SecureStorage>();
|
||||
final accessToken = await storage.read('access_token');
|
||||
final accessTokenExpiry = await storage.read('token_expiry');
|
||||
final mpin = await storage.read('mpin');
|
||||
final biometric = await storage.read('biometric_enabled');
|
||||
setState(() {
|
||||
_isLoggedIn = accessToken != null &&
|
||||
accessTokenExpiry != null &&
|
||||
DateTime.parse(accessTokenExpiry).isAfter(DateTime.now());
|
||||
_hasMPin = mpin != null;
|
||||
_biometricEnabled = biometric == 'true';
|
||||
_checking = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> _tryBiometric() async {
|
||||
if (_biometricTried) return false;
|
||||
_biometricTried = true;
|
||||
final localAuth = LocalAuthentication();
|
||||
final canCheck = await localAuth.canCheckBiometrics;
|
||||
if (!canCheck) return false;
|
||||
try {
|
||||
final didAuth = await localAuth.authenticate(
|
||||
localizedReason: 'Authenticate to access kMobile',
|
||||
options: const AuthenticationOptions(
|
||||
stickyAuth: true,
|
||||
biometricOnly: true,
|
||||
),
|
||||
);
|
||||
return didAuth;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* @override
|
||||
Widget build(BuildContext context) {
|
||||
if (_checking) {
|
||||
return const SplashScreen();
|
||||
}
|
||||
if (_isLoggedIn) {
|
||||
if (_hasMPin) {
|
||||
if (_biometricEnabled) {
|
||||
return FutureBuilder<bool>(
|
||||
future: _tryBiometric(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const SplashScreen();
|
||||
}
|
||||
if (snapshot.data == true) {
|
||||
// Authenticated with biometrics, go to dashboard
|
||||
return const NavigationScaffold();
|
||||
}
|
||||
// If not authenticated or user dismissed, show mPIN screen
|
||||
return MPinScreen(
|
||||
mode: MPinMode.enter,
|
||||
onCompleted: (_) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const NavigationScaffold()),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return MPinScreen(
|
||||
mode: MPinMode.enter,
|
||||
onCompleted: (_) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return MPinScreen(
|
||||
mode: MPinMode.set,
|
||||
onCompleted: (_) async {
|
||||
final storage = getIt<SecureStorage>();
|
||||
final localAuth = LocalAuthentication();
|
||||
|
||||
// 1) Prompt user to opt‐in for biometric
|
||||
final optIn = await showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false, // force choice
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Enable Fingerprint Login?'),
|
||||
content: const Text(
|
||||
'Would you like to enable fingerprint authentication for faster login?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(false),
|
||||
child: const Text('No'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(ctx).pop(true),
|
||||
child: const Text('Yes'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
// 2) If opted in, perform biometric auth
|
||||
if (optIn == true) {
|
||||
final canCheck = await localAuth.canCheckBiometrics;
|
||||
bool didAuth = false;
|
||||
if (canCheck) {
|
||||
didAuth = await localAuth.authenticate(
|
||||
localizedReason: 'Authenticate to enable fingerprint login',
|
||||
options: const AuthenticationOptions(
|
||||
stickyAuth: true,
|
||||
biometricOnly: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
await storage.write(
|
||||
'biometric_enabled', didAuth ? 'true' : 'false');
|
||||
} else {
|
||||
await storage.write('biometric_enabled', 'false');
|
||||
}
|
||||
|
||||
// 3) Finally go to your main scaffold
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
return const LoginScreen();
|
||||
}
|
||||
}*/
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_checking) {
|
||||
return const SplashScreen();
|
||||
}
|
||||
|
||||
if (_isLoggedIn) {
|
||||
if (_hasMPin) {
|
||||
if (_biometricEnabled) {
|
||||
return FutureBuilder<bool>(
|
||||
future: _tryBiometric(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const SplashScreen();
|
||||
}
|
||||
|
||||
if (snapshot.data == true) {
|
||||
return const NavigationScaffold(); // Authenticated
|
||||
}
|
||||
|
||||
// Failed or dismissed biometric → Show MPIN
|
||||
return MPinScreen(
|
||||
mode: MPinMode.enter,
|
||||
onCompleted: (_) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const NavigationScaffold(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return MPinScreen(
|
||||
mode: MPinMode.enter,
|
||||
onCompleted: (_) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const NavigationScaffold(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return MPinScreen(
|
||||
mode: MPinMode.set,
|
||||
onCompleted: (_) async {
|
||||
final storage = getIt<SecureStorage>();
|
||||
final localAuth = LocalAuthentication();
|
||||
final optin = await showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: 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 (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');
|
||||
}
|
||||
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const NavigationScaffold(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 🔻 Show Welcome screen before login if not logged in
|
||||
if (_showWelcome) {
|
||||
return WelcomeScreen(
|
||||
onContinue: () {
|
||||
setState(() {
|
||||
_showWelcome = false;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
return const LoginScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class NavigationScaffold extends StatefulWidget {
|
||||
const NavigationScaffold({super.key});
|
||||
|
||||
@override
|
||||
State<NavigationScaffold> createState() => _NavigationScaffoldState();
|
||||
}
|
||||
|
||||
class _NavigationScaffoldState extends State<NavigationScaffold> {
|
||||
final PageController _pageController = PageController();
|
||||
int _selectedIndex = 0;
|
||||
|
||||
final List<Widget> _pages = [
|
||||
const DashboardScreen(),
|
||||
const CardManagementScreen(),
|
||||
const ServiceScreen(),
|
||||
];
|
||||
|
||||
void _onItemTapped(int index) {
|
||||
setState(() {
|
||||
_selectedIndex = index;
|
||||
});
|
||||
_pageController.jumpToPage(index);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvokedWithResult: (didPop, result) async {
|
||||
if (!didPop) {
|
||||
final shouldExit = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: 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,
|
||||
backgroundColor: const Color(0xFFE0F7FA), // Light blue background
|
||||
selectedItemColor: Colors.blue[800],
|
||||
unselectedItemColor: Colors.black54,
|
||||
onTap: _onItemTapped,
|
||||
items: [
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.home_filled),
|
||||
label: AppLocalizations.of(context).home,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.credit_card),
|
||||
label: AppLocalizations.of(context).card,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.miscellaneous_services),
|
||||
label: AppLocalizations.of(context).services,
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SplashScreen extends StatelessWidget {
|
||||
const SplashScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: 20),
|
||||
Text('Loading...',
|
||||
style: Theme.of(context).textTheme.headlineMedium),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add this widget at the end of the file
|
||||
class BiometricPromptScreen extends StatelessWidget {
|
||||
final VoidCallback onCompleted;
|
||||
const BiometricPromptScreen({super.key, required this.onCompleted});
|
||||
|
||||
Future<void> _handleBiometric(BuildContext context) async {
|
||||
final localAuth = LocalAuthentication();
|
||||
final canCheck = await localAuth.canCheckBiometrics;
|
||||
if (!canCheck) {
|
||||
onCompleted();
|
||||
return;
|
||||
}
|
||||
final didAuth = await localAuth.authenticate(
|
||||
localizedReason: 'Enable fingerprint authentication for quick login?',
|
||||
options: const AuthenticationOptions(
|
||||
stickyAuth: true,
|
||||
biometricOnly: true,
|
||||
),
|
||||
);
|
||||
final storage = getIt<SecureStorage>();
|
||||
if (didAuth) {
|
||||
await storage.write('biometric_enabled', 'true');
|
||||
} else {
|
||||
await storage.write('biometric_enabled', 'false');
|
||||
}
|
||||
onCompleted();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Future.microtask(() => _showDialog(context));
|
||||
return const SplashScreen();
|
||||
}
|
||||
|
||||
Future<void> _showDialog(BuildContext context) async {
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Enable Fingerprint Login?'),
|
||||
content: const Text(
|
||||
'Would you like to enable fingerprint authentication for faster login?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop(false);
|
||||
},
|
||||
child: const Text('No'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop(true);
|
||||
},
|
||||
child: const Text('Yes'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (result == true) {
|
||||
await _handleBiometric(context);
|
||||
} else {
|
||||
final storage = getIt<SecureStorage>();
|
||||
await storage.write('biometric_enabled', 'false');
|
||||
onCompleted();
|
||||
}
|
||||
}
|
||||
}
|
90
lib/config/routes.dart
Normal file
90
lib/config/routes.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:kmobile/features/auth/screens/mpin_screen.dart';
|
||||
import '../app.dart';
|
||||
import '../features/auth/screens/login_screen.dart';
|
||||
// import '../features/auth/screens/forgot_password_screen.dart';
|
||||
// import '../features/auth/screens/register_screen.dart';
|
||||
import '../features/dashboard/screens/dashboard_screen.dart';
|
||||
// import '../features/accounts/screens/accounts_screen.dart';
|
||||
// import '../features/transactions/screens/transactions_screen.dart';
|
||||
// import '../features/payments/screens/payments_screen.dart';
|
||||
// import '../features/settings/screens/settings_screen.dart';
|
||||
|
||||
class AppRoutes {
|
||||
// Private constructor to prevent instantiation
|
||||
AppRoutes._();
|
||||
|
||||
// Route names
|
||||
static const String splash = '/';
|
||||
static const String login = '/login';
|
||||
static const String mPin = '/mPin';
|
||||
static const String register = '/register';
|
||||
static const String forgotPassword = '/forgot-password';
|
||||
static const String navigationScaffold = '/navigation-scaffold';
|
||||
static const String dashboard = '/dashboard';
|
||||
static const String accounts = '/accounts';
|
||||
static const String transactions = '/transactions';
|
||||
static const String payments = '/payments';
|
||||
|
||||
// Route generator
|
||||
static Route<dynamic> generateRoute(RouteSettings settings) {
|
||||
switch (settings.name) {
|
||||
case splash:
|
||||
return MaterialPageRoute(builder: (_) => const SplashScreen());
|
||||
case login:
|
||||
return MaterialPageRoute(builder: (_) => const LoginScreen());
|
||||
|
||||
case mPin:
|
||||
return MaterialPageRoute(builder: (_) => const MPinScreen(mode: MPinMode.enter,));
|
||||
|
||||
case register:
|
||||
// Placeholder - create the RegisterScreen class and uncomment
|
||||
// return MaterialPageRoute(builder: (_) => const RegisterScreen());
|
||||
return _errorRoute();
|
||||
|
||||
case forgotPassword:
|
||||
// Placeholder - create the ForgotPasswordScreen class and uncomment
|
||||
// return MaterialPageRoute(builder: (_) => const ForgotPasswordScreen());
|
||||
return _errorRoute();
|
||||
|
||||
case navigationScaffold:
|
||||
return MaterialPageRoute(builder: (_) => const NavigationScaffold());
|
||||
|
||||
case dashboard:
|
||||
return MaterialPageRoute(builder: (_) => const DashboardScreen());
|
||||
|
||||
case accounts:
|
||||
// Placeholder - create the AccountsScreen class and uncomment
|
||||
// return MaterialPageRoute(builder: (_) => const AccountsScreen());
|
||||
return _errorRoute();
|
||||
|
||||
case transactions:
|
||||
// Placeholder - create the TransactionsScreen class and uncomment
|
||||
// return MaterialPageRoute(builder: (_) => const TransactionsScreen());
|
||||
return _errorRoute();
|
||||
|
||||
case payments:
|
||||
// Placeholder - create the PaymentsScreen class and uncomment
|
||||
// return MaterialPageRoute(builder: (_) => const PaymentsScreen());
|
||||
return _errorRoute();
|
||||
|
||||
|
||||
default:
|
||||
return _errorRoute();
|
||||
}
|
||||
}
|
||||
|
||||
// Error route
|
||||
static Route<dynamic> _errorRoute() {
|
||||
return MaterialPageRoute(builder: (_) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Error'),
|
||||
),
|
||||
body: const Center(
|
||||
child: Text('Route not found!'),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
266
lib/config/themes.dart
Normal file
266
lib/config/themes.dart
Normal file
@@ -0,0 +1,266 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppThemes {
|
||||
// Private constructor to prevent instantiation
|
||||
AppThemes._();
|
||||
|
||||
// Light theme colors
|
||||
static const Color _primaryColorLight = Color(0xFF1E88E5); // Blue 600
|
||||
static const Color _secondaryColorLight = Color(0xFF26A69A); // Teal 400
|
||||
static const Color _errorColorLight = Color(0xFFE53935); // Red 600
|
||||
static const Color _surfaceColorLight = Colors.white;
|
||||
|
||||
// Dark theme colors
|
||||
static const Color _primaryColorDark = Color(0xFF42A5F5); // Blue 400
|
||||
static const Color _secondaryColorDark = Color(0xFF4DB6AC); // Teal 300
|
||||
static const Color _errorColorDark = Color(0xFFEF5350); // Red 400
|
||||
static const Color _surfaceColorDark = Color(0xFF1E1E1E);
|
||||
|
||||
// Text themes
|
||||
static const TextTheme _textThemeLight = TextTheme(
|
||||
displayLarge: TextStyle(
|
||||
fontSize: 96,
|
||||
fontWeight: FontWeight.w300,
|
||||
color: Color(0xFF212121),
|
||||
fontFamily: 'Rubik',
|
||||
),
|
||||
displayMedium: TextStyle(
|
||||
fontSize: 60,
|
||||
fontWeight: FontWeight.w300,
|
||||
color: Color(0xFF212121),
|
||||
fontFamily: 'Rubik',
|
||||
),
|
||||
displaySmall: TextStyle(
|
||||
fontSize: 48,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF212121),
|
||||
fontFamily: 'Rubik',
|
||||
),
|
||||
headlineMedium: TextStyle(
|
||||
fontSize: 34,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF212121),
|
||||
fontFamily: 'Rubik',
|
||||
),
|
||||
headlineSmall: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF212121),
|
||||
fontFamily: 'Rubik',
|
||||
),
|
||||
titleLarge: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF212121),
|
||||
fontFamily: 'Rubik',
|
||||
),
|
||||
bodyLarge: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF212121),
|
||||
fontFamily: 'Rubik',
|
||||
),
|
||||
bodyMedium: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF212121),
|
||||
fontFamily: 'Rubik',
|
||||
),
|
||||
bodySmall: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF757575),
|
||||
fontFamily: 'Rubik',
|
||||
),
|
||||
labelLarge: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF212121),
|
||||
fontFamily: 'Rubik',
|
||||
),
|
||||
);
|
||||
|
||||
static final TextTheme _textThemeDark = _textThemeLight.copyWith(
|
||||
displayLarge: _textThemeLight.displayLarge?.copyWith(color: Colors.white),
|
||||
displayMedium: _textThemeLight.displayMedium?.copyWith(color: Colors.white),
|
||||
displaySmall: _textThemeLight.displaySmall?.copyWith(color: Colors.white),
|
||||
headlineMedium:
|
||||
_textThemeLight.headlineMedium?.copyWith(color: Colors.white),
|
||||
headlineSmall: _textThemeLight.headlineSmall?.copyWith(color: Colors.white),
|
||||
titleLarge: _textThemeLight.titleLarge?.copyWith(color: Colors.white),
|
||||
bodyLarge: _textThemeLight.bodyLarge?.copyWith(color: Colors.white),
|
||||
bodyMedium: _textThemeLight.bodyMedium?.copyWith(color: Colors.white),
|
||||
bodySmall: _textThemeLight.bodySmall?.copyWith(color: Colors.white70),
|
||||
labelLarge: _textThemeLight.labelLarge?.copyWith(color: Colors.white),
|
||||
);
|
||||
|
||||
// Light theme
|
||||
static final ThemeData lightTheme = ThemeData(
|
||||
useMaterial3: true,
|
||||
fontFamily: 'Rubik',
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: _primaryColorLight,
|
||||
secondary: _secondaryColorLight,
|
||||
error: _errorColorLight,
|
||||
surface: _surfaceColorLight,
|
||||
onPrimary: Colors.white,
|
||||
onSecondary: Colors.white,
|
||||
onSurface: Colors.black87,
|
||||
onError: Colors.white,
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
textTheme: _textThemeLight,
|
||||
appBarTheme: const AppBarTheme(
|
||||
elevation: 0,
|
||||
backgroundColor: _surfaceColorLight,
|
||||
foregroundColor: Color(0xFF212121),
|
||||
centerTitle: true,
|
||||
),
|
||||
cardTheme: CardTheme(
|
||||
//Earlier CardThemeData
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: _primaryColorLight,
|
||||
elevation: 2,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: _primaryColorLight,
|
||||
side: const BorderSide(color: _primaryColorLight),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: _primaryColorLight,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: Colors.grey[100],
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide(color: Colors.grey[300]!),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: _primaryColorLight, width: 2),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: _errorColorLight, width: 2),
|
||||
),
|
||||
labelStyle: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
||||
selectedItemColor: _primaryColorLight,
|
||||
unselectedItemColor: Colors.grey,
|
||||
),
|
||||
);
|
||||
|
||||
// Dark theme
|
||||
static final ThemeData darkTheme = ThemeData(
|
||||
fontFamily: 'Rubik',
|
||||
useMaterial3: true,
|
||||
colorScheme: const ColorScheme.dark(
|
||||
primary: _primaryColorDark,
|
||||
secondary: _secondaryColorDark,
|
||||
error: _errorColorDark,
|
||||
surface: _surfaceColorDark,
|
||||
onPrimary: Colors.black,
|
||||
onSecondary: Colors.black,
|
||||
onSurface: Colors.white,
|
||||
onError: Colors.black,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
textTheme: _textThemeDark,
|
||||
appBarTheme: const AppBarTheme(
|
||||
elevation: 0,
|
||||
backgroundColor: _surfaceColorDark,
|
||||
foregroundColor: Colors.white,
|
||||
centerTitle: true,
|
||||
),
|
||||
cardTheme: CardTheme(
|
||||
//Earlier was CardThemeData
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
color: const Color(0xFF2C2C2C),
|
||||
),
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
foregroundColor: Colors.black,
|
||||
backgroundColor: _primaryColorDark,
|
||||
elevation: 2,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: _primaryColorDark,
|
||||
side: const BorderSide(color: _primaryColorDark),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: _primaryColorDark,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: const Color(0xFF2A2A2A),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: Color(0xFF3A3A3A)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: _primaryColorDark, width: 2),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: _errorColorDark, width: 2),
|
||||
),
|
||||
labelStyle: const TextStyle(color: Colors.grey),
|
||||
),
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
||||
selectedItemColor: _primaryColorDark,
|
||||
unselectedItemColor: Colors.grey,
|
||||
backgroundColor: _surfaceColorDark,
|
||||
),
|
||||
);
|
||||
}
|
24
lib/core/errors/exceptions.dart
Normal file
24
lib/core/errors/exceptions.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
class AppException implements Exception {
|
||||
final String message;
|
||||
|
||||
AppException(this.message);
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
class NetworkException extends AppException {
|
||||
NetworkException(super.message);
|
||||
}
|
||||
|
||||
class AuthException implements Exception{
|
||||
final String message;
|
||||
AuthException(this.message);
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
class UnexpectedException extends AppException {
|
||||
UnexpectedException(super.message);
|
||||
}
|
19
lib/data/models/payment_response.dart
Normal file
19
lib/data/models/payment_response.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
class PaymentResponse {
|
||||
final bool isSuccess;
|
||||
final DateTime? date;
|
||||
final String? creditedAccount;
|
||||
final String? amount;
|
||||
final String? currency;
|
||||
final String? errorMessage;
|
||||
final String? errorCode;
|
||||
|
||||
PaymentResponse({
|
||||
required this.isSuccess,
|
||||
this.date,
|
||||
this.creditedAccount,
|
||||
this.amount,
|
||||
this.currency,
|
||||
this.errorMessage,
|
||||
this.errorCode,
|
||||
});
|
||||
}
|
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 String? 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 String?,
|
||||
type: json['type'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
45
lib/data/models/transfer.dart
Normal file
45
lib/data/models/transfer.dart
Normal file
@@ -0,0 +1,45 @@
|
||||
class Transfer {
|
||||
final String fromAccount;
|
||||
final String toAccount;
|
||||
final String toAccountType;
|
||||
final String amount;
|
||||
String? tpin;
|
||||
|
||||
Transfer({
|
||||
required this.fromAccount,
|
||||
required this.toAccount,
|
||||
required this.toAccountType,
|
||||
required this.amount,
|
||||
this.tpin,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'fromAccount': fromAccount,
|
||||
'toAccount': toAccount,
|
||||
'toAccountType': toAccountType,
|
||||
'amount': amount,
|
||||
'tpin': tpin,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class TransferResponse {
|
||||
final String? status;
|
||||
final String? message;
|
||||
final String? error;
|
||||
|
||||
TransferResponse({
|
||||
required this.status,
|
||||
required this.message,
|
||||
required this.error,
|
||||
});
|
||||
|
||||
factory TransferResponse.fromJson(Map<String, dynamic> json) {
|
||||
return TransferResponse(
|
||||
status: json['status'] as String?,
|
||||
message: json['message'] as String?,
|
||||
error: json['error'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
113
lib/data/models/user.dart
Normal file
113
lib/data/models/user.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
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? productType;
|
||||
final String? approvedAmount;
|
||||
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,
|
||||
this.bookingNumber,
|
||||
required this.branchId,
|
||||
required this.currency,
|
||||
this.productType,
|
||||
this.approvedAmount,
|
||||
required this.availableBalance,
|
||||
required this.currentBalance,
|
||||
required this.name,
|
||||
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['stBranchNo'],
|
||||
currency: json['stCurrency'],
|
||||
productType: json['stProductType'],
|
||||
approvedAmount: json['stApprovedAmount'],
|
||||
availableBalance: json['stAvailableBalance'],
|
||||
currentBalance: json['stCurrentBalance'],
|
||||
name: json['custname'],
|
||||
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,
|
||||
transactions,
|
||||
activeAccounts,
|
||||
cifNumber,
|
||||
primaryId,
|
||||
];
|
||||
}
|
62
lib/data/repositories/auth_repository.dart
Normal file
62
lib/data/repositories/auth_repository.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
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';
|
||||
import '../../data/models/user.dart';
|
||||
import '../../security/secure_storage.dart';
|
||||
|
||||
class AuthRepository {
|
||||
final AuthService _authService;
|
||||
final UserService _userService;
|
||||
final SecureStorage _secureStorage;
|
||||
|
||||
static const _accessTokenKey = 'access_token';
|
||||
static const _tokenExpiryKey = 'token_expiry';
|
||||
|
||||
AuthRepository(this._authService, this._userService, this._secureStorage);
|
||||
|
||||
Future<List<User>> login(String customerNo, String password) async {
|
||||
// Create credentials and call service
|
||||
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 users = await _userService.getUserDetails();
|
||||
return users;
|
||||
}
|
||||
|
||||
Future<bool> isLoggedIn() async {
|
||||
final token = await _getAuthToken();
|
||||
return token != null && !token.isExpired;
|
||||
}
|
||||
|
||||
Future<String?> getAccessToken() async {
|
||||
final token = await _getAuthToken();
|
||||
if (token == null) return null;
|
||||
|
||||
return token.accessToken;
|
||||
}
|
||||
|
||||
// Private helper methods
|
||||
Future<void> _saveAuthToken(AuthToken token) async {
|
||||
await _secureStorage.write(_accessTokenKey, token.accessToken);
|
||||
await _secureStorage.write(_tokenExpiryKey, token.expiresAt.toIso8601String());
|
||||
}
|
||||
|
||||
Future<AuthToken?> _getAuthToken() async {
|
||||
final accessToken = await _secureStorage.read(_accessTokenKey);
|
||||
final expiryString = await _secureStorage.read(_tokenExpiryKey);
|
||||
|
||||
if (accessToken != null && expiryString != null) {
|
||||
return AuthToken(
|
||||
accessToken: accessToken,
|
||||
expiresAt: DateTime.parse(expiryString),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
28
lib/data/repositories/transaction_repository.dart
Normal file
28
lib/data/repositories/transaction_repository.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:kmobile/data/models/transaction.dart';
|
||||
|
||||
abstract class TransactionRepository {
|
||||
Future<List<Transaction>> fetchTransactions(String accountNo);
|
||||
}
|
||||
|
||||
class TransactionRepositoryImpl implements TransactionRepository {
|
||||
final Dio _dio;
|
||||
TransactionRepositoryImpl(this._dio);
|
||||
|
||||
@override
|
||||
Future<List<Transaction>> fetchTransactions(String accountNo) async {
|
||||
|
||||
final resp = await _dio.get('/api/transactions/account/$accountNo');
|
||||
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(
|
||||
'Error fetching transactions: ${resp.statusCode} ${resp.statusMessage}',
|
||||
);
|
||||
}
|
||||
|
||||
final List<dynamic> data = resp.data as List<dynamic>;
|
||||
return data
|
||||
.map((e) => Transaction.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
}
|
28
lib/data/repositories/transfer_repository.dart
Normal file
28
lib/data/repositories/transfer_repository.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
import 'package:kmobile/data/models/transfer.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
|
||||
abstract class TransferRepository {
|
||||
Future<TransferResponse> transfer(Transfer transfer);
|
||||
}
|
||||
|
||||
class TransferRepositoryImpl implements TransferRepository {
|
||||
final Dio _dio;
|
||||
TransferRepositoryImpl(this._dio);
|
||||
|
||||
@override
|
||||
Future<TransferResponse> transfer(Transfer transfer) async {
|
||||
final resp = await _dio.post(
|
||||
'/api/payment/transfer',
|
||||
data: transfer.toJson(),
|
||||
);
|
||||
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(
|
||||
'Error transferring funds: ${resp.statusCode} ${resp.statusMessage}',
|
||||
);
|
||||
}
|
||||
|
||||
return TransferResponse.fromJson(resp.data as Map<String, dynamic>);
|
||||
}
|
||||
}
|
72
lib/di/injection.dart
Normal file
72
lib/di/injection.dart
Normal file
@@ -0,0 +1,72 @@
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:kmobile/api/services/payment_service.dart';
|
||||
import 'package:kmobile/api/services/user_service.dart';
|
||||
import 'package:kmobile/data/repositories/transaction_repository.dart';
|
||||
import '../api/services/auth_service.dart';
|
||||
import '../api/interceptors/auth_interceptor.dart';
|
||||
import '../data/repositories/auth_repository.dart';
|
||||
import '../features/auth/controllers/auth_cubit.dart';
|
||||
import '../security/secure_storage.dart';
|
||||
|
||||
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<UserService>(), getIt<SecureStorage>()),
|
||||
);
|
||||
getIt.registerSingleton<TransactionRepository>(
|
||||
TransactionRepositoryImpl(getIt<Dio>()));
|
||||
|
||||
getIt.registerSingleton<PaymentService>(PaymentService(getIt<Dio>()));
|
||||
|
||||
// Add auth interceptor after repository is available
|
||||
getIt<Dio>().interceptors.add(
|
||||
AuthInterceptor(getIt<AuthRepository>(), getIt<Dio>()),
|
||||
);
|
||||
|
||||
// Register controllers/cubits
|
||||
getIt.registerFactory<AuthCubit>(
|
||||
() => AuthCubit(getIt<AuthRepository>(), getIt<UserService>()));
|
||||
}
|
||||
|
||||
Dio _createDioClient() {
|
||||
final dio = Dio(
|
||||
BaseOptions(
|
||||
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: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Add logging interceptor for development
|
||||
dio.interceptors.add(LogInterceptor(
|
||||
request: true,
|
||||
requestHeader: true,
|
||||
requestBody: true,
|
||||
responseHeader: true,
|
||||
responseBody: true,
|
||||
error: true,
|
||||
));
|
||||
|
||||
return dio;
|
||||
}
|
15
lib/features/accounts/models/account.dart
Normal file
15
lib/features/accounts/models/account.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
class Account {
|
||||
final String id;
|
||||
final String accountNumber;
|
||||
final String accountType;
|
||||
final double balance;
|
||||
final String currency;
|
||||
|
||||
Account({
|
||||
required this.id,
|
||||
required this.accountNumber,
|
||||
required this.accountType,
|
||||
required this.balance,
|
||||
required this.currency,
|
||||
});
|
||||
}
|
109
lib/features/accounts/screens/account_info_screen.dart
Normal file
109
lib/features/accounts/screens/account_info_screen.dart
Normal file
@@ -0,0 +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 AccountInfoScreen extends StatefulWidget {
|
||||
final User user;
|
||||
|
||||
const AccountInfoScreen({super.key, required this.user});
|
||||
|
||||
@override
|
||||
State<AccountInfoScreen> createState() => _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),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
title: const Text(
|
||||
'Account Info',
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
||||
),
|
||||
centerTitle: false,
|
||||
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: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InfoRow extends StatelessWidget {
|
||||
final String title;
|
||||
final String value;
|
||||
|
||||
const InfoRow({required this.title, required this.value, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
319
lib/features/accounts/screens/account_statement_screen.dart
Normal file
319
lib/features/accounts/screens/account_statement_screen.dart
Normal file
@@ -0,0 +1,319 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
import 'package:kmobile/data/models/transaction.dart';
|
||||
import 'package:kmobile/data/repositories/transaction_repository.dart';
|
||||
import 'package:kmobile/di/injection.dart';
|
||||
|
||||
class AccountStatementScreen extends StatefulWidget {
|
||||
final String accountNo;
|
||||
const AccountStatementScreen({
|
||||
super.key,
|
||||
required this.accountNo,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AccountStatementScreen> createState() => _AccountStatementScreen();
|
||||
}
|
||||
|
||||
class _AccountStatementScreen extends State<AccountStatementScreen> {
|
||||
DateTime? fromDate;
|
||||
DateTime? toDate;
|
||||
bool _txLoading = true;
|
||||
List<Transaction> _transactions = [];
|
||||
final _minAmountController = TextEditingController();
|
||||
final _maxAmountController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadTransactions();
|
||||
}
|
||||
|
||||
Future<void> _loadTransactions() async {
|
||||
setState(() {
|
||||
_txLoading = true;
|
||||
_transactions = [];
|
||||
});
|
||||
try {
|
||||
final repo = getIt<TransactionRepository>();
|
||||
final txs = await repo.fetchTransactions(widget.accountNo);
|
||||
setState(() => _transactions = txs);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to load transactions: $e')),
|
||||
);
|
||||
} finally {
|
||||
setState(() => _txLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _selectFromDate(BuildContext context) async {
|
||||
final now = DateTime.now();
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: fromDate ?? now,
|
||||
firstDate: DateTime(2020),
|
||||
lastDate: now,
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
fromDate = picked;
|
||||
toDate = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _selectToDate(BuildContext context) async {
|
||||
if (fromDate == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Please select From Date first')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final now = DateTime.now();
|
||||
final maxToDate = fromDate!.add(const Duration(days: 31)).isBefore(now)
|
||||
? fromDate!.add(const Duration(days: 31))
|
||||
: now;
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: toDate ?? fromDate!,
|
||||
firstDate: fromDate!,
|
||||
lastDate: maxToDate,
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() => toDate = picked);
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDate(DateTime date) {
|
||||
return "${date.day.toString().padLeft(2, '0')}-"
|
||||
"${date.month.toString().padLeft(2, '0')}-"
|
||||
"${date.year}";
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_minAmountController.dispose();
|
||||
_maxAmountController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@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(
|
||||
'Account Statement',
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
||||
),
|
||||
centerTitle: false,
|
||||
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: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Filters', style: TextStyle(fontSize: 17)),
|
||||
const SizedBox(height: 15),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => _selectFromDate(context),
|
||||
child: buildDateBox("From Date", fromDate),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => _selectToDate(context),
|
||||
child: buildDateBox("To Date", toDate),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _minAmountController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Min Amount',
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _maxAmountController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Max Amount',
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.done,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
width: 70,
|
||||
child: ElevatedButton(
|
||||
onPressed: _loadTransactions,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).primaryColor,
|
||||
),
|
||||
child: const Icon(
|
||||
Symbols.arrow_forward,
|
||||
color: Colors.white,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 35),
|
||||
if (!_txLoading && _transactions.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: Text(
|
||||
'Showing last 10 transactions.',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _txLoading
|
||||
? ListView.builder(
|
||||
itemCount: 3,
|
||||
itemBuilder: (_, __) => ListTile(
|
||||
leading: Shimmer.fromColors(
|
||||
baseColor: Colors.grey[300]!,
|
||||
highlightColor: Colors.grey[100]!,
|
||||
child: const CircleAvatar(
|
||||
radius: 12, backgroundColor: Colors.white),
|
||||
),
|
||||
title: Shimmer.fromColors(
|
||||
baseColor: Colors.grey[300]!,
|
||||
highlightColor: Colors.grey[100]!,
|
||||
child: Container(
|
||||
height: 10, width: 100, color: Colors.white),
|
||||
),
|
||||
subtitle: Shimmer.fromColors(
|
||||
baseColor: Colors.grey[300]!,
|
||||
highlightColor: Colors.grey[100]!,
|
||||
child: Container(
|
||||
height: 8, width: 60, color: Colors.white),
|
||||
),
|
||||
),
|
||||
)
|
||||
: _transactions.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
'No transactions found for this account.',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: _transactions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final txn = _transactions[index];
|
||||
final isCredit = txn.type == 'CR';
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(txn.date ?? '',
|
||||
style: const TextStyle(color: Colors.grey)),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(txn.name ?? '',
|
||||
style: const TextStyle(fontSize: 16)),
|
||||
),
|
||||
Text(
|
||||
"${isCredit ? '+' : '-'}₹${txn.amount}",
|
||||
style: TextStyle(
|
||||
color: isCredit
|
||||
? Colors.green
|
||||
: Colors.red,
|
||||
// fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildDateBox(String label, DateTime? date) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
date != null ? _formatDate(date) : label,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: date != null ? Colors.black : Colors.grey,
|
||||
),
|
||||
),
|
||||
const Icon(Icons.arrow_drop_down),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
50
lib/features/auth/controllers/auth_cubit.dart
Normal file
50
lib/features/auth/controllers/auth_cubit.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'package:bloc/bloc.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, this._userService) : super(AuthInitial()) {
|
||||
checkAuthStatus();
|
||||
}
|
||||
|
||||
Future<void> checkAuthStatus() async {
|
||||
emit(AuthLoading());
|
||||
try {
|
||||
final isLoggedIn = await _authRepository.isLoggedIn();
|
||||
if (isLoggedIn) {
|
||||
final users = await _userService.getUserDetails();
|
||||
emit(Authenticated(users));
|
||||
} else {
|
||||
emit(Unauthenticated());
|
||||
}
|
||||
} catch (e) {
|
||||
emit(AuthError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
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 users = await _authRepository.login(customerNo, password);
|
||||
emit(Authenticated(users));
|
||||
} catch (e) {
|
||||
emit(AuthError(e is AuthException ? e.message : e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
31
lib/features/auth/controllers/auth_state.dart
Normal file
31
lib/features/auth/controllers/auth_state.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../../data/models/user.dart';
|
||||
|
||||
abstract class AuthState extends Equatable {
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class AuthInitial extends AuthState {}
|
||||
|
||||
class AuthLoading extends AuthState {}
|
||||
|
||||
class Authenticated extends AuthState {
|
||||
final List<User> users;
|
||||
|
||||
Authenticated(this.users);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [users];
|
||||
}
|
||||
|
||||
class Unauthenticated extends AuthState {}
|
||||
|
||||
class AuthError extends AuthState {
|
||||
final String message;
|
||||
|
||||
AuthError(this.message);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
11
lib/features/auth/models/auth_credentials.dart
Normal file
11
lib/features/auth/models/auth_credentials.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
class AuthCredentials {
|
||||
final String customerNo;
|
||||
final String password;
|
||||
|
||||
AuthCredentials({required this.customerNo, required this.password});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'customerNo': customerNo,
|
||||
'password': password,
|
||||
};
|
||||
}
|
48
lib/features/auth/models/auth_token.dart
Normal file
48
lib/features/auth/models/auth_token.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class AuthToken extends Equatable {
|
||||
final String accessToken;
|
||||
final DateTime expiresAt;
|
||||
|
||||
const AuthToken({
|
||||
required this.accessToken,
|
||||
required this.expiresAt,
|
||||
});
|
||||
|
||||
factory AuthToken.fromJson(Map<String, dynamic> json) {
|
||||
return AuthToken(
|
||||
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, expiresAt];
|
||||
}
|
474
lib/features/auth/screens/login_screen.dart
Normal file
474
lib/features/auth/screens/login_screen.dart
Normal file
@@ -0,0 +1,474 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({super.key});
|
||||
|
||||
@override
|
||||
LoginScreenState createState() => LoginScreenState();
|
||||
}
|
||||
|
||||
class LoginScreenState extends State<LoginScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _customerNumberController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
bool _obscurePassword = true;
|
||||
//bool _showWelcome = true;
|
||||
late AnimationController _logoController;
|
||||
late Animation<double> _logoAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_logoController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 1),
|
||||
)..repeat(reverse: true);
|
||||
|
||||
_logoAnimation = Tween<double>(begin: 0.2, end: 1).animate(_logoController);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_logoController.dispose();
|
||||
_customerNumberController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _submitForm() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
context.read<AuthCubit>().login(
|
||||
_customerNumberController.text.trim(),
|
||||
_passwordController.text,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
// appBar: AppBar(title: const Text('Login')),
|
||||
body: BlocConsumer<AuthCubit, AuthState>(
|
||||
listener: (context, state) async {
|
||||
if (state is Authenticated) {
|
||||
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)),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// 🔁 Animated Blinking Logo
|
||||
FadeTransition(
|
||||
opacity: _logoAnimation,
|
||||
child: 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: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
|
||||
TextFormField(
|
||||
controller: _customerNumberController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Customer Number',
|
||||
// prefixIcon: Icon(Icons.person),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your username';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Password
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
obscureText: _obscurePassword,
|
||||
textInputAction: TextInputAction.done,
|
||||
onFieldSubmitted: (_) =>
|
||||
_submitForm(), // ⌨️ Enter key submits
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password',
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscurePassword
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_obscurePassword = !_obscurePassword;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your password';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
//Login Button
|
||||
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,
|
||||
),
|
||||
child: state is AuthLoading
|
||||
? const CircularProgressIndicator()
|
||||
: const Text(
|
||||
"Login",
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Divider()),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text('OR'),
|
||||
),
|
||||
Expanded(child: Divider()),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 25),
|
||||
|
||||
// Register Button
|
||||
SizedBox(
|
||||
width: 250,
|
||||
child: ElevatedButton(
|
||||
//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),
|
||||
child: const Text('Register'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({super.key});
|
||||
|
||||
@override
|
||||
LoginScreenState createState() => LoginScreenState();
|
||||
}
|
||||
|
||||
class LoginScreenState extends State<LoginScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _customerNumberController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
bool _obscurePassword = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_customerNumberController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _submitForm() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
context.read<AuthCubit>().login(
|
||||
_customerNumberController.text.trim(),
|
||||
_passwordController.text,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
// appBar: AppBar(title: const Text('Login')),
|
||||
body: BlocConsumer<AuthCubit, AuthState>(
|
||||
listener: (context, state) async {
|
||||
if (state is Authenticated) {
|
||||
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)),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
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: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
|
||||
TextFormField(
|
||||
controller: _customerNumberController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Customer Number',
|
||||
// prefixIcon: Icon(Icons.person),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your username';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password',
|
||||
// prefixIcon: const Icon(Icons.lock),
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscurePassword
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_obscurePassword = !_obscurePassword;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
textInputAction: TextInputAction.done,
|
||||
obscureText: _obscurePassword,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter your password';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
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),
|
||||
child: state is AuthLoading
|
||||
? const CircularProgressIndicator()
|
||||
: const Text(
|
||||
'Login',
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Divider()),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text('OR'),
|
||||
),
|
||||
Expanded(child: Divider()),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
|
||||
// Register Button
|
||||
SizedBox(
|
||||
width: 250,
|
||||
child: ElevatedButton(
|
||||
//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),
|
||||
child: const Text('Register'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
*/
|
249
lib/features/auth/screens/mpin_screen.dart
Normal file
249
lib/features/auth/screens/mpin_screen.dart
Normal file
@@ -0,0 +1,249 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.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 {
|
||||
final MPinMode mode;
|
||||
final String? initialPin;
|
||||
final void Function(String pin)? onCompleted;
|
||||
|
||||
const MPinScreen({
|
||||
super.key,
|
||||
required this.mode,
|
||||
this.initialPin,
|
||||
this.onCompleted,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MPinScreen> createState() => _MPinScreenState();
|
||||
}
|
||||
|
||||
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 != null && 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void deleteDigit() {
|
||||
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,
|
||||
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) {
|
||||
_handleComplete();
|
||||
} else {
|
||||
setState(() {
|
||||
errorText = "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(Icons.check)
|
||||
: Text(
|
||||
key == '<' ? '⌫' : key,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: key == 'Enter' ? Colors.blue : Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const Spacer(),
|
||||
// Logo
|
||||
Image.asset('assets/images/logo.png', height: 100),
|
||||
const SizedBox(height: 20),
|
||||
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(),
|
||||
buildNumberPad(),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
141
lib/features/auth/screens/welcome_screen.dart
Normal file
141
lib/features/auth/screens/welcome_screen.dart
Normal file
@@ -0,0 +1,141 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
|
||||
class WelcomeScreen extends StatefulWidget {
|
||||
final VoidCallback onContinue;
|
||||
|
||||
const WelcomeScreen({super.key, required this.onContinue});
|
||||
|
||||
@override
|
||||
State<WelcomeScreen> createState() => _WelcomeScreenState();
|
||||
}
|
||||
|
||||
class _WelcomeScreenState extends State<WelcomeScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Automatically go to login after 6 seconds
|
||||
Timer(const Duration(seconds: 6), () {
|
||||
widget.onContinue();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
/// 🔹 Background Image
|
||||
Positioned.fill(
|
||||
child: Image.asset(
|
||||
'assets/images/kconnect2.webp',
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
|
||||
/// 🔹 Centered Text Overlay
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
Text(
|
||||
'Kconnect',
|
||||
style: TextStyle(
|
||||
fontSize: 36,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
letterSpacing: 1.5,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
'Kangra Central Co-operative Bank',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.white,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
/// 🔹 Loading Spinner at Bottom
|
||||
const Positioned(
|
||||
bottom: 40,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*import 'package:flutter/material.dart';
|
||||
|
||||
class WelcomeScreen extends StatelessWidget {
|
||||
final VoidCallback onContinue;
|
||||
|
||||
const WelcomeScreen({super.key, required this.onContinue});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Spacer(),
|
||||
const Text(
|
||||
'Welcome to',
|
||||
style: TextStyle(fontSize: 28, color: Colors.black87),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'KCCB',
|
||||
style: TextStyle(
|
||||
fontSize: 42,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.indigo,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
Image.asset(
|
||||
'assets/images/logo.png',
|
||||
width: 150,
|
||||
height: 150,
|
||||
),
|
||||
const Spacer(),
|
||||
ElevatedButton(
|
||||
onPressed: onContinue,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.indigo,
|
||||
foregroundColor: Colors.white,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 28, vertical: 20),
|
||||
),
|
||||
child: const Text('Proceed to Login'),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
533
lib/features/beneficiaries/screens/add_beneficiary_screen.dart
Normal file
533
lib/features/beneficiaries/screens/add_beneficiary_screen.dart
Normal file
@@ -0,0 +1,533 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class AddBeneficiaryScreen extends StatefulWidget {
|
||||
const AddBeneficiaryScreen({super.key});
|
||||
|
||||
@override
|
||||
State<AddBeneficiaryScreen> createState() => _AddBeneficiaryScreen();
|
||||
}
|
||||
|
||||
class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
final TextEditingController accountNumberController = TextEditingController();
|
||||
final TextEditingController confirmAccountNumberController =
|
||||
TextEditingController();
|
||||
final TextEditingController nameController = TextEditingController();
|
||||
final TextEditingController bankNameController = TextEditingController();
|
||||
final TextEditingController branchNameController = TextEditingController();
|
||||
final TextEditingController ifscController = TextEditingController();
|
||||
final TextEditingController phoneController = TextEditingController();
|
||||
|
||||
late String accountType;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
accountType = AppLocalizations.of(context).savings;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _submitForm() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
// Handle successful submission
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: Colors.grey[900],
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: const EdgeInsets.all(12),
|
||||
duration: const Duration(seconds: 5),
|
||||
content: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
AppLocalizations.of(context).beneficiaryAdded,
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// Navigate to Payment Screen or do something
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.blue[200],
|
||||
),
|
||||
child: Text(AppLocalizations.of(context).payNow),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _validateIFSC() async {
|
||||
final ifsc = ifscController.text.trim().toUpperCase();
|
||||
|
||||
// 🔹 Format check
|
||||
final isValidFormat = RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$').hasMatch(ifsc);
|
||||
if (!isValidFormat) {
|
||||
bankNameController.clear();
|
||||
branchNameController.clear();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(AppLocalizations.of(context).invalidIfscFormat)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
await Future.delayed(
|
||||
const Duration(seconds: 2)); //Mock delay for 2 sec to imitate api call
|
||||
// 🔹 Mock IFSC lookup (you can replace this with API)
|
||||
const mockIfscData = {
|
||||
'KCCB0001234': {
|
||||
'bank': 'Kangra Central Co-operative Bank',
|
||||
'branch': 'Dharamshala',
|
||||
},
|
||||
'SBIN0004567': {
|
||||
'bank': 'State Bank of India',
|
||||
'branch': 'Connaught Place',
|
||||
},
|
||||
};
|
||||
|
||||
if (mockIfscData.containsKey(ifsc)) {
|
||||
final data = mockIfscData[ifsc]!;
|
||||
bankNameController.text = data['bank']!;
|
||||
branchNameController.text = data['branch']!;
|
||||
} else {
|
||||
bankNameController.clear();
|
||||
branchNameController.clear();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(AppLocalizations.of(context).noIfscDetails)),
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
🔸 Optional: Use real IFSC API like:
|
||||
final response = await http.get(Uri.parse('https://ifsc.razorpay.com/$ifsc'));
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
bankNameController.text = data['BANK'];
|
||||
branchNameController.text = data['BRANCH'];
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Symbols.arrow_back_ios_new),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
AppLocalizations.of(context).addBeneficiary,
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
||||
),
|
||||
centerTitle: false,
|
||||
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: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: accountNumberController,
|
||||
decoration: InputDecoration(
|
||||
labelText:
|
||||
AppLocalizations.of(context).accountNumber,
|
||||
// prefixIcon: Icon(Icons.person),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
obscureText: true,
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
if (value == null || value.length < 10) {
|
||||
return AppLocalizations.of(context)
|
||||
.enterValidAccountNumber;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Confirm Account Number
|
||||
TextFormField(
|
||||
controller: confirmAccountNumberController,
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context)
|
||||
.confirmAccountNumber,
|
||||
// prefixIcon: Icon(Icons.person),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return AppLocalizations.of(context)
|
||||
.reenterAccountNumber;
|
||||
}
|
||||
if (value != accountNumberController.text) {
|
||||
return AppLocalizations.of(context)
|
||||
.accountMismatch;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
TextFormField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context).name,
|
||||
// prefixIcon: Icon(Icons.person),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) => value == null || value.isEmpty
|
||||
? AppLocalizations.of(context).nameRequired
|
||||
: null,
|
||||
),
|
||||
/*const SizedBox(height: 24),
|
||||
TextFormField(
|
||||
controller: bankNameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Beneficiary Bank Name',
|
||||
// prefixIcon: Icon(Icons.person),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) =>
|
||||
value == null || value.isEmpty ? "Bank name is required" : null,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
TextFormField(
|
||||
controller: branchNameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Branch Name',
|
||||
// prefixIcon: Icon(Icons.person),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) =>
|
||||
value == null || value.isEmpty ? "Branch name is required" : null,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: TextFormField(
|
||||
controller: ifscController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'IFSC Code',
|
||||
// prefixIcon: Icon(Icons.person),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) => value == null || value.length < 5
|
||||
? "Enter a valid IFSC"
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: accountType,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Account Type',
|
||||
// prefixIcon: Icon(Icons.person),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
|
||||
items: ['Savings', 'Current']
|
||||
.map((type) => DropdownMenuItem(
|
||||
value: type,
|
||||
child: Text(type),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
accountType = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),*/
|
||||
const SizedBox(height: 24),
|
||||
// 🔹 IFSC Code Field
|
||||
TextFormField(
|
||||
controller: ifscController,
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context).ifscCode,
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
textCapitalization: TextCapitalization.characters,
|
||||
textInputAction: TextInputAction.next,
|
||||
onFieldSubmitted: (_) {
|
||||
_validateIFSC();
|
||||
},
|
||||
onChanged: (value) {
|
||||
final trimmed = value.trim().toUpperCase();
|
||||
if (trimmed.length < 11) {
|
||||
// clear bank/branch if backspace or changed
|
||||
bankNameController.clear();
|
||||
branchNameController.clear();
|
||||
}
|
||||
},
|
||||
validator: (value) {
|
||||
final pattern = RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$');
|
||||
if (value == null || value.trim().isEmpty) {
|
||||
return AppLocalizations.of(context).enterIfsc;
|
||||
} else if (!pattern
|
||||
.hasMatch(value.trim().toUpperCase())) {
|
||||
return AppLocalizations.of(context)
|
||||
.invalidIfscFormat;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
// 🔹 Bank Name (Disabled)
|
||||
TextFormField(
|
||||
controller: bankNameController,
|
||||
enabled: false, // changed from readOnly to disabled
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context).bankName,
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white, // disabled color
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
// 🔹 Branch Name (Disabled)
|
||||
TextFormField(
|
||||
controller: branchNameController,
|
||||
enabled: false, // changed from readOnly to disabled
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context).branchName,
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
// 🔹 Account Type Dropdown
|
||||
DropdownButtonFormField<String>(
|
||||
value: accountType,
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context).accountType,
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
items: [
|
||||
AppLocalizations.of(context).savings,
|
||||
AppLocalizations.of(context).current
|
||||
]
|
||||
.map((type) => DropdownMenuItem(
|
||||
value: type,
|
||||
child: Text(type),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
accountType = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
TextFormField(
|
||||
controller: phoneController,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context).phone,
|
||||
prefixIcon: Icon(Icons.phone),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide:
|
||||
BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
textInputAction: TextInputAction.done,
|
||||
validator: (value) =>
|
||||
value == null || value.length != 10
|
||||
? AppLocalizations.of(context).enterValidPhone
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 35),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SizedBox(
|
||||
width: 250,
|
||||
child: ElevatedButton(
|
||||
onPressed: _submitForm,
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: const StadiumBorder(),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
backgroundColor: Colors.blue[900],
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: Text(AppLocalizations.of(context).validateAndAdd),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,104 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:kmobile/features/beneficiaries/screens/add_beneficiary_screen.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class ManageBeneficiariesScreen extends StatefulWidget {
|
||||
const ManageBeneficiariesScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ManageBeneficiariesScreen> createState() =>
|
||||
_ManageBeneficiariesScreen();
|
||||
}
|
||||
|
||||
class _ManageBeneficiariesScreen extends State<ManageBeneficiariesScreen> {
|
||||
final List<Map<String, String>> beneficiaries = [
|
||||
{
|
||||
'bank': 'State Bank Of India',
|
||||
'name': 'Trina Bakshi',
|
||||
},
|
||||
{
|
||||
'bank': 'State Bank Of India',
|
||||
'name': 'Sheetal Rao',
|
||||
},
|
||||
{
|
||||
'bank': 'Punjab National Bank',
|
||||
'name': 'Manoj Kumar',
|
||||
},
|
||||
{
|
||||
'bank': 'State Bank Of India',
|
||||
'name': 'Rohit Mehra',
|
||||
},
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Symbols.arrow_back_ios_new),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
AppLocalizations.of(context).beneficiaries,
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
||||
),
|
||||
centerTitle: false,
|
||||
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: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ListView.builder(
|
||||
itemCount: beneficiaries.length,
|
||||
itemBuilder: (context, index) {
|
||||
final beneficiary = beneficiaries[index];
|
||||
return ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Colors.blue, child: Text('A')),
|
||||
title: Text(beneficiary['name']!),
|
||||
subtitle: Text(beneficiary['bank']!),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Symbols.delete_forever, color: Colors.red),
|
||||
onPressed: () {
|
||||
// Delete action
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
floatingActionButton: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const AddBeneficiaryScreen()));
|
||||
},
|
||||
backgroundColor: Colors.grey[300],
|
||||
foregroundColor: Colors.blue[900],
|
||||
elevation: 5,
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
213
lib/features/card/screens/block_card_screen.dart
Normal file
213
lib/features/card/screens/block_card_screen.dart
Normal file
@@ -0,0 +1,213 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
|
||||
class BlockCardScreen extends StatefulWidget {
|
||||
const BlockCardScreen({super.key});
|
||||
|
||||
@override
|
||||
State<BlockCardScreen> createState() => _BlockCardScreen();
|
||||
}
|
||||
|
||||
class _BlockCardScreen extends State<BlockCardScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _cardController = TextEditingController();
|
||||
final _cvvController = TextEditingController();
|
||||
final _expiryController = TextEditingController();
|
||||
final _phoneController = TextEditingController();
|
||||
|
||||
Future<void> _pickExpiryDate() async {
|
||||
final now = DateTime.now();
|
||||
final selectedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: now,
|
||||
firstDate: now,
|
||||
lastDate: DateTime(now.year + 10),
|
||||
);
|
||||
if (selectedDate != null) {
|
||||
_expiryController.text = DateFormat('dd/MM/yyyy').format(selectedDate);
|
||||
}
|
||||
}
|
||||
|
||||
void _blockCard() {
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
// Call your backend logic here
|
||||
|
||||
final snackBar = SnackBar(
|
||||
content: const Text('Card has been blocked'),
|
||||
action: SnackBarAction(
|
||||
label: 'X',
|
||||
onPressed: () {
|
||||
// Just close the SnackBar
|
||||
},
|
||||
textColor: Colors.white,
|
||||
),
|
||||
backgroundColor: Colors.black,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
}
|
||||
|
||||
@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(
|
||||
'Block Card',
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
||||
),
|
||||
centerTitle: false,
|
||||
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: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
TextFormField(
|
||||
controller: _cardController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Card Number',
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) => value != null && value.length == 11
|
||||
? null
|
||||
: 'Enter valid card number',
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _cvvController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'CVV',
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
obscureText: true,
|
||||
validator: (value) => value != null && value.length == 3
|
||||
? null
|
||||
: 'CVV must be 3 digits',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _expiryController,
|
||||
readOnly: true,
|
||||
onTap: _pickExpiryDate,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Expiry Date',
|
||||
suffixIcon: Icon(Icons.calendar_today),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
validator: (value) => value != null && value.isNotEmpty
|
||||
? null
|
||||
: 'Select expiry date',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
TextFormField(
|
||||
controller: _phoneController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Phone',
|
||||
prefixIcon: Icon(Icons.phone),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
textInputAction: TextInputAction.done,
|
||||
keyboardType: TextInputType.phone,
|
||||
validator: (value) => value != null && value.length >= 10
|
||||
? null
|
||||
: 'Enter valid phone number',
|
||||
),
|
||||
const SizedBox(height: 45),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: SizedBox(
|
||||
width: 250,
|
||||
child: ElevatedButton(
|
||||
onPressed: _blockCard,
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: const StadiumBorder(),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
backgroundColor: Colors.blue[900],
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Block'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
105
lib/features/card/screens/card_management_screen.dart
Normal file
105
lib/features/card/screens/card_management_screen.dart
Normal file
@@ -0,0 +1,105 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:kmobile/features/card/screens/block_card_screen.dart';
|
||||
import 'package:kmobile/features/card/screens/card_pin_change_details_screen.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
|
||||
class CardManagementScreen extends StatefulWidget {
|
||||
const CardManagementScreen({super.key});
|
||||
|
||||
@override
|
||||
State<CardManagementScreen> createState() => _CardManagementScreen();
|
||||
}
|
||||
|
||||
class _CardManagementScreen extends State<CardManagementScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
title: const Text(
|
||||
'Card Management',
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
||||
),
|
||||
centerTitle: false,
|
||||
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: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
CardManagementTile(
|
||||
icon: Symbols.add,
|
||||
label: 'Apply Debit Card',
|
||||
onTap: () {},
|
||||
),
|
||||
const Divider(
|
||||
height: 1,
|
||||
),
|
||||
CardManagementTile(
|
||||
icon: Symbols.remove_moderator,
|
||||
label: 'Block / Unblock Card',
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const BlockCardScreen()));
|
||||
},
|
||||
),
|
||||
const Divider(
|
||||
height: 1,
|
||||
),
|
||||
CardManagementTile(
|
||||
icon: Symbols.password_2,
|
||||
label: 'Change Card PIN',
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const CardPinChangeDetailsScreen()));
|
||||
},
|
||||
),
|
||||
const Divider(
|
||||
height: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CardManagementTile extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const CardManagementTile({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: Icon(icon),
|
||||
title: Text(label),
|
||||
trailing: const Icon(Symbols.arrow_right, size: 20),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
203
lib/features/card/screens/card_pin_change_details_screen.dart
Normal file
203
lib/features/card/screens/card_pin_change_details_screen.dart
Normal file
@@ -0,0 +1,203 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:kmobile/features/card/screens/card_pin_set_screen.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
|
||||
class CardPinChangeDetailsScreen extends StatefulWidget {
|
||||
const CardPinChangeDetailsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<CardPinChangeDetailsScreen> createState() =>
|
||||
_CardPinChangeDetailsScreen();
|
||||
}
|
||||
|
||||
class _CardPinChangeDetailsScreen extends State<CardPinChangeDetailsScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _cardController = TextEditingController();
|
||||
final _cvvController = TextEditingController();
|
||||
final _expiryController = TextEditingController();
|
||||
final _phoneController = TextEditingController();
|
||||
|
||||
Future<void> _pickExpiryDate() async {
|
||||
final now = DateTime.now();
|
||||
final selectedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: now,
|
||||
firstDate: now,
|
||||
lastDate: DateTime(now.year + 10),
|
||||
);
|
||||
if (selectedDate != null) {
|
||||
_expiryController.text = DateFormat('dd/MM/yyyy').format(selectedDate);
|
||||
}
|
||||
}
|
||||
|
||||
void _nextButton() {
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
// Call your backend logic here
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(builder: (context) => const CardPinSetScreen()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@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(
|
||||
'Card Details',
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
||||
),
|
||||
centerTitle: false,
|
||||
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: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
TextFormField(
|
||||
controller: _cardController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Card Number',
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) => value != null && value.length == 11
|
||||
? null
|
||||
: 'Enter valid card number',
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _cvvController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'CVV',
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
obscureText: true,
|
||||
validator: (value) => value != null && value.length == 3
|
||||
? null
|
||||
: 'CVV must be 3 digits',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _expiryController,
|
||||
readOnly: true,
|
||||
onTap: _pickExpiryDate,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Expiry Date',
|
||||
suffixIcon: Icon(Icons.calendar_today),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
validator: (value) => value != null && value.isNotEmpty
|
||||
? null
|
||||
: 'Select expiry date',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
TextFormField(
|
||||
controller: _phoneController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Phone',
|
||||
prefixIcon: Icon(Icons.phone),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
textInputAction: TextInputAction.done,
|
||||
keyboardType: TextInputType.phone,
|
||||
validator: (value) => value != null && value.length >= 10
|
||||
? null
|
||||
: 'Enter valid phone number',
|
||||
),
|
||||
const SizedBox(height: 45),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: SizedBox(
|
||||
width: 250,
|
||||
child: ElevatedButton(
|
||||
onPressed: _nextButton,
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: const StadiumBorder(),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
backgroundColor: Colors.blue[900],
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Next'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
158
lib/features/card/screens/card_pin_set_screen.dart
Normal file
158
lib/features/card/screens/card_pin_set_screen.dart
Normal file
@@ -0,0 +1,158 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
|
||||
class CardPinSetScreen extends StatefulWidget {
|
||||
const CardPinSetScreen({super.key});
|
||||
|
||||
@override
|
||||
State<CardPinSetScreen> createState() => _CardPinSetScreen();
|
||||
}
|
||||
|
||||
class _CardPinSetScreen extends State<CardPinSetScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _pinController = TextEditingController();
|
||||
final _confirmPinController = TextEditingController();
|
||||
|
||||
void _submit() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
// Handle PIN submission logic here
|
||||
final snackBar = SnackBar(
|
||||
content: const Text('PIN set successfully'),
|
||||
action: SnackBarAction(
|
||||
label: 'X',
|
||||
onPressed: () {
|
||||
// Just close the SnackBar
|
||||
},
|
||||
textColor: Colors.white,
|
||||
),
|
||||
backgroundColor: Colors.black,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(snackBar);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pinController.dispose();
|
||||
_confirmPinController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@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(
|
||||
'Card PIN',
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
||||
),
|
||||
centerTitle: false,
|
||||
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: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _pinController,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Enter new PIN',
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter new PIN';
|
||||
}
|
||||
if (value.length < 4) {
|
||||
return 'PIN must be at least 4 digits';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
TextFormField(
|
||||
controller: _confirmPinController,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Enter Again',
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.done,
|
||||
validator: (value) {
|
||||
if (value != _pinController.text) {
|
||||
return 'PINs do not match';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 45),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: SizedBox(
|
||||
width: 250,
|
||||
child: ElevatedButton(
|
||||
onPressed: _submit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: const StadiumBorder(),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
backgroundColor: Colors.blue[900],
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Submit'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
128
lib/features/cheque/screens/cheque_management_screen.dart
Normal file
128
lib/features/cheque/screens/cheque_management_screen.dart
Normal file
@@ -0,0 +1,128 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:kmobile/features/enquiry/screens/enquiry_screen.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
|
||||
class ChequeManagementScreen extends StatefulWidget {
|
||||
const ChequeManagementScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ChequeManagementScreen> createState() => _ChequeManagementScreen();
|
||||
}
|
||||
|
||||
class _ChequeManagementScreen extends State<ChequeManagementScreen> {
|
||||
@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(
|
||||
'Cheque Management',
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
||||
),
|
||||
centerTitle: false,
|
||||
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: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
const SizedBox(height: 15),
|
||||
ChequeManagementTile(
|
||||
icon: Symbols.add,
|
||||
label: 'Request Checkbook',
|
||||
onTap: () {},
|
||||
),
|
||||
const Divider(
|
||||
height: 1,
|
||||
),
|
||||
ChequeManagementTile(
|
||||
icon: Symbols.data_alert,
|
||||
label: 'Enquiry',
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const EnquiryScreen()));
|
||||
},
|
||||
),
|
||||
const Divider(
|
||||
height: 1,
|
||||
),
|
||||
ChequeManagementTile(
|
||||
icon: Symbols.approval_delegation,
|
||||
label: 'Cheque Deposit',
|
||||
onTap: () {},
|
||||
),
|
||||
const Divider(
|
||||
height: 1,
|
||||
),
|
||||
ChequeManagementTile(
|
||||
icon: Symbols.front_hand,
|
||||
label: 'Stop Cheque',
|
||||
onTap: () {},
|
||||
),
|
||||
const Divider(
|
||||
height: 1,
|
||||
),
|
||||
ChequeManagementTile(
|
||||
icon: Symbols.cancel_presentation,
|
||||
label: 'Revoke Stop',
|
||||
onTap: () {},
|
||||
),
|
||||
const Divider(
|
||||
height: 1,
|
||||
),
|
||||
ChequeManagementTile(
|
||||
icon: Symbols.payments,
|
||||
label: 'Positive Pay',
|
||||
onTap: () {},
|
||||
),
|
||||
const Divider(
|
||||
height: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChequeManagementTile extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const ChequeManagementTile({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: Icon(icon),
|
||||
title: Text(label),
|
||||
trailing: const Icon(Symbols.arrow_right, size: 20),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
147
lib/features/customer_info/screens/customer_info_screen.dart
Normal file
147
lib/features/customer_info/screens/customer_info_screen.dart
Normal file
@@ -0,0 +1,147 @@
|
||||
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';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class CustomerInfoScreen extends StatefulWidget {
|
||||
final User user;
|
||||
const CustomerInfoScreen({super.key, required this.user});
|
||||
|
||||
@override
|
||||
State<CustomerInfoScreen> createState() => _CustomerInfoScreenState();
|
||||
}
|
||||
|
||||
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: Text(
|
||||
AppLocalizations.of(context).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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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(
|
||||
'${AppLocalizations.of(context).cif}: ${user.cifNumber ?? 'N/A'}',
|
||||
style:
|
||||
const TextStyle(fontSize: 16, color: Colors.grey),
|
||||
),
|
||||
SizedBox(height: 30),
|
||||
InfoField(
|
||||
label: AppLocalizations.of(context).activeAccounts,
|
||||
value: user.activeAccounts?.toString() ?? '6'),
|
||||
InfoField(
|
||||
label: AppLocalizations.of(context).mobileNumber,
|
||||
value: user.mobileNo ?? 'N/A'),
|
||||
InfoField(
|
||||
label: AppLocalizations.of(context).dateOfBirth,
|
||||
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: AppLocalizations.of(context).branchCode,
|
||||
value: user.branchId ?? 'N/A'),
|
||||
InfoField(
|
||||
label: AppLocalizations.of(context).branchAddress,
|
||||
value: user.address ??
|
||||
'N/A'), // Replace with Aadhar if available
|
||||
InfoField(
|
||||
label: AppLocalizations.of(context).primaryId,
|
||||
value: user.primaryId ??
|
||||
'N/A'), // Replace with PAN if available
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
class InfoField extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
const InfoField({super.key, required this.label, required this.value});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
574
lib/features/dashboard/screens/dashboard_screen.dart
Normal file
574
lib/features/dashboard/screens/dashboard_screen.dart
Normal file
@@ -0,0 +1,574 @@
|
||||
import 'dart:developer';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:kmobile/data/repositories/transaction_repository.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/enquiry/screens/enquiry_screen.dart';
|
||||
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_beneficiary_screen.dart';
|
||||
import 'package:kmobile/features/profile/profile_screen.dart';
|
||||
import 'package:kmobile/features/quick_pay/screens/quick_pay_screen.dart';
|
||||
import 'package:kmobile/security/secure_storage.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';
|
||||
import 'package:kmobile/data/models/transaction.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class DashboardScreen extends StatefulWidget {
|
||||
const DashboardScreen({super.key});
|
||||
|
||||
@override
|
||||
State<DashboardScreen> createState() => _DashboardScreenState();
|
||||
}
|
||||
|
||||
class _DashboardScreenState extends State<DashboardScreen> {
|
||||
int selectedAccountIndex = 0;
|
||||
bool isVisible = false;
|
||||
bool isRefreshing = false;
|
||||
bool isBalanceLoading = false;
|
||||
bool _biometricPromptShown = false;
|
||||
bool _txLoading = false;
|
||||
List<Transaction> _transactions = [];
|
||||
bool _txInitialized = false;
|
||||
|
||||
Future<void> _loadTransactions(String accountNo) async {
|
||||
setState(() {
|
||||
_txLoading = true;
|
||||
_transactions = [];
|
||||
});
|
||||
try {
|
||||
final repo = getIt<TransactionRepository>();
|
||||
final txs = await repo.fetchTransactions(accountNo);
|
||||
var fiveTxns = <Transaction>[];
|
||||
//only take the first 5 transactions
|
||||
if (txs.length > 5) {
|
||||
fiveTxns = txs.sublist(0, 5);
|
||||
} else {
|
||||
fiveTxns = txs;
|
||||
}
|
||||
setState(() => _transactions = fiveTxns);
|
||||
} catch (e) {
|
||||
log(accountNo, error: e);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content:
|
||||
Text(AppLocalizations.of(context).failedToLoad(e.toString()))));
|
||||
} finally {
|
||||
setState(() => _txLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
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(AppLocalizations.of(context).failedToRefresh)),
|
||||
);
|
||||
}
|
||||
setState(() {
|
||||
isRefreshing = false;
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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 AppLocalizations.of(context).savingsAccount;
|
||||
case 'ln':
|
||||
return AppLocalizations.of(context).loanAccount;
|
||||
case 'td':
|
||||
return AppLocalizations.of(context).termDeposit;
|
||||
case 'rd':
|
||||
return AppLocalizations.of(context).recurringDeposit;
|
||||
default:
|
||||
return AppLocalizations.of(context).unknownAccount;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showBiometricOptInDialog() async {
|
||||
final storage = SecureStorage();
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(context).enableBiometric),
|
||||
content: Text(AppLocalizations.of(context).useBiometricPrompt),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await storage.write('biometric_prompt_shown', 'true');
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).later),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final auth = LocalAuthentication();
|
||||
final canCheck = await auth.canCheckBiometrics;
|
||||
bool ok = false;
|
||||
if (canCheck) {
|
||||
ok = await auth.authenticate(
|
||||
localizedReason: AppLocalizations.of(context).scanBiometric,
|
||||
);
|
||||
}
|
||||
if (ok) {
|
||||
await storage.write('biometric_enabled', 'true');
|
||||
}
|
||||
await storage.write('biometric_prompt_shown', 'true');
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).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(
|
||||
AppLocalizations.of(context).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 ProfileScreen()),
|
||||
);
|
||||
},
|
||||
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];
|
||||
// first‐time load
|
||||
if (!_txInitialized) {
|
||||
_txInitialized = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_loadTransactions(currAccount.accountNo!);
|
||||
});
|
||||
}
|
||||
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(
|
||||
"${AppLocalizations.of(context).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: [
|
||||
Text(AppLocalizations.of(context).accountNumber,
|
||||
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(milliseconds: 200));
|
||||
setState(() {
|
||||
isBalanceLoading = false;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
selectedAccountIndex = newIndex;
|
||||
});
|
||||
}
|
||||
await _loadTransactions(
|
||||
users[newIndex].accountNo!);
|
||||
},
|
||||
),
|
||||
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),
|
||||
Text(
|
||||
AppLocalizations.of(context).quickLinks,
|
||||
style: TextStyle(fontSize: 17),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Quick Links
|
||||
GridView.count(
|
||||
crossAxisCount: 4,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
_buildQuickLink(Symbols.id_card,
|
||||
AppLocalizations.of(context).customerInfo, () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CustomerInfoScreen(
|
||||
user: users[selectedAccountIndex],
|
||||
)));
|
||||
}),
|
||||
_buildQuickLink(Symbols.currency_rupee,
|
||||
AppLocalizations.of(context).quickPay, () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => QuickPayScreen(
|
||||
debitAccount: currAccount.accountNo!)));
|
||||
}),
|
||||
_buildQuickLink(Symbols.send_money,
|
||||
AppLocalizations.of(context).fundTransfer, () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const FundTransferBeneficiaryScreen()));
|
||||
}, disable: false),
|
||||
_buildQuickLink(Symbols.server_person,
|
||||
AppLocalizations.of(context).accountInfo, () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AccountInfoScreen(
|
||||
user: users[selectedAccountIndex])));
|
||||
}),
|
||||
_buildQuickLink(Symbols.receipt_long,
|
||||
AppLocalizations.of(context).accountStatement, () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AccountStatementScreen(
|
||||
accountNo: users[selectedAccountIndex]
|
||||
.accountNo!,
|
||||
)));
|
||||
}),
|
||||
_buildQuickLink(Symbols.checkbook,
|
||||
AppLocalizations.of(context).handleCheque, () {},
|
||||
disable: false),
|
||||
_buildQuickLink(Icons.group,
|
||||
AppLocalizations.of(context).manageBeneficiary, () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const ManageBeneficiariesScreen()));
|
||||
}, disable: false),
|
||||
_buildQuickLink(Symbols.support_agent,
|
||||
AppLocalizations.of(context).contactUs, () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const EnquiryScreen()));
|
||||
}),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
|
||||
// Recent Transactions
|
||||
Text(
|
||||
AppLocalizations.of(context).recentTransactions,
|
||||
style: TextStyle(fontSize: 17),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (_txLoading)
|
||||
..._buildTransactionShimmer()
|
||||
else if (_transactions.isNotEmpty)
|
||||
..._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 != null
|
||||
? (tx.name!.length > 18
|
||||
? tx.name!.substring(0, 20)
|
||||
: tx.name!)
|
||||
: '',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
subtitle: Text(tx.date ?? '',
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
trailing: Text("₹${tx.amount}",
|
||||
style: const TextStyle(fontSize: 16)),
|
||||
))
|
||||
else
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
AppLocalizations.of(context).noTransactions,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Center(
|
||||
child: Text(AppLocalizations.of(context).somethingWentWrong));
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildTransactionShimmer() {
|
||||
return List.generate(3, (i) {
|
||||
return ListTile(
|
||||
leading: Shimmer.fromColors(
|
||||
baseColor: Colors.grey[300]!,
|
||||
highlightColor: Colors.grey[100]!,
|
||||
child: const CircleAvatar(radius: 12, backgroundColor: Colors.white),
|
||||
),
|
||||
title: Shimmer.fromColors(
|
||||
baseColor: Colors.grey[300]!,
|
||||
highlightColor: Colors.grey[100]!,
|
||||
child: Container(height: 10, width: 100, color: Colors.white),
|
||||
),
|
||||
subtitle: Shimmer.fromColors(
|
||||
baseColor: Colors.grey[300]!,
|
||||
highlightColor: Colors.grey[100]!,
|
||||
child: Container(height: 8, width: 60, color: Colors.white),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildQuickLink(IconData icon, String label, VoidCallback onTap,
|
||||
{bool disable = false}) {
|
||||
return InkWell(
|
||||
onTap: disable ? null : onTap,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
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: 13)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
87
lib/features/dashboard/widgets/account_card.dart
Normal file
87
lib/features/dashboard/widgets/account_card.dart
Normal file
@@ -0,0 +1,87 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../accounts/models/account.dart';
|
||||
|
||||
class AccountCard extends StatelessWidget {
|
||||
final Account account;
|
||||
|
||||
const AccountCard({
|
||||
super.key,
|
||||
required this.account,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 300,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Theme.of(context).primaryColor,
|
||||
Theme.of(context).primaryColor.withBlue(200),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withAlpha((0.1 * 255).toInt()),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
account.accountType,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
account.accountType == 'Savings'
|
||||
? Icons.savings
|
||||
: Icons.account_balance,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
account.accountNumber,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Text(
|
||||
'${account.currency} ${account.balance.toStringAsFixed(2)}',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
const Text(
|
||||
'Available Balance',
|
||||
style: TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
109
lib/features/dashboard/widgets/transaction_list_item.dart
Normal file
109
lib/features/dashboard/widgets/transaction_list_item.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../transactions/models/transaction.dart';
|
||||
|
||||
class TransactionListItem extends StatelessWidget {
|
||||
final Transaction transaction;
|
||||
|
||||
const TransactionListItem({
|
||||
super.key,
|
||||
required this.transaction,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool isIncome = transaction.amount > 0;
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Row(
|
||||
children: [
|
||||
// Category icon
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: _getCategoryColor(transaction.category).withAlpha((0.1 * 255).toInt()),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
_getCategoryIcon(transaction.category),
|
||||
color: _getCategoryColor(transaction.category),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Transaction details
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
transaction.description,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
DateFormat('MMM dd, yyyy • h:mm a').format(transaction.date),
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Amount
|
||||
Text(
|
||||
'${isIncome ? '+' : ''}${transaction.amount.toStringAsFixed(2)} USD',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: isIncome ? Colors.green : Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getCategoryColor(String category) {
|
||||
switch (category.toLowerCase()) {
|
||||
case 'food & drink':
|
||||
return Colors.orange;
|
||||
case 'shopping':
|
||||
return Colors.purple;
|
||||
case 'transportation':
|
||||
return Colors.blue;
|
||||
case 'utilities':
|
||||
return Colors.red;
|
||||
case 'income':
|
||||
return Colors.green;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getCategoryIcon(String category) {
|
||||
switch (category.toLowerCase()) {
|
||||
case 'food & drink':
|
||||
return Icons.restaurant;
|
||||
case 'shopping':
|
||||
return Icons.shopping_bag;
|
||||
case 'transportation':
|
||||
return Icons.directions_car;
|
||||
case 'utilities':
|
||||
return Icons.power;
|
||||
case 'income':
|
||||
return Icons.attach_money;
|
||||
default:
|
||||
return Icons.category;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
136
lib/features/enquiry/screens/enquiry_screen.dart
Normal file
136
lib/features/enquiry/screens/enquiry_screen.dart
Normal file
@@ -0,0 +1,136 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class EnquiryScreen extends StatefulWidget {
|
||||
const EnquiryScreen({super.key});
|
||||
|
||||
@override
|
||||
State<EnquiryScreen> createState() => _EnquiryScreen();
|
||||
}
|
||||
|
||||
class _EnquiryScreen extends State<EnquiryScreen> {
|
||||
Future<void> _launchEmailAddress(String email) async {
|
||||
final Uri emailUri = Uri(scheme: 'mailto', path: email);
|
||||
if (await canLaunchUrl(emailUri)) {
|
||||
await launchUrl(emailUri);
|
||||
} else {
|
||||
debugPrint('Could not launch email client for $email');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _launchPhoneNumber(String phone) async {
|
||||
final Uri phoneUri = Uri(scheme: 'tel', path: phone);
|
||||
if (await canLaunchUrl(phoneUri)) {
|
||||
await launchUrl(phoneUri);
|
||||
} else {
|
||||
debugPrint('Could not launch dialer for $phone');
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildContactItem(String role, String email, String phone) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(role, style: const TextStyle(color: Colors.grey)),
|
||||
const SizedBox(height: 4),
|
||||
GestureDetector(
|
||||
onTap: () => _launchEmailAddress(email),
|
||||
child: Text(email, style: const TextStyle(color: Colors.blue)),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
GestureDetector(
|
||||
onTap: () => _launchPhoneNumber(phone),
|
||||
child: Text(phone, style: const TextStyle(color: Colors.blue)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@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(
|
||||
'Enquiry',
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
||||
),
|
||||
centerTitle: false,
|
||||
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: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// … existing Mail us / Call us / Write to us …
|
||||
|
||||
const SizedBox(height: 20),
|
||||
const Text("Write to us", style: TextStyle(color: Colors.grey)),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
"complaint@kccb.in",
|
||||
style: TextStyle(color: Colors.blue),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
"Key Contacts",
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
// horizontal line
|
||||
),
|
||||
Divider(color: Colors.grey[300]),
|
||||
const SizedBox(height: 16),
|
||||
_buildContactItem(
|
||||
"Chairman",
|
||||
"chairman@kccb.in",
|
||||
"01892-222677",
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildContactItem(
|
||||
"Managing Director",
|
||||
"md@kccb.in",
|
||||
"01892-224969",
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildContactItem(
|
||||
"General Manager (West)",
|
||||
"gmw@kccb.in",
|
||||
"01892-223280",
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildContactItem(
|
||||
"General Manager (North)",
|
||||
"gmn@kccb.in",
|
||||
"01892-224607",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,114 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:kmobile/features/beneficiaries/screens/add_beneficiary_screen.dart';
|
||||
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_screen.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
|
||||
class FundTransferBeneficiaryScreen extends StatefulWidget {
|
||||
const FundTransferBeneficiaryScreen({super.key});
|
||||
|
||||
@override
|
||||
State<FundTransferBeneficiaryScreen> createState() =>
|
||||
_FundTransferBeneficiaryScreen();
|
||||
}
|
||||
|
||||
class _FundTransferBeneficiaryScreen
|
||||
extends State<FundTransferBeneficiaryScreen> {
|
||||
final List<Map<String, String>> beneficiaries = [
|
||||
{
|
||||
'bank': 'State Bank Of India',
|
||||
'name': 'Trina Bakshi',
|
||||
},
|
||||
{
|
||||
'bank': 'State Bank Of India',
|
||||
'name': 'Sheetal Rao',
|
||||
},
|
||||
{
|
||||
'bank': 'Punjab National Bank',
|
||||
'name': 'Manoj Kumar',
|
||||
},
|
||||
{
|
||||
'bank': 'State Bank Of India',
|
||||
'name': 'Rohit Mehra',
|
||||
},
|
||||
];
|
||||
|
||||
@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(
|
||||
'Fund Transfer - Beneficiary',
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
||||
),
|
||||
centerTitle: false,
|
||||
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: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ListView.builder(
|
||||
itemCount: beneficiaries.length,
|
||||
itemBuilder: (context, index) {
|
||||
final beneficiary = beneficiaries[index];
|
||||
return ListTile(
|
||||
leading: const CircleAvatar(
|
||||
backgroundColor: Colors.blue, child: Text('A')),
|
||||
title: Text(beneficiary['name']!),
|
||||
subtitle: Text(beneficiary['bank']!),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(
|
||||
Symbols.arrow_right,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
// Delete action
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const FundTransferScreen()));
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
floatingActionButton: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: FloatingActionButton(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const AddBeneficiaryScreen()));
|
||||
},
|
||||
backgroundColor: Colors.grey[300],
|
||||
foregroundColor: Colors.blue[900],
|
||||
elevation: 5,
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
147
lib/features/fund_transfer/screens/fund_transfer_screen.dart
Normal file
147
lib/features/fund_transfer/screens/fund_transfer_screen.dart
Normal file
@@ -0,0 +1,147 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:kmobile/features/fund_transfer/screens/transaction_pin_screen.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
|
||||
class FundTransferScreen extends StatefulWidget {
|
||||
const FundTransferScreen({super.key});
|
||||
|
||||
@override
|
||||
State<FundTransferScreen> createState() => _FundTransferScreen();
|
||||
}
|
||||
|
||||
class _FundTransferScreen extends State<FundTransferScreen> {
|
||||
String amount = "";
|
||||
|
||||
void onKeyTap(String key) {
|
||||
setState(() {
|
||||
if (key == 'back') {
|
||||
if (amount.isNotEmpty) {
|
||||
amount = amount.substring(0, amount.length - 1);
|
||||
}
|
||||
} else if (key == 'done') {
|
||||
if (kDebugMode) {
|
||||
print('Amount entered: $amount');
|
||||
// Navigator.push(
|
||||
// context,
|
||||
// MaterialPageRoute(
|
||||
// builder: (context) => const TransactionPinScreen(
|
||||
// transactionData: {},
|
||||
// transactionCode: 'TRANSFER'
|
||||
// )));
|
||||
}
|
||||
} else {
|
||||
amount += key;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Widget buildKey(String value) {
|
||||
if (value == 'done') {
|
||||
return GestureDetector(
|
||||
onTap: () => onKeyTap(value),
|
||||
child: const Icon(Symbols.check, size: 30),
|
||||
);
|
||||
} else if (value == 'back') {
|
||||
return GestureDetector(
|
||||
onTap: () => onKeyTap(value),
|
||||
child: const Icon(Symbols.backspace, size: 30));
|
||||
} else {
|
||||
return GestureDetector(
|
||||
onTap: () => onKeyTap(value),
|
||||
child: Center(
|
||||
child: Text(value,
|
||||
style: const TextStyle(fontSize: 24, color: Colors.black)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final keys = [
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
'done',
|
||||
'0',
|
||||
'back',
|
||||
];
|
||||
|
||||
@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(
|
||||
'Fund Transfer',
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
||||
),
|
||||
centerTitle: false,
|
||||
actions: const [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: 10.0),
|
||||
child: CircleAvatar(
|
||||
backgroundImage: AssetImage('assets/images/avatar.jpg'),
|
||||
// Replace with your image
|
||||
radius: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
const Spacer(),
|
||||
const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('Debit from:'),
|
||||
Text(
|
||||
'0300015678903456',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text('Enter Amount', style: TextStyle(fontSize: 20)),
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
amount.isEmpty ? "0" : amount,
|
||||
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
color: Colors.white,
|
||||
child: GridView.count(
|
||||
crossAxisCount: 3,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: 1.2,
|
||||
children:
|
||||
keys.map((key) => buildKey(key)).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
205
lib/features/fund_transfer/screens/payment_animation.dart
Normal file
205
lib/features/fund_transfer/screens/payment_animation.dart
Normal file
@@ -0,0 +1,205 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:kmobile/data/models/payment_response.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class PaymentAnimationScreen extends StatefulWidget {
|
||||
final Future<PaymentResponse> paymentResponse;
|
||||
|
||||
const PaymentAnimationScreen({
|
||||
super.key,
|
||||
required this.paymentResponse,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PaymentAnimationScreen> createState() => _PaymentAnimationScreenState();
|
||||
}
|
||||
|
||||
class _PaymentAnimationScreenState extends State<PaymentAnimationScreen> {
|
||||
final GlobalKey _shareKey = GlobalKey();
|
||||
|
||||
Future<void> _shareScreenshot() async {
|
||||
try {
|
||||
RenderRepaintBoundary boundary =
|
||||
_shareKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
|
||||
ui.Image image = await boundary.toImage(pixelRatio: 3.0);
|
||||
ByteData? byteData =
|
||||
await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
Uint8List pngBytes = byteData!.buffer.asUint8List();
|
||||
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final file = await File('${tempDir.path}/payment_result.png').create();
|
||||
await file.writeAsBytes(pngBytes);
|
||||
|
||||
await Share.shareXFiles([XFile(file.path)], text: 'Payment Result');
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to share screenshot: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: FutureBuilder<PaymentResponse>(
|
||||
future: widget.paymentResponse,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Center(
|
||||
child: Lottie.asset(
|
||||
'assets/animations/rupee.json',
|
||||
width: 200,
|
||||
height: 200,
|
||||
repeat: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final response = snapshot.data!;
|
||||
final isSuccess = response.isSuccess;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: RepaintBoundary(
|
||||
key: _shareKey,
|
||||
child: Container(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 80),
|
||||
Lottie.asset(
|
||||
isSuccess
|
||||
? 'assets/animations/done.json'
|
||||
: 'assets/animations/error.json',
|
||||
width: 200,
|
||||
height: 200,
|
||||
repeat: false,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
isSuccess
|
||||
? Column(
|
||||
children: [
|
||||
const Text(
|
||||
'Payment Successful!',
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.green),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (response.amount != null)
|
||||
Text(
|
||||
'Amount: ${response.amount} ${response.currency ?? ''}',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFamily: 'Rubik'),
|
||||
),
|
||||
if (response.creditedAccount != null)
|
||||
Text(
|
||||
'Credited Account: ${response.creditedAccount}',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontFamily: 'Rubik'),
|
||||
),
|
||||
if (response.date != null)
|
||||
Text(
|
||||
'Date: ${response.date!.toLocal().toIso8601String()}',
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
const Text(
|
||||
'Payment Failed',
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.red),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (response.errorMessage != null)
|
||||
Text(
|
||||
response.errorMessage!,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Buttons at the bottom
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 80,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: _shareScreenshot,
|
||||
icon: Icon(
|
||||
Icons.share_rounded,
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
label: Text('Share',
|
||||
style:
|
||||
TextStyle(color: Theme.of(context).primaryColor)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 32, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).primaryColor,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black),
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(context)
|
||||
.popUntil((route) => route.isFirst);
|
||||
},
|
||||
label: const Text('Done'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 45, vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
159
lib/features/fund_transfer/screens/tpin_otp_screen.dart
Normal file
159
lib/features/fund_transfer/screens/tpin_otp_screen.dart
Normal file
@@ -0,0 +1,159 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:kmobile/features/fund_transfer/screens/tpin_set_screen.dart';
|
||||
|
||||
class TpinOtpScreen extends StatefulWidget {
|
||||
const TpinOtpScreen({super.key});
|
||||
|
||||
@override
|
||||
State<TpinOtpScreen> createState() => _TpinOtpScreenState();
|
||||
}
|
||||
|
||||
class _TpinOtpScreenState extends State<TpinOtpScreen> {
|
||||
final List<FocusNode> _focusNodes = List.generate(4, (_) => FocusNode());
|
||||
final List<TextEditingController> _controllers =
|
||||
List.generate(4, (_) => TextEditingController());
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final node in _focusNodes) {
|
||||
node.dispose();
|
||||
}
|
||||
for (final ctrl in _controllers) {
|
||||
ctrl.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onOtpChanged(int idx, String value) {
|
||||
if (value.length == 1 && idx < 3) {
|
||||
_focusNodes[idx + 1].requestFocus();
|
||||
}
|
||||
if (value.isEmpty && idx > 0) {
|
||||
_focusNodes[idx - 1].requestFocus();
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
String get _enteredOtp => _controllers.map((c) => c.text).join();
|
||||
|
||||
void _verifyOtp() {
|
||||
if (_enteredOtp == '0000') {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => TpinSetScreen(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Invalid OTP')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Enter OTP'),
|
||||
centerTitle: true,
|
||||
elevation: 0,
|
||||
),
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
// mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.lock_outline,
|
||||
size: 48, color: theme.colorScheme.primary),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'OTP Verification',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Enter the 4-digit OTP sent to your mobile number.',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(4, (i) {
|
||||
return Container(
|
||||
width: 48,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: TextField(
|
||||
controller: _controllers[i],
|
||||
focusNode: _focusNodes[i],
|
||||
keyboardType: TextInputType.number,
|
||||
textAlign: TextAlign.center,
|
||||
maxLength: 1,
|
||||
obscureText: true,
|
||||
obscuringCharacter: '⬤',
|
||||
decoration: InputDecoration(
|
||||
counterText: '',
|
||||
filled: true,
|
||||
fillColor: Colors.blue[50],
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: theme.colorScheme.primary,
|
||||
width: 2.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
onChanged: (val) => _onOtpChanged(i, val),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.verified_user_rounded),
|
||||
label: const Text(
|
||||
'Verify OTP',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 14, horizontal: 28),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
),
|
||||
onPressed: _enteredOtp.length == 4 ? _verifyOtp : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// Resend OTP logic here
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('OTP resent (mock)')),
|
||||
);
|
||||
},
|
||||
child: const Text('Resend OTP'),
|
||||
),
|
||||
const SizedBox(height: 60),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
77
lib/features/fund_transfer/screens/tpin_prompt_screen.dart
Normal file
77
lib/features/fund_transfer/screens/tpin_prompt_screen.dart
Normal file
@@ -0,0 +1,77 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:kmobile/features/fund_transfer/screens/tpin_otp_screen.dart';
|
||||
|
||||
class TpinSetupPromptScreen extends StatelessWidget {
|
||||
const TpinSetupPromptScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Set TPIN'),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 100.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.lock_person_rounded,
|
||||
size: 60, color: theme.colorScheme.primary),
|
||||
const SizedBox(height: 18),
|
||||
Text(
|
||||
'TPIN Required',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'You need to set your TPIN to continue with secure transactions.',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.arrow_forward_rounded),
|
||||
label: const Text(
|
||||
'Set TPIN',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 14, horizontal: 32),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const TpinOtpScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0),
|
||||
child: Text(
|
||||
'Your TPIN is a 6-digit code used to authorize transactions. Keep it safe and do not share it with anyone.',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
215
lib/features/fund_transfer/screens/tpin_set_screen.dart
Normal file
215
lib/features/fund_transfer/screens/tpin_set_screen.dart
Normal file
@@ -0,0 +1,215 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:kmobile/api/services/auth_service.dart';
|
||||
import 'package:kmobile/di/injection.dart';
|
||||
|
||||
enum TPinMode { set, confirm }
|
||||
|
||||
class TpinSetScreen extends StatefulWidget {
|
||||
const TpinSetScreen({super.key});
|
||||
|
||||
@override
|
||||
State<TpinSetScreen> createState() => _TpinSetScreenState();
|
||||
}
|
||||
|
||||
class _TpinSetScreenState extends State<TpinSetScreen> {
|
||||
TPinMode _mode = TPinMode.set;
|
||||
final List<String> _tpin = [];
|
||||
String? _initialTpin;
|
||||
String? _errorText;
|
||||
|
||||
void addDigit(String digit) {
|
||||
if (_tpin.length < 6) {
|
||||
setState(() {
|
||||
_tpin.add(digit);
|
||||
_errorText = null;
|
||||
});
|
||||
if (_tpin.length == 6) {
|
||||
_handleComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void deleteDigit() {
|
||||
if (_tpin.isNotEmpty) {
|
||||
setState(() {
|
||||
_tpin.removeLast();
|
||||
_errorText = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _handleComplete() async {
|
||||
final pin = _tpin.join();
|
||||
if (_mode == TPinMode.set) {
|
||||
setState(() {
|
||||
_initialTpin = pin;
|
||||
_tpin.clear();
|
||||
_mode = TPinMode.confirm;
|
||||
});
|
||||
} else if (_mode == TPinMode.confirm) {
|
||||
if (_initialTpin == pin) {
|
||||
final authService = getIt<AuthService>();
|
||||
try {
|
||||
await authService.setTpin(pin);
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_errorText = "Failed to set TPIN. Please try again.";
|
||||
_tpin.clear();
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!mounted) return;
|
||||
// Show success dialog before popping
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (ctx) => AlertDialog(
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
|
||||
title: const Column(
|
||||
children: [
|
||||
Icon(Icons.check_circle, color: Colors.green, size: 60),
|
||||
SizedBox(height: 12),
|
||||
Text('Success!', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
content: const Text(
|
||||
'Your TPIN was set up successfully.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
},
|
||||
child: const Text('OK', style: TextStyle(fontSize: 16)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
} else {
|
||||
setState(() {
|
||||
_errorText = "Pins do not match. Try again.";
|
||||
_tpin.clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildPinDots() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(6, (index) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(8),
|
||||
width: 15,
|
||||
height: 15,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: index < _tpin.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 (_tpin.length == 6) {
|
||||
_handleComplete();
|
||||
} else {
|
||||
setState(() {
|
||||
_errorText = "Please enter 6 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(Icons.check)
|
||||
: Text(
|
||||
key == '<' ? '⌫' : key,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
color: key == 'Enter' ? Colors.blue : Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
String getTitle() {
|
||||
switch (_mode) {
|
||||
case TPinMode.set:
|
||||
return "Set your new TPIN";
|
||||
case TPinMode.confirm:
|
||||
return "Confirm your new TPIN";
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Set TPIN')),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const Spacer(),
|
||||
const Icon(Icons.lock_outline, size: 60, color: Colors.blue),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
getTitle(),
|
||||
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
buildPinDots(),
|
||||
if (_errorText != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(_errorText!,
|
||||
style: const TextStyle(color: Colors.red)),
|
||||
),
|
||||
const Spacer(),
|
||||
buildNumberPad(),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
184
lib/features/fund_transfer/screens/transaction_pin_screen.dart
Normal file
184
lib/features/fund_transfer/screens/transaction_pin_screen.dart
Normal file
@@ -0,0 +1,184 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:kmobile/api/services/auth_service.dart';
|
||||
import 'package:kmobile/api/services/payment_service.dart';
|
||||
import 'package:kmobile/data/models/transfer.dart';
|
||||
import 'package:kmobile/di/injection.dart';
|
||||
import 'package:kmobile/features/fund_transfer/screens/payment_animation.dart';
|
||||
import 'package:kmobile/features/fund_transfer/screens/tpin_prompt_screen.dart';
|
||||
import 'package:kmobile/features/fund_transfer/screens/transaction_success_screen.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
|
||||
class TransactionPinScreen extends StatefulWidget {
|
||||
final Transfer transactionData;
|
||||
const TransactionPinScreen({super.key, required this.transactionData});
|
||||
|
||||
@override
|
||||
State<TransactionPinScreen> createState() => _TransactionPinScreen();
|
||||
}
|
||||
|
||||
class _TransactionPinScreen extends State<TransactionPinScreen> {
|
||||
final List<String> _pin = [];
|
||||
bool _loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_checkIfTpinIsSet();
|
||||
}
|
||||
|
||||
Future<void> _checkIfTpinIsSet() async {
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
final authService = getIt<AuthService>();
|
||||
final isSet = await authService.checkTpin();
|
||||
if (!isSet && mounted) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const TpinSetupPromptScreen(),
|
||||
),
|
||||
);
|
||||
} else if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Failed to check TPIN status')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onKeyPressed(String value) {
|
||||
setState(() {
|
||||
if (value == 'back') {
|
||||
if (_pin.isNotEmpty) _pin.removeLast();
|
||||
} else if (_pin.length < 6) {
|
||||
_pin.add(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildPinIndicators() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(6, (index) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||
width: 20,
|
||||
height: 20,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.blue, width: 2),
|
||||
color: index < _pin.length ? Colors.blue : Colors.transparent,
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKey(String label, {IconData? icon}) {
|
||||
return Expanded(
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
if (label == 'back') {
|
||||
_onKeyPressed('back');
|
||||
} else if (label == 'done') {
|
||||
// Handle submit if needed
|
||||
if (_pin.length == 6) {
|
||||
await sendTransaction();
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("Please enter a 6-digit TPIN")),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_onKeyPressed(label);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
child: Center(
|
||||
child: icon != null
|
||||
? Icon(icon, size: 28)
|
||||
: Text(label, style: const TextStyle(fontSize: 24)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKeypad() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(children: [_buildKey('1'), _buildKey('2'), _buildKey('3')]),
|
||||
Row(children: [_buildKey('4'), _buildKey('5'), _buildKey('6')]),
|
||||
Row(children: [_buildKey('7'), _buildKey('8'), _buildKey('9')]),
|
||||
Row(children: [
|
||||
_buildKey('done', icon: Icons.check),
|
||||
_buildKey('0'),
|
||||
_buildKey('back', icon: Icons.backspace_outlined),
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> sendTransaction() async {
|
||||
final paymentService = getIt<PaymentService>();
|
||||
final transfer = widget.transactionData;
|
||||
transfer.tpin = _pin.join();
|
||||
try {
|
||||
final paymentResponse = paymentService.processQuickPayWithinBank(transfer);
|
||||
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => PaymentAnimationScreen(paymentResponse: paymentResponse)
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(e.toString())),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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(
|
||||
'TPIN',
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
||||
),
|
||||
centerTitle: false,
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20.0),
|
||||
child: Column(
|
||||
children: [
|
||||
const Spacer(),
|
||||
const Text(
|
||||
'Enter Your TPIN',
|
||||
style: TextStyle(fontSize: 18),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildPinIndicators(),
|
||||
const Spacer(),
|
||||
_buildKeypad(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,141 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:screenshot/screenshot.dart';
|
||||
|
||||
import '../../../app.dart';
|
||||
|
||||
class TransactionSuccessScreen extends StatefulWidget {
|
||||
final String creditAccount;
|
||||
const TransactionSuccessScreen({super.key, required this.creditAccount});
|
||||
|
||||
@override
|
||||
State<TransactionSuccessScreen> createState() => _TransactionSuccessScreen();
|
||||
}
|
||||
|
||||
class _TransactionSuccessScreen extends State<TransactionSuccessScreen> {
|
||||
final ScreenshotController _screenshotController = ScreenshotController();
|
||||
|
||||
Future<void> _shareScreenshot() async {
|
||||
final Uint8List? imageBytes = await _screenshotController.capture();
|
||||
if (imageBytes != null) {
|
||||
final directory = await getTemporaryDirectory();
|
||||
final imagePath = File('${directory.path}/transaction_success.png');
|
||||
await imagePath.writeAsBytes(imageBytes);
|
||||
|
||||
// SocialShare.shareOptions(
|
||||
// "Transaction Successful",
|
||||
// imagePath: imagePath.path,
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String transactionDate =
|
||||
DateTime.now().toLocal().toString().split(' ')[0];
|
||||
final String creditAccount = widget.creditAccount;
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
// Main content
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
radius: 50,
|
||||
backgroundColor: Colors.blue,
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
color: Colors.white,
|
||||
size: 60,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
"Transaction Successful",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
"On $transactionDate",
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.black54,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"To Account Number: $creditAccount",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black87,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Buttons at the bottom
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(36, 0, 36, 25),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: _shareScreenshot,
|
||||
icon: const Icon(Icons.share, size: 18),
|
||||
label: const Text("Share"),
|
||||
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),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
// Done action
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const NavigationScaffold()));
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: const StadiumBorder(),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
backgroundColor: Colors.blue[900],
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text("Done"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
52
lib/features/profile/preferences/language_dialog.dart
Normal file
52
lib/features/profile/preferences/language_dialog.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:kmobile/app.dart';
|
||||
|
||||
class LanguageDialog extends StatelessWidget {
|
||||
const LanguageDialog({super.key});
|
||||
|
||||
String getLocaleName(AppLocalizations localizations, String code) {
|
||||
final localeCodeMap = {
|
||||
'en': localizations.english,
|
||||
'hi': localizations.hindi,
|
||||
};
|
||||
return localeCodeMap[code] ?? 'Unknown';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
final localizations = AppLocalizations.of(context);
|
||||
|
||||
final supportedLocales = [
|
||||
const Locale('en'),
|
||||
const Locale('hi'),
|
||||
];
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(localizations.language),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: supportedLocales.map((locale) {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.language),
|
||||
title: Text(getLocaleName(localizations, locale.languageCode)),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
KMobile.setLocale(context, locale);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(localizations.cancel),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
32
lib/features/profile/preferences/preference_screen.dart
Normal file
32
lib/features/profile/preferences/preference_screen.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'language_dialog.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class PreferenceScreen extends StatelessWidget {
|
||||
const PreferenceScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(loc.preferences), // Localized "Preferences"
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.language),
|
||||
title: Text(loc.language), // Localized "Language"
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => LanguageDialog(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
35
lib/features/profile/profile_screen.dart
Normal file
35
lib/features/profile/profile_screen.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:kmobile/features/profile/preferences/preference_screen.dart';
|
||||
|
||||
class ProfileScreen extends StatelessWidget {
|
||||
const ProfileScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(loc.profile), // Localized "Profile"
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
title: Text(loc.preferences), // Localized "Preferences"
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const PreferenceScreen(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
// You can add more profile options here later
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,430 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:flutter_swipe_button/flutter_swipe_button.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
|
||||
|
||||
class QuickPayOutsideBankScreen extends StatefulWidget {
|
||||
final String debitAccount;
|
||||
const QuickPayOutsideBankScreen({super.key, required this.debitAccount});
|
||||
|
||||
@override
|
||||
State<QuickPayOutsideBankScreen> createState() =>
|
||||
_QuickPayOutsideBankScreen();
|
||||
}
|
||||
|
||||
class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
// Controllers
|
||||
final accountNumberController = TextEditingController();
|
||||
final confirmAccountNumberController = TextEditingController();
|
||||
final nameController = TextEditingController();
|
||||
final bankNameController = TextEditingController();
|
||||
final branchNameController = TextEditingController();
|
||||
final ifscController = TextEditingController();
|
||||
final phoneController = TextEditingController();
|
||||
final amountController = TextEditingController();
|
||||
|
||||
String accountType = 'Savings';
|
||||
final List<String> transactionModes = ['NEFT', 'RTGS', 'IMPS'];
|
||||
int selectedTransactionIndex = 0;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Dispose controllers
|
||||
accountNumberController.dispose();
|
||||
confirmAccountNumberController.dispose();
|
||||
nameController.dispose();
|
||||
bankNameController.dispose();
|
||||
branchNameController.dispose();
|
||||
ifscController.dispose();
|
||||
phoneController.dispose();
|
||||
amountController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@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(
|
||||
'Quick Pay - Outside Bank',
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
||||
),
|
||||
centerTitle: false,
|
||||
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: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
const Text('Debit from:'),
|
||||
Text(
|
||||
widget.debitAccount,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Account Number',
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
controller: accountNumberController,
|
||||
keyboardType: TextInputType.number,
|
||||
obscureText: true,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Account number is required';
|
||||
} else if (value.length != 11) {
|
||||
return 'Enter a valid account number';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
TextFormField(
|
||||
controller: confirmAccountNumberController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Confirm Account Number',
|
||||
// prefixIcon: Icon(Icons.person),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Re-enter account number';
|
||||
}
|
||||
if (value != accountNumberController.text) {
|
||||
return 'Account numbers do not match';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Name',
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
controller: nameController,
|
||||
keyboardType: TextInputType.name,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Name is required';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Beneficiary Bank Name",
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
controller: bankNameController,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Beneficiary Bank Name is required';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Branch Name",
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
controller: branchNameController,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Branch Name is required';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: "IFSC Code",
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
controller: ifscController,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'IFSC Code is required';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
)),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
value: accountType,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Account Type",
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
items: ['Savings', 'Current']
|
||||
.map((e) => DropdownMenuItem(
|
||||
value: e,
|
||||
child: Text(e),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) => setState(() {
|
||||
accountType = value!;
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 25),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: phoneController,
|
||||
keyboardType: TextInputType.phone,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Phone",
|
||||
prefixIcon: Icon(Icons.phone),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) => value == null || value.isEmpty
|
||||
? "Phone number is Required"
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Amount',
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
controller: amountController,
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Amount is required';
|
||||
}
|
||||
final amount = double.tryParse(value);
|
||||
if (amount == null || amount <= 0) {
|
||||
return 'Enter a valid amount';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Row(
|
||||
children: [
|
||||
const Text("Transaction Mode",
|
||||
style: TextStyle(fontWeight: FontWeight.w500)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: buildTransactionModeSelector()),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 45),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: SwipeButton.expand(
|
||||
thumb: const Icon(
|
||||
Icons.arrow_forward,
|
||||
color: Colors.white,
|
||||
),
|
||||
activeThumbColor: Colors.blue[900],
|
||||
activeTrackColor: Colors.blue.shade100,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
height: 56,
|
||||
child: const Text(
|
||||
"Swipe to Pay",
|
||||
style:
|
||||
TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
onSwipe: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
// Perform payment logic
|
||||
final selectedMode =
|
||||
transactionModes[selectedTransactionIndex];
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Paying via $selectedMode...')),
|
||||
);
|
||||
// Navigator.push(
|
||||
// context,
|
||||
// MaterialPageRoute(
|
||||
// builder: (context) =>
|
||||
// const TransactionPinScreen(
|
||||
// transactionData: {},
|
||||
// transactionCode: 'PAYMENT',
|
||||
// )));
|
||||
}
|
||||
},
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTransactionModeSelector() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Row(
|
||||
children: List.generate(transactionModes.length, (index) {
|
||||
bool isSelected = selectedTransactionIndex == index;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() => selectedTransactionIndex = index);
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? Colors.blue[200] : Colors.white,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
border: Border.all(
|
||||
color: isSelected ? Colors.blue : Colors.grey,
|
||||
width: isSelected ? 0 : 1.2,
|
||||
),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
transactionModes[index],
|
||||
style: const TextStyle(
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
109
lib/features/quick_pay/screens/quick_pay_screen.dart
Normal file
109
lib/features/quick_pay/screens/quick_pay_screen.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:kmobile/features/quick_pay/screens/quick_pay_outside_bank_screen.dart';
|
||||
import 'package:kmobile/features/quick_pay/screens/quick_pay_within_bank_screen.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class QuickPayScreen extends StatefulWidget {
|
||||
final String debitAccount;
|
||||
const QuickPayScreen({super.key, required this.debitAccount});
|
||||
|
||||
@override
|
||||
State<QuickPayScreen> createState() => _QuickPayScreen();
|
||||
}
|
||||
|
||||
class _QuickPayScreen extends State<QuickPayScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Symbols.arrow_back_ios_new),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
AppLocalizations.of(context).quickPay,
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
||||
),
|
||||
centerTitle: false,
|
||||
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: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
children: [
|
||||
QuickPayManagementTile(
|
||||
icon: Symbols.input_circle,
|
||||
label: AppLocalizations.of(context).ownBank,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => QuickPayWithinBankScreen(
|
||||
debitAccount: widget.debitAccount)));
|
||||
},
|
||||
),
|
||||
const Divider(
|
||||
height: 1,
|
||||
),
|
||||
QuickPayManagementTile(
|
||||
//disable: true,
|
||||
icon: Symbols.output_circle,
|
||||
label: AppLocalizations.of(context).outsideBank,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => QuickPayOutsideBankScreen(
|
||||
debitAccount: widget.debitAccount)));
|
||||
},
|
||||
),
|
||||
const Divider(
|
||||
height: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class QuickPayManagementTile extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
final bool disable;
|
||||
|
||||
const QuickPayManagementTile({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
this.disable = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: Icon(icon),
|
||||
title: Text(label),
|
||||
trailing: const Icon(Symbols.arrow_right, size: 20),
|
||||
onTap: onTap,
|
||||
enabled: !disable,
|
||||
);
|
||||
}
|
||||
}
|
303
lib/features/quick_pay/screens/quick_pay_within_bank_screen.dart
Normal file
303
lib/features/quick_pay/screens/quick_pay_within_bank_screen.dart
Normal file
@@ -0,0 +1,303 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:flutter_swipe_button/flutter_swipe_button.dart';
|
||||
import 'package:kmobile/data/models/transfer.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import '../../fund_transfer/screens/transaction_pin_screen.dart';
|
||||
|
||||
class QuickPayWithinBankScreen extends StatefulWidget {
|
||||
final String debitAccount;
|
||||
const QuickPayWithinBankScreen({super.key, required this.debitAccount});
|
||||
|
||||
@override
|
||||
State<QuickPayWithinBankScreen> createState() => _QuickPayWithinBankScreen();
|
||||
}
|
||||
|
||||
class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
final TextEditingController accountNumberController = TextEditingController();
|
||||
final TextEditingController confirmAccountNumberController =
|
||||
TextEditingController();
|
||||
final TextEditingController amountController = TextEditingController();
|
||||
String? _selectedAccountType;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Symbols.arrow_back_ios_new),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
title: Text(
|
||||
AppLocalizations.of(context).quickPayOwnBank,
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
||||
),
|
||||
centerTitle: false,
|
||||
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: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context).debitAccountNumber,
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
readOnly: true,
|
||||
controller: TextEditingController(text: widget.debitAccount),
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
enabled: false,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context).accountNumber,
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
controller: accountNumberController,
|
||||
keyboardType: TextInputType.number,
|
||||
obscureText: true,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return AppLocalizations.of(context).accountNumberRequired;
|
||||
} else if (value.length != 11) {
|
||||
return AppLocalizations.of(context).validAccountNumber;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
// const Align(
|
||||
// alignment: Alignment.topLeft,
|
||||
// child: Padding(
|
||||
// padding: EdgeInsets.only(left: 15.0, top: 5),
|
||||
// child: Text(
|
||||
// 'Beneficiary Account Number',
|
||||
// style: TextStyle(color: Colors.black54),
|
||||
// ),
|
||||
// )),
|
||||
const SizedBox(height: 25),
|
||||
TextFormField(
|
||||
controller: confirmAccountNumberController,
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context).confirmAccountNumber,
|
||||
// prefixIcon: Icon(Icons.person),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return AppLocalizations.of(context).reenterAccountNumber;
|
||||
}
|
||||
if (value != accountNumberController.text) {
|
||||
return AppLocalizations.of(context).accountMismatch;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
DropdownButtonFormField<String>(
|
||||
decoration: InputDecoration(
|
||||
labelText:
|
||||
AppLocalizations.of(context).beneficiaryAccountType,
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
value: _selectedAccountType,
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 'SB',
|
||||
child: Text(AppLocalizations.of(context).savings),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'LN',
|
||||
child: Text(AppLocalizations.of(context).loan),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_selectedAccountType = value;
|
||||
});
|
||||
},
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return AppLocalizations.of(context).selectAccountType;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
// const Align(
|
||||
// alignment: Alignment.topLeft,
|
||||
// child: Padding(
|
||||
// padding: EdgeInsets.only(left: 15.0, top: 5),
|
||||
// child: Text(
|
||||
// 'Beneficiary Account Type',
|
||||
// style: TextStyle(color: Colors.black54),
|
||||
// ),
|
||||
// )),
|
||||
const SizedBox(height: 25),
|
||||
TextFormField(
|
||||
decoration: InputDecoration(
|
||||
labelText: AppLocalizations.of(context).amount,
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
controller: amountController,
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return AppLocalizations.of(context).amountRequired;
|
||||
}
|
||||
final amount = double.tryParse(value);
|
||||
if (amount == null || amount <= 0) {
|
||||
return AppLocalizations.of(context).validAmount;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 45),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: SwipeButton.expand(
|
||||
thumb: const Icon(
|
||||
Icons.arrow_forward,
|
||||
color: Colors.white,
|
||||
),
|
||||
activeThumbColor: Theme.of(context).primaryColor,
|
||||
activeTrackColor:
|
||||
Theme.of(context).colorScheme.secondary.withAlpha(100),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
height: 56,
|
||||
child: Text(
|
||||
AppLocalizations.of(context).swipeToPay,
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
onSwipe: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
// Perform payment logic
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TransactionPinScreen(
|
||||
transactionData: Transfer(
|
||||
fromAccount: widget.debitAccount,
|
||||
toAccount: accountNumberController.text,
|
||||
toAccountType: _selectedAccountType!,
|
||||
amount: amountController.text,
|
||||
),
|
||||
)));
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
// SliderButton(
|
||||
// action: () async {
|
||||
// ///Do something here OnSlide
|
||||
// return true;
|
||||
// },
|
||||
// label: const Text(
|
||||
// "Slide to pay",
|
||||
// style: TextStyle(
|
||||
// color: Color(0xff4a4a4a),
|
||||
// fontWeight: FontWeight.w500,
|
||||
// fontSize: 17),
|
||||
// ),
|
||||
// icon: Icon(Symbols.arrow_forward,
|
||||
// color: Theme.of(context).primaryColor, weight: 200),
|
||||
// )
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTextFormField({
|
||||
required String label,
|
||||
required TextEditingController controller,
|
||||
TextInputType keyboardType = TextInputType.text,
|
||||
String? Function(String?)? validator,
|
||||
}) {
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
validator: validator,
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black),
|
||||
),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
113
lib/features/service/screens/service_screen.dart
Normal file
113
lib/features/service/screens/service_screen.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
|
||||
class ServiceScreen extends StatefulWidget {
|
||||
const ServiceScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ServiceScreen> createState() => _ServiceScreen();
|
||||
}
|
||||
|
||||
class _ServiceScreen extends State<ServiceScreen>{
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
title: const Text('Services', style: TextStyle(color: Colors.black,
|
||||
fontWeight: FontWeight.w500),),
|
||||
centerTitle: false,
|
||||
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 CustomerInfoScreen()));
|
||||
},
|
||||
child: const CircleAvatar(
|
||||
backgroundImage: AssetImage('assets/images/avatar.jpg'), // Replace with your image
|
||||
radius: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
body: ListView(
|
||||
children: [
|
||||
ServiceManagementTile(
|
||||
icon: Symbols.add,
|
||||
label: 'Account Opening Request - Deposit',
|
||||
onTap: () {
|
||||
|
||||
},
|
||||
),
|
||||
|
||||
const Divider(height: 1,),
|
||||
|
||||
ServiceManagementTile(
|
||||
icon: Symbols.add,
|
||||
label: 'Account Opening Request - Loan',
|
||||
onTap: () {
|
||||
|
||||
},
|
||||
),
|
||||
|
||||
const Divider(height: 1,),
|
||||
|
||||
ServiceManagementTile(
|
||||
icon: Symbols.captive_portal,
|
||||
label: 'Quick Links',
|
||||
onTap: () {
|
||||
|
||||
},
|
||||
),
|
||||
|
||||
const Divider(height: 1,),
|
||||
|
||||
ServiceManagementTile(
|
||||
icon: Symbols.missing_controller,
|
||||
label: 'Branch Locator',
|
||||
onTap: () {
|
||||
|
||||
},
|
||||
),
|
||||
|
||||
const Divider(height: 1,)
|
||||
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ServiceManagementTile extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const ServiceManagementTile({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: Icon(icon),
|
||||
title: Text(label),
|
||||
trailing: const Icon(Symbols.arrow_right, size: 20),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
15
lib/features/transactions/models/transaction.dart
Normal file
15
lib/features/transactions/models/transaction.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
class Transaction {
|
||||
final String id;
|
||||
final String description;
|
||||
final double amount;
|
||||
final DateTime date;
|
||||
final String category;
|
||||
|
||||
Transaction({
|
||||
required this.id,
|
||||
required this.description,
|
||||
required this.amount,
|
||||
required this.date,
|
||||
required this.category,
|
||||
});
|
||||
}
|
87
lib/l10n/app_en.arb
Normal file
87
lib/l10n/app_en.arb
Normal file
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"@@locale": "en",
|
||||
"profile": "Profile",
|
||||
"preferences": "Preferences",
|
||||
"language": "Language",
|
||||
"selectLanguage": "Select Language",
|
||||
"english": "English",
|
||||
"hindi": "Hindi",
|
||||
"cancel": "Cancel",
|
||||
"home": "Home",
|
||||
"card": "Card",
|
||||
"services": "Services",
|
||||
"quickPay": "Quick \n Pay",
|
||||
"quickLinks": "Quick Links",
|
||||
"recentTransactions": "Recent Transactions",
|
||||
"accountNumber": "Account Number:",
|
||||
"enableBiometric": "Enable Biometric Authentication",
|
||||
"useBiometricPrompt": "Use fingerprint/face ID for faster login?",
|
||||
"later": "Later",
|
||||
"enable": "Enable",
|
||||
"noTransactions": "No transactions found for this account.",
|
||||
"somethingWentWrong": "Something went wrong",
|
||||
"failedToLoad": "Failed to load transactions: {error}",
|
||||
"failedToRefresh": "Failed to refresh data",
|
||||
"hi": "Hi",
|
||||
"kMobile": "kMobile",
|
||||
"scanBiometric": "Scan to enable Biometric login",
|
||||
"savingsAccount": "Savings Account",
|
||||
"loanAccount": "Loan Account",
|
||||
"termDeposit": "Term Deposit Account",
|
||||
"recurringDeposit": "Recurring Deposit Account",
|
||||
"unknownAccount": "Unknown Account Type",
|
||||
"customerInfo": "Customer \n Info",
|
||||
"fundTransfer": "Fund \n Transfer",
|
||||
"accountInfo": "Account \n Info",
|
||||
"accountStatement": "Account \n Statement",
|
||||
"handleCheque": "Handle \n Cheque",
|
||||
"manageBeneficiary": "Manage \n Beneficiary",
|
||||
"contactUs": "Contact \n Us",
|
||||
"addBeneficiary": "Add Beneficiary",
|
||||
"confirmAccountNumber": "Confirm Account Number",
|
||||
"name": "Name",
|
||||
"ifscCode": "IFSC Code",
|
||||
"bankName": "Beneficiary Bank Name",
|
||||
"branchName": "Branch Name",
|
||||
"accountType": "Account Type",
|
||||
"savings": "Savings",
|
||||
"current": "Current",
|
||||
"phone": "Phone",
|
||||
"validateAndAdd": "Validate and Add",
|
||||
"beneficiaryAdded": "Beneficiary Added Successfully",
|
||||
"invalidIfscFormat": "Invalid IFSC Format",
|
||||
"noIfscDetails": "No details found for IFSC",
|
||||
"enterValidAccountNumber": "Enter a valid account number",
|
||||
"reenterAccountNumber": "Re-enter account number",
|
||||
"accountMismatch": "Account numbers do not match",
|
||||
"nameRequired": "Name is required",
|
||||
"enterIfsc": "Enter IFSC code",
|
||||
"enterValidPhone": "Enter a valid phone number",
|
||||
"payNow": "Pay Now",
|
||||
"beneficiaries": "Beneficiaries",
|
||||
"cif": "CIF",
|
||||
"activeAccounts": "Number of Active Accounts",
|
||||
"mobileNumber": "Mobile Number",
|
||||
"dateOfBirth": "Date of Birth",
|
||||
"branchCode": "Branch Code",
|
||||
"branchAddress": "Branch Address",
|
||||
"primaryId": "Primary ID",
|
||||
"quickPayOwnBank": "Quick Pay - Own Bank",
|
||||
"debitAccountNumber": "Debit Account Number",
|
||||
"accountNumber": "Account Number",
|
||||
"accountNumberRequired": "Account Number is required",
|
||||
"validAccountNumber": "Enter a valid account number",
|
||||
"confirmAccountNumber": "Confirm Account Number",
|
||||
"reenterAccountNumber": "Re-enter Account Number",
|
||||
"accountMismatch": "Account Numbers do not match",
|
||||
"beneficiaryAccountType": "Beneficiary Account Type",
|
||||
"loan": "Loan",
|
||||
"selectAccountType": "Please select account type",
|
||||
"amount": "Amount",
|
||||
"amountRequired": "Amount is required",
|
||||
"validAmount": "Enter a valid amount",
|
||||
"swipeToPay": "Swipe to Pay",
|
||||
"outsideBank": "Outside Bank",
|
||||
"ownBank": "Own Bank"
|
||||
}
|
||||
|
86
lib/l10n/app_hi.arb
Normal file
86
lib/l10n/app_hi.arb
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"@@locale": "hi",
|
||||
"profile": "प्रोफ़ाइल",
|
||||
"preferences": "वरीयताएँ",
|
||||
"language": "भाषा",
|
||||
"selectLanguage": "भाषा चुनिए",
|
||||
"english": "अंग्रेज़ी",
|
||||
"hindi": "हिंदी",
|
||||
"cancel": "रद्द करें",
|
||||
"home": "होम",
|
||||
"card": "कार्ड",
|
||||
"services": "सेवाएं",
|
||||
"quickPay": "क्विक \n पे",
|
||||
"quickLinks": "त्वरित लिंक",
|
||||
"recentTransactions": "हाल की लेनदेन",
|
||||
"accountNumber": "खाता संख्या:",
|
||||
"enableBiometric": "बायोमेट्रिक प्रमाणीकरण सक्षम करें",
|
||||
"useBiometricPrompt": "तेज़ लॉगिन के लिए फिंगरप्रिंट/फेस आईडी का उपयोग करें?",
|
||||
"later": "बाद में",
|
||||
"enable": "सक्षम करें",
|
||||
"noTransactions": "इस खाते के लिए कोई लेनदेन नहीं मिला।",
|
||||
"somethingWentWrong": "कुछ गलत हो गया",
|
||||
"failedToLoad": "लेनदेन लोड करने में विफल: {error}",
|
||||
"failedToRefresh": "डेटा रिफ्रेश करने में विफल",
|
||||
"hi": "नमस्ते",
|
||||
"kMobile": "के मोबाइल",
|
||||
"scanBiometric": "बायोमेट्रिक लॉगिन सक्षम करने के लिए स्कैन करें",
|
||||
"savingsAccount": "बचत खाता",
|
||||
"loanAccount": "ऋण खाता",
|
||||
"termDeposit": "मियादी जमा खाता",
|
||||
"recurringDeposit": "आवर्ती जमा खाता",
|
||||
"unknownAccount": "अज्ञात खाता प्रकार",
|
||||
"customerInfo": "ग्राहक \n जानकारी",
|
||||
"fundTransfer": "फंड \n ट्रांसफर",
|
||||
"accountInfo": "खाता \n जानकारी",
|
||||
"accountStatement": "खाता \n विवरण",
|
||||
"handleCheque": "चेक \n संभालें",
|
||||
"manageBeneficiary": "लाभार्थी \n प्रबंधन",
|
||||
"contactUs": "संपर्क \n करें",
|
||||
"addBeneficiary": "लाभार्थी जोड़ें",
|
||||
"confirmAccountNumber": "खाता संख्या की पुष्टि करें",
|
||||
"name": "नाम",
|
||||
"ifscCode": "आईएफ़एससी कोड",
|
||||
"bankName": "लाभार्थी बैंक का नाम",
|
||||
"branchName": "शाखा का नाम",
|
||||
"accountType": "खाते प्रकार",
|
||||
"savings": "बचत",
|
||||
"current": "चालू",
|
||||
"phone": "फ़ोन",
|
||||
"validateAndAdd": "सत्यापित करें और जोड़ें",
|
||||
"beneficiaryAdded": "लाभार्थी सफलतापूर्वक जोड़ा गया",
|
||||
"invalidIfscFormat": "अमान्य IFSC प्रारूप",
|
||||
"noIfscDetails": "इस IFSC के लिए कोई विवरण नहीं मिला",
|
||||
"enterValidAccountNumber": "कृपया एक मान्य खाता संख्या दर्ज करें",
|
||||
"reenterAccountNumber": "खाता संख्या दोबारा दर्ज करें",
|
||||
"accountMismatch": "खाता संख्याएँ मेल नहीं खा रहीं",
|
||||
"nameRequired": "नाम आवश्यक है",
|
||||
"enterIfsc": "IFSC कोड दर्ज करें",
|
||||
"enterValidPhone": "कृपया एक मान्य फोन नंबर दर्ज करें",
|
||||
"payNow": "अब भुगतान करें",
|
||||
"beneficiaries": "लाभार्थी",
|
||||
"cif": "सीआईएफ",
|
||||
"activeAccounts": "सक्रिय खातों की संख्या",
|
||||
"mobileNumber": "मोबाइल नंबर",
|
||||
"dateOfBirth": "जन्म तिथि",
|
||||
"branchCode": "शाखा कोड",
|
||||
"branchAddress": "शाखा पता",
|
||||
"primaryId": "प्राथमिक पहचान",
|
||||
"quickPayOwnBank": "क्विक पे - स्वयं का बैंक",
|
||||
"debitAccountNumber": "डेबिट खाता संख्या",
|
||||
"accountNumber": "खाता संख्या",
|
||||
"accountNumberRequired": "खाता संख्या आवश्यक है",
|
||||
"validAccountNumber": "एक मान्य खाता संख्या दर्ज करें",
|
||||
"confirmAccountNumber": "खाता संख्या की पुष्टि करें",
|
||||
"reenterAccountNumber": "फिर से खाता संख्या दर्ज करें",
|
||||
"accountMismatch": "खाता संख्याएँ मेल नहीं खा रही हैं",
|
||||
"beneficiaryAccountType": "लाभार्थी खाता प्रकार",
|
||||
"loan": "ऋण",
|
||||
"selectAccountType": "कृपया खाता प्रकार चुनें",
|
||||
"amount": "राशि",
|
||||
"amountRequired": "राशि आवश्यक है",
|
||||
"validAmount": "एक मान्य राशि दर्ज करें",
|
||||
"swipeToPay": "भुगतान करने के लिए स्वाइप करें",
|
||||
"outsideBank": "बाहरी बैंक",
|
||||
"ownBank": "स्वयं का बैंक"
|
||||
}
|
19
lib/main.dart
Normal file
19
lib/main.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'di/injection.dart';
|
||||
import 'app.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Set preferred orientations
|
||||
await SystemChrome.setPreferredOrientations([
|
||||
DeviceOrientation.portraitUp,
|
||||
DeviceOrientation.portraitDown,
|
||||
]);
|
||||
|
||||
// Initialize dependencies
|
||||
await setupDependencies();
|
||||
|
||||
runApp(const KMobile());
|
||||
}
|
34
lib/security/secure_storage.dart
Normal file
34
lib/security/secure_storage.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
class SecureStorage {
|
||||
final FlutterSecureStorage _storage;
|
||||
|
||||
SecureStorage(): _storage = const FlutterSecureStorage();
|
||||
|
||||
Future<void> write(String key, dynamic value) async {
|
||||
final stringValue = value is String
|
||||
? value
|
||||
: json.encode(value);
|
||||
await _storage.write(key: key, value: stringValue);
|
||||
}
|
||||
|
||||
Future<dynamic> read(String key) async {
|
||||
final value = await _storage.read(key: key);
|
||||
if (value == null) return null;
|
||||
|
||||
try {
|
||||
return json.decode(value);
|
||||
} catch (_) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> delete(String key) async {
|
||||
await _storage.delete(key: key);
|
||||
}
|
||||
|
||||
Future<void> deleteAll() async {
|
||||
await _storage.deleteAll();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user