Files
velocity-pay-flutter/lib/screens/accounts/widgets/account_balance_widget.dart
2026-06-11 23:48:26 +02:00

390 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:qpay/screens/accounts/account_provider.dart';
import 'package:qpay/screens/accounts/models/account_model.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AccountBalanceWidget extends StatefulWidget {
const AccountBalanceWidget({super.key});
@override
State<AccountBalanceWidget> createState() => _AccountBalanceWidgetState();
}
class _AccountBalanceWidgetState extends State<AccountBalanceWidget>
with SingleTickerProviderStateMixin {
late AccountProvider _accountProvider;
AccountModel? _usdAccount;
AccountModel? _zwgAccount;
bool _isLoadingUsd = false;
bool _isLoadingZwG = false;
String? _usdError;
String? _zwgError;
String? _phoneNumber;
late AnimationController _pulseController;
@override
void initState() {
super.initState();
_pulseController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_accountProvider = AccountProvider();
_loadBalances();
}
@override
void dispose() {
_pulseController.dispose();
_accountProvider.dispose();
super.dispose();
}
Future<void> _loadBalances() async {
final prefs = await SharedPreferences.getInstance();
_phoneNumber = prefs.getString('phone');
if (_phoneNumber == null || _phoneNumber!.isEmpty) return;
setState(() {
_isLoadingUsd = true;
_isLoadingZwG = true;
});
// Fetch USD balance
final usdResult = await _accountProvider.fetchAccount(
'USD$_phoneNumber',
);
if (mounted) {
setState(() {
_isLoadingUsd = false;
if (usdResult.isSuccess && usdResult.data != null) {
_usdAccount = usdResult.data;
_usdError = null;
} else {
_usdError = usdResult.error;
}
});
}
// Fetch ZWG balance
final zwgResult = await _accountProvider.fetchAccount(
'ZWG$_phoneNumber',
);
if (mounted) {
setState(() {
_isLoadingZwG = false;
if (zwgResult.isSuccess && zwgResult.data != null) {
_zwgAccount = zwgResult.data;
_zwgError = null;
} else {
_zwgError = zwgResult.error;
}
});
}
if (mounted) {
_pulseController.forward();
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
if (_phoneNumber == null || _phoneNumber!.isEmpty) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionLabel(
theme,
isDark,
Icons.account_balance_wallet_rounded,
'Account Balances',
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildBalanceCard(
theme: theme,
isDark: isDark,
currencyCode: 'USD',
currencyName: 'US Dollar',
flagAsset: 'united-states.png',
balance: _usdAccount?.balance,
isLoading: _isLoadingUsd,
error: _usdError,
gradientColors: [
const Color(0xFF0D47A1),
const Color(0xFF1976D2),
],
),
),
const SizedBox(width: 12),
Expanded(
child: _buildBalanceCard(
theme: theme,
isDark: isDark,
currencyCode: 'ZWG',
currencyName: 'Zimbabwe Gold',
flagAsset: 'zimbabwe.png',
balance: _zwgAccount?.balance,
isLoading: _isLoadingZwG,
error: _zwgError,
gradientColors: [
const Color(0xFF1B5E20),
const Color(0xFF388E3C),
],
),
),
],
),
const SizedBox(height: 8),
Center(
child: TextButton.icon(
onPressed: _isLoadingUsd || _isLoadingZwG
? null
: () {
_pulseController.reset();
_loadBalances();
},
icon: AnimatedBuilder(
animation: _pulseController,
builder: (context, child) {
return Transform.rotate(
angle: _pulseController.value * 6.2832,
child: Icon(
Icons.refresh_rounded,
size: 16,
color: _isLoadingUsd || _isLoadingZwG
? (isDark ? Colors.white24 : Colors.grey.shade400)
: theme.colorScheme.primary,
),
);
},
),
label: Text(
_isLoadingUsd || _isLoadingZwG
? 'Refreshing...'
: 'Refresh Balances',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: _isLoadingUsd || _isLoadingZwG
? (isDark ? Colors.white24 : Colors.grey.shade400)
: theme.colorScheme.primary,
),
),
),
),
],
),
);
}
Widget _buildBalanceCard({
required ThemeData theme,
required bool isDark,
required String currencyCode,
required String currencyName,
required String flagAsset,
double? balance,
required bool isLoading,
String? error,
required List<Color> gradientColors,
}) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
colors: gradientColors,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: gradientColors[0].withValues(alpha: 0.3),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top row: flag + currency code
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: Colors.white.withValues(alpha: 0.3),
width: 1.5,
),
),
child: ClipOval(
child: Image.asset(
flagAsset,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Icon(
Icons.monetization_on_outlined,
size: 14,
color: Colors.white.withValues(alpha: 0.8),
),
),
),
),
const SizedBox(width: 8),
Text(
currencyCode,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: Colors.white,
letterSpacing: 0.5,
),
),
],
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
),
child: Text(
currencyName,
style: TextStyle(
fontSize: 8,
fontWeight: FontWeight.w500,
color: Colors.white.withValues(alpha: 0.9),
),
),
),
],
),
const SizedBox(height: 16),
// Balance amount
if (isLoading)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 80,
height: 14,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 4),
Container(
width: 50,
height: 10,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(4),
),
),
],
)
else if (error != null)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.error_outline_rounded,
color: Colors.white.withValues(alpha: 0.7),
size: 20,
),
const SizedBox(height: 4),
Text(
'Unavailable',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: Colors.white.withValues(alpha: 0.7),
),
),
],
)
else ...[
Text(
_formatBalance(balance ?? 0.0),
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w800,
color: Colors.white,
letterSpacing: 0.5,
height: 1.1,
),
),
const SizedBox(height: 4),
Text(
currencyCode == 'USD' ? 'Available Balance' : 'Available Balance',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w400,
color: Colors.white.withValues(alpha: 0.7),
),
),
],
],
),
);
}
Widget _buildSectionLabel(
ThemeData theme,
bool isDark,
IconData icon,
String label,
) {
return Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: theme.colorScheme.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, size: 16, color: theme.colorScheme.primary),
),
const SizedBox(width: 10),
Text(
label,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: isDark ? Colors.white : Colors.black87,
letterSpacing: 0.3,
),
),
],
);
}
String _formatBalance(double balance) {
final formatter = NumberFormat('#,##0.00', 'en_US');
return '\$${formatter.format(balance)}';
}
}