Skip to content

Commit 1db50d8

Browse files
authored
feat: add WalletService
* feat: Add WalletService with createWallet logic * Implement wallet service create/load flows * Add wallet setup pages and wire routes * Handle unmounted wallet flows and add regression tests * Fix for CI throwing connectivity_plus package by pinning the package
1 parent 8ab1fa5 commit 1db50d8

17 files changed

Lines changed: 1647 additions & 13 deletions

bdk_demo/lib/core/router/app_router.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import 'package:go_router/go_router.dart';
22
import 'package:bdk_demo/features/shared/widgets/placeholder_page.dart';
3+
import 'package:bdk_demo/features/wallet_setup/active_wallets_page.dart';
4+
import 'package:bdk_demo/features/wallet_setup/create_wallet_page.dart';
35
import 'package:bdk_demo/features/wallet_setup/wallet_choice_page.dart';
46

57
abstract final class AppRoutes {
@@ -30,14 +32,12 @@ GoRouter createRouter() => GoRouter(
3032
GoRoute(
3133
path: AppRoutes.activeWallets,
3234
name: 'activeWallets',
33-
builder: (context, state) =>
34-
const PlaceholderPage(title: 'Active Wallets'),
35+
builder: (context, state) => const ActiveWalletsPage(),
3536
),
3637
GoRoute(
3738
path: AppRoutes.createWallet,
3839
name: 'createWallet',
39-
builder: (context, state) =>
40-
const PlaceholderPage(title: 'Create Wallet'),
40+
builder: (context, state) => const CreateWalletPage(),
4141
),
4242
GoRoute(
4343
path: AppRoutes.recoverWallet,

bdk_demo/lib/features/shared/widgets/secondary_app_bar.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:flutter/material.dart';
2+
import 'package:go_router/go_router.dart';
23

34
class SecondaryAppBar extends StatelessWidget implements PreferredSizeWidget {
45
final String title;
@@ -10,7 +11,15 @@ class SecondaryAppBar extends StatelessWidget implements PreferredSizeWidget {
1011
return AppBar(
1112
leading: IconButton(
1213
icon: const Icon(Icons.arrow_back),
13-
onPressed: () => Navigator.of(context).maybePop(),
14+
onPressed: () {
15+
final navigator = Navigator.of(context);
16+
if (navigator.canPop()) {
17+
navigator.pop();
18+
return;
19+
}
20+
21+
context.go('/');
22+
},
1423
),
1524
title: Text(title),
1625
centerTitle: true,
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import 'package:bdk_demo/core/router/app_router.dart';
2+
import 'package:bdk_demo/features/shared/widgets/secondary_app_bar.dart';
3+
import 'package:bdk_demo/models/wallet_record.dart';
4+
import 'package:bdk_demo/providers/wallet_providers.dart';
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter_riverpod/flutter_riverpod.dart';
7+
import 'package:go_router/go_router.dart';
8+
9+
class ActiveWalletsPage extends ConsumerStatefulWidget {
10+
const ActiveWalletsPage({super.key});
11+
12+
@override
13+
ConsumerState<ActiveWalletsPage> createState() => _ActiveWalletsPageState();
14+
}
15+
16+
class _ActiveWalletsPageState extends ConsumerState<ActiveWalletsPage> {
17+
String? _loadingWalletId;
18+
19+
Future<void> _onLoadWallet(WalletRecord record) async {
20+
if (_loadingWalletId != null) return;
21+
22+
setState(() => _loadingWalletId = record.id);
23+
final walletDisposer = ref.read(walletDisposerProvider);
24+
25+
try {
26+
final wallet = await ref
27+
.read(walletServiceProvider)
28+
.loadWalletFromRecord(record);
29+
30+
if (!mounted) {
31+
walletDisposer(wallet);
32+
return;
33+
}
34+
35+
ref.read(activeWalletProvider.notifier).set(wallet);
36+
ref.read(activeWalletRecordProvider.notifier).set(record);
37+
context.go(AppRoutes.home);
38+
} on StateError {
39+
if (!mounted) return;
40+
_showSnackBar('Secrets not found for this wallet');
41+
} catch (_) {
42+
if (!mounted) return;
43+
_showSnackBar('Failed to load wallet. Please try again.');
44+
} finally {
45+
if (mounted) setState(() => _loadingWalletId = null);
46+
}
47+
}
48+
49+
void _showSnackBar(String message) {
50+
ScaffoldMessenger.of(context)
51+
..hideCurrentSnackBar()
52+
..showSnackBar(SnackBar(content: Text(message)));
53+
}
54+
55+
@override
56+
Widget build(BuildContext context) {
57+
final records = ref.watch(walletRecordsProvider);
58+
final theme = Theme.of(context);
59+
60+
return Scaffold(
61+
appBar: const SecondaryAppBar(title: 'Active Wallets'),
62+
body: records.isEmpty
63+
? _buildEmptyState(theme)
64+
: _buildWalletList(records, theme),
65+
);
66+
}
67+
68+
Widget _buildEmptyState(ThemeData theme) {
69+
return Center(
70+
child: Padding(
71+
padding: const EdgeInsets.symmetric(horizontal: 32),
72+
child: Column(
73+
mainAxisAlignment: MainAxisAlignment.center,
74+
children: [
75+
Icon(
76+
Icons.account_balance_wallet_outlined,
77+
size: 64,
78+
color: theme.colorScheme.onSurface.withAlpha(102),
79+
),
80+
const SizedBox(height: 16),
81+
Text(
82+
'No wallets yet',
83+
style: theme.textTheme.titleMedium?.copyWith(
84+
color: theme.colorScheme.onSurface.withAlpha(153),
85+
),
86+
),
87+
const SizedBox(height: 24),
88+
FilledButton.tonal(
89+
onPressed: () => context.push(AppRoutes.createWallet),
90+
child: const Text('Create a Wallet'),
91+
),
92+
],
93+
),
94+
),
95+
);
96+
}
97+
98+
Widget _buildWalletList(List<WalletRecord> records, ThemeData theme) {
99+
return ListView.separated(
100+
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
101+
itemCount: records.length,
102+
separatorBuilder: (_, __) => const SizedBox(height: 8),
103+
itemBuilder: (context, index) {
104+
final record = records[index];
105+
final isLoading = _loadingWalletId == record.id;
106+
final isDisabled = _loadingWalletId != null;
107+
108+
return _WalletCard(
109+
record: record,
110+
isLoading: isLoading,
111+
isDisabled: isDisabled,
112+
onTap: () => _onLoadWallet(record),
113+
);
114+
},
115+
);
116+
}
117+
}
118+
119+
class _WalletCard extends StatelessWidget {
120+
final WalletRecord record;
121+
final bool isLoading;
122+
final bool isDisabled;
123+
final VoidCallback onTap;
124+
125+
const _WalletCard({
126+
required this.record,
127+
required this.isLoading,
128+
required this.isDisabled,
129+
required this.onTap,
130+
});
131+
132+
@override
133+
Widget build(BuildContext context) {
134+
final theme = Theme.of(context);
135+
136+
return Card(
137+
child: InkWell(
138+
onTap: isDisabled ? null : onTap,
139+
borderRadius: BorderRadius.circular(16),
140+
child: Padding(
141+
padding: const EdgeInsets.all(20),
142+
child: Row(
143+
children: [
144+
Icon(
145+
Icons.account_balance_wallet,
146+
size: 36,
147+
color: theme.colorScheme.primary,
148+
),
149+
const SizedBox(width: 16),
150+
Expanded(
151+
child: Column(
152+
crossAxisAlignment: CrossAxisAlignment.start,
153+
children: [
154+
Text(
155+
record.name,
156+
style: theme.textTheme.titleMedium?.copyWith(
157+
fontWeight: FontWeight.w600,
158+
),
159+
),
160+
const SizedBox(height: 8),
161+
Wrap(
162+
spacing: 8,
163+
children: [
164+
Chip(
165+
label: Text(
166+
record.network.displayName,
167+
style: theme.textTheme.labelSmall,
168+
),
169+
visualDensity: VisualDensity.compact,
170+
materialTapTargetSize:
171+
MaterialTapTargetSize.shrinkWrap,
172+
),
173+
Chip(
174+
label: Text(
175+
record.scriptType.shortName,
176+
style: theme.textTheme.labelSmall,
177+
),
178+
visualDensity: VisualDensity.compact,
179+
materialTapTargetSize:
180+
MaterialTapTargetSize.shrinkWrap,
181+
),
182+
],
183+
),
184+
],
185+
),
186+
),
187+
if (isLoading)
188+
const SizedBox(
189+
width: 24,
190+
height: 24,
191+
child: CircularProgressIndicator(strokeWidth: 2),
192+
)
193+
else
194+
Icon(
195+
Icons.chevron_right,
196+
color: theme.colorScheme.onSurface.withAlpha(102),
197+
),
198+
],
199+
),
200+
),
201+
),
202+
);
203+
}
204+
}

0 commit comments

Comments
 (0)