added wallet functionality
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:qpay/http/http.dart';
|
||||
import 'package:qpay/models/api_response.dart';
|
||||
import 'package:qpay/screens/accounts/models/account_model.dart';
|
||||
import 'package:qpay/screens/accounts/models/statement_model.dart';
|
||||
|
||||
class AccountProvider extends ChangeNotifier {
|
||||
final Http http = Http();
|
||||
|
||||
AccountProvider();
|
||||
|
||||
AccountModel? _account;
|
||||
StatementModel? _statement;
|
||||
bool _isLoadingAccount = false;
|
||||
bool _isLoadingStatement = false;
|
||||
String? _accountError;
|
||||
String? _statementError;
|
||||
|
||||
AccountModel? get account => _account;
|
||||
StatementModel? get statement => _statement;
|
||||
bool get isLoadingAccount => _isLoadingAccount;
|
||||
bool get isLoadingStatement => _isLoadingStatement;
|
||||
String? get accountError => _accountError;
|
||||
String? get statementError => _statementError;
|
||||
|
||||
Future<ApiResponse<AccountModel>> fetchAccount(String phoneNumber) async {
|
||||
_isLoadingAccount = true;
|
||||
_accountError = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
Map<String, dynamic> response = await http.get(
|
||||
'/accounts/phone/$phoneNumber',
|
||||
);
|
||||
|
||||
_account = AccountModel.fromJson(response);
|
||||
_isLoadingAccount = false;
|
||||
notifyListeners();
|
||||
return ApiResponse.success(_account!);
|
||||
} on DioException catch (e, s) {
|
||||
logger.e(s);
|
||||
_isLoadingAccount = false;
|
||||
_accountError = 'Failed to fetch account. Please try again.';
|
||||
notifyListeners();
|
||||
return ApiResponse.failure(_accountError!);
|
||||
} catch (e, s) {
|
||||
logger.e(s);
|
||||
_isLoadingAccount = false;
|
||||
_accountError = 'An unexpected error occurred. Please try again.';
|
||||
notifyListeners();
|
||||
return ApiResponse.failure(_accountError!);
|
||||
}
|
||||
}
|
||||
|
||||
Future<ApiResponse<StatementModel>> fetchStatement({
|
||||
required String phoneNumber,
|
||||
required String startDate,
|
||||
required String endDate,
|
||||
}) async {
|
||||
_isLoadingStatement = true;
|
||||
_statementError = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
Map<String, dynamic> response = await http.get(
|
||||
'/accounts/phone/$phoneNumber/statement'
|
||||
'?startDate=$startDate&endDate=$endDate',
|
||||
);
|
||||
|
||||
_statement = StatementModel.fromJson(response);
|
||||
_isLoadingStatement = false;
|
||||
notifyListeners();
|
||||
return ApiResponse.success(_statement!);
|
||||
} on DioException catch (e, s) {
|
||||
logger.e(s);
|
||||
_isLoadingStatement = false;
|
||||
_statementError = 'Failed to fetch statement. Please try again.';
|
||||
notifyListeners();
|
||||
return ApiResponse.failure(_statementError!);
|
||||
} catch (e, s) {
|
||||
logger.e(s);
|
||||
_isLoadingStatement = false;
|
||||
_statementError = 'An unexpected error occurred. Please try again.';
|
||||
notifyListeners();
|
||||
return ApiResponse.failure(_statementError!);
|
||||
}
|
||||
}
|
||||
|
||||
void clearAccount() {
|
||||
_account = null;
|
||||
_accountError = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearStatement() {
|
||||
_statement = null;
|
||||
_statementError = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
67
lib/screens/accounts/models/account_model.dart
Normal file
67
lib/screens/accounts/models/account_model.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'account_model.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class CurrencyModel {
|
||||
final String id;
|
||||
final String? createdAt;
|
||||
final String name;
|
||||
final String symbol;
|
||||
final String code;
|
||||
final double rate;
|
||||
final bool defaultCurrency;
|
||||
|
||||
CurrencyModel({
|
||||
required this.id,
|
||||
this.createdAt,
|
||||
required this.name,
|
||||
required this.symbol,
|
||||
required this.code,
|
||||
required this.rate,
|
||||
required this.defaultCurrency,
|
||||
});
|
||||
|
||||
factory CurrencyModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$CurrencyModelFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$CurrencyModelToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class AccountModel {
|
||||
final String name;
|
||||
final String description;
|
||||
final String accountNumber;
|
||||
final String alternateAccountNumber;
|
||||
final String type;
|
||||
final String category;
|
||||
final String ledger;
|
||||
final double balance;
|
||||
final String partyType;
|
||||
final String party;
|
||||
final CurrencyModel currency;
|
||||
final String customerStringId;
|
||||
final String companyStringId;
|
||||
|
||||
AccountModel({
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.accountNumber,
|
||||
required this.alternateAccountNumber,
|
||||
required this.type,
|
||||
required this.category,
|
||||
required this.ledger,
|
||||
required this.balance,
|
||||
required this.partyType,
|
||||
required this.party,
|
||||
required this.currency,
|
||||
required this.customerStringId,
|
||||
required this.companyStringId,
|
||||
});
|
||||
|
||||
factory AccountModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$AccountModelFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$AccountModelToJson(this);
|
||||
}
|
||||
62
lib/screens/accounts/models/account_model.g.dart
Normal file
62
lib/screens/accounts/models/account_model.g.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'account_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
CurrencyModel _$CurrencyModelFromJson(Map<String, dynamic> json) =>
|
||||
CurrencyModel(
|
||||
id: json['id'] as String,
|
||||
createdAt: json['createdAt'] as String?,
|
||||
name: json['name'] as String,
|
||||
symbol: json['symbol'] as String,
|
||||
code: json['code'] as String,
|
||||
rate: (json['rate'] as num).toDouble(),
|
||||
defaultCurrency: json['defaultCurrency'] as bool,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CurrencyModelToJson(CurrencyModel instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'createdAt': instance.createdAt,
|
||||
'name': instance.name,
|
||||
'symbol': instance.symbol,
|
||||
'code': instance.code,
|
||||
'rate': instance.rate,
|
||||
'defaultCurrency': instance.defaultCurrency,
|
||||
};
|
||||
|
||||
AccountModel _$AccountModelFromJson(Map<String, dynamic> json) => AccountModel(
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String,
|
||||
accountNumber: json['accountNumber'] as String,
|
||||
alternateAccountNumber: json['alternateAccountNumber'] as String,
|
||||
type: json['type'] as String,
|
||||
category: json['category'] as String,
|
||||
ledger: json['ledger'] as String,
|
||||
balance: (json['balance'] as num).toDouble(),
|
||||
partyType: json['partyType'] as String,
|
||||
party: json['party'] as String,
|
||||
currency: CurrencyModel.fromJson(json['currency'] as Map<String, dynamic>),
|
||||
customerStringId: json['customerStringId'] as String,
|
||||
companyStringId: json['companyStringId'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AccountModelToJson(AccountModel instance) =>
|
||||
<String, dynamic>{
|
||||
'name': instance.name,
|
||||
'description': instance.description,
|
||||
'accountNumber': instance.accountNumber,
|
||||
'alternateAccountNumber': instance.alternateAccountNumber,
|
||||
'type': instance.type,
|
||||
'category': instance.category,
|
||||
'ledger': instance.ledger,
|
||||
'balance': instance.balance,
|
||||
'partyType': instance.partyType,
|
||||
'party': instance.party,
|
||||
'currency': instance.currency,
|
||||
'customerStringId': instance.customerStringId,
|
||||
'companyStringId': instance.companyStringId,
|
||||
};
|
||||
63
lib/screens/accounts/models/statement_model.dart
Normal file
63
lib/screens/accounts/models/statement_model.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'statement_model.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class TransactionModel {
|
||||
final String name;
|
||||
final String narration;
|
||||
final String party;
|
||||
final String company;
|
||||
final String postingDate;
|
||||
final double amount;
|
||||
final String account;
|
||||
final String accountName;
|
||||
final String currency;
|
||||
final String type;
|
||||
final double balance;
|
||||
|
||||
TransactionModel({
|
||||
required this.name,
|
||||
required this.narration,
|
||||
required this.party,
|
||||
required this.company,
|
||||
required this.postingDate,
|
||||
required this.amount,
|
||||
required this.account,
|
||||
required this.accountName,
|
||||
required this.currency,
|
||||
required this.type,
|
||||
required this.balance,
|
||||
});
|
||||
|
||||
factory TransactionModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$TransactionModelFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$TransactionModelToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class StatementModel {
|
||||
final int startBalance;
|
||||
final double endBalance;
|
||||
final List<TransactionModel> transactions;
|
||||
final String accountName;
|
||||
final String accountNumber;
|
||||
final String startDate;
|
||||
final String endDate;
|
||||
|
||||
StatementModel({
|
||||
required this.startBalance,
|
||||
required this.endBalance,
|
||||
required this.transactions,
|
||||
required this.accountName,
|
||||
required this.accountNumber,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
});
|
||||
|
||||
factory StatementModel.fromJson(Map<String, dynamic> json) =>
|
||||
_$StatementModelFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$StatementModelToJson(this);
|
||||
}
|
||||
61
lib/screens/accounts/models/statement_model.g.dart
Normal file
61
lib/screens/accounts/models/statement_model.g.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'statement_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
TransactionModel _$TransactionModelFromJson(Map<String, dynamic> json) =>
|
||||
TransactionModel(
|
||||
name: json['name'] as String,
|
||||
narration: json['narration'] as String,
|
||||
party: json['party'] as String,
|
||||
company: json['company'] as String,
|
||||
postingDate: json['postingDate'] as String,
|
||||
amount: (json['amount'] as num).toDouble(),
|
||||
account: json['account'] as String,
|
||||
accountName: json['accountName'] as String,
|
||||
currency: json['currency'] as String,
|
||||
type: json['type'] as String,
|
||||
balance: (json['balance'] as num).toDouble(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TransactionModelToJson(TransactionModel instance) =>
|
||||
<String, dynamic>{
|
||||
'name': instance.name,
|
||||
'narration': instance.narration,
|
||||
'party': instance.party,
|
||||
'company': instance.company,
|
||||
'postingDate': instance.postingDate,
|
||||
'amount': instance.amount,
|
||||
'account': instance.account,
|
||||
'accountName': instance.accountName,
|
||||
'currency': instance.currency,
|
||||
'type': instance.type,
|
||||
'balance': instance.balance,
|
||||
};
|
||||
|
||||
StatementModel _$StatementModelFromJson(Map<String, dynamic> json) =>
|
||||
StatementModel(
|
||||
startBalance: (json['startBalance'] as num).toInt(),
|
||||
endBalance: (json['endBalance'] as num).toDouble(),
|
||||
transactions: (json['transactions'] as List<dynamic>)
|
||||
.map((e) => TransactionModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
accountName: json['accountName'] as String,
|
||||
accountNumber: json['accountNumber'] as String,
|
||||
startDate: json['startDate'] as String,
|
||||
endDate: json['endDate'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$StatementModelToJson(StatementModel instance) =>
|
||||
<String, dynamic>{
|
||||
'startBalance': instance.startBalance,
|
||||
'endBalance': instance.endBalance,
|
||||
'transactions': instance.transactions,
|
||||
'accountName': instance.accountName,
|
||||
'accountNumber': instance.accountNumber,
|
||||
'startDate': instance.startDate,
|
||||
'endDate': instance.endDate,
|
||||
};
|
||||
390
lib/screens/accounts/widgets/account_balance_widget.dart
Normal file
390
lib/screens/accounts/widgets/account_balance_widget.dart
Normal file
@@ -0,0 +1,390 @@
|
||||
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)}';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user