Cooldown Added in Beneficiary

This commit is contained in:
2025-11-10 16:42:29 +05:30
parent 078e715d20
commit d6f61ebb31
4 changed files with 366 additions and 121 deletions

View File

@@ -1,7 +1,10 @@
import 'dart:convert';
class Beneficiary { class Beneficiary {
final String accountNo; final String accountNo;
final String accountType; final String accountType;
final String name; final String name;
final DateTime? createdAt;
final String ifscCode; final String ifscCode;
final String? bankName; final String? bankName;
final String? branchName; final String? branchName;
@@ -11,6 +14,7 @@ class Beneficiary {
required this.accountNo, required this.accountNo,
required this.accountType, required this.accountType,
required this.name, required this.name,
this.createdAt,
required this.ifscCode, required this.ifscCode,
this.bankName, this.bankName,
this.branchName, this.branchName,
@@ -21,6 +25,7 @@ class Beneficiary {
return Beneficiary( return Beneficiary(
accountNo: json['account_no'] ?? json['accountNo'] ?? '', accountNo: json['account_no'] ?? json['accountNo'] ?? '',
accountType: json['account_type'] ?? json['accountType'] ?? '', accountType: json['account_type'] ?? json['accountType'] ?? '',
createdAt: json['createdAt'] == null ? null : DateTime.tryParse(json['createdAt']),
name: json['name'] ?? '', name: json['name'] ?? '',
ifscCode: json['ifsc_code'] ?? json['ifscCode'] ?? '', ifscCode: json['ifsc_code'] ?? json['ifscCode'] ?? '',
bankName: json['bank_name'] ?? json['bankName'] ?? '', bankName: json['bank_name'] ?? json['bankName'] ?? '',

View File

@@ -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<CooldownTimer> {
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,
),
),
],
);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/features/fund_transfer/screens/cooldown.dart';
import 'package:kmobile/widgets/bank_logos.dart'; import 'package:kmobile/widgets/bank_logos.dart';
import 'package:kmobile/data/models/beneficiary.dart'; import 'package:kmobile/data/models/beneficiary.dart';
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_amount_screen.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) { if (_beneficiaries.isEmpty) {
return Center( return Center(
child: Text(AppLocalizations.of(context).noBeneficiaryFound)); child: Text(AppLocalizations.of(context).noBeneficiaryFound));
@@ -81,43 +83,79 @@ class _FundTransferBeneficiaryScreenState
itemCount: _beneficiaries.length, itemCount: _beneficiaries.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final beneficiary = _beneficiaries[index]; final beneficiary = _beneficiaries[index];
return ListTile(
leading: CircleAvatar( // --- Cooldown Logic ---
radius: 24, bool isCoolingDown = false;
backgroundColor: Colors.transparent, if (beneficiary.createdAt != null) {
child: getBankLogo(beneficiary.bankName, context), 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(

View File

@@ -1,99 +1,211 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/features/auth/screens/tnc_required_screen.dart';
class TncDialog extends StatefulWidget { class TncDialog extends StatefulWidget {
// Add a callback function for when the user proceeds final Future<void> Function() onProceed;
final Future<void> Function() onProceed;
const TncDialog({Key? key, required this.onProceed}) : super(key: key); const TncDialog({Key? key, required this.onProceed}) : super(key: key);
@override @override
_TncDialogState createState() => _TncDialogState(); _TncDialogState createState() => _TncDialogState();
} }
class _TncDialogState extends State<TncDialog> { class _TncDialogState extends State<TncDialog> {
bool _isAgreed = false; bool _isAgreed = false;
bool _isLoading = false; bool _isLoading = false;
// --- NEW: ScrollController for the TNC text ---
final ScrollController _scrollController = ScrollController();
void _handleProceed() async { final String _termsAndConditionsText = """
if (_isLoading) return; Effective Date: November 10, 2025
setState(() { These Terms and Conditions ("Terms") govern your access to and use of The Bank mobile banking application (the "App") and the services
_isLoading = true; provided through it (the "Services").
});
// Call the provided onProceed function, which will trigger the cubit By downloading, installing, accessing, or using the App, you agree to be bound by these Terms and our Privacy Policy. If you do not
await widget.onProceed(); 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 1. Definitions
// so we don't need to pop here. If for some reason it's still visible, - App: Refers to The Bank mobile banking application.
// we can add a mounted check and pop. - Bank/We/Us/Our: Refers to The Bank.
if (mounted) { - 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(() { setState(() {
_isLoading = false; _isLoading = false;
}); });
} }
} }
@override @override
Widget build(BuildContext context) { void dispose() {
return AlertDialog( _scrollController.dispose(); // --- NEW: Dispose the ScrollController ---
title: const Text('Terms and Conditions'), super.dispose();
content: SingleChildScrollView( }
child: _isLoading
? const Center( @override
child: Padding( Widget build(BuildContext context) {
padding: EdgeInsets.all(16.0), final screenSize = MediaQuery.of(context).size;
child: CircularProgressIndicator(),
), return AlertDialog(
) title: const Text('Terms and Conditions'),
: Column( content: SizedBox(
mainAxisSize: MainAxisSize.min, height: screenSize.height * 0.5, // 50% of screen height
crossAxisAlignment: CrossAxisAlignment.start, width: screenSize.width * 0.9, // 90% of screen width
children: [ // --- MODIFIED: Use a Column to separate scrollable text from fixed checkbox ---
const Text( child: Column(
'Please read and accept our terms and conditions to continue. ' mainAxisSize: MainAxisSize.min,
'This is a placeholder for the actual terms and conditions text.'), children: [
const SizedBox(height: 16), // --- NEW: Expanded Scrollbar for the TNC text ---
Row( Expanded(
children: [ child: Scrollbar(
Checkbox( controller: _scrollController,
value: _isAgreed, thumbVisibility: true, // Always show the scrollbar thumb
onChanged: (bool? value) { // To place the scrollbar on the left, you might need to wrap
setState(() { // this in a Directionality widget or use a custom scrollbar.
_isAgreed = value ?? false; // For now, it will appear on the right as is standard.
}); child: SingleChildScrollView(
}, controller: _scrollController,
), child: _isLoading
const Flexible( ? const Center(
child: Text('I agree to the Terms and Conditions')), child: Padding(
], padding: EdgeInsets.all(16.0),
), child: CircularProgressIndicator(),
], ),
), )
), : Text(_termsAndConditionsText),
actions: [ ),
TextButton( ),
// Disable button while loading ),
onPressed: _isLoading const SizedBox(height: 16), // Space between text and checkbox
? null // --- MODIFIED: Checkbox Row is now outside the SingleChildScrollView ---
: () { Row(
ScaffoldMessenger.of(context).showSnackBar( children: [
const SnackBar( Checkbox(
content: Text( value: _isAgreed,
'You must agree to the terms and conditions to proceed.'), onChanged: (bool? value) {
behavior: SnackBarBehavior.floating, setState(() {
), _isAgreed = value ?? false;
); });
}, },
child: const Text('Disagree'), ),
), const Flexible(
ElevatedButton( child: Text('I agree to the Terms and Conditions')),
// Disable button if not agreed or while loading ],
onPressed: _isAgreed && !_isLoading ? _handleProceed : null, ),
child: const Text('Proceed'), ],
), ),
], ),
); 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'),
),
],
);
}
}