Cooldown Added in Beneficiary
This commit is contained in:
@@ -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'] ?? '',
|
||||||
|
|||||||
90
lib/features/fund_transfer/screens/cooldown.dart
Normal file
90
lib/features/fund_transfer/screens/cooldown.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,6 +73,7 @@ class _FundTransferBeneficiaryScreenState
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Widget _buildBeneficiaryList() {
|
Widget _buildBeneficiaryList() {
|
||||||
if (_beneficiaries.isEmpty) {
|
if (_beneficiaries.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
@@ -81,7 +83,22 @@ class _FundTransferBeneficiaryScreenState
|
|||||||
itemCount: _beneficiaries.length,
|
itemCount: _beneficiaries.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final beneficiary = _beneficiaries[index];
|
final beneficiary = _beneficiaries[index];
|
||||||
return ListTile(
|
|
||||||
|
// --- 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(
|
leading: CircleAvatar(
|
||||||
radius: 24,
|
radius: 24,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
@@ -100,7 +117,25 @@ class _FundTransferBeneficiaryScreenState
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
trailing: isCoolingDown
|
||||||
|
? CooldownTimer(
|
||||||
|
createdAt: beneficiary.createdAt!,
|
||||||
|
onTimerFinish: () {
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
onTap: () {
|
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(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -112,12 +147,15 @@ class _FundTransferBeneficiaryScreenState
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
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);
|
||||||
@@ -14,6 +12,104 @@ class TncDialog extends StatefulWidget {
|
|||||||
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();
|
||||||
|
|
||||||
|
final String _termsAndConditionsText = """
|
||||||
|
Effective Date: November 10, 2025
|
||||||
|
|
||||||
|
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").
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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 {
|
void _handleProceed() async {
|
||||||
if (_isLoading) return;
|
if (_isLoading) return;
|
||||||
@@ -22,12 +118,8 @@ class _TncDialogState extends State<TncDialog> {
|
|||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Call the provided onProceed function, which will trigger the cubit
|
|
||||||
await widget.onProceed();
|
await widget.onProceed();
|
||||||
|
|
||||||
// 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) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
@@ -35,11 +127,35 @@ class _TncDialogState extends State<TncDialog> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose(); // --- NEW: Dispose the ScrollController ---
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final screenSize = MediaQuery.of(context).size;
|
||||||
|
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Terms and Conditions'),
|
title: const Text('Terms and Conditions'),
|
||||||
content: SingleChildScrollView(
|
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
|
child: _isLoading
|
||||||
? const Center(
|
? const Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -47,14 +163,12 @@ class _TncDialogState extends State<TncDialog> {
|
|||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Column(
|
: Text(_termsAndConditionsText),
|
||||||
mainAxisSize: MainAxisSize.min,
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
),
|
||||||
children: [
|
),
|
||||||
const Text(
|
const SizedBox(height: 16), // Space between text and checkbox
|
||||||
'Please read and accept our terms and conditions to continue. '
|
// --- MODIFIED: Checkbox Row is now outside the SingleChildScrollView ---
|
||||||
'This is a placeholder for the actual terms and conditions text.'),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Checkbox(
|
Checkbox(
|
||||||
@@ -74,7 +188,6 @@ class _TncDialogState extends State<TncDialog> {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
// Disable button while loading
|
|
||||||
onPressed: _isLoading
|
onPressed: _isLoading
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
@@ -89,7 +202,6 @@ class _TncDialogState extends State<TncDialog> {
|
|||||||
child: const Text('Disagree'),
|
child: const Text('Disagree'),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
// Disable button if not agreed or while loading
|
|
||||||
onPressed: _isAgreed && !_isLoading ? _handleProceed : null,
|
onPressed: _isAgreed && !_isLoading ? _handleProceed : null,
|
||||||
child: const Text('Proceed'),
|
child: const Text('Proceed'),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user