Files
velocity-pay-flutter/lib/screens/accounts/widgets/account_balance_widget.dart
2026-06-24 18:31:21 +02:00

527 lines
16 KiB
Dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.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 {
final String? selectedCurrency;
final ValueChanged<String>? onCurrencySelected;
const AccountBalanceWidget({
super.key,
this.selectedCurrency,
this.onCurrencySelected,
});
@override
State<AccountBalanceWidget> createState() => _AccountBalanceWidgetState();
}
class _AccountBalanceWidgetState extends State<AccountBalanceWidget> {
bool _isLoggedIn = false;
String? _workspaceId;
AccountModel? _usdAccount;
AccountModel? _zwgAccount;
bool _isLoadingUsd = false;
bool _isLoadingZwG = false;
String? _usdError;
String? _zwgError;
bool _showAccountError = false;
@override
void initState() {
super.initState();
_loadAuthStateAndBalances();
}
Future<void> _loadAuthStateAndBalances() async {
final prefs = await SharedPreferences.getInstance();
if (prefs.getString("token") != null) {
setState(() {
_isLoggedIn = true;
});
}
_workspaceId = prefs.getString('workspaceId');
if (_workspaceId == null || _workspaceId!.isEmpty) {
return;
}
if (!_isLoggedIn) {
return;
}
if (!mounted) return;
final accountProvider = context.read<AccountProvider>();
setState(() {
_isLoadingUsd = true;
_isLoadingZwG = true;
});
// Try the cached balances first
final cachedUsd = accountProvider.balanceForCurrency('USD');
final cachedZwg = accountProvider.balanceForCurrency('ZWG');
if (cachedUsd != null && cachedZwg != null) {
if (mounted) {
setState(() {
_usdAccount = cachedUsd;
_zwgAccount = cachedZwg;
_isLoadingUsd = false;
_isLoadingZwG = false;
});
}
return;
}
// Fetch USD balance
final usdResult = await accountProvider.fetchAccount(_workspaceId, 'USD');
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(_workspaceId, 'ZWG');
if (mounted) {
setState(() {
_isLoadingZwG = false;
if (zwgResult.isSuccess && zwgResult.data != null) {
_zwgAccount = zwgResult.data;
_zwgError = null;
} else {
_zwgError = zwgResult.error;
}
});
}
// If both fetches failed, mark as not logged in
if (mounted &&
!usdResult.isSuccess &&
!zwgResult.isSuccess &&
_isLoggedIn) {
setState(() {
_showAccountError = true;
});
}
}
Future<void> _refreshBalances() async {
final prefs = await SharedPreferences.getInstance();
_workspaceId = prefs.getString('workspaceId');
if (_workspaceId == null || _workspaceId!.isEmpty) return;
if (!mounted) return;
final accountProvider = context.read<AccountProvider>();
setState(() {
_isLoadingUsd = true;
_isLoadingZwG = true;
});
final usdResult = await accountProvider.fetchAccount(_workspaceId, 'USD');
if (mounted) {
setState(() {
_isLoadingUsd = false;
if (usdResult.isSuccess && usdResult.data != null) {
_usdAccount = usdResult.data;
_usdError = null;
} else {
_usdError = usdResult.error;
}
});
}
final zwgResult = await accountProvider.fetchAccount(_workspaceId, 'ZWG');
if (mounted) {
setState(() {
_isLoadingZwG = false;
if (zwgResult.isSuccess && zwgResult.data != null) {
_zwgAccount = zwgResult.data;
_zwgError = null;
} else {
_zwgError = zwgResult.error;
}
});
}
if (mounted &&
!usdResult.isSuccess &&
!zwgResult.isSuccess &&
_isLoggedIn) {
setState(() {
_showAccountError = true;
});
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
if (!_isLoggedIn) {
return _buildLoginPrompt(theme, isDark);
}
if (_workspaceId == null || _workspaceId!.isEmpty) {
return const SizedBox.shrink();
}
if (_showAccountError) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 18),
decoration: BoxDecoration(
color: theme.colorScheme.error.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: theme.colorScheme.error.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
children: [
Icon(
Icons.error_outline_rounded,
color: theme.colorScheme.error.withValues(alpha: 0.7),
size: 16,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'Unable to load account balances. Please try refreshing.',
style: TextStyle(
fontSize: 12,
color: theme.colorScheme.error.withValues(alpha: 0.7),
),
),
),
],
),
),
);
}
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: _buildCompactCard(
theme: theme,
isDark: isDark,
currencyCode: 'USD',
flagAsset: 'united-states.png',
balance: _usdAccount?.balance,
isLoading: _isLoadingUsd,
error: _usdError,
isSelected: widget.selectedCurrency == 'USD',
gradientColors: [
const Color(0xFF0D47A1),
const Color(0xFF1976D2),
],
),
),
const SizedBox(width: 10),
Expanded(
child: _buildCompactCard(
theme: theme,
isDark: isDark,
currencyCode: 'ZWG',
flagAsset: 'zimbabwe.png',
balance: _zwgAccount?.balance,
isLoading: _isLoadingZwG,
error: _zwgError,
isSelected: widget.selectedCurrency == 'ZWG',
gradientColors: [
const Color(0xFF1B5E20),
const Color(0xFF388E3C),
],
),
),
],
),
const SizedBox(height: 6),
Center(
child: TextButton.icon(
onPressed: _isLoadingUsd || _isLoadingZwG
? null
: () {
_refreshBalances();
},
icon: Icon(
Icons.refresh_rounded,
size: 14,
color: _isLoadingUsd || _isLoadingZwG
? (isDark ? Colors.white24 : Colors.grey.shade400)
: theme.colorScheme.primary,
),
label: Text(
_isLoadingUsd || _isLoadingZwG ? 'Refreshing...' : 'Refresh',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: _isLoadingUsd || _isLoadingZwG
? (isDark ? Colors.white24 : Colors.grey.shade400)
: theme.colorScheme.primary,
),
),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
),
],
),
);
}
// ──────────────────────────────────────────────────────────────
// Login Prompt
// ──────────────────────────────────────────────────────────────
Widget _buildLoginPrompt(ThemeData theme, bool isDark) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 18),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
theme.colorScheme.primary.withValues(alpha: 0.12),
theme.colorScheme.primary.withValues(alpha: 0.04),
],
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: theme.colorScheme.primary.withValues(alpha: 0.15),
width: 1,
),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Register or Login for more features',
style: TextStyle(
fontSize: 14,
color: isDark ? Colors.white : Colors.black87,
),
),
],
),
),
ElevatedButton(
onPressed: () {
context.push('/onboarding/landing');
},
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
child: const Text(
'Get Started',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
),
),
],
),
),
);
}
Widget _buildCompactCard({
required ThemeData theme,
required bool isDark,
required String currencyCode,
required String flagAsset,
double? balance,
required bool isLoading,
String? error,
required bool isSelected,
required List<Color> gradientColors,
}) {
return GestureDetector(
onTap: () {
widget.onCurrencySelected?.call(currencyCode);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
gradient: LinearGradient(
colors: gradientColors,
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
boxShadow: [
BoxShadow(
color: gradientColors[0].withValues(
alpha: isSelected ? 0.5 : 0.25,
),
blurRadius: isSelected ? 12 : 6,
offset: Offset(0, isSelected ? 4 : 2),
),
],
border: isSelected
? Border.all(color: Colors.white.withValues(alpha: 0.8), width: 2)
: Border.all(
color: Colors.white.withValues(alpha: 0.15),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Top row: flag + currency code
Row(
children: [
Container(
width: 22,
height: 22,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: Colors.white.withValues(alpha: 0.3),
width: 1.5,
),
),
child: ClipOval(
child: Image.asset(
'assets/$flagAsset',
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Icon(
Icons.monetization_on_outlined,
size: 12,
color: Colors.white.withValues(alpha: 0.8),
),
),
),
),
const SizedBox(width: 6),
Expanded(
child: Text(
currencyCode,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: Colors.white,
letterSpacing: 0.5,
),
),
),
if (isSelected)
Container(
width: 18,
height: 18,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.3),
shape: BoxShape.circle,
),
child: const Icon(
Icons.check_rounded,
size: 12,
color: Colors.white,
),
),
],
),
const SizedBox(height: 10),
// Balance amount
if (isLoading)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 70,
height: 12,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(4),
),
),
],
)
else if (error != null)
Row(
children: [
Icon(
Icons.error_outline_rounded,
color: Colors.white.withValues(alpha: 0.7),
size: 16,
),
const SizedBox(width: 4),
Text(
'Unavailable',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: Colors.white.withValues(alpha: 0.7),
),
),
],
)
else ...[
Text(
_formatBalance(balance ?? 0.0),
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w800,
color: Colors.white,
letterSpacing: 0.5,
height: 1.1,
),
),
],
],
),
),
);
}
String _formatBalance(double balance) {
final formatter = NumberFormat('#,##0.00', 'en_US');
return '\$${formatter.format(balance)}';
}
}