173 lines
5.1 KiB
Dart
173 lines
5.1 KiB
Dart
/// Balance tab: connect bank CTA, current balance, and refresh.
|
|
library;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../providers/bank_provider.dart';
|
|
import '../utils/connect_bank.dart';
|
|
import '../utils/helpers.dart';
|
|
import '../widgets/empty_state.dart';
|
|
import '../widgets/loading_view.dart';
|
|
import '../widgets/responsive_body.dart';
|
|
|
|
class BalanceScreen extends StatelessWidget {
|
|
const BalanceScreen({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final bank = context.watch<BankController>();
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Balance'),
|
|
actions: [
|
|
IconButton(
|
|
tooltip: 'Refresh',
|
|
onPressed: bank.isLoading ? null : bank.refresh,
|
|
icon: const Icon(Icons.refresh),
|
|
),
|
|
],
|
|
),
|
|
body: RefreshIndicator(
|
|
onRefresh: bank.refresh,
|
|
child: ResponsiveBody(
|
|
child: _BalanceBody(bank: bank),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _BalanceBody extends StatelessWidget {
|
|
const _BalanceBody({required this.bank});
|
|
|
|
final BankController bank;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (bank.isLoading && bank.balance == null && !bank.isLinked) {
|
|
return const LoadingView(message: 'Checking bank link…');
|
|
}
|
|
|
|
if (!bank.isLinked) {
|
|
return ListView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
children: [
|
|
EmptyState(
|
|
icon: Icons.account_balance_outlined,
|
|
title: 'Bank not linked',
|
|
message: bank.error ??
|
|
'Connect opens a sandbox bank login in-app. '
|
|
'Stay in that WebView until it returns here. '
|
|
'You should then see a balance and transactions.',
|
|
actionLabel: bank.isConnecting ? 'Opening…' : 'Connect bank',
|
|
onAction: () => runConnectBank(context, bank),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
if (bank.error != null && bank.balance == null) {
|
|
return ListView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
children: [
|
|
EmptyState(
|
|
icon: Icons.error_outline,
|
|
title: 'Could not load balance',
|
|
message: bank.error!,
|
|
actionLabel: 'Retry',
|
|
onAction: bank.refresh,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
final balance = bank.balance;
|
|
final theme = Theme.of(context);
|
|
final colors = theme.colorScheme;
|
|
|
|
return ListView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
padding: const EdgeInsets.only(top: 8, bottom: 32),
|
|
children: [
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(24),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(20),
|
|
gradient: LinearGradient(
|
|
colors: [colors.primary, colors.tertiary],
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: colors.primary.withValues(alpha: 0.28),
|
|
blurRadius: 20,
|
|
offset: const Offset(0, 10),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Current balance',
|
|
style: theme.textTheme.labelMedium?.copyWith(
|
|
color: colors.onPrimary.withValues(alpha: 0.8),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
balance == null
|
|
? '—'
|
|
: formatMoney(balance.amount, balance.currency),
|
|
style: theme.textTheme.headlineMedium?.copyWith(
|
|
color: colors.onPrimary,
|
|
),
|
|
),
|
|
if (balance?.balanceType != null) ...[
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
balance!.balanceType!,
|
|
style: theme.textTheme.labelMedium?.copyWith(
|
|
color: colors.onPrimary.withValues(alpha: 0.75),
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
ListTile(
|
|
contentPadding: EdgeInsets.zero,
|
|
leading: Icon(
|
|
Icons.check_circle_outline,
|
|
color: colors.primary,
|
|
),
|
|
title: const Text('Linked'),
|
|
subtitle: Text(
|
|
bank.status.accountId == null
|
|
? bank.status.status
|
|
: 'Account ${bank.status.accountId}',
|
|
),
|
|
),
|
|
if (bank.error != null) ...[
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
bank.error!,
|
|
style: theme.textTheme.bodyMedium?.copyWith(color: colors.error),
|
|
),
|
|
],
|
|
const SizedBox(height: 12),
|
|
OutlinedButton.icon(
|
|
onPressed: bank.isConnecting
|
|
? null
|
|
: () => runConnectBank(context, bank),
|
|
icon: const Icon(Icons.link),
|
|
label: Text(bank.isConnecting ? 'Opening…' : 'Reconnect bank'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|