added wallet functionality

This commit is contained in:
Prince
2026-06-11 23:48:26 +02:00
parent ad79bf2af8
commit 2bcee59fd7
17 changed files with 1309 additions and 292 deletions

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:qpay/screens/confirm/confirm_controller.dart';
import 'package:qpay/screens/groups/models/group_batch_model.dart';
import 'package:qpay/models/responsive_policy.dart';
import 'package:qpay/screens/groups/controllers/batch_controller.dart';
@@ -27,6 +28,8 @@ class BatchDetailScreen extends StatefulWidget {
class _BatchDetailScreenState extends State<BatchDetailScreen>
with TickerProviderStateMixin {
late BatchController controller;
ConfirmController? _confirmController;
GroupBatchUpdateRequest? _cachedUpdateReq;
bool _showPollButton = false;
String _userId = '';
int _selectedTabIndex = 0;
@@ -140,6 +143,7 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
_headerAnimController.dispose();
_cardAnimController.dispose();
_contentAnimController.dispose();
_confirmController?.dispose();
controller.dispose();
super.dispose();
}
@@ -168,6 +172,9 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
return;
}
// Cache the update request so _onRequest can check the processor
_cachedUpdateReq = updateReq;
final result = await controller.confirmBatch(batch.id!, _userId);
if (!mounted) return;
if (!result.isSuccess) {
@@ -446,21 +453,54 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
Future<void> _onRequest() async {
final batch = _batch;
if (batch == null) return;
final result = await controller.requestBatch(batch.id!, _userId);
if (!mounted) return;
await _init(); // refresh batch and items after request
if (!result.isSuccess) {
_showError(result.error ?? 'Request failed.');
return;
}
// Check if the selected payment processor is WALLET
final isWallet =
_cachedUpdateReq?.paymentProcessorLabel == 'WALLET' ||
_selectedProcessor?.label == 'WALLET' ||
_batch?.paymentProcessorLabel == 'WALLET';
if (isWallet) {
// Show OTP bottom sheet for WALLET processor
final otp = await _showOtpBottomSheet();
if (!mounted) return;
if (otp == null) {
_showError('OTP verification cancelled.');
return;
}
// Initialize confirm controller if needed
_confirmController ??= ConfirmController(context);
final txn = result.data!;
final otpResult = await _confirmController!.updateVelocityTransactionOtp(
txn.id!,
{'verificationCode': otp},
);
if (!mounted) return;
if (!otpResult.isSuccess) {
_showError(otpResult.error ?? 'OTP verification failed.');
return;
}
}
// Normal flow after request (and optional OTP verification)
await _init();
final txn = result.data!;
final pollingStatus = txn.pollingStatus?.toUpperCase();
if (pollingStatus != null && _terminalStatuses.contains(pollingStatus)) {
await _init(); // refresh batch and items after request
await _init();
if (_successStatuses.contains(pollingStatus)) {
if (mounted) {
AppSnackBar.showSuccess(context, 'Payment completed successfully.');
@@ -473,13 +513,185 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
setState(() => _showPollButton = false);
} else {
setState(() => _showPollButton = true);
_showError('Auto-poll failed. You can retry manually.');
_showError(
pollResult.error ?? 'Auto-poll failed. You can retry manually.',
);
}
await _init(); // refresh batch and items after request
await _init();
}
}
/// Shows a bottom sheet for OTP input when the payment processor is WALLET.
/// Returns the entered OTP string, or `null` if dismissed.
Future<String?> _showOtpBottomSheet() async {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final otpController = TextEditingController();
final formKey = GlobalKey<FormState>();
return showModalBottomSheet<String>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (sheetContext) {
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(sheetContext).viewInsets.bottom,
),
child: Container(
decoration: BoxDecoration(
color: isDark ? const Color(0xFF1A1A2E) : Colors.white,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(24),
),
),
padding: const EdgeInsets.fromLTRB(20, 12, 20, 24),
child: Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(2),
),
),
),
Center(
child: Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: theme.colorScheme.primary.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.verified_user_rounded,
size: 28,
color: theme.colorScheme.primary,
),
),
),
const SizedBox(height: 16),
Text(
'OTP Verification',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: isDark ? Colors.white : Colors.black87,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'An OTP has been sent to your registered mobile number/email. Please enter it below to proceed.',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.grey.shade500,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
TextFormField(
controller: otpController,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
maxLength: 6,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
letterSpacing: 8,
color: isDark ? Colors.white : Colors.black87,
),
decoration: InputDecoration(
counterText: '',
hintText: '------',
hintStyle: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w700,
letterSpacing: 8,
color: Colors.grey.shade400,
),
filled: true,
fillColor: isDark
? Colors.white.withValues(alpha: 0.06)
: Colors.grey.shade50,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(
color: theme.colorScheme.primary,
width: 2,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide(color: Colors.red.shade400),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter the OTP';
}
if (value.trim().length < 4) {
return 'OTP must be at least 4 digits';
}
return null;
},
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
if (formKey.currentState?.validate() ?? false) {
Navigator.of(
sheetContext,
).pop(otpController.text.trim());
}
},
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
elevation: 0,
),
child: const Text(
'Submit',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w700,
),
),
),
),
],
),
),
),
);
},
);
}
Future<void> _onPoll() async {
final batch = _batch;
if (batch == null) return;
@@ -678,65 +890,6 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
);
}
// ──────────────────────────────────────────────────────────────
// Status Badge
// ──────────────────────────────────────────────────────────────
Widget _buildStatusBadge(String status) {
final upper = status.toUpperCase();
final bool isSuccess = _successStatuses.contains(upper);
final bool isProcessing = upper == 'PROCESSING';
final bool isPending = upper == 'CREATED' || upper == 'CONFIRMED_ALL';
final Color bgColor = isSuccess
? const Color(0xFFD1FAE5)
: isProcessing
? const Color(0xFFDBEAFE)
: isPending
? const Color(0xFFFEF3C7)
: const Color(0xFFFEE2E2);
final Color textColor = isSuccess
? const Color(0xFF065F46)
: isProcessing
? const Color(0xFF1E40AF)
: isPending
? const Color(0xFF92400E)
: const Color(0xFF991B1B);
final Color dotColor = isSuccess
? const Color(0xFF10B981)
: isProcessing
? const Color(0xFF3B82F6)
: isPending
? const Color(0xFFF59E0B)
: const Color(0xFFEF4444);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(color: dotColor, shape: BoxShape.circle),
),
const SizedBox(width: 6),
Text(
status,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: textColor,
),
),
],
),
);
}
// ──────────────────────────────────────────────────────────────
// Refresh Button (sits next to status pill; reloads the batch)
// ──────────────────────────────────────────────────────────────
@@ -836,7 +989,7 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildStatusBadge(status),
StatusChip(status: status),
const SizedBox(width: 8),
_buildRefreshButton(theme, isDark),
],
@@ -878,7 +1031,10 @@ class _BatchDetailScreenState extends State<BatchDetailScreen>
),
const SizedBox(height: 16),
_buildDottedDivider(isDark),
..._buildHeaderActionButtons(batch, theme),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [..._buildHeaderActionButtons(batch, theme)],
),
],
),
);