diff --git a/lib/data/models/beneficiary.dart b/lib/data/models/beneficiary.dart index a674fb1..425e03f 100644 --- a/lib/data/models/beneficiary.dart +++ b/lib/data/models/beneficiary.dart @@ -1,7 +1,10 @@ +import 'dart:convert'; + class Beneficiary { final String accountNo; final String accountType; final String name; + final DateTime? createdAt; final String ifscCode; final String? bankName; final String? branchName; @@ -11,6 +14,7 @@ class Beneficiary { required this.accountNo, required this.accountType, required this.name, + this.createdAt, required this.ifscCode, this.bankName, this.branchName, @@ -21,6 +25,7 @@ class Beneficiary { return Beneficiary( accountNo: json['account_no'] ?? json['accountNo'] ?? '', accountType: json['account_type'] ?? json['accountType'] ?? '', + createdAt: json['createdAt'] == null ? null : DateTime.tryParse(json['createdAt']), name: json['name'] ?? '', ifscCode: json['ifsc_code'] ?? json['ifscCode'] ?? '', bankName: json['bank_name'] ?? json['bankName'] ?? '', diff --git a/lib/features/fund_transfer/screens/cooldown.dart b/lib/features/fund_transfer/screens/cooldown.dart new file mode 100644 index 0000000..ff1fc47 --- /dev/null +++ b/lib/features/fund_transfer/screens/cooldown.dart @@ -0,0 +1,90 @@ + import 'dart:async'; + import 'package:flutter/material.dart'; + + class CooldownTimer extends StatefulWidget { + final DateTime createdAt; + final VoidCallback onTimerFinish; + + const CooldownTimer({ + Key? key, + required this.createdAt, + required this.onTimerFinish, + }) : super(key: key); + + @override + _CooldownTimerState createState() => _CooldownTimerState(); + } + + class _CooldownTimerState extends State { + late Timer _timer; + late Duration _timeRemaining; + + @override + void initState() { + super.initState(); + _updateRemainingTime(); + // Update the timer every second + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + _updateRemainingTime(); + }); + } + + void _updateRemainingTime() { + final cooldownEnd = widget.createdAt.add(const Duration(minutes: 60)); + final now = DateTime.now(); + + if (now.isAfter(cooldownEnd)) { + _timeRemaining = Duration.zero; + _timer.cancel(); + // Notify the parent widget that the timer is done + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.onTimerFinish(); + }); + } else { + _timeRemaining = cooldownEnd.difference(now); + } + // Trigger a rebuild + setState(() {}); + } + + @override + void dispose() { + _timer.cancel(); + super.dispose(); + } + + String _formatDuration(Duration duration) { + final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0'); + final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0'); + return '$minutes:$seconds'; + } + + @override + Widget build(BuildContext context) { + if (_timeRemaining == Duration.zero) { + return const SizedBox.shrink(); // Or some other widget indicating it's enabled + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + 'Enabled after:', + style: TextStyle( + color: Colors.red.shade700, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + Text( + _formatDuration(_timeRemaining), + style: TextStyle( + color: Colors.red.shade700, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ], + ); + } + } 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 f958f4b..9bd01c7 100644 --- a/lib/features/fund_transfer/screens/fund_transfer_beneficiary_screen.dart +++ b/lib/features/fund_transfer/screens/fund_transfer_beneficiary_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:kmobile/features/fund_transfer/screens/cooldown.dart'; import 'package:kmobile/widgets/bank_logos.dart'; import 'package:kmobile/data/models/beneficiary.dart'; import 'package:kmobile/features/fund_transfer/screens/fund_transfer_amount_screen.dart'; @@ -72,7 +73,8 @@ class _FundTransferBeneficiaryScreenState ); } - Widget _buildBeneficiaryList() { + + Widget _buildBeneficiaryList() { if (_beneficiaries.isEmpty) { return Center( child: Text(AppLocalizations.of(context).noBeneficiaryFound)); @@ -81,43 +83,79 @@ class _FundTransferBeneficiaryScreenState itemCount: _beneficiaries.length, itemBuilder: (context, index) { final beneficiary = _beneficiaries[index]; - return ListTile( - leading: CircleAvatar( - radius: 24, - backgroundColor: Colors.transparent, - child: getBankLogo(beneficiary.bankName, context), + + // --- Cooldown Logic --- + bool isCoolingDown = false; + if (beneficiary.createdAt != null) { + final sixtyMinutesAgo = + DateTime.now().subtract(const Duration(minutes: 60)); + isCoolingDown = beneficiary.createdAt!.isAfter(sixtyMinutesAgo); + } + // --- End of Cooldown Logic --- + + // By wrapping the ListTile in an Opacity widget, we can make it look + // disabled while ensuring the onTap callback still works. + return Opacity( + opacity: isCoolingDown ? 0.5 : 1.0, + child: ListTile( + // REMOVED the 'enabled' property from here. + leading: CircleAvatar( + radius: 24, + backgroundColor: Colors.transparent, + child: getBankLogo(beneficiary.bankName, context), + ), + 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]), + ), + ], + ), + trailing: isCoolingDown + ? CooldownTimer( + createdAt: beneficiary.createdAt!, + onTimerFinish: () { + setState(() {}); + }, + ) + : null, + onTap: () { + if (isCoolingDown) { + // This will now execute correctly on tap + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Beneficiary will be enabled after the cooldown period.'), + behavior: SnackBarBehavior.floating, + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => FundTransferAmountScreen( + debitAccountNo: widget.creditAccountNo, + creditBeneficiary: beneficiary, + remitterName: widget.remitterName, + isOwnBank: widget.isOwnBank, + ), + ), + ); + } + }, ), - 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, - isOwnBank: widget.isOwnBank, - ), - ), - ); - }, ); }, ); } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/lib/widgets/tnc_dialog.dart b/lib/widgets/tnc_dialog.dart index 9a24862..9584466 100644 --- a/lib/widgets/tnc_dialog.dart +++ b/lib/widgets/tnc_dialog.dart @@ -1,99 +1,211 @@ -import 'package:flutter/material.dart'; -import 'package:kmobile/features/auth/screens/tnc_required_screen.dart'; + import 'package:flutter/material.dart'; -class TncDialog extends StatefulWidget { - // Add a callback function for when the user proceeds - final Future Function() onProceed; + class TncDialog extends StatefulWidget { + final Future Function() onProceed; - const TncDialog({Key? key, required this.onProceed}) : super(key: key); + const TncDialog({Key? key, required this.onProceed}) : super(key: key); - @override - _TncDialogState createState() => _TncDialogState(); -} + @override + _TncDialogState createState() => _TncDialogState(); + } -class _TncDialogState extends State { - bool _isAgreed = false; - bool _isLoading = false; + class _TncDialogState extends State { + bool _isAgreed = false; + bool _isLoading = false; + // --- NEW: ScrollController for the TNC text --- + final ScrollController _scrollController = ScrollController(); - void _handleProceed() async { - if (_isLoading) return; + final String _termsAndConditionsText = """ + Effective Date: November 10, 2025 - setState(() { - _isLoading = true; - }); + These Terms and Conditions ("Terms") govern your access to and use of The Bank mobile banking application (the "App") and the services + provided through it (the "Services"). - // Call the provided onProceed function, which will trigger the cubit - await widget.onProceed(); + By downloading, installing, accessing, or using the App, you agree to be bound by these Terms and our Privacy Policy. If you do not + agree to these Terms, you must not download, install, access, or use the App. - // The dialog will be dismissed by the navigation that happens in the BlocListener - // so we don't need to pop here. If for some reason it's still visible, - // we can add a mounted check and pop. - if (mounted) { + 1. Definitions + - App: Refers to The Bank mobile banking application. + - Bank/We/Us/Our: Refers to The Bank. + - User/You/Your: Refers to the individual using the App. + - Device: Refers to any compatible mobile phone, tablet, or other device on which you install and use the App. + - Security Credentials: Refers to your username, password, PIN, biometric data (e.g., fingerprint, facial recognition), and any other + authentication methods used to access the App and Services. + + 2. Acceptance of Terms + Your use of the App constitutes your acceptance of these Terms. We recommend that you print or save a copy of these Terms for your + records. + + 3. License to Use + We grant you a limited, non-exclusive, non-transferable, revocable license to install and use the App on a Device that you own or + control, solely for your personal, non-commercial use in connection with your accounts at The Bank. This license does not permit you to + use the App on any Device that you do not own or control. + + 4. User Responsibilities + You agree to: + - Use the App only for lawful purposes and in accordance with these Terms. + - Keep your Device and Security Credentials secure and confidential. + - Notify us immediately if you suspect any unauthorized use of your Security Credentials or Device, or if your Device is lost or stolen. + - Ensure that any information you provide to us through the App is accurate and up-to-date. + - Comply with all reasonable instructions we issue regarding the safe use of your Device and the App. + - Not use the App in any unlawful manner, for any unlawful purpose, or in any manner inconsistent with these Terms, or act fraudulently + or maliciously (e.g., by hacking into or inserting malicious code into the App or your Device's operating system). + - Not download the App from anywhere other than an app store approved by us (e.g., Apple App Store, Google Play Store) or install or use + it on a jail-broken or rooted device. + - Delete the App if you change or dispose of a Device that you use to access the Services. + + 5. Security + We employ reasonable security measures to protect your information and transactions conducted through the App. However, you acknowledge + that no system is entirely secure. You are responsible for maintaining the security of your Device and Security Credentials. We are not + liable for damages arising from virus contamination in your IT system or if the parameters of your browser are different from the + required technical conditions. + + 6. Privacy + Your privacy is important to us. Our Privacy Policy explains how we collect, use, and protect your personal information in connection + with your use of the App and Services. By using the App, you consent to such collection, use, and protection as described in our Privacy + Policy. + + 7. Limitations of Liability and Disclaimer of Warranty + THE APP AND SERVICES ARE PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT + NOT LIMITED TO, THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. We do not + guarantee continuous, uninterrupted, or secure access to any part of our Service, and operation of the App or the Services may be + interfered with by numerous factors outside of our control. + + IN NO EVENT SHALL WE OR OUR AFFILIATES, LICENSORS, OR CONTRACTORS BE LIABLE FOR ANY CLAIM, ARISING FROM OR RELATED TO THE MOBILE BANKING + APP OR THE SERVICES, THAT YOU DO NOT STATE IN WRITING IN A COMPLAINT FILED IN A COURT OR ARBITRATION PROCEEDING WITHIN TWO (2) YEARS OF + THE DATE THAT THE EVENT GIVING RISE TO THE CLAIM OCCURRED. THESE LIMITATIONS WILL APPLY TO ALL CAUSES OF ACTION, WHETHER ARISING FROM + BREACH OF CONTRACT, TORT (INCLUDING NEGLIGENCE) OR ANY OTHER LEGAL THEORY. + + 8. Intellectual Property + All intellectual property rights in the App and its content (excluding user-generated content) are owned by The Bank or its licensors. + You are granted a limited license to use the App as set forth in these Terms, but no ownership rights are transferred to you. You must + not remove or tamper with any copyright notice attached to or contained within the App. + + 9. Termination + We may terminate or suspend your access to the App and Services immediately, without prior notice or liability, for any reason + whatsoever, including without limitation if you breach these Terms. You may stop using the App at any time. If you wish to deregister + your digital banking access, you need to notify us. + + 10. Changes to Terms + We reserve the right to modify or replace these Terms at any time. If a revision is material, we will provide at least 30 days' notice + prior to any new terms taking effect. Your continued use of the App after any such changes constitutes your acceptance of the new Terms. + + 11. Governing Law and Dispute Resolution + These Terms shall be governed and construed in accordance with the laws of your local jurisdiction, without regard to its conflict of + law provisions. Any dispute arising under these Terms shall be resolved in the courts located in your local jurisdiction. + + 12. Contact Information + If you have any questions about these Terms, please contact us through the channels provided on our official website or within the App. + + 13. Electronic Communications + By using the App, you consent to receive electronic communications from us. These communications may include notices about your account, + transactional information, and marketing materials. + + 14. Third-Party Services + The App may integrate with or provide links to third-party services. We are not responsible for the content, privacy policies, or + practices of any third-party websites or services. + + 15. Indemnification + You agree to indemnify and hold harmless The Bank, its affiliates, officers, directors, employees, and agents from any and all claims, + liabilities, damages, losses, and expenses, including reasonable attorneys' fees, arising out of or in any way connected with your + access to or use of the App and Services. + """; + + void _handleProceed() async { + if (_isLoading) return; + + setState(() { + _isLoading = true; + }); + + await widget.onProceed(); + + if (mounted) { setState(() { - _isLoading = false; - }); - } - } + _isLoading = false; + }); + } + } - @override - Widget build(BuildContext context) { - return AlertDialog( - title: const Text('Terms and Conditions'), - content: SingleChildScrollView( - child: _isLoading - ? const Center( - child: Padding( - padding: EdgeInsets.all(16.0), - child: CircularProgressIndicator(), - ), - ) - : Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Please read and accept our terms and conditions to continue. ' - 'This is a placeholder for the actual terms and conditions text.'), - const SizedBox(height: 16), - Row( - children: [ - Checkbox( - value: _isAgreed, - onChanged: (bool? value) { - setState(() { - _isAgreed = value ?? false; - }); - }, - ), - const Flexible( - child: Text('I agree to the Terms and Conditions')), - ], - ), - ], - ), - ), - actions: [ - TextButton( - // Disable button while loading - onPressed: _isLoading - ? null - : () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'You must agree to the terms and conditions to proceed.'), - behavior: SnackBarBehavior.floating, - ), - ); - }, - child: const Text('Disagree'), - ), - ElevatedButton( - // Disable button if not agreed or while loading - onPressed: _isAgreed && !_isLoading ? _handleProceed : null, - child: const Text('Proceed'), - ), - ], - ); - } -} \ No newline at end of file + @override + void dispose() { + _scrollController.dispose(); // --- NEW: Dispose the ScrollController --- + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + + return AlertDialog( + title: const Text('Terms and Conditions'), + content: SizedBox( + height: screenSize.height * 0.5, // 50% of screen height + width: screenSize.width * 0.9, // 90% of screen width + // --- MODIFIED: Use a Column to separate scrollable text from fixed checkbox --- + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // --- NEW: Expanded Scrollbar for the TNC text --- + Expanded( + child: Scrollbar( + controller: _scrollController, + thumbVisibility: true, // Always show the scrollbar thumb + // To place the scrollbar on the left, you might need to wrap + // this in a Directionality widget or use a custom scrollbar. + // For now, it will appear on the right as is standard. + child: SingleChildScrollView( + controller: _scrollController, + child: _isLoading + ? const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: CircularProgressIndicator(), + ), + ) + : Text(_termsAndConditionsText), + ), + ), + ), + const SizedBox(height: 16), // Space between text and checkbox + // --- MODIFIED: Checkbox Row is now outside the SingleChildScrollView --- + Row( + children: [ + Checkbox( + value: _isAgreed, + onChanged: (bool? value) { + setState(() { + _isAgreed = value ?? false; + }); + }, + ), + const Flexible( + child: Text('I agree to the Terms and Conditions')), + ], + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: _isLoading + ? null + : () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'You must agree to the terms and conditions to proceed.'), + behavior: SnackBarBehavior.floating, + ), + ); + }, + child: const Text('Disagree'), + ), + ElevatedButton( + onPressed: _isAgreed && !_isLoading ? _handleProceed : null, + child: const Text('Proceed'), + ), + ], + ); + } + } \ No newline at end of file