chitfund/luckychit/lib/shared/widgets/upi_qr_payment_dialog.dart

665 lines
20 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'dart:async';
import '../../core/models/chit_group.dart';
import '../../core/models/group_member.dart';
import '../../core/services/api_service.dart';
/// UPI QR Payment Dialog
/// Shows QR code for direct UPI payment + auto-checks for payment confirmation
class UPIQRPaymentDialog extends StatefulWidget {
final ChitGroup group;
final GroupMember member;
final int month;
final int year;
const UPIQRPaymentDialog({
super.key,
required this.group,
required this.member,
required this.month,
required this.year,
});
@override
State<UPIQRPaymentDialog> createState() => _UPIQRPaymentDialogState();
}
class _UPIQRPaymentDialogState extends State<UPIQRPaymentDialog> {
final _apiService = ApiService();
bool _isLoading = true;
bool _isCheckingStatus = false;
String? _error;
String? _qrCodeData;
String? _upiId;
String? _upiReference;
double? _amount;
Timer? _statusCheckTimer;
int _checkCount = 0;
static const int _maxChecks = 60; // Check for 5 minutes (60 x 5 seconds)
@override
void initState() {
super.initState();
_loadQRCode();
}
@override
void dispose() {
_statusCheckTimer?.cancel();
super.dispose();
}
Future<void> _loadQRCode() async {
try {
setState(() {
_isLoading = true;
_error = null;
});
final response = await _apiService.get(
'/payments/phonepe/qr/${widget.group.id}/${widget.month}/${widget.year}',
);
if (response['success'] == true) {
final data = response['data'];
setState(() {
_qrCodeData = data['qr_code_data'];
_upiId = data['upi_id'];
_upiReference = data['upi_reference'];
_amount = double.tryParse(data['amount'].toString()) ?? 0.0;
_isLoading = false;
});
// Start checking for payment status
_startStatusChecking();
} else {
setState(() {
_error = response['message'] ?? 'Failed to load QR code';
_isLoading = false;
});
}
} catch (e) {
setState(() {
_error = 'Error loading QR code: $e';
_isLoading = false;
});
}
}
void _startStatusChecking() {
_statusCheckTimer = Timer.periodic(
const Duration(seconds: 5),
(timer) async {
if (_checkCount >= _maxChecks || !mounted) {
timer.cancel();
return;
}
_checkCount++;
await _checkPaymentStatus();
},
);
}
Future<void> _checkPaymentStatus() async {
if (_isCheckingStatus || _upiReference == null) return;
try {
setState(() {
_isCheckingStatus = true;
});
final response = await _apiService.get(
'/payments/phonepe/status/$_upiReference',
);
if (response['success'] == true && response['data']['status'] == 'success') {
// Payment confirmed!
_statusCheckTimer?.cancel();
if (mounted) {
Get.snackbar(
'Payment Confirmed!',
'Your payment has been received and recorded',
backgroundColor: Colors.green.shade100,
colorText: Colors.green.shade800,
icon: Icon(Icons.check_circle, color: Colors.green.shade600),
duration: const Duration(seconds: 4),
);
Navigator.of(context).pop(true); // Return success
}
}
} catch (e) {
// Silently fail - will check again
print('Status check error: $e');
} finally {
if (mounted) {
setState(() {
_isCheckingStatus = false;
});
}
}
}
void _copyToClipboard(String text, String label) {
Clipboard.setData(ClipboardData(text: text));
Get.snackbar(
'Copied!',
'$label copied to clipboard',
backgroundColor: Colors.blue.shade50,
colorText: Colors.blue.shade900,
duration: const Duration(seconds: 2),
snackPosition: SnackPosition.BOTTOM,
);
}
@override
Widget build(BuildContext context) {
final monthNames = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
child: Container(
width: double.infinity,
constraints: BoxConstraints(
maxWidth: 450.w,
maxHeight: 0.9.sh,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Container(
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
const Color(0xFF5F259F),
const Color(0xFF7E3BB4),
],
),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.r),
topRight: Radius.circular(16.r),
),
),
child: Row(
children: [
Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8.r),
),
child: Icon(
Icons.qr_code_2,
color: Colors.white,
size: 24.w,
),
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Pay via UPI',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
Text(
'Scan QR or use UPI ID',
style: TextStyle(
fontSize: 14.sp,
color: Colors.white.withOpacity(0.9),
),
),
],
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: Icon(
Icons.close,
size: 24.w,
color: Colors.white,
),
),
],
),
),
// Content
Flexible(
child: SingleChildScrollView(
padding: EdgeInsets.all(20.w),
child: _isLoading
? _buildLoadingState()
: _error != null
? _buildErrorState()
: _buildQRContent(monthNames),
),
),
// Footer with status
if (!_isLoading && _error == null)
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: _isCheckingStatus
? Colors.orange.shade50
: Colors.blue.shade50,
border: Border(
top: BorderSide(color: Colors.grey.shade200),
),
),
child: Row(
children: [
if (_isCheckingStatus)
SizedBox(
width: 16.w,
height: 16.h,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.orange.shade600,
),
),
)
else
Icon(
Icons.info_outline,
size: 16.w,
color: Colors.blue.shade700,
),
SizedBox(width: 8.w),
Expanded(
child: Text(
_isCheckingStatus
? 'Checking for payment...'
: 'Payment will be auto-detected once completed',
style: TextStyle(
fontSize: 12.sp,
color: _isCheckingStatus
? Colors.orange.shade800
: Colors.blue.shade700,
),
),
),
],
),
),
],
),
),
);
}
Widget _buildLoadingState() {
return Center(
child: Padding(
padding: EdgeInsets.all(40.w),
child: Column(
children: [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
const Color(0xFF5F259F),
),
),
SizedBox(height: 16.h),
Text(
'Generating QR Code...',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey.shade600,
),
),
],
),
),
);
}
Widget _buildErrorState() {
return Center(
child: Padding(
padding: EdgeInsets.all(20.w),
child: Column(
children: [
Icon(
Icons.error_outline,
size: 48.w,
color: Colors.red.shade400,
),
SizedBox(height: 16.h),
Text(
_error!,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.sp,
color: Colors.red.shade700,
),
),
SizedBox(height: 16.h),
ElevatedButton.icon(
onPressed: _loadQRCode,
icon: Icon(Icons.refresh, size: 20.w),
label: Text('Retry'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade600,
foregroundColor: Colors.white,
),
),
],
),
),
);
}
Widget _buildQRContent(List<String> monthNames) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Payment Details
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(color: Colors.grey.shade200),
),
child: Column(
children: [
_buildInfoRow('Group', widget.group.name),
_buildInfoRow('Month', '${monthNames[widget.month - 1]} ${widget.year}'),
Divider(height: 16.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Amount',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w700,
color: Colors.grey.shade800,
),
),
Text(
'${_amount?.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 24.sp,
fontWeight: FontWeight.w800,
color: Colors.green.shade700,
),
),
],
),
],
),
),
SizedBox(height: 24.h),
// QR Code
Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8.r,
offset: Offset(0, 2.h),
),
],
),
child: Column(
children: [
QrImageView(
data: _qrCodeData!,
version: QrVersions.auto,
size: 200.w,
backgroundColor: Colors.white,
),
SizedBox(height: 12.h),
Text(
'Scan with any UPI app',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
],
),
),
SizedBox(height: 24.h),
// UPI ID
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: Colors.blue.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Or pay using UPI ID',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.blue.shade900,
),
),
SizedBox(height: 8.h),
Row(
children: [
Expanded(
child: SelectableText(
_upiId!,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w700,
color: Colors.blue.shade700,
fontFamily: 'monospace',
),
),
),
IconButton(
onPressed: () => _copyToClipboard(_upiId!, 'UPI ID'),
icon: Icon(Icons.copy, size: 20.w),
style: IconButton.styleFrom(
backgroundColor: Colors.blue.shade100,
),
tooltip: 'Copy UPI ID',
),
],
),
],
),
),
SizedBox(height: 16.h),
// Reference Number
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: Colors.orange.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.warning_amber_rounded,
size: 18.w,
color: Colors.orange.shade700,
),
SizedBox(width: 8.w),
Text(
'Important: Add this reference',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.orange.shade900,
),
),
],
),
SizedBox(height: 8.h),
Row(
children: [
Expanded(
child: SelectableText(
_upiReference!,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w700,
color: Colors.orange.shade700,
fontFamily: 'monospace',
),
),
),
IconButton(
onPressed: () => _copyToClipboard(_upiReference!, 'Reference'),
icon: Icon(Icons.copy, size: 20.w),
style: IconButton.styleFrom(
backgroundColor: Colors.orange.shade100,
),
tooltip: 'Copy Reference',
),
],
),
SizedBox(height: 4.h),
Text(
'Add this in remarks/note for auto-matching',
style: TextStyle(
fontSize: 12.sp,
color: Colors.orange.shade700,
),
),
],
),
),
SizedBox(height: 24.h),
// Instructions
Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(8.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'How to pay:',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.green.shade900,
),
),
SizedBox(height: 8.h),
_buildInstructionItem('1. Open any UPI app (PhonePe, GPay, Paytm, etc.)'),
_buildInstructionItem('2. Scan the QR code above'),
_buildInstructionItem('3. Or enter the UPI ID manually'),
_buildInstructionItem('4. Add the reference number in remarks'),
_buildInstructionItem('5. Complete the payment'),
SizedBox(height: 8.h),
Text(
'✓ Payment will be auto-detected within seconds!',
style: TextStyle(
fontSize: 13.sp,
fontWeight: FontWeight.w600,
color: Colors.green.shade700,
),
),
],
),
),
],
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 4.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey.shade700,
),
),
Flexible(
child: Text(
value,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.grey.shade900,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
Widget _buildInstructionItem(String text) {
return Padding(
padding: EdgeInsets.only(bottom: 4.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('', style: TextStyle(fontSize: 14.sp, color: Colors.green.shade700)),
Expanded(
child: Text(
text,
style: TextStyle(
fontSize: 12.sp,
color: Colors.green.shade700,
height: 1.4,
),
),
),
],
),
);
}
}