diff --git a/assets/images/sbi_logo.png b/assets/images/sbi_logo.png new file mode 100644 index 0000000..347f543 Binary files /dev/null and b/assets/images/sbi_logo.png differ diff --git a/lib/api/services/beneficiary_service.dart b/lib/api/services/beneficiary_service.dart index 8c4b700..793e4c8 100644 --- a/lib/api/services/beneficiary_service.dart +++ b/lib/api/services/beneficiary_service.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:dio/dio.dart'; import 'package:kmobile/data/models/ifsc.dart'; import 'package:kmobile/data/models/beneficiary.dart'; @@ -9,14 +11,16 @@ class BeneficiaryService { Future validateBeneficiaryWithinBank(String accountNumber) async { try { - final response = await _dio.get('/api/beneficiary/validate/within-bank', queryParameters: { + final response = await _dio + .get('/api/beneficiary/validate/within-bank', queryParameters: { 'accountNumber': accountNumber, }); if (response.statusCode == 200) { return response.data['name']; } else { - throw Exception(response.data['error'] ?? 'Failed to validate beneficiary'); + throw Exception( + response.data['error'] ?? 'Failed to validate beneficiary'); } } on DioException catch (e) { throw Exception('Network error: ${e.message}'); @@ -24,147 +28,76 @@ class BeneficiaryService { throw Exception('Unexpected error: ${e.toString()}'); } } - - - Future validateIFSC(String ifscCode) async { - try { - final response = await _dio.get('/api/beneficiary/ifsc-details', queryParameters: { - "ifscCode": ifscCode - } - ); - if (response.statusCode == 200) { - return ifsc.fromJson(response.data); + Future validateIFSC(String ifscCode) async { + try { + final response = await _dio.get('/api/beneficiary/ifsc-details', + queryParameters: {"ifscCode": ifscCode}); + + if (response.statusCode == 200) { + return Ifsc.fromJson(response.data); + } + } on DioException catch (e) { + if (e.response?.statusCode == 404) { + throw Exception('INVALID IFSC CODE'); + } + } catch (e) { + rethrow; } - } on DioException catch (e) { - if (e.response?.statusCode == 404) { - print('Invalid IFSC code.'); - } else { - print('API error: ${e.message}'); - } - } catch (e) { - print('Unexpected error: $e'); + return Ifsc.fromJson({}); } - - return null; -} - ///Step 1: Validate beneficiary (returns refNo) - Future validateBeneficiary({ + Future validateBeneficiary({ required String accountNo, required String ifscCode, required String remitterName, }) async { - - try { - final response = await _dio.post( - '/api/beneficiary/validate/outside_bank', - queryParameters: { - 'accountNo': accountNo, - 'ifscCode': ifscCode, - 'remitterName': remitterName, - }, - ); - - if (response.statusCode == 200) { - return response.data['refNo']; - } else { - return null; - } - } catch (e) { - print("Error validating beneficiary: $e"); - return null; - } - } - - /// Step 2: Check validation status (returns Beneficiary name if success) - Future checkValidationStatus(String refNo) async { - try { - final response = await _dio.get( - '/api/beneficiary/check', - queryParameters: { - 'refNo': refNo, - }, - ); - - if (response.statusCode == 200 && response.data != null) { - return Beneficiary.fromJson(response.data).name; - } else { - return null; - } - } catch (e) { - print("Error checking validation status: $e"); - return null; + log('inside validate beneficiary service'); + final response = await _dio.get( + '/api/beneficiary/validate/outside-bank', + queryParameters: { + 'accountNo': accountNo, + 'ifscCode': ifscCode, + 'remitterName': remitterName, + }, + ); + if (response.statusCode != 200) { + throw Exception("Invalid Beneficiary Details"); } + return response.data['name']; } // Send Data for Validation - Future sendForValidation(Beneficiary beneficiary) async { + Future sendForValidation(Beneficiary beneficiary) async { try { - print(beneficiary.toJson()); final response = await _dio.post( - '/api/beneficiary/add', + '/api/beneficiary', data: beneficiary.toJson(), ); if (response.statusCode == 200) { - print("SENT FOR VALIDATION"); + return true; } else { - print("VALIDATION REQUEST FAILED: ${response.statusCode}"); + return false; } } catch (e) { - print("ERROR IN SENDING REQUEST: $e"); rethrow; } } - // Poll to check if beneficiary is found - Future checkIfFound(String accountNo) async { - const int timeoutInSeconds = 30; - const int intervalInSeconds = 2; - const int maxTries = timeoutInSeconds ~/ intervalInSeconds; - - int attempts = 0; - - while (attempts < maxTries) { - try { - final response = await _dio.get( - '/api/beneficiary/check?', - queryParameters: { - 'accountNo': accountNo - } - ); - - if (response.statusCode == 200) { - print("FOUND"); - return true; - } else if (response.statusCode == 404) { - print("NOT FOUND"); - } - } catch (e) { - print("ERROR DURING STATUS: $e"); - } - attempts++; - } - - print("Beneficiary not found within timeout."); - return false; - } - - Future> fetchBeneficiaryList() async{ + Future> fetchBeneficiaryList() async { try { final response = await _dio.get( - "/api/beneficiary/get", - options: Options( + "/api/beneficiary", + options: Options( headers: { "Content-Type": "application/json", }, ), ); - - if (response.statusCode == 200) { - return Beneficiary.listFromJson(response.data); + if (response.statusCode == 200) { + return Beneficiary.listFromJson(response.data); } else { throw Exception("Failed to fetch beneficiaries"); } @@ -173,7 +106,4 @@ class BeneficiaryService { return []; } } -} - - - +} diff --git a/lib/api/services/neft_service.dart b/lib/api/services/neft_service.dart new file mode 100644 index 0000000..1b2a869 --- /dev/null +++ b/lib/api/services/neft_service.dart @@ -0,0 +1,30 @@ +import 'package:dio/dio.dart'; +import 'package:kmobile/data/models/neft_response.dart'; +import 'package:kmobile/data/models/neft_transaction.dart'; + +class NeftService { + final Dio _dio; + + NeftService(this._dio); + + Future processNeftTransaction(NeftTransaction transaction) async { + try { + await Future.delayed(const Duration(seconds: 3)); + final response = await _dio.post( + '/api/payment/neft', + data: transaction.toJson(), + ); + + if (response.statusCode == 200) { + return NeftResponse.fromJson(response.data); + } else { + throw Exception( + 'NEFT transaction failed with status code: ${response.statusCode}'); + } + } on DioException catch (e) { + throw Exception('Failed to process NEFT transaction: ${e.message}'); + } catch (e) { + throw Exception('An unexpected error occurred: ${e.toString()}'); + } + } +} diff --git a/lib/api/services/rtgs_service.dart b/lib/api/services/rtgs_service.dart new file mode 100644 index 0000000..b9014ad --- /dev/null +++ b/lib/api/services/rtgs_service.dart @@ -0,0 +1,31 @@ +import 'package:dio/dio.dart'; +import 'package:kmobile/data/models/rtgs_response.dart'; +import 'package:kmobile/data/models/rtgs_transaction.dart'; + +class RtgsService { + final Dio _dio; + + RtgsService(this._dio); + + Future processRtgsTransaction( + RtgsTransaction transaction) async { + try { + await Future.delayed(const Duration(seconds: 3)); + final response = await _dio.post( + '/api/payment/rtgs', + data: transaction.toJson(), + ); + + if (response.statusCode == 200) { + return RtgsResponse.fromJson(response.data); + } else { + throw Exception( + 'RTGS transaction failed with status code: ${response.statusCode}'); + } + } on DioException catch (e) { + throw Exception('Failed to process RTGS transaction: ${e.message}'); + } catch (e) { + throw Exception('An unexpected error occurred: ${e.toString()}'); + } + } +} diff --git a/lib/app.dart b/lib/app.dart index 783aabf..5115ce2 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -15,6 +15,7 @@ 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 'config/themes.dart'; import 'features/auth/screens/mpin_screen.dart'; import 'package:local_auth/local_auth.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -26,7 +27,8 @@ class KMobile extends StatefulWidget { State createState() => _KMobileState(); static void setLocale(BuildContext context, Locale newLocale) { - final _KMobileState? state = context.findAncestorStateOfType<_KMobileState>(); + final _KMobileState? state = + context.findAncestorStateOfType<_KMobileState>(); state?.setLocale(newLocale); } } @@ -35,7 +37,6 @@ class _KMobileState extends State { bool showSplash = true; Locale? _locale; - @override void initState() { super.initState(); @@ -57,7 +58,6 @@ class _KMobileState extends State { _locale = Locale(langCode); }); } - } void setLocale(Locale locale) { @@ -66,54 +66,51 @@ class _KMobileState extends State { }); } - @override -Widget build(BuildContext context) { + @override + Widget build(BuildContext context) { // Set status bar color and brightness -SystemChrome.setSystemUIOverlayStyle( -const SystemUiOverlayStyle( -statusBarColor: Colors.transparent, -statusBarIconBrightness: Brightness.dark, -), -); + SystemChrome.setSystemUIOverlayStyle( + const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: Brightness.dark, + ), + ); -return MultiBlocProvider( -providers: [ -BlocProvider(create: (_) => getIt()), -BlocProvider(create: (_) => getIt()), -], -child: BlocBuilder( -builder: (context, themeState) { - print('global theme state changed'); - print(themeState); -return MaterialApp( -debugShowCheckedModeBanner: false, -locale: _locale ?? const Locale('en'), -supportedLocales: const [ -Locale('en'), -Locale('hi'), -], -localizationsDelegates: const [ -AppLocalizations.delegate, -GlobalMaterialLocalizations.delegate, -GlobalWidgetsLocalizations.delegate, -GlobalCupertinoLocalizations.delegate, -], -title: 'kMobile', -//theme: AppThemes.getLightTheme(themeState.themeType), -theme: themeState.getThemeData(), -// darkTheme: AppThemes.getDarkTheme(themeState.themeType), -themeMode: ThemeMode.system, -onGenerateRoute: AppRoutes.generateRoute, -initialRoute: AppRoutes.splash, -home: showSplash ? const SplashScreen() : const AuthGate(), -); -}, -), -); + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => getIt()), + BlocProvider(create: (_) => ThemeCubit()), + ], + child: BlocBuilder( + builder: (context, themeState) { + print('global theme state changed'); + return MaterialApp( + debugShowCheckedModeBanner: false, + locale: _locale ?? const Locale('en'), + supportedLocales: const [ + Locale('en'), + Locale('hi'), + ], + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + title: 'kMobile', + theme: themeState.getThemeData(), + darkTheme: themeState.getThemeData(), + themeMode: ThemeMode.system, + onGenerateRoute: AppRoutes.generateRoute, + initialRoute: AppRoutes.splash, + home: showSplash ? const SplashScreen() : const AuthGate(), + ); + }, + ), + ); + } } - -} class AuthGate extends StatefulWidget { const AuthGate({super.key}); @@ -142,8 +139,7 @@ class _AuthGateState extends State { final mpin = await storage.read('mpin'); final biometric = await storage.read('biometric_enabled'); setState(() { - _isLoggedIn = - accessToken != null && + _isLoggedIn = accessToken != null && accessTokenExpiry != null && DateTime.parse(accessTokenExpiry).isAfter(DateTime.now()); _hasMPin = mpin != null; @@ -172,7 +168,6 @@ class _AuthGateState extends State { } } - @override Widget build(BuildContext context) { if (_checking) { @@ -319,6 +314,8 @@ class _NavigationScaffoldState extends State { @override Widget build(BuildContext context) { + print( + "--- NavigationScaffold is rebuilding with theme color: ${Theme.of(context).primaryColor}"); return PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) async { @@ -357,10 +354,16 @@ class _NavigationScaffoldState extends State { bottomNavigationBar: BottomNavigationBar( currentIndex: _selectedIndex, type: BottomNavigationBarType.fixed, - backgroundColor: Theme.of(context).scaffoldBackgroundColor, // Light blue background + backgroundColor: Theme.of(context) + .scaffoldBackgroundColor, // Light blue background selectedItemColor: Theme.of(context).primaryColor, unselectedItemColor: Colors.black54, - onTap: _onItemTapped, + onTap: (index) { + setState(() { + _selectedIndex = index; + }); + _pageController.jumpToPage(index); + }, items: [ BottomNavigationBarItem( icon: const Icon(Icons.home_filled), diff --git a/lib/data/models/beneficiary.dart b/lib/data/models/beneficiary.dart index 9fee201..3a47af6 100644 --- a/lib/data/models/beneficiary.dart +++ b/lib/data/models/beneficiary.dart @@ -1,5 +1,3 @@ - - // ignore_for_file: non_constant_identifier_names class Beneficiary { @@ -9,6 +7,7 @@ class Beneficiary { final String ifscCode; final String? bankName; final String? branchName; + final String? tpin; Beneficiary({ required this.accountNo, @@ -17,30 +16,43 @@ class Beneficiary { required this.ifscCode, this.bankName, this.branchName, + this.tpin, }); factory Beneficiary.fromJson(Map json) { return Beneficiary( - accountNo: json['accountNo'] ?? '', - accountType: json['accountType'] ?? '', + accountNo: json['account_no'] ?? json['accountNo'] ?? '', + accountType: json['account_type'] ?? json['accountType'] ?? '', name: json['name'] ?? '', - ifscCode: json['ifscCode'] ?? '', - bankName: json['bankName'] ?? '', - branchName: json['branchName'] ?? '', + ifscCode: json['ifsc_code'] ?? json['ifscCode'] ?? '', + bankName: json['bank_name'] ?? json['bankName'] ?? '', + branchName: json['branch_name'] ?? json['branchName'] ?? '', ); } - + Map toJson() { - return { + final Map json = { 'accountNo': accountNo, 'accountType': accountType, 'name': name, - 'ifscCode' : ifscCode, + 'ifscCode': ifscCode, }; + if (bankName != null && bankName!.isNotEmpty) { + json['bankName'] = bankName; + } + if (branchName != null && branchName!.isNotEmpty) { + json['branchName'] = branchName; + } + if (tpin != null && tpin!.isNotEmpty) { + json['tpin'] = tpin; + } + return json; } static List listFromJson(List jsonList) { - final beneficiaryList = jsonList.map((beneficiary) => Beneficiary.fromJson(beneficiary)).toList(); + final beneficiaryList = jsonList + .map((beneficiary) => Beneficiary.fromJson(beneficiary)) + .toList(); print(beneficiaryList); return beneficiaryList; } @@ -49,4 +61,5 @@ class Beneficiary { String toString() { return 'Beneficiary(accountNo: $accountNo, accountType: $accountType, ifscCode: $ifscCode, name: $name)'; } -} \ No newline at end of file +} + diff --git a/lib/data/models/ifsc.dart b/lib/data/models/ifsc.dart index c6102c9..c1e3b79 100644 --- a/lib/data/models/ifsc.dart +++ b/lib/data/models/ifsc.dart @@ -1,16 +1,16 @@ -class ifsc { +class Ifsc { final String ifscCode; final String bankName; final String branchName; - ifsc({ + Ifsc({ required this.ifscCode, required this.bankName, required this.branchName, }); - factory ifsc.fromJson(Map json) { - return ifsc( + factory Ifsc.fromJson(Map json) { + return Ifsc( ifscCode: json['ifsc_code'] ?? '', bankName: json['bank_name'] ?? '', branchName: json['branch_name'] ?? '', @@ -29,4 +29,5 @@ class ifsc { String toString() { return 'IFSC(ifscCode: $ifscCode, bankName: $bankName, branchName: $branchName)'; } -} \ No newline at end of file +} + diff --git a/lib/data/models/neft_response.dart b/lib/data/models/neft_response.dart new file mode 100644 index 0000000..df42b28 --- /dev/null +++ b/lib/data/models/neft_response.dart @@ -0,0 +1,16 @@ +class NeftResponse { + final String message; + final String utr; + + NeftResponse({ + required this.message, + required this.utr, + }); + + factory NeftResponse.fromJson(Map json) { + return NeftResponse( + message: json['message'] ?? '', + utr: json['utr'] ?? '', + ); + } +} diff --git a/lib/data/models/neft_transaction.dart b/lib/data/models/neft_transaction.dart new file mode 100644 index 0000000..1d4b8d3 --- /dev/null +++ b/lib/data/models/neft_transaction.dart @@ -0,0 +1,31 @@ +class NeftTransaction { + final String fromAccount; + final String toAccount; + final String amount; + final String ifscCode; + final String remitterName; + final String beneficiaryName; + final String tpin; + + NeftTransaction({ + required this.fromAccount, + required this.toAccount, + required this.amount, + required this.ifscCode, + required this.remitterName, + required this.beneficiaryName, + required this.tpin, + }); + + Map toJson() { + return { + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'amount': amount, + 'ifscCode': ifscCode, + 'remitterName': remitterName, + 'beneficiaryName': beneficiaryName, + 'tpin': tpin, + }; + } +} diff --git a/lib/data/models/payment_response.dart b/lib/data/models/payment_response.dart index 9bf7a35..e5a733f 100644 --- a/lib/data/models/payment_response.dart +++ b/lib/data/models/payment_response.dart @@ -6,6 +6,7 @@ class PaymentResponse { final String? currency; final String? errorMessage; final String? errorCode; + final String? utr; PaymentResponse({ required this.isSuccess, @@ -15,5 +16,6 @@ class PaymentResponse { this.currency, this.errorMessage, this.errorCode, + this.utr, }); } diff --git a/lib/data/models/rtgs_response.dart b/lib/data/models/rtgs_response.dart new file mode 100644 index 0000000..b572690 --- /dev/null +++ b/lib/data/models/rtgs_response.dart @@ -0,0 +1,16 @@ +class RtgsResponse { + final String message; + final String utr; + + RtgsResponse({ + required this.message, + required this.utr, + }); + + factory RtgsResponse.fromJson(Map json) { + return RtgsResponse( + message: json['message'] ?? '', + utr: json['utr'] ?? '', + ); + } +} diff --git a/lib/data/models/rtgs_transaction.dart b/lib/data/models/rtgs_transaction.dart new file mode 100644 index 0000000..9539eac --- /dev/null +++ b/lib/data/models/rtgs_transaction.dart @@ -0,0 +1,31 @@ +class RtgsTransaction { + final String fromAccount; + final String toAccount; + final String amount; + final String ifscCode; + final String remitterName; + final String beneficiaryName; + final String tpin; + + RtgsTransaction({ + required this.fromAccount, + required this.toAccount, + required this.amount, + required this.ifscCode, + required this.remitterName, + required this.beneficiaryName, + required this.tpin, + }); + + Map toJson() { + return { + 'fromAccount': fromAccount, + 'toAccount': toAccount, + 'amount': amount, + 'ifscCode': ifscCode, + 'remitterName': remitterName, + 'beneficiaryName': beneficiaryName, + 'tpin': tpin, + }; + } +} diff --git a/lib/data/repositories/transaction_repository.dart b/lib/data/repositories/transaction_repository.dart index ae8c6d7..164ed6c 100644 --- a/lib/data/repositories/transaction_repository.dart +++ b/lib/data/repositories/transaction_repository.dart @@ -1,8 +1,12 @@ +import 'dart:developer'; + import 'package:dio/dio.dart'; +import 'package:intl/intl.dart'; import 'package:kmobile/data/models/transaction.dart'; abstract class TransactionRepository { - Future> fetchTransactions(String accountNo); + Future> fetchTransactions(String accountNo, + {DateTime? fromDate, DateTime? toDate}); } class TransactionRepositoryImpl implements TransactionRepository { @@ -10,9 +14,23 @@ class TransactionRepositoryImpl implements TransactionRepository { TransactionRepositoryImpl(this._dio); @override - Future> fetchTransactions(String accountNo) async { + Future> fetchTransactions(String accountNo, + {DateTime? fromDate, DateTime? toDate}) async { + final queryParameters = {}; - final resp = await _dio.get('/api/transactions/account/$accountNo'); + if (fromDate != null) { + queryParameters['fromDate'] = DateFormat('ddMMyyyy').format(fromDate); + } + if (toDate != null) { + queryParameters['toDate'] = DateFormat('ddMMyyyy').format(toDate); + } + + log('query params below'); + log(queryParameters.toString()); + final resp = await _dio.get( + '/api/transactions/account/$accountNo', + queryParameters: queryParameters.isNotEmpty ? queryParameters : null, + ); if (resp.statusCode != 200) { throw Exception( @@ -25,4 +43,4 @@ class TransactionRepositoryImpl implements TransactionRepository { .map((e) => Transaction.fromJson(e as Map)) .toList(); } -} \ No newline at end of file +} diff --git a/lib/di/injection.dart b/lib/di/injection.dart index 337236f..4a0ac66 100644 --- a/lib/di/injection.dart +++ b/lib/di/injection.dart @@ -1,3 +1,5 @@ +import 'package:kmobile/api/services/rtgs_service.dart'; +import 'package:kmobile/api/services/neft_service.dart'; import 'package:get_it/get_it.dart'; import 'package:dio/dio.dart'; import 'package:kmobile/api/services/beneficiary_service.dart'; @@ -11,14 +13,12 @@ import '../data/repositories/auth_repository.dart'; import '../features/auth/controllers/auth_cubit.dart'; import '../security/secure_storage.dart'; - final getIt = GetIt.instance; Future setupDependencies() async { - //getIt.registerLazySingleton(() => ThemeController()); //getIt.registerLazySingleton(() => ThemeModeController()); - getIt.registerSingleton( ThemeCubit()); + getIt.registerSingleton(ThemeCubit()); // Register Dio client getIt.registerSingleton(_createDioClient()); @@ -42,6 +42,8 @@ Future setupDependencies() async { getIt.registerSingleton(PaymentService(getIt())); getIt.registerSingleton(BeneficiaryService(getIt())); + getIt.registerSingleton(NeftService(getIt())); + getIt.registerSingleton(RtgsService(getIt())); // Add auth interceptor after repository is available getIt().interceptors.add( @@ -51,17 +53,16 @@ Future setupDependencies() async { // Register controllers/cubits getIt.registerFactory( () => AuthCubit(getIt(), getIt())); - } Dio _createDioClient() { final dio = Dio( BaseOptions( baseUrl: - //'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080', - 'http://localhost:8081', + 'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080', + // 'http://localhost:8081', connectTimeout: const Duration(seconds: 5), - receiveTimeout: const Duration(seconds: 3), + receiveTimeout: const Duration(seconds: 10), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', diff --git a/lib/features/accounts/screens/account_statement_screen.dart b/lib/features/accounts/screens/account_statement_screen.dart index f7b9630..bc36ce1 100644 --- a/lib/features/accounts/screens/account_statement_screen.dart +++ b/lib/features/accounts/screens/account_statement_screen.dart @@ -36,7 +36,11 @@ class _AccountStatementScreen extends State { }); try { final repo = getIt(); - final txs = await repo.fetchTransactions(widget.accountNo); + final txs = await repo.fetchTransactions( + widget.accountNo, + fromDate: fromDate, + toDate: toDate, + ); setState(() => _transactions = txs); } catch (e) { if (!mounted) return; @@ -115,7 +119,8 @@ class _AccountStatementScreen extends State { ), title: Text( AppLocalizations.of(context).accountStatement, - style: const TextStyle(color: Colors.black, fontWeight: FontWeight.w500), + style: + const TextStyle(color: Colors.black, fontWeight: FontWeight.w500), ), centerTitle: false, actions: [ @@ -181,56 +186,28 @@ class _AccountStatementScreen extends State { ], ), const SizedBox(height: 20), - Row( - children: [ - Expanded( - child: TextFormField( - controller: _minAmountController, - decoration: InputDecoration( - labelText: AppLocalizations.of(context).minAmount, - border: const OutlineInputBorder(), - isDense: true, - filled: true, - fillColor: Theme.of(context).scaffoldBackgroundColor, - ), - keyboardType: TextInputType.number, - textInputAction: TextInputAction.next, + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _loadTransactions, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColor, + padding: const EdgeInsets.symmetric(vertical: 16), + ), + child: Text( + "Search", + style: TextStyle( + color: Theme.of(context).scaffoldBackgroundColor, + fontSize: 16, ), ), - const SizedBox(width: 8), - Expanded( - child: TextFormField( - controller: _maxAmountController, - decoration: InputDecoration( - labelText: AppLocalizations.of(context).maxAmount, - border: const OutlineInputBorder(), - isDense: true, - filled: true, - fillColor: Theme.of(context).scaffoldBackgroundColor, - ), - 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: Icon( - Symbols.arrow_forward, - color: Theme.of(context).scaffoldBackgroundColor, - size: 30, - ), - ), - ), - ], + ), ), const SizedBox(height: 35), - if (!_txLoading && _transactions.isNotEmpty) + if (!_txLoading && + _transactions.isNotEmpty && + fromDate == null && + toDate == null) Padding( padding: const EdgeInsets.only(bottom: 12.0), child: Text( @@ -252,7 +229,8 @@ class _AccountStatementScreen extends State { highlightColor: Colors.grey[100]!, child: CircleAvatar( radius: 12, - backgroundColor: Theme.of(context).scaffoldBackgroundColor, + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, ), ), title: Shimmer.fromColors( @@ -298,7 +276,7 @@ class _AccountStatementScreen extends State { title: Text( tx.name != null ? (tx.name!.length > 18 - ? tx.name!.substring(0, 20) + ? tx.name!.substring(0, 22) : tx.name!) : '', style: const TextStyle(fontSize: 14), diff --git a/lib/features/auth/controllers/theme_cubit.dart b/lib/features/auth/controllers/theme_cubit.dart index 193e6a0..70f973d 100644 --- a/lib/features/auth/controllers/theme_cubit.dart +++ b/lib/features/auth/controllers/theme_cubit.dart @@ -1,48 +1,25 @@ -import 'dart:developer'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'theme_state.dart'; import 'package:kmobile/config/theme_type.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'theme_state.dart'; + class ThemeCubit extends Cubit { - ThemeCubit(): super(ThemeViolet()) { + ThemeCubit() : super(const ThemeState(themeType: ThemeType.violet)) { loadTheme(); } Future loadTheme() async { final prefs = await SharedPreferences.getInstance(); final themeIndex = prefs.getInt('theme_type') ?? 0; - // final isDark = prefs.getBool('is_dark_mode') ?? false; - final type = ThemeType.values[themeIndex]; - switch(type) { - case ThemeType.blue: - emit(ThemeBlue()); - case ThemeType.violet: - emit(ThemeViolet()); - default: - emit(ThemeViolet()); - } + emit(ThemeState(themeType: type)); } Future changeTheme(ThemeType type) async { + print("Attempting to change theme to: ${type.toString()}"); final prefs = await SharedPreferences.getInstance(); await prefs.setInt('theme_type', type.index); - log("Mode Change"); - print("mode changed"); - switch(type) { - case ThemeType.blue: - emit(ThemeBlue()); - print('blue matched'); - break; - case ThemeType.violet: - emit(ThemeViolet()); - print('violet matched'); - break; - default: - emit(ThemeBlue()); - print('default macthed'); - } + emit(ThemeState(themeType: type)); } } - diff --git a/lib/features/auth/controllers/theme_state.dart b/lib/features/auth/controllers/theme_state.dart index 44db439..ef57938 100644 --- a/lib/features/auth/controllers/theme_state.dart +++ b/lib/features/auth/controllers/theme_state.dart @@ -1,27 +1,17 @@ import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; import 'package:kmobile/config/theme_type.dart'; import 'package:kmobile/config/themes.dart'; +class ThemeState extends Equatable { + final ThemeType themeType; -abstract class ThemeState extends Equatable { - getThemeData(); - @override - List get props => []; -} + const ThemeState({required this.themeType}); -class ThemeBlue extends ThemeState { - - @override - getThemeData() { - print('returning blue theme'); - return AppThemes.getLightTheme(ThemeType.blue); + ThemeData getThemeData() { + return AppThemes.getLightTheme(themeType); } -} -class ThemeViolet extends ThemeState { @override - getThemeData() { - print('returning violet theme'); - return AppThemes.getLightTheme(ThemeType.violet); - } -} \ No newline at end of file + List get props => [themeType]; +} diff --git a/lib/features/beneficiaries/screens/add_beneficiary_screen.dart b/lib/features/beneficiaries/screens/add_beneficiary_screen.dart index 5b4b109..f56e325 100644 --- a/lib/features/beneficiaries/screens/add_beneficiary_screen.dart +++ b/lib/features/beneficiaries/screens/add_beneficiary_screen.dart @@ -1,3 +1,4 @@ +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:kmobile/api/services/beneficiary_service.dart'; @@ -8,15 +9,14 @@ import 'beneficiary_result_page.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart'; import '../../../di/injection.dart'; import '../../../l10n/app_localizations.dart'; +import 'package:kmobile/features/fund_transfer/screens/transaction_pin_screen.dart'; class AddBeneficiaryScreen extends StatefulWidget { - final List? users; - final int? selectedIndex; + final String customerName; const AddBeneficiaryScreen({ super.key, - this.users, - this.selectedIndex, + required this.customerName, }); @override @@ -26,7 +26,6 @@ class AddBeneficiaryScreen extends StatefulWidget { class _AddBeneficiaryScreen extends State { final _formKey = GlobalKey(); - late User selectedUser = (widget.users ?? [])[widget.selectedIndex!]; final TextEditingController accountNumberController = TextEditingController(); final TextEditingController confirmAccountNumberController = TextEditingController(); @@ -35,13 +34,14 @@ class _AddBeneficiaryScreen extends State { final TextEditingController branchNameController = TextEditingController(); final TextEditingController ifscController = TextEditingController(); final TextEditingController phoneController = TextEditingController(); - + final service = getIt(); + String? _beneficiaryName; bool _isValidating = false; bool _isBeneficiaryValidated = false; String? _validationError; - late String accountType; + final String _selectedAccountType = 'Savings'; // default value @override void initState() { @@ -53,167 +53,159 @@ class _AddBeneficiaryScreen extends State { }); } - - -ifsc? _ifscData; -bool _isLoading = false; //for validateIFSC() - - void _validateIFSC() async { - var beneficiaryService = getIt(); - final ifsc = ifscController.text.trim().toUpperCase(); - if (ifsc.isEmpty) return; - setState(() { - _isLoading = true; - _ifscData = null; - }); + void _validateIFSC() async { + var beneficiaryService = getIt(); + final ifsc = ifscController.text.trim().toUpperCase(); + if (ifsc.isEmpty) return; final result = await beneficiaryService.validateIFSC(ifsc); - setState(() { - _isLoading = false; - _ifscData = result; - }); - - if (result == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(AppLocalizations.of(context).invalidIfsc)), - ); - bankNameController.clear(); - branchNameController.clear(); - } else { - print("${AppLocalizations.of(context).validIfsc}: ${result.bankName}, ${result.branchName}"); - bankNameController.text = result.bankName; - branchNameController.text = result.branchName; - } - } - - -Future _validateBeneficiary() async { - // start spinner / disable button - setState(() { - _isValidating = true; - _validationError = null; - _isBeneficiaryValidated = false; - nameController.text = ''; // clear previous name - }); - - final String accountNo = accountNumberController.text.trim(); - final String ifsc = ifscController.text.trim(); - final String remitter = selectedUser.name ?? ''; - - final service = getIt(); - try { - // Step 1: call validate API -> get refNo - final String? refNo = await service.validateBeneficiary( - accountNo: accountNo, - ifscCode: ifsc, - remitterName: remitter, - ); - - if (refNo == null || refNo.isEmpty) { - setState(() { - _validationError = 'Validation request failed. Please check details.'; - _isBeneficiaryValidated = false; - }); - return; - } - - // Step 2: poll checkValidationStatus for up to 30 seconds - const int timeoutSeconds = 30; - const int intervalSeconds = 2; - int elapsed = 0; - String? foundName; - - while (elapsed < timeoutSeconds) { - final String? name = await service.checkValidationStatus(refNo); - - if (name != null && name.trim().isNotEmpty) { - foundName = name.trim(); - break; + if (mounted) { + if (result.bankName == '') { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(AppLocalizations.of(context).invalidIfsc)), + ); + bankNameController.clear(); + branchNameController.clear(); + } else { + bankNameController.text = result.bankName; + branchNameController.text = result.branchName; } - - await Future.delayed(const Duration(seconds: intervalSeconds)); - elapsed += intervalSeconds; } + } + + void _validateBeneficiary() async { + FocusScope.of(context).unfocus(); + setState(() { + _isValidating = true; + _validationError = null; + _isBeneficiaryValidated = false; + nameController.text = ''; // clear previous name + }); + + final String accountNo = accountNumberController.text.trim(); + final String ifsc = ifscController.text.trim(); + final String remitter = widget.customerName; + + final service = getIt(); + try { + final String beneficiaryName = await service.validateBeneficiary( + accountNo: accountNo, + ifscCode: ifsc, + remitterName: remitter, + ); - if (foundName != null) { setState(() { - nameController.text = foundName!; + nameController.text = beneficiaryName; _isBeneficiaryValidated = true; _validationError = null; }); - } else { + } catch (e) { setState(() { - _validationError = 'Beneficiary not found within timeout.'; + _validationError = e.toString(); _isBeneficiaryValidated = false; }); - } - } catch (e, st) { - // handle unexpected errors - // print or log if you want - debugPrint('Error validating beneficiary: $e\n$st'); - setState(() { - _validationError = 'Something went wrong. Please try again.'; - _isBeneficiaryValidated = false; - }); - } finally { - if (mounted) { - setState(() { - _isValidating = false; - }); + } finally { + if (mounted) { + setState(() { + _isValidating = false; + }); + } } } -} - - - String _selectedAccountType = 'Savings'; // default value - -void validateAndAddBeneficiary() async { - // Show spinner and disable UI - showDialog( - context: context, - barrierDismissible: false, // Prevent dismiss on tap outside - builder: (BuildContext context) { - return WillPopScope( - onWillPop: () async => false, // Disable back button - child: const Center( - child: CircularProgressIndicator(), - ), - ); - }, - ); - - final beneficiary = Beneficiary( - accountNo: accountNumberController.text.trim(), - accountType: _selectedAccountType, - name: nameController.text.trim(), - ifscCode: ifscController.text.trim(), - ); - - var service = getIt(); - - try { - await service.sendForValidation(beneficiary); - bool isFound = await service.checkIfFound(beneficiary.accountNo); - - if (context.mounted) { - Navigator.pop(context); // Close the spinner - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => BeneficiaryResultPage(isSuccess: isFound), - ), - ); + void validateAndAddBeneficiary() async { + // First, validate the form fields (account number, confirm account, ifsc, etc.) + if (!_formKey.currentState!.validate()) { + return; } - } catch (e) { - Navigator.pop(context); // Close the spinner - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(AppLocalizations.of(context).somethingWentWrong)), + + // Ensure beneficiary is validated before proceeding to TPIN + if (!_isBeneficiaryValidated) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please validate beneficiary details first.')), + ); + return; + } + + // Create the beneficiary object without TPIN for now + final beneficiaryWithoutTpin = Beneficiary( + accountNo: accountNumberController.text.trim(), + accountType: accountType, + name: nameController.text.trim(), + ifscCode: ifscController.text.trim(), + bankName: bankNameController.text.trim(), + branchName: branchNameController.text.trim(), + ); + + // Navigate to TPIN screen + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TransactionPinScreen( + onPinCompleted: (pinScreenContext, tpin) async { + // Show loading spinner while processing + showDialog( + context: pinScreenContext, + barrierDismissible: false, + builder: (BuildContext ctx) { + return WillPopScope( + onWillPop: () async => false, + child: const Center( + child: CircularProgressIndicator(), + ), + ); + }, + ); + + // Create a new Beneficiary object with the TPIN + final beneficiaryWithTpin = Beneficiary( + accountNo: beneficiaryWithoutTpin.accountNo, + accountType: beneficiaryWithoutTpin.accountType, + name: beneficiaryWithoutTpin.name, + ifscCode: beneficiaryWithoutTpin.ifscCode, + bankName: beneficiaryWithoutTpin.bankName, + branchName: beneficiaryWithoutTpin.branchName, + tpin: tpin, + ); + + try { + final isSuccess = await service.sendForValidation(beneficiaryWithTpin); + + if (pinScreenContext.mounted) { + Navigator.pop(pinScreenContext); // Close the spinner + Navigator.pushReplacement( + pinScreenContext, + MaterialPageRoute( + builder: (ctx) => BeneficiaryResultPage(isSuccess: isSuccess), + ), + ); + } + } on DioException catch (e) { + if (pinScreenContext.mounted) { + Navigator.pop(pinScreenContext); // Close the spinner + ScaffoldMessenger.of(pinScreenContext).showSnackBar( + SnackBar( + content: Text(e.response?.statusCode == 409 + ? 'Beneficiary already exists' + : 'Something went Wrong')), + ); + } + } catch (e) { + if (pinScreenContext.mounted) { + Navigator.pop(pinScreenContext); // Close the spinner + ScaffoldMessenger.of(pinScreenContext).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context).somethingWentWrong)), + ); + } + } + }, + ), + ), ); } -} - @override Widget build(BuildContext context) { @@ -227,7 +219,8 @@ void validateAndAddBeneficiary() async { ), title: Text( AppLocalizations.of(context).addBeneficiary, - style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500), + style: + const TextStyle(color: Colors.black, fontWeight: FontWeight.w500), ), centerTitle: false, actions: [ @@ -265,10 +258,11 @@ void validateAndAddBeneficiary() async { context, ).accountNumber, // prefixIcon: Icon(Icons.person), - border: OutlineInputBorder(), + border: const OutlineInputBorder(), isDense: true, filled: true, - fillColor: Theme.of(context).scaffoldBackgroundColor, + fillColor: + Theme.of(context).scaffoldBackgroundColor, enabledBorder: const OutlineInputBorder( borderSide: BorderSide(color: Colors.black), ), @@ -282,6 +276,12 @@ void validateAndAddBeneficiary() async { obscureText: true, keyboardType: TextInputType.number, textInputAction: TextInputAction.next, + onChanged: (value) { + nameController.clear(); + setState(() { + _isBeneficiaryValidated = false; + }); + }, validator: (value) { if (value == null || value.length < 10) { return AppLocalizations.of( @@ -300,10 +300,11 @@ void validateAndAddBeneficiary() async { context, ).confirmAccountNumber, // prefixIcon: Icon(Icons.person), - border: OutlineInputBorder(), + border: const OutlineInputBorder(), isDense: true, filled: true, - fillColor: Theme.of(context).scaffoldBackgroundColor, + fillColor: + Theme.of(context).scaffoldBackgroundColor, enabledBorder: const OutlineInputBorder( borderSide: BorderSide(color: Colors.black), ), @@ -339,7 +340,8 @@ void validateAndAddBeneficiary() async { border: const OutlineInputBorder(), isDense: true, filled: true, - fillColor: Theme.of(context).scaffoldBackgroundColor, + fillColor: + Theme.of(context).scaffoldBackgroundColor, enabledBorder: const OutlineInputBorder( borderSide: BorderSide(color: Colors.black), ), @@ -377,7 +379,6 @@ void validateAndAddBeneficiary() async { return null; }, ), - const SizedBox(height: 24), // ЁЯФ╣ Bank Name (Disabled) TextFormField( @@ -388,7 +389,8 @@ void validateAndAddBeneficiary() async { border: const OutlineInputBorder(), isDense: true, filled: true, - fillColor: Theme.of(context).dialogBackgroundColor, // disabled color + fillColor: Theme.of(context) + .dialogBackgroundColor, // disabled color enabledBorder: const OutlineInputBorder( borderSide: BorderSide(color: Colors.black), ), @@ -400,7 +402,6 @@ void validateAndAddBeneficiary() async { ), ), ), - const SizedBox(height: 24), // ЁЯФ╣ Branch Name (Disabled) TextFormField( @@ -423,59 +424,62 @@ void validateAndAddBeneficiary() async { ), ), ), -//Validate Beneficiary Name -if (!_isBeneficiaryValidated) - Padding( - padding: const EdgeInsets.only(top: 12.0), - child: SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: _isValidating - ? null - : () { - if ( - confirmAccountNumberController.text == - accountNumberController.text) { - _validateBeneficiary(); - } else { - setState(() { - _validationError = - 'Please enter a valid and matching account number.'; - }); - } - }, - child: _isValidating - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Text('Validate Beneficiary'), - ), - ), - ), + const SizedBox(height: 24), + if (!_isBeneficiaryValidated) + Padding( + padding: const EdgeInsetsGeometry.only(bottom: 24), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _isValidating + ? null + : () { + if (confirmAccountNumberController + .text == + accountNumberController.text) { + _validateBeneficiary(); + } else { + setState(() { + _validationError = + 'Please enter a valid and matching account number.'; + }); + } + }, + child: _isValidating + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2), + ) + : const Text('Validate Beneficiary'), + ), + ), + ), //Beneficiary Name (Disabled) TextFormField( - controller: nameController, - enabled: false, - decoration: InputDecoration( - labelText: AppLocalizations.of(context).beneficiaryName, - border: const OutlineInputBorder(), - isDense: true, - filled: true, - fillColor: Theme.of(context).dialogBackgroundColor, - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Colors.black), - ), - focusedBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Colors.black, width: 2), - ), - ), - textInputAction: TextInputAction.next, - validator: (value) => value == null || value.isEmpty - ? AppLocalizations.of(context).nameRequired - : null, -), + controller: nameController, + enabled: false, + decoration: InputDecoration( + labelText: + AppLocalizations.of(context).beneficiaryName, + border: const OutlineInputBorder(), + isDense: true, + filled: true, + fillColor: Theme.of(context).dialogBackgroundColor, + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.black), + ), + focusedBorder: const 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), // ЁЯФ╣ Account Type Dropdown DropdownButtonFormField( @@ -485,7 +489,8 @@ if (!_isBeneficiaryValidated) border: const OutlineInputBorder(), isDense: true, filled: true, - fillColor: Theme.of(context).scaffoldBackgroundColor, + fillColor: + Theme.of(context).scaffoldBackgroundColor, enabledBorder: const OutlineInputBorder( borderSide: BorderSide(color: Colors.black), ), @@ -496,18 +501,17 @@ if (!_isBeneficiaryValidated) ), ), ), - items: - [ - AppLocalizations.of(context).savings, - AppLocalizations.of(context).current, - ] - .map( - (type) => DropdownMenuItem( - value: type, - child: Text(type), - ), - ) - .toList(), + items: [ + AppLocalizations.of(context).savings, + AppLocalizations.of(context).current, + ] + .map( + (type) => DropdownMenuItem( + value: type, + child: Text(type), + ), + ) + .toList(), onChanged: (value) { setState(() { accountType = value!; @@ -525,7 +529,8 @@ if (!_isBeneficiaryValidated) border: const OutlineInputBorder(), isDense: true, filled: true, - fillColor: Theme.of(context).scaffoldBackgroundColor, + fillColor: + Theme.of(context).scaffoldBackgroundColor, enabledBorder: const OutlineInputBorder( borderSide: BorderSide(color: Colors.black), ), @@ -539,8 +544,8 @@ if (!_isBeneficiaryValidated) textInputAction: TextInputAction.done, validator: (value) => value == null || value.length != 10 - ? AppLocalizations.of(context).enterValidPhone - : null, + ? AppLocalizations.of(context).enterValidPhone + : null, ), const SizedBox(height: 35), ], @@ -553,13 +558,13 @@ if (!_isBeneficiaryValidated) child: SizedBox( width: 250, child: ElevatedButton( - onPressed: - validateAndAddBeneficiary, + onPressed: validateAndAddBeneficiary, style: ElevatedButton.styleFrom( shape: const StadiumBorder(), padding: const EdgeInsets.symmetric(vertical: 16), backgroundColor: Theme.of(context).primaryColorDark, - foregroundColor: Theme.of(context).scaffoldBackgroundColor, + foregroundColor: + Theme.of(context).scaffoldBackgroundColor, ), child: Text(AppLocalizations.of(context).validateAndAdd), ), diff --git a/lib/features/beneficiaries/screens/manage_beneficiaries_screen.dart b/lib/features/beneficiaries/screens/manage_beneficiaries_screen.dart index 942699d..e4c555f 100644 --- a/lib/features/beneficiaries/screens/manage_beneficiaries_screen.dart +++ b/lib/features/beneficiaries/screens/manage_beneficiaries_screen.dart @@ -1,18 +1,14 @@ import 'package:flutter/material.dart'; import 'package:kmobile/data/models/beneficiary.dart'; import 'package:kmobile/features/beneficiaries/screens/add_beneficiary_screen.dart'; -import '../../../data/models/user.dart'; import '../../../l10n/app_localizations.dart'; import '../../../di/injection.dart'; import 'package:kmobile/api/services/beneficiary_service.dart'; import 'package:shimmer/shimmer.dart'; -//import 'package:kmobile/data/models/user.dart'; class ManageBeneficiariesScreen extends StatefulWidget { - // final List users; - // final int selectedIndex; - - const ManageBeneficiariesScreen({super.key}); + final String customerName; + const ManageBeneficiariesScreen({super.key, required this.customerName}); @override State createState() => @@ -21,12 +17,8 @@ class ManageBeneficiariesScreen extends StatefulWidget { class _ManageBeneficiariesScreen extends State { var service = getIt(); -// late User selectedUser = widget.users[widget.selectedIndex]; - //final BeneficiaryService _service = BeneficiaryService(); bool _isLoading = true; - int selectedAccountIndex = 0; List _beneficiaries = []; - @override void initState() { @@ -37,7 +29,7 @@ class _ManageBeneficiariesScreen extends State { Future _loadBeneficiaries() async { final data = await service.fetchBeneficiaryList(); setState(() { - _beneficiaries = data ; + _beneficiaries = data; _isLoading = false; }); } @@ -68,9 +60,26 @@ class _ManageBeneficiariesScreen extends State { ); } + Widget _getBankLogo(String? bankName) { + if (bankName != null && bankName.toLowerCase().contains('state bank of india')) { + return Image.asset( + 'assets/images/sbi_logo.png', + width: 40, + height: 40, + ); + } else { + return const Icon( + Icons.account_balance, + size: 40, + color: Colors.grey, + ); + } + } + Widget _buildBeneficiaryList() { if (_beneficiaries.isEmpty) { - return Center(child: Text(AppLocalizations.of(context).noBeneficiaryFound)); + return Center( + child: Text(AppLocalizations.of(context).noBeneficiaryFound)); } return ListView.builder( itemCount: _beneficiaries.length, @@ -79,16 +88,21 @@ class _ManageBeneficiariesScreen extends State { return ListTile( leading: CircleAvatar( radius: 24, - backgroundColor: Theme.of(context).primaryColor.withOpacity(0.2), - child: Text( - item.name.isNotEmpty - ? item.name[0].toUpperCase() - : '?', - style: const TextStyle(fontWeight: FontWeight.bold), - ), + backgroundColor: Colors.transparent, + child: _getBankLogo(item.bankName), ), title: Text(item.name ?? 'Unknown'), - subtitle: Text(item.accountNo ?? 'No account number'), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(item.accountNo ?? 'No account number'), + if (item.bankName != null && item.bankName!.isNotEmpty) + Text( + item.bankName!, + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + ], + ), ); }, ); @@ -96,6 +110,7 @@ class _ManageBeneficiariesScreen extends State { @override Widget build(BuildContext context) { + String customerName = widget.customerName; return Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context).beneficiaries), @@ -108,7 +123,8 @@ class _ManageBeneficiariesScreen extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => AddBeneficiaryScreen(), + builder: (context) => + AddBeneficiaryScreen(customerName: customerName), ), ); }, @@ -120,4 +136,4 @@ class _ManageBeneficiariesScreen extends State { ), ); } -} \ No newline at end of file +} diff --git a/lib/features/dashboard/screens/dashboard_screen.dart b/lib/features/dashboard/screens/dashboard_screen.dart index a489622..3157cd8 100644 --- a/lib/features/dashboard/screens/dashboard_screen.dart +++ b/lib/features/dashboard/screens/dashboard_screen.dart @@ -95,7 +95,10 @@ class _DashboardScreenState extends State { return Shimmer.fromColors( baseColor: Theme.of(context).dialogBackgroundColor, highlightColor: Theme.of(context).dialogBackgroundColor, - child: Container(width: 100, height: 32, color: Theme.of(context).scaffoldBackgroundColor), + child: Container( + width: 100, + height: 32, + color: Theme.of(context).scaffoldBackgroundColor), ); } @@ -199,7 +202,7 @@ class _DashboardScreenState extends State { child: Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( - backgroundColor:Theme.of(context).scaffoldBackgroundColor, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, automaticallyImplyLeading: false, title: Text( AppLocalizations.of(context).kconnect, @@ -291,7 +294,8 @@ class _DashboardScreenState extends State { Text( "${AppLocalizations.of(context).accountNumber}: ", style: TextStyle( - color: Theme.of(context).dialogBackgroundColor, + color: + Theme.of(context).dialogBackgroundColor, fontSize: 12, ), ), @@ -300,9 +304,11 @@ class _DashboardScreenState extends State { dropdownColor: Theme.of(context).primaryColor, underline: const SizedBox(), icon: const Icon(Icons.keyboard_arrow_down), - iconEnabledColor:Theme.of(context).dialogBackgroundColor, + iconEnabledColor: + Theme.of(context).dialogBackgroundColor, style: TextStyle( - color: Theme.of(context).dialogBackgroundColor, + color: + Theme.of(context).dialogBackgroundColor, fontSize: 14, ), items: List.generate(users.length, (index) { @@ -311,7 +317,8 @@ class _DashboardScreenState extends State { child: Text( users[index].accountNo ?? 'N/A', style: TextStyle( - color: Theme.of(context).dialogBackgroundColor, + color: Theme.of(context) + .dialogBackgroundColor, fontSize: 14, ), ), @@ -351,13 +358,15 @@ class _DashboardScreenState extends State { width: 20, height: 20, child: CircularProgressIndicator( - color: Theme.of(context).dialogBackgroundColor, + color: Theme.of(context) + .dialogBackgroundColor, strokeWidth: 2, ), ) - : Icon( + : Icon( Icons.refresh, - color: Theme.of(context).dialogBackgroundColor, + color: Theme.of(context) + .dialogBackgroundColor, ), onPressed: isRefreshing ? null @@ -380,7 +389,8 @@ class _DashboardScreenState extends State { Text( "тВ╣ ", style: TextStyle( - color: Theme.of(context).dialogBackgroundColor, + color: + Theme.of(context).dialogBackgroundColor, fontSize: 40, fontWeight: FontWeight.w700, ), @@ -393,7 +403,8 @@ class _DashboardScreenState extends State { '0.00' : '********', style: TextStyle( - color: Theme.of(context).dialogBackgroundColor, + color: Theme.of(context) + .dialogBackgroundColor, fontSize: 40, fontWeight: FontWeight.w700, ), @@ -423,7 +434,8 @@ class _DashboardScreenState extends State { isVisible ? Symbols.visibility_lock : Symbols.visibility, - color: Theme.of(context).scaffoldBackgroundColor, + color: Theme.of(context) + .scaffoldBackgroundColor, ), ), ], @@ -473,21 +485,21 @@ class _DashboardScreenState extends State { ); }, ), + _buildQuickLink(Symbols.send_money, + AppLocalizations.of(context).fundTransfer, () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + FundTransferBeneficiaryScreen(creditAccountNo: users[selectedAccountIndex].accountNo!, remitterName: users[selectedAccountIndex].name!))); + }, disable: false), _buildQuickLink( - Symbols.send_money, - AppLocalizations.of(context).fundTransfer, + Symbols.server_person, + AppLocalizations.of(context).accountInfo, () { Navigator.push( context, MaterialPageRoute( - builder: (context) => - const FundTransferBeneficiaryScreen())); - }, disable: true), - _buildQuickLink(Symbols.server_person, - AppLocalizations.of(context).accountInfo, () { - Navigator.push( - context, - MaterialPageRoute( builder: (context) => AccountInfoScreen( users: users, selectedIndex: selectedAccountIndex, @@ -496,39 +508,42 @@ class _DashboardScreenState extends State { ); }, ), - _buildQuickLink( - Symbols.receipt_long, - AppLocalizations.of(context).accountStatement, - () { - Navigator.push( + _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: true), - _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), + builder: (context) => + AccountStatementScreen( + accountNo: users[selectedAccountIndex] + .accountNo!, + ))); + }), + _buildQuickLink(Symbols.checkbook, + AppLocalizations.of(context).handleCheque, () {}, + disable: true), + _buildQuickLink(Icons.group, + AppLocalizations.of(context).manageBeneficiary, + () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ManageBeneficiariesScreen( + customerName: currAccount.name!))); + }, 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( @@ -599,17 +614,25 @@ class _DashboardScreenState extends State { leading: Shimmer.fromColors( baseColor: Colors.grey[300]!, highlightColor: Colors.grey[100]!, - child: CircleAvatar(radius: 12, backgroundColor: Theme.of(context).scaffoldBackgroundColor), + child: CircleAvatar( + radius: 12, + backgroundColor: Theme.of(context).scaffoldBackgroundColor), ), title: Shimmer.fromColors( baseColor: Colors.grey[300]!, highlightColor: Colors.grey[100]!, - child: Container(height: 10, width: 100, color: Theme.of(context).scaffoldBackgroundColor), + child: Container( + height: 10, + width: 100, + color: Theme.of(context).scaffoldBackgroundColor), ), subtitle: Shimmer.fromColors( baseColor: Colors.grey[300]!, highlightColor: Colors.grey[100]!, - child: Container(height: 8, width: 60, color: Theme.of(context).scaffoldBackgroundColor), + child: Container( + height: 8, + width: 60, + color: Theme.of(context).scaffoldBackgroundColor), ), ); }); diff --git a/lib/features/dashboard/widgets/account_card.dart b/lib/features/dashboard/widgets/account_card.dart index c0ac225..176daf4 100644 --- a/lib/features/dashboard/widgets/account_card.dart +++ b/lib/features/dashboard/widgets/account_card.dart @@ -13,14 +13,15 @@ class AccountCard extends StatelessWidget { 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), - ], - ), + // gradient: LinearGradient( + // begin: Alignment.topLeft, + // end: Alignment.bottomRight, + // colors: [ + // Theme.of(context).primaryColor, + // Theme.of(context).primaryColor.withBlue(200), + // ], + // ), + color: Theme.of(context).primaryColor, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( @@ -56,12 +57,13 @@ class AccountCard extends StatelessWidget { const SizedBox(height: 20), Text( account.accountNumber, - style: TextStyle(color: Theme.of(context).dialogBackgroundColor, fontSize: 16), + style: TextStyle( + color: Theme.of(context).dialogBackgroundColor, fontSize: 16), ), const SizedBox(height: 30), Text( '${account.currency} ${account.balance.toStringAsFixed(2)}', - style: TextStyle( + style: TextStyle( color: Theme.of(context).scaffoldBackgroundColor, fontSize: 22, fontWeight: FontWeight.bold, @@ -70,7 +72,8 @@ class AccountCard extends StatelessWidget { const SizedBox(height: 5), Text( AppLocalizations.of(context).availableBalance, - style: TextStyle(color: Theme.of(context).dialogBackgroundColor, fontSize: 12), + style: TextStyle( + color: Theme.of(context).dialogBackgroundColor, fontSize: 12), ), ], ), diff --git a/lib/features/fund_transfer/screens/fund_transfer_amount_screen.dart b/lib/features/fund_transfer/screens/fund_transfer_amount_screen.dart new file mode 100644 index 0000000..f3cee23 --- /dev/null +++ b/lib/features/fund_transfer/screens/fund_transfer_amount_screen.dart @@ -0,0 +1,309 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:kmobile/api/services/neft_service.dart'; +import 'package:kmobile/api/services/rtgs_service.dart'; +import 'package:kmobile/data/models/beneficiary.dart'; +import 'package:kmobile/data/models/neft_transaction.dart'; +import 'package:kmobile/data/models/payment_response.dart'; +import 'package:kmobile/data/models/rtgs_transaction.dart'; +import 'package:kmobile/di/injection.dart'; +import 'package:kmobile/features/fund_transfer/screens/payment_animation.dart'; +import 'package:kmobile/features/fund_transfer/screens/transaction_pin_screen.dart'; +import '../../../l10n/app_localizations.dart'; + +enum TransactionMode { neft, rtgs } + +class FundTransferAmountScreen extends StatefulWidget { + final String debitAccountNo; + final Beneficiary creditBeneficiary; + final String remitterName; + + const FundTransferAmountScreen({ + super.key, + required this.debitAccountNo, + required this.creditBeneficiary, + required this.remitterName, + }); + + @override + State createState() => + _FundTransferAmountScreenState(); +} + +class _FundTransferAmountScreenState extends State { + final _amountController = TextEditingController(); + final _formKey = GlobalKey(); + TransactionMode _selectedMode = TransactionMode.neft; + + @override + void dispose() { + _amountController.dispose(); + super.dispose(); + } + + Widget _getBankLogo(String? bankName) { + if (bankName != null && bankName.toLowerCase().contains('state bank of india')) { + return Image.asset( + 'assets/images/sbi_logo.png', + width: 40, + height: 40, + ); + } else { + return const Icon( + Icons.account_balance, + size: 40, + color: Colors.grey, + ); + } + } + + void _onProceed() { + if (_formKey.currentState!.validate()) { + final amount = double.tryParse(_amountController.text) ?? 0; + + if (_selectedMode == TransactionMode.rtgs && amount < 200000) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Invalid Amount for RTGS'), + content: const Text( + 'RTGS transactions require a minimum amount of 200,000. Please enter a higher amount or select NEFT as the transaction mode.'), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('OK'), + ), + ], + ), + ); + return; // Stop further execution + } + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TransactionPinScreen( + onPinCompleted: (pinScreenContext, tpin) async { + if (_selectedMode == TransactionMode.neft) { + final neftTx = NeftTransaction( + fromAccount: widget.debitAccountNo, + toAccount: widget.creditBeneficiary.accountNo, + amount: _amountController.text, + ifscCode: widget.creditBeneficiary.ifscCode, + remitterName: widget.remitterName, + beneficiaryName: widget.creditBeneficiary.name, + tpin: tpin, + ); + final neftService = getIt(); + + final completer = Completer(); + + Navigator.of(pinScreenContext).pushReplacement( + MaterialPageRoute( + builder: (_) => PaymentAnimationScreen( + paymentResponse: completer.future), + ), + ); + + try { + final neftResponse = + await neftService.processNeftTransaction(neftTx); + final paymentResponse = PaymentResponse( + isSuccess: neftResponse.message.toUpperCase() == 'SUCCESS', + date: DateTime.now(), + creditedAccount: neftTx.toAccount, + amount: neftTx.amount, + currency: 'INR', + utr: neftResponse.utr, + ); + completer.complete(paymentResponse); + } catch (e) { + final paymentResponse = PaymentResponse( + isSuccess: false, + errorMessage: e.toString(), + ); + completer.complete(paymentResponse); + } + } else { + final rtgsTx = RtgsTransaction( + fromAccount: widget.debitAccountNo, + toAccount: widget.creditBeneficiary.accountNo, + amount: _amountController.text, + ifscCode: widget.creditBeneficiary.ifscCode, + remitterName: widget.remitterName, + beneficiaryName: widget.creditBeneficiary.name, + tpin: tpin, + ); + final rtgsService = getIt(); + final completer = Completer(); + + Navigator.of(pinScreenContext).pushReplacement( + MaterialPageRoute( + builder: (_) => PaymentAnimationScreen( + paymentResponse: completer.future), + ), + ); + + try { + final rtgsResponse = + await rtgsService.processRtgsTransaction(rtgsTx); + final paymentResponse = PaymentResponse( + isSuccess: rtgsResponse.message.toUpperCase() == 'SUCCESS', + date: DateTime.now(), + creditedAccount: rtgsTx.toAccount, + amount: rtgsTx.amount, + currency: 'INR', + utr: rtgsResponse.utr, + ); + completer.complete(paymentResponse); + } catch (e) { + final paymentResponse = PaymentResponse( + isSuccess: false, + errorMessage: e.toString(), + ); + completer.complete(paymentResponse); + } + } + }, + ), + ), + ); + } + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return Scaffold( + appBar: AppBar( + title: Text(loc.fundTransfer), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Debit Account (User) + Text( + loc.debitFrom, + style: Theme.of(context).textTheme.titleSmall, + ), + Card( + elevation: 0, + margin: const EdgeInsets.symmetric(vertical: 8.0), + child: ListTile( + leading: Image.asset( + 'assets/images/logo.png', + width: 40, + height: 40, + ), + title: Text(widget.remitterName), + subtitle: Text(widget.debitAccountNo), + ), + ), + const SizedBox(height: 24), + + // Credit Account (Beneficiary) + Text( + "Credit To", + style: Theme.of(context).textTheme.titleSmall, + ), + Card( + elevation: 0, + margin: const EdgeInsets.symmetric(vertical: 8.0), + child: ListTile( + leading: _getBankLogo(widget.creditBeneficiary.bankName), + title: Text(widget.creditBeneficiary.name), + subtitle: Text(widget.creditBeneficiary.accountNo), + ), + ), + const SizedBox(height: 24), + + // Transaction Mode Selection + Text( + "Select Transaction Mode", + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 12), + Container( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + child: ToggleButtons( + isSelected: [ + _selectedMode == TransactionMode.neft, + _selectedMode == TransactionMode.rtgs + ], + onPressed: (index) { + setState(() { + _selectedMode = TransactionMode.values[index]; + }); + }, + borderRadius: BorderRadius.circular(10), + selectedColor: Theme.of(context).colorScheme.onPrimary, + fillColor: Theme.of(context).primaryColor, + color: Theme.of(context).colorScheme.onSurface, + borderColor: Colors.transparent, + selectedBorderColor: Colors.transparent, + splashColor: Theme.of(context).primaryColor.withOpacity(0.1), + highlightColor: Theme.of(context).primaryColor.withOpacity(0.05), + children: const [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), + child: Text('NEFT'), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), + child: Text('RTGS'), + ), + ], + ), + ), + const SizedBox(height: 24), + + // Amount + TextFormField( + controller: _amountController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: loc.amount, + border: const OutlineInputBorder(), + prefixIcon: const Icon(Icons.currency_rupee), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return loc.amountRequired; + } + if (double.tryParse(value) == null || + double.parse(value) <= 0) { + return loc.validAmount; + } + return null; + }, + ), + const Spacer(), + + // Proceed Button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _onProceed, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + ), + child: const Text("Proceed"), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/fund_transfer/screens/fund_transfer_beneficiary_screen.dart b/lib/features/fund_transfer/screens/fund_transfer_beneficiary_screen.dart index 50406b4..1dfb0fc 100644 --- a/lib/features/fund_transfer/screens/fund_transfer_beneficiary_screen.dart +++ b/lib/features/fund_transfer/screens/fund_transfer_beneficiary_screen.dart @@ -90,24 +90,26 @@ class _FundTransferBeneficiaryScreen import 'package:flutter/material.dart'; import 'package:kmobile/data/models/beneficiary.dart'; -//import 'package:kmobile/features/beneficiaries/screens/add_beneficiary_screen.dart'; +import 'package:kmobile/features/fund_transfer/screens/fund_transfer_amount_screen.dart'; import '../../../l10n/app_localizations.dart'; import '../../../di/injection.dart'; import 'package:kmobile/api/services/beneficiary_service.dart'; import 'package:shimmer/shimmer.dart'; - class FundTransferBeneficiaryScreen extends StatefulWidget { - const FundTransferBeneficiaryScreen({super.key}); + final String creditAccountNo; + final String remitterName; + const FundTransferBeneficiaryScreen( + {super.key, required this.creditAccountNo, required this.remitterName}); @override State createState() => - _ManageBeneficiariesScreen(); + _FundTransferBeneficiaryScreenState(); } -class _ManageBeneficiariesScreen extends State { +class _FundTransferBeneficiaryScreenState + extends State { var service = getIt(); - //final BeneficiaryService _service = BeneficiaryService(); bool _isLoading = true; List _beneficiaries = []; @@ -120,7 +122,7 @@ class _ManageBeneficiariesScreen extends State { Future _loadBeneficiaries() async { final data = await service.fetchBeneficiaryList(); setState(() { - _beneficiaries = data ; + _beneficiaries = data; _isLoading = false; }); } @@ -132,7 +134,7 @@ class _ManageBeneficiariesScreen extends State { baseColor: Colors.grey.shade300, highlightColor: Colors.grey.shade100, child: ListTile( - leading: CircleAvatar( + leading: const CircleAvatar( radius: 24, backgroundColor: Colors.white, ), @@ -151,27 +153,61 @@ class _ManageBeneficiariesScreen extends State { ); } + Widget _getBankLogo(String? bankName) { + if (bankName != null && bankName.toLowerCase().contains('state bank of india')) { + return Image.asset( + 'assets/images/sbi_logo.png', + width: 40, + height: 40, + ); + } else { + return const Icon( + Icons.account_balance, + size: 40, + color: Colors.grey, + ); + } + } + Widget _buildBeneficiaryList() { if (_beneficiaries.isEmpty) { - return Center(child: Text(AppLocalizations.of(context).noBeneficiaryFound)); + return Center( + child: Text(AppLocalizations.of(context).noBeneficiaryFound)); } return ListView.builder( itemCount: _beneficiaries.length, itemBuilder: (context, index) { - final item = _beneficiaries[index]; + final beneficiary = _beneficiaries[index]; return ListTile( leading: CircleAvatar( radius: 24, - backgroundColor: Theme.of(context).primaryColor.withOpacity(0.2), - child: Text( - item.name.isNotEmpty - ? item.name[0].toUpperCase() - : '?', - style: const TextStyle(fontWeight: FontWeight.bold), - ), + backgroundColor: Colors.transparent, + child: _getBankLogo(beneficiary.bankName), ), - title: Text(item.name ?? 'Unknown'), - subtitle: Text(item.accountNo ?? 'No account number'), + title: Text(beneficiary.name), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(beneficiary.accountNo), + if (beneficiary.bankName != null && beneficiary.bankName!.isNotEmpty) + Text( + beneficiary.bankName!, + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + ], + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => FundTransferAmountScreen( + debitAccountNo: widget.creditAccountNo, + creditBeneficiary: beneficiary, + remitterName: widget.remitterName, + ), + ), + ); + }, ); }, ); diff --git a/lib/features/fund_transfer/screens/payment_animation.dart b/lib/features/fund_transfer/screens/payment_animation.dart index 3ada32d..329d3fd 100644 --- a/lib/features/fund_transfer/screens/payment_animation.dart +++ b/lib/features/fund_transfer/screens/payment_animation.dart @@ -375,6 +375,14 @@ class _PaymentAnimationScreenState extends State { "Date: ${response.date!.toLocal().toIso8601String()}", style: const TextStyle(fontSize: 16), ), + if (response.utr != null && response.utr!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + 'UTR: ${response.utr}', + style: const TextStyle(fontSize: 16), + ), + ), ], ) : Column( diff --git a/lib/features/fund_transfer/screens/transaction_pin_screen.dart b/lib/features/fund_transfer/screens/transaction_pin_screen.dart index 8a7b204..d5170db 100644 --- a/lib/features/fund_transfer/screens/transaction_pin_screen.dart +++ b/lib/features/fund_transfer/screens/transaction_pin_screen.dart @@ -1,27 +1,24 @@ -// ignore_for_file: unused_field - -import '../../../l10n/app_localizations.dart'; - 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:material_symbols_icons/material_symbols_icons.dart'; +import '../../../l10n/app_localizations.dart'; + class TransactionPinScreen extends StatefulWidget { - final Transfer transactionData; - const TransactionPinScreen({super.key, required this.transactionData}); + final Future Function(BuildContext, String) onPinCompleted; + + const TransactionPinScreen({super.key, required this.onPinCompleted}); @override - State createState() => _TransactionPinScreen(); + State createState() => _TransactionPinScreenState(); } -class _TransactionPinScreen extends State { +class _TransactionPinScreenState extends State { final List _pin = []; bool _loading = true; + bool _isSubmitting = false; @override void initState() { @@ -55,6 +52,7 @@ class _TransactionPinScreen extends State { } void _onKeyPressed(String value) { + if (_isSubmitting) return; setState(() { if (value == 'back') { if (_pin.isNotEmpty) _pin.removeLast(); @@ -64,6 +62,27 @@ class _TransactionPinScreen extends State { }); } + Future _submitPin() async { + if (_isSubmitting) return; + + if (_pin.length == 6) { + setState(() => _isSubmitting = true); + try { + await widget.onPinCompleted(context, _pin.join()); + } finally { + if (mounted) { + setState(() => _isSubmitting = false); + } + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(AppLocalizations.of(context).enter6DigitTpin), + ), + ); + } + } + Widget _buildPinIndicators() { return Row( mainAxisAlignment: MainAxisAlignment.center, @@ -75,7 +94,9 @@ class _TransactionPinScreen extends State { decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all(color: Theme.of(context).primaryColor, width: 2), - color: index < _pin.length ? Theme.of(context).primaryColor : Colors.transparent, + color: index < _pin.length + ? Theme.of(context).primaryColor + : Colors.transparent, ), ); }), @@ -85,30 +106,15 @@ class _TransactionPinScreen extends State { 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( - SnackBar( - content: Text(AppLocalizations.of(context).enter6DigitTpin), - ), - ); - } - } else { - _onKeyPressed(label); - } - }, + onTap: () => label == 'done' ? _submitPin() : _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)), + child: _isSubmitting && label == 'done' + ? const CircularProgressIndicator() + : icon != null + ? Icon(icon, size: 28) + : Text(label, style: const TextStyle(fontSize: 24)), ), ), ), @@ -133,30 +139,6 @@ class _TransactionPinScreen extends State { ); } - Future sendTransaction() async { - final paymentService = getIt(); - 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( @@ -169,26 +151,28 @@ class _TransactionPinScreen extends State { ), title: Text( AppLocalizations.of(context).tpin, - style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500), + style: const TextStyle(color: Colors.black, fontWeight: FontWeight.w500), ), centerTitle: false, ), - body: Padding( - padding: const EdgeInsets.only(bottom: 20.0), - child: Column( - children: [ - const Spacer(), - Text( - AppLocalizations.of(context).enterTpin, - style: TextStyle(fontSize: 18), + body: _loading + ? const Center(child: CircularProgressIndicator()) + : Padding( + padding: const EdgeInsets.only(bottom: 20.0), + child: Column( + children: [ + const Spacer(), + Text( + AppLocalizations.of(context).enterTpin, + style: const TextStyle(fontSize: 18), + ), + const SizedBox(height: 20), + _buildPinIndicators(), + const Spacer(), + _buildKeypad(), + ], + ), ), - const SizedBox(height: 20), - _buildPinIndicators(), - const Spacer(), - _buildKeypad(), - ], - ), - ), ); } } diff --git a/lib/features/profile/preferences/color_theme_dialog.dart b/lib/features/profile/preferences/color_theme_dialog.dart index 53970e0..aa5d60a 100644 --- a/lib/features/profile/preferences/color_theme_dialog.dart +++ b/lib/features/profile/preferences/color_theme_dialog.dart @@ -20,22 +20,22 @@ class ColorThemeDialog extends StatelessWidget { Navigator.pop(context); }, ), - // ListTile( - // leading: const CircleAvatar(backgroundColor: Colors.green), - // title: const Text('Green'), - // onTap: () { - // context.read().changeTheme(ThemeType.green); - // Navigator.pop(context); - // }, - // ), - // ListTile( - // leading: const CircleAvatar(backgroundColor: Colors.orange), - // title: const Text('Orange'), - // onTap: () { - // context.read().changeTheme(ThemeType.orange); - // Navigator.pop(context); - // }, - // ), + ListTile( + leading: const CircleAvatar(backgroundColor: Colors.green), + title: const Text('Green'), + onTap: () { + context.read().changeTheme(ThemeType.green); + Navigator.pop(context); + }, + ), + ListTile( + leading: const CircleAvatar(backgroundColor: Colors.orange), + title: const Text('Orange'), + onTap: () { + context.read().changeTheme(ThemeType.orange); + Navigator.pop(context); + }, + ), ListTile( leading: const CircleAvatar(backgroundColor: Colors.blue), title: Text(AppLocalizations.of(context).blue), @@ -48,4 +48,3 @@ class ColorThemeDialog extends StatelessWidget { ); } } - diff --git a/lib/features/quick_pay/screens/quick_pay_outside_bank_screen.dart b/lib/features/quick_pay/screens/quick_pay_outside_bank_screen.dart index 0545423..2c193be 100644 --- a/lib/features/quick_pay/screens/quick_pay_outside_bank_screen.dart +++ b/lib/features/quick_pay/screens/quick_pay_outside_bank_screen.dart @@ -1,9 +1,19 @@ +import 'dart:async'; +import 'package:dio/dio.dart'; +import 'package:kmobile/api/services/neft_service.dart'; +import 'package:kmobile/api/services/rtgs_service.dart'; +import 'package:kmobile/data/models/neft_transaction.dart'; +import 'package:kmobile/data/models/payment_response.dart'; +import 'package:kmobile/data/models/rtgs_transaction.dart'; +import 'package:kmobile/features/fund_transfer/screens/payment_animation.dart'; +import 'package:kmobile/features/fund_transfer/screens/transaction_pin_screen.dart'; import '../../../l10n/app_localizations.dart'; - 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'; +import '../../../../di/injection.dart'; +import '../../../../api/services/beneficiary_service.dart'; class QuickPayOutsideBankScreen extends StatefulWidget { final String debitAccount; @@ -26,9 +36,12 @@ class _QuickPayOutsideBankScreen extends State { final ifscController = TextEditingController(); final phoneController = TextEditingController(); final amountController = TextEditingController(); + final service = getIt(); - //String accountType = 'Savings'; late String accountType; + bool _isValidating = false; + bool _isBeneficiaryValidated = false; + String? _validationError; @override void initState() { @@ -40,12 +53,71 @@ class _QuickPayOutsideBankScreen extends State { }); } - //final List transactionModes = ['NEFT', 'RTGS', 'IMPS']; + void _validateIFSC() async { + final ifsc = ifscController.text.trim().toUpperCase(); + if (ifsc.isEmpty) return; + + final result = await service.validateIFSC(ifsc); + + if (mounted) { + if (result.bankName == '') { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(AppLocalizations.of(context).invalidIfsc)), + ); + bankNameController.clear(); + branchNameController.clear(); + } else { + bankNameController.text = result.bankName; + branchNameController.text = result.branchName; + } + } + } + + void _validateBeneficiary() async { + FocusScope.of(context).unfocus(); + setState(() { + _isValidating = true; + _validationError = null; + _isBeneficiaryValidated = false; + nameController.text = ''; // clear previous name + }); + + final String accountNo = accountNumberController.text.trim(); + final String ifsc = ifscController.text.trim(); + // TODO: Replace with actual remitter name + final String remitter = "Unknown"; + + final service = getIt(); + try { + final String beneficiaryName = await service.validateBeneficiary( + accountNo: accountNo, + ifscCode: ifsc, + remitterName: remitter, + ); + + setState(() { + nameController.text = beneficiaryName; + _isBeneficiaryValidated = true; + _validationError = null; + }); + } catch (e) { + setState(() { + _validationError = e.toString(); + _isBeneficiaryValidated = false; + }); + } finally { + if (mounted) { + setState(() { + _isValidating = false; + }); + } + } + } + List transactionModes(BuildContext context) { return [ AppLocalizations.of(context).neft, AppLocalizations.of(context).rtgs, - AppLocalizations.of(context).imps, ]; } @@ -65,6 +137,131 @@ class _QuickPayOutsideBankScreen extends State { super.dispose(); } + void _onProceedToPay() { + if (_formKey.currentState!.validate()) { + if (!_isBeneficiaryValidated) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please validate beneficiary details first.')), + ); + return; + } + + final amount = double.tryParse(amountController.text) ?? 0; + final selectedMode = transactionModes(context)[selectedTransactionIndex]; + final isRtgs = selectedMode == AppLocalizations.of(context).rtgs; + + if (isRtgs && amount < 200000) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Invalid Amount for RTGS'), + content: const Text( + 'RTGS transactions require a minimum amount of 200,000. Please enter a higher amount or select NEFT as the transaction mode.'), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('OK'), + ), + ], + ), + ); + return; + } + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TransactionPinScreen( + onPinCompleted: (pinScreenContext, tpin) async { + if (!isRtgs) { + // NEFT + final neftTx = NeftTransaction( + fromAccount: widget.debitAccount, + toAccount: accountNumberController.text, + amount: amountController.text, + ifscCode: ifscController.text, + remitterName: "Unknown", // TODO: Get actual remitter name + beneficiaryName: nameController.text, + tpin: tpin, + ); + final neftService = getIt(); + final completer = Completer(); + + Navigator.of(pinScreenContext).pushReplacement( + MaterialPageRoute( + builder: (_) => + PaymentAnimationScreen(paymentResponse: completer.future), + ), + ); + + try { + final neftResponse = + await neftService.processNeftTransaction(neftTx); + final paymentResponse = PaymentResponse( + isSuccess: neftResponse.message.toUpperCase() == 'SUCCESS', + date: DateTime.now(), + creditedAccount: neftTx.toAccount, + amount: neftTx.amount, + currency: 'INR', + utr: neftResponse.utr, + ); + completer.complete(paymentResponse); + } catch (e) { + final paymentResponse = PaymentResponse( + isSuccess: false, + errorMessage: e.toString(), + ); + completer.complete(paymentResponse); + } + } else { + // RTGS + final rtgsTx = RtgsTransaction( + fromAccount: widget.debitAccount, + toAccount: accountNumberController.text, + amount: amountController.text, + ifscCode: ifscController.text, + remitterName: "Unknown", // TODO: Get actual remitter name + beneficiaryName: nameController.text, + tpin: tpin, + ); + final rtgsService = getIt(); + final completer = Completer(); + + Navigator.of(pinScreenContext).pushReplacement( + MaterialPageRoute( + builder: (_) => + PaymentAnimationScreen(paymentResponse: completer.future), + ), + ); + + try { + final rtgsResponse = + await rtgsService.processRtgsTransaction(rtgsTx); + final paymentResponse = PaymentResponse( + isSuccess: rtgsResponse.message.toUpperCase() == 'SUCCESS', + date: DateTime.now(), + creditedAccount: rtgsTx.toAccount, + amount: rtgsTx.amount, + currency: 'INR', + utr: rtgsResponse.utr, + ); + completer.complete(paymentResponse); + } catch (e) { + final paymentResponse = PaymentResponse( + isSuccess: false, + errorMessage: e.toString(), + ); + completer.complete(paymentResponse); + } + } + }, + ), + ), + ); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -103,19 +300,24 @@ class _QuickPayOutsideBankScreen extends State { child: ListView( children: [ const SizedBox(height: 10), - Row( - children: [ - Text(AppLocalizations.of(context).debitFrom), - Text( - widget.debitAccount, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - ), - ), - ], + Text( + AppLocalizations.of(context).debitFrom, + style: Theme.of(context).textTheme.titleSmall, ), - const SizedBox(height: 20), + Card( + elevation: 0, + margin: const EdgeInsets.symmetric(vertical: 8.0), + child: ListTile( + leading: Image.asset( + 'assets/images/logo.png', + width: 40, + height: 40, + ), + title: Text(widget.debitAccount), + subtitle: Text(AppLocalizations.of(context).ownBank), + ), + ), + const SizedBox(height: 24), TextFormField( decoration: InputDecoration( labelText: AppLocalizations.of(context).accountNumber, @@ -134,11 +336,17 @@ class _QuickPayOutsideBankScreen extends State { keyboardType: TextInputType.number, obscureText: true, textInputAction: TextInputAction.next, + onChanged: (value) { + nameController.clear(); + setState(() { + _isBeneficiaryValidated = false; + }); + }, validator: (value) { if (value == null || value.isEmpty) { return AppLocalizations.of(context).accountNumberRequired; - } else if (value.length != 11) { - return AppLocalizations.of(context).validAccountNumber; + } else if (value.length < 7 || value.length > 20) { + return 'Account number must be between 7 and 20 digits'; } return null; }, @@ -173,79 +381,6 @@ class _QuickPayOutsideBankScreen extends State { }, ), const SizedBox(height: 25), - TextFormField( - decoration: InputDecoration( - labelText: AppLocalizations.of(context).name, - border: OutlineInputBorder(), - isDense: true, - filled: true, - fillColor: Theme.of(context).scaffoldBackgroundColor, - 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 AppLocalizations.of(context).nameRequired; - } - return null; - }, - ), - const SizedBox(height: 25), - TextFormField( - decoration: InputDecoration( - labelText: AppLocalizations.of(context).bankName, - border: OutlineInputBorder(), - isDense: true, - filled: true, - fillColor: Theme.of(context).scaffoldBackgroundColor, - 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 AppLocalizations.of(context).bankNameRequired; - } - return null; - }, - ), - const SizedBox(height: 25), - TextFormField( - decoration: InputDecoration( - labelText: AppLocalizations.of(context).branchName, - border: OutlineInputBorder(), - isDense: true, - filled: true, - fillColor: Theme.of(context).scaffoldBackgroundColor, - 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 AppLocalizations.of(context).branchNameRequired; - } - return null; - }, - ), - const SizedBox(height: 25), Row( children: [ Expanded( @@ -265,9 +400,27 @@ class _QuickPayOutsideBankScreen extends State { ), controller: ifscController, 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) { - if (value == null || value.isEmpty) { - return AppLocalizations.of(context).ifscRequired; + 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; }, @@ -307,6 +460,113 @@ class _QuickPayOutsideBankScreen extends State { ), ], ), + + const SizedBox(height: 25), + TextFormField( + controller: bankNameController, + enabled: false, + decoration: InputDecoration( + labelText: AppLocalizations.of(context).bankName, + border: const OutlineInputBorder(), + isDense: true, + filled: true, + fillColor: Theme.of(context).dialogBackgroundColor, + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.black), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.black, + width: 2, + ), + ), + ), + ), + const SizedBox(height: 25), + TextFormField( + controller: branchNameController, + enabled: false, + decoration: InputDecoration( + labelText: AppLocalizations.of(context).branchName, + border: const OutlineInputBorder(), + isDense: true, + filled: true, + fillColor: Theme.of(context).dialogBackgroundColor, + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.black), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.black, + width: 2, + ), + ), + ), + ), + const SizedBox(height: 24), + if (!_isBeneficiaryValidated) + Padding( + padding: const EdgeInsets.only(bottom: 24), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _isValidating + ? null + : () { + if (confirmAccountNumberController + .text == + accountNumberController.text) { + _validateBeneficiary(); + } else { + setState(() { + _validationError = + 'Account numbers do not match.'; + }); + } + }, + child: _isValidating + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2), + ) + : const Text('Validate Beneficiary'), + ), + ), + ), + if (_validationError != null) + Padding( + padding: const EdgeInsets.only(bottom: 24.0), + child: Text( + _validationError!, + style: TextStyle(color: Colors.red), + ), + ), + TextFormField( + controller: nameController, + enabled: false, + decoration: InputDecoration( + labelText: AppLocalizations.of(context).name, + border: const OutlineInputBorder(), + isDense: true, + filled: true, + fillColor: Theme.of(context).dialogBackgroundColor, + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.black), + ), + focusedBorder: const OutlineInputBorder( + borderSide: + BorderSide(color: Colors.black, width: 2), + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return AppLocalizations.of(context).nameRequired; + } + return null; + }, + ), const SizedBox(height: 25), Row( children: [ @@ -382,38 +642,16 @@ class _QuickPayOutsideBankScreen extends State { Align( alignment: Alignment.center, child: SwipeButton.expand( - thumb: Icon(Icons.arrow_forward, color: Theme.of(context).scaffoldBackgroundColor), - activeThumbColor: Theme.of(context).primaryColorDark, - activeTrackColor: Theme.of(context).primaryColorLight, + thumb: Icon(Icons.arrow_forward, color: Theme.of(context).dialogBackgroundColor), + activeThumbColor: Theme.of(context).primaryColor, + activeTrackColor: Theme.of(context).colorScheme.secondary.withAlpha(100), borderRadius: BorderRadius.circular(30), height: 56, + onSwipe: _onProceedToPay, child: Text( AppLocalizations.of(context).swipeToPay, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), - onSwipe: () { - if (_formKey.currentState!.validate()) { - // Perform payment logic - final selectedMode = transactionModes( - context, - )[selectedTransactionIndex]; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - '${AppLocalizations.of(context).payingVia} $selectedMode...', - ), - ), - ); - // Navigator.push( - // context, - // MaterialPageRoute( - // builder: (context) => - // const TransactionPinScreen( - // transactionData: {}, - // transactionCode: 'PAYMENT', - // ))); - } - }, ), ), ], @@ -457,4 +695,4 @@ class _QuickPayOutsideBankScreen extends State { ), ); } -} +} \ No newline at end of file diff --git a/lib/features/quick_pay/screens/quick_pay_screen.dart b/lib/features/quick_pay/screens/quick_pay_screen.dart index d8af4b5..0035a3f 100644 --- a/lib/features/quick_pay/screens/quick_pay_screen.dart +++ b/lib/features/quick_pay/screens/quick_pay_screen.dart @@ -64,7 +64,6 @@ class _QuickPayScreen extends State { ), const Divider(height: 1), QuickPayManagementTile( - disable: true, icon: Symbols.output_circle, label: AppLocalizations.of(context).outsideBank, onTap: () { diff --git a/lib/features/quick_pay/screens/quick_pay_within_bank_screen.dart b/lib/features/quick_pay/screens/quick_pay_within_bank_screen.dart index 7b5ea79..d4b3b8e 100644 --- a/lib/features/quick_pay/screens/quick_pay_within_bank_screen.dart +++ b/lib/features/quick_pay/screens/quick_pay_within_bank_screen.dart @@ -1,9 +1,13 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:flutter_swipe_button/flutter_swipe_button.dart'; import 'package:kmobile/api/services/beneficiary_service.dart'; +import 'package:kmobile/api/services/payment_service.dart'; +import 'package:kmobile/data/models/payment_response.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:material_symbols_icons/material_symbols_icons.dart'; import '../../../l10n/app_localizations.dart'; import '../../fund_transfer/screens/transaction_pin_screen.dart'; @@ -363,12 +367,26 @@ class _QuickPayWithinBankScreen extends State { context, MaterialPageRoute( builder: (context) => TransactionPinScreen( - transactionData: Transfer( - fromAccount: widget.debitAccount, - toAccount: accountNumberController.text, - toAccountType: _selectedAccountType!, - amount: amountController.text, - ), + onPinCompleted: (pinScreenContext, tpin) async { + final transfer = Transfer( + fromAccount: widget.debitAccount, + toAccount: accountNumberController.text, + toAccountType: _selectedAccountType!, + amount: amountController.text, + tpin: tpin, + ); + + final paymentService = getIt(); + final paymentResponseFuture = paymentService + .processQuickPayWithinBank(transfer); + + Navigator.of(pinScreenContext).pushReplacement( + MaterialPageRoute( + builder: (_) => PaymentAnimationScreen( + paymentResponse: paymentResponseFuture), + ), + ); + }, ), ), ); diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 076b262..41c35a1 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -62,7 +62,8 @@ import 'app_localizations_hi.dart'; /// be consistent with the languages listed in the AppLocalizations.supportedLocales /// property. abstract class AppLocalizations { - AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); final String localeName; @@ -70,7 +71,8 @@ abstract class AppLocalizations { return Localizations.of(context, AppLocalizations)!; } - static const LocalizationsDelegate delegate = _AppLocalizationsDelegate(); + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); /// A list of this localizations delegate along with the default localizations /// delegates. @@ -82,7 +84,8 @@ abstract class AppLocalizations { /// Additional delegates can be added by appending to this list in /// MaterialApp. This list does not have to be used at all if a custom list /// of delegates is preferred or required. - static const List> localizationsDelegates = >[ + static const List> localizationsDelegates = + >[ delegate, GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, @@ -1404,7 +1407,8 @@ abstract class AppLocalizations { String get beneficiaryName; } -class _AppLocalizationsDelegate extends LocalizationsDelegate { +class _AppLocalizationsDelegate + extends LocalizationsDelegate { const _AppLocalizationsDelegate(); @override @@ -1413,25 +1417,25 @@ class _AppLocalizationsDelegate extends LocalizationsDelegate } @override - bool isSupported(Locale locale) => ['en', 'hi'].contains(locale.languageCode); + bool isSupported(Locale locale) => + ['en', 'hi'].contains(locale.languageCode); @override bool shouldReload(_AppLocalizationsDelegate old) => false; } AppLocalizations lookupAppLocalizations(Locale locale) { - - // Lookup logic when only language code is specified. switch (locale.languageCode) { - case 'en': return AppLocalizationsEn(); - case 'hi': return AppLocalizationsHi(); + case 'en': + return AppLocalizationsEn(); + case 'hi': + return AppLocalizationsHi(); } throw FlutterError( - 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' - 'an issue with the localizations generation tool. Please file an issue ' - 'on GitHub with a reproducible sample app and the gen-l10n configuration ' - 'that was used.' - ); + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.'); } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index b268fbe..39652bc 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -486,7 +488,8 @@ class AppLocalizationsEn extends AppLocalizations { String get otpVerification => 'OTP Verification'; @override - String get otpSentMessage => 'Enter the 4-digit OTP sent to your mobile number'; + String get otpSentMessage => + 'Enter the 4-digit OTP sent to your mobile number'; @override String get verifyOtp => 'Verify OTP'; @@ -504,13 +507,15 @@ class AppLocalizationsEn extends AppLocalizations { String get tpinRequired => 'TPIN Required'; @override - String get tpinRequiredMessage => 'You need to set your TPIN to continue with secure transactions'; + String get tpinRequiredMessage => + 'You need to set your TPIN to continue with secure transactions'; @override String get setTpinTitle => 'Set TPIN'; @override - String get tpinInfo => 'Your TPIN is a 6-digit code used to authorize transactions. Keep it safe and do not share it with anyone.'; + String get tpinInfo => + 'Your TPIN is a 6-digit code used to authorize transactions. Keep it safe and do not share it with anyone.'; @override String get tpinFailed => 'Failed to set TPIN. Please try again.'; @@ -564,7 +569,8 @@ class AppLocalizationsEn extends AppLocalizations { String get enableFingerprintLogin => 'Enable Fingerprint Login?'; @override - String get enableFingerprintMessage => 'Would you like to enable fingerprint authentication for faster login?'; + String get enableFingerprintMessage => + 'Would you like to enable fingerprint authentication for faster login?'; @override String get no => 'No'; @@ -585,7 +591,8 @@ class AppLocalizationsEn extends AppLocalizations { String get loading => 'Loading......'; @override - String get enableFingerprintQuick => 'Enable fingerprint authentication for quick login?'; + String get enableFingerprintQuick => + 'Enable fingerprint authentication for quick login?'; @override String get kccb => 'KCCB'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index a19fc95..1ee9946 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -52,7 +54,8 @@ class AppLocalizationsHi extends AppLocalizations { String get enableBiometric => 'рдмрд╛рдпреЛрдореЗрдЯреНрд░рд┐рдХ рдкреНрд░рдорд╛рдгреАрдХрд░рдг рд╕рдХреНрд╖рдо рдХрд░реЗрдВ'; @override - String get useBiometricPrompt => 'рддреЗрдЬрд╝ рд▓реЙрдЧрд┐рди рдХреЗ рд▓рд┐рдП рдлрд┐рдВрдЧрд░рдкреНрд░рд┐рдВрдЯ/рдлреЗрд╕ рдЖрдИрдбреА рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░реЗрдВ?'; + String get useBiometricPrompt => + 'рддреЗрдЬрд╝ рд▓реЙрдЧрд┐рди рдХреЗ рд▓рд┐рдП рдлрд┐рдВрдЧрд░рдкреНрд░рд┐рдВрдЯ/рдлреЗрд╕ рдЖрдИрдбреА рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░реЗрдВ?'; @override String get later => 'рдмрд╛рдж рдореЗрдВ'; @@ -486,7 +489,8 @@ class AppLocalizationsHi extends AppLocalizations { String get otpVerification => 'рдУрдЯреАрдкреА рд╕рддреНрдпрд╛рдкрди'; @override - String get otpSentMessage => 'рдЕрдкрдиреЗ рдореЛрдмрд╛рдЗрд▓ рдирдВрдмрд░ рдкрд░ рднреЗрдЬрд╛ рдЧрдпрд╛ 4-рдЕрдВрдХреЛрдВ рдХрд╛ рдУрдЯреАрдкреА рджрд░реНрдЬ рдХрд░реЗрдВ'; + String get otpSentMessage => + 'рдЕрдкрдиреЗ рдореЛрдмрд╛рдЗрд▓ рдирдВрдмрд░ рдкрд░ рднреЗрдЬрд╛ рдЧрдпрд╛ 4-рдЕрдВрдХреЛрдВ рдХрд╛ рдУрдЯреАрдкреА рджрд░реНрдЬ рдХрд░реЗрдВ'; @override String get verifyOtp => 'рдУрдЯреАрдкреА рд╕рддреНрдпрд╛рдкрд┐рдд рдХрд░реЗрдВ'; @@ -504,13 +508,15 @@ class AppLocalizationsHi extends AppLocalizations { String get tpinRequired => 'рдЯреА-рдкрд┐рди рдЖрд╡рд╢реНрдпрдХ рд╣реИ'; @override - String get tpinRequiredMessage => 'рд╕реБрд░рдХреНрд╖рд┐рдд рд▓реЗрдирджреЗрди рдХреЗ рд▓рд┐рдП рдЯреА-рдкрд┐рди рд╕реЗрдЯ рдХрд░рдирд╛ рдЖрд╡рд╢реНрдпрдХ рд╣реИ'; + String get tpinRequiredMessage => + 'рд╕реБрд░рдХреНрд╖рд┐рдд рд▓реЗрдирджреЗрди рдХреЗ рд▓рд┐рдП рдЯреА-рдкрд┐рди рд╕реЗрдЯ рдХрд░рдирд╛ рдЖрд╡рд╢реНрдпрдХ рд╣реИ'; @override String get setTpinTitle => 'рдЯреА-рдкрд┐рди рд╕реЗрдЯ рдХрд░реЗрдВ'; @override - String get tpinInfo => 'рдЖрдкрдХрд╛ рдЯреА-рдкрд┐рди 6 рдЕрдВрдХреЛрдВ рдХрд╛ рдХреЛрдб рд╣реИ рдЬрд┐рд╕рдХрд╛ рдЙрдкрдпреЛрдЧ рд▓реЗрди-рджреЗрди рдХреЛ рдкреНрд░рдорд╛рдгрд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИред рдЗрд╕реЗ рд╕реБрд░рдХреНрд╖рд┐рдд рд░рдЦреЗрдВ рдФрд░ рдХрд┐рд╕реА рд╕реЗ рд╕рд╛рдЭрд╛ рди рдХрд░реЗрдВред'; + String get tpinInfo => + 'рдЖрдкрдХрд╛ рдЯреА-рдкрд┐рди 6 рдЕрдВрдХреЛрдВ рдХрд╛ рдХреЛрдб рд╣реИ рдЬрд┐рд╕рдХрд╛ рдЙрдкрдпреЛрдЧ рд▓реЗрди-рджреЗрди рдХреЛ рдкреНрд░рдорд╛рдгрд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИред рдЗрд╕реЗ рд╕реБрд░рдХреНрд╖рд┐рдд рд░рдЦреЗрдВ рдФрд░ рдХрд┐рд╕реА рд╕реЗ рд╕рд╛рдЭрд╛ рди рдХрд░реЗрдВред'; @override String get tpinFailed => 'рдЯреА-рдкрд┐рди рд╕реЗрдЯ рдХрд░рдиреЗ рдореЗрдВ рд╡рд┐рдлрд▓ред рдХреГрдкрдпрд╛ рдкреБрдирдГ рдкреНрд░рдпрд╛рд╕ рдХрд░реЗрдВред'; @@ -564,7 +570,8 @@ class AppLocalizationsHi extends AppLocalizations { String get enableFingerprintLogin => 'рдлрд┐рдВрдЧрд░рдкреНрд░рд┐рдВрдЯ рд▓реЙрдЧрд┐рди рд╕рдХреНрд╖рдо рдХрд░реЗрдВ?'; @override - String get enableFingerprintMessage => 'рдХреНрдпрд╛ рдЖрдк рддреЗрдЬ рд▓реЙрдЧрд┐рди рдХреЗ рд▓рд┐рдП рдлрд┐рдВрдЧрд░рдкреНрд░рд┐рдВрдЯ рдкреНрд░рдорд╛рдгреАрдХрд░рдг рд╕рдХреНрд╖рдо рдХрд░рдирд╛ рдЪрд╛рд╣реЗрдВрдЧреЗ?'; + String get enableFingerprintMessage => + 'рдХреНрдпрд╛ рдЖрдк рддреЗрдЬ рд▓реЙрдЧрд┐рди рдХреЗ рд▓рд┐рдП рдлрд┐рдВрдЧрд░рдкреНрд░рд┐рдВрдЯ рдкреНрд░рдорд╛рдгреАрдХрд░рдг рд╕рдХреНрд╖рдо рдХрд░рдирд╛ рдЪрд╛рд╣реЗрдВрдЧреЗ?'; @override String get no => 'рдирд╣реАрдВ'; @@ -573,7 +580,8 @@ class AppLocalizationsHi extends AppLocalizations { String get yes => 'рд╣рд╛рдБ'; @override - String get authenticateToEnable => 'рдлрд┐рдВрдЧрд░рдкреНрд░рд┐рдВрдЯ рд▓реЙрдЧрд┐рди рд╕рдХреНрд╖рдо рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдкреНрд░рдорд╛рдгреАрдХрд░рдг рдХрд░реЗрдВ'; + String get authenticateToEnable => + 'рдлрд┐рдВрдЧрд░рдкреНрд░рд┐рдВрдЯ рд▓реЙрдЧрд┐рди рд╕рдХреНрд╖рдо рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдкреНрд░рдорд╛рдгреАрдХрд░рдг рдХрд░реЗрдВ'; @override String get exitApp => 'рдРрдк рдмрдВрдж рдХрд░реЗрдВ'; @@ -585,7 +593,8 @@ class AppLocalizationsHi extends AppLocalizations { String get loading => 'рд▓реЛрдб рд╣реЛ рд░рд╣рд╛ рд╣реИ......'; @override - String get enableFingerprintQuick => 'рддреЗрдЬрд╝ рд▓реЙрдЧрд┐рди рдХреЗ рд▓рд┐рдП рдлрд┐рдВрдЧрд░рдкреНрд░рд┐рдВрдЯ рдкреНрд░рдорд╛рдгреАрдХрд░рдг рд╕рдХреНрд╖рдо рдХрд░реЗрдВ?'; + String get enableFingerprintQuick => + 'рддреЗрдЬрд╝ рд▓реЙрдЧрд┐рди рдХреЗ рд▓рд┐рдП рдлрд┐рдВрдЧрд░рдкреНрд░рд┐рдВрдЯ рдкреНрд░рдорд╛рдгреАрдХрд░рдг рд╕рдХреНрд╖рдо рдХрд░реЗрдВ?'; @override String get kccb => 'рдХреЗрд╕реАрд╕реАрдмреА'; diff --git a/pubspec.lock b/pubspec.lock index 832df6d..4c68879 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" bloc: dependency: "direct main" description: @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" chalkdart: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -77,18 +77,18 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" confetti: dependency: "direct main" description: @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" ffi: dependency: transitive description: @@ -353,10 +353,10 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" js: dependency: transitive description: @@ -377,18 +377,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -457,10 +457,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -481,10 +481,10 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: transitive description: @@ -505,10 +505,10 @@ packages: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_parsing: dependency: transitive description: @@ -689,15 +689,15 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" sprintf: dependency: transitive description: @@ -710,42 +710,42 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.4" typed_data: dependency: transitive description: @@ -862,10 +862,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "15.0.0" web: dependency: transitive description: @@ -907,5 +907,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index cc61e0a..960f3d6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -91,6 +91,7 @@ flutter: - assets/images/kccb_logo.svg - assets/images/avatar.jpg - assets/images/logo.png + - assets/images/sbi_logo.png - assets/images/kmobile_splash.jpg - assets/images/kmobile_splash2.png - assets/images/icon.svg @@ -127,4 +128,4 @@ flutter: flutter_icons: android: true ios: false - image_path: assets/images/icon.png \ No newline at end of file + image_path: assets/images/icon.png