dev
dart 417 lines 15 KB
Raw
1 import 'package:cake_wallet/entities/qr_view_data.dart';
2 import 'package:cake_wallet/generated/i18n.dart';
3 import 'package:cake_wallet/routes.dart';
4 import 'package:cake_wallet/src/screens/base_page.dart';
5 import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
6 import 'package:cake_wallet/src/widgets/primary_button.dart';
7 import 'package:cake_wallet/src/widgets/seedphrase_grid_widget.dart';
8 import 'package:cake_wallet/src/widgets/text_info_box.dart';
9 import 'package:cake_wallet/src/widgets/warning_box_widget.dart';
10 import 'package:cake_wallet/utils/brightness_util.dart';
11 import 'package:cake_wallet/utils/clipboard_util.dart';
12 import 'package:cake_wallet/utils/show_bar.dart';
13 import 'package:cake_wallet/view_model/wallet_keys_view_model.dart';
14 import 'package:flutter/material.dart';
15 import 'package:flutter/services.dart';
16 import 'package:flutter_mobx/flutter_mobx.dart';
17 import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
18 import 'package:qr_flutter/qr_flutter.dart';
19
20 class WalletKeysPage extends BasePage {
21 WalletKeysPage(this.walletKeysViewModel);
22
23 @override
24 String get title => walletKeysViewModel.title;
25
26 final WalletKeysViewModel walletKeysViewModel;
27
28 @override
29 Widget body(BuildContext context) {
30 return Container(
31 padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
32 child: Column(
33 children: [
34 Padding(
35 padding: EdgeInsets.only(left: 14, right: 14, bottom: 8),
36 child: WarningBox(
37 key: const ValueKey('wallet_keys_page_share_warning_text_key'),
38 content: S.of(context).do_not_share_warning_text.toUpperCase(),
39 ),
40 ),
41 Expanded(
42 child: WalletKeysPageBody(
43 walletKeysViewModel: walletKeysViewModel,
44 ),
45 ),
46 ],
47 ),
48 );
49 }
50 }
51
52 class WalletKeysPageBody extends StatefulWidget {
53 WalletKeysPageBody({
54 required this.walletKeysViewModel,
55 });
56
57 final WalletKeysViewModel walletKeysViewModel;
58
59 @override
60 State<StatefulWidget> createState() => _WalletKeysPageBodyState();
61 }
62
63 class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
64 with SingleTickerProviderStateMixin {
65 late TabController _tabController;
66 late bool showKeyTab;
67 late bool showSilentPaymentsTab;
68 late bool showLegacySeedTab;
69 late bool isLegacySeedOnly;
70
71 bool get _hasSeeds =>
72 widget.walletKeysViewModel.legacySeedSplit.length > 10 ||
73 widget.walletKeysViewModel.seedSplit.length > 10;
74
75 @override
76 void initState() {
77 super.initState();
78
79 showKeyTab = widget.walletKeysViewModel.items.isNotEmpty;
80 showSilentPaymentsTab =
81 widget.walletKeysViewModel.isBitcoin && widget.walletKeysViewModel.silentPaymentItems.isNotEmpty;
82 showLegacySeedTab = widget.walletKeysViewModel.legacySeedSplit.isNotEmpty;
83 isLegacySeedOnly = widget.walletKeysViewModel.isLegacySeedOnly;
84
85 final totalTabs = (_hasSeeds ? 1 : 0) +
86 (showKeyTab ? 1 : 0) +
87 (showLegacySeedTab ? 1 : 0) +
88 (showSilentPaymentsTab ? 1 : 0);
89
90 _tabController = TabController(length: totalTabs, vsync: this);
91 }
92
93 @override
94 void dispose() {
95 _tabController.dispose();
96 super.dispose();
97 }
98
99 @override
100 Widget build(BuildContext context) {
101 return Container(
102 padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
103 child: Column(
104 children: [
105 Padding(
106 padding: const EdgeInsets.only(left: 22, right: 22, top: 0),
107 child: TabBar(
108 controller: _tabController,
109 splashFactory: NoSplash.splashFactory,
110 indicatorSize: TabBarIndicatorSize.label,
111 isScrollable: true,
112 labelStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
113 fontSize: 18,
114 fontWeight: FontWeight.w600,
115 color: Theme.of(context).colorScheme.onSurfaceVariant,
116 ),
117 unselectedLabelStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
118 fontSize: 18,
119 fontWeight: FontWeight.w600,
120 color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.5),
121 ),
122 labelColor: Theme.of(context).colorScheme.onSurfaceVariant,
123 indicatorColor: Theme.of(context).colorScheme.onSurfaceVariant,
124 indicatorPadding: EdgeInsets.zero,
125 labelPadding: const EdgeInsets.only(right: 24),
126 tabAlignment: TabAlignment.start,
127 dividerColor: Colors.transparent,
128 padding: EdgeInsets.zero,
129 tabs: [
130 if (_hasSeeds)
131 Tab(text: S.of(context).widgets_seed, key: ValueKey('wallet_keys_page_seed')),
132 if (showKeyTab)
133 Tab(
134 text: S.of(context).keys,
135 key: ValueKey('wallet_keys_page_keys'),
136 ),
137 if (showSilentPaymentsTab)
138 Tab(
139 text: S.of(context).silent_payments,
140 key: ValueKey('wallet_keys_silent_payments_keys'),
141 ),
142 if (showLegacySeedTab)
143 Tab(text: S.of(context).legacy, key: ValueKey('wallet_keys_page_seed_legacy')),
144 ],
145 ),
146 ),
147 const SizedBox(height: 20),
148 Expanded(
149 child: TabBarView(
150 controller: _tabController,
151 children: [
152 if (_hasSeeds)
153 Padding(
154 padding: const EdgeInsets.only(left: 22, right: 22),
155 child: _buildSeedTab(context, false),
156 ),
157 if (showKeyTab)
158 Padding(
159 padding: const EdgeInsets.only(left: 22, right: 22),
160 child: _buildKeysTab(context, widget.walletKeysViewModel.items),
161 ),
162 if (showSilentPaymentsTab)
163 Padding(
164 padding: const EdgeInsets.only(left: 22, right: 22),
165 child: _buildKeysTab(context, widget.walletKeysViewModel.silentPaymentItems),
166 ),
167 if (showLegacySeedTab)
168 Padding(
169 padding: const EdgeInsets.only(left: 22, right: 22),
170 child: _buildSeedTab(context, showLegacySeedTab),
171 ),
172 ],
173 ),
174 ),
175 ],
176 ),
177 );
178 }
179
180 Widget _buildSeedTab(BuildContext context, bool isLegacySeed) {
181 return Column(
182 children: [
183 if (isLegacySeedOnly || isLegacySeed || widget.walletKeysViewModel.shouldShowHeightBox) ...[
184 _buildHeightBox(),
185 const SizedBox(height: 20),
186 ],
187 (_buildPassphraseBox() ?? Container()),
188 if (widget.walletKeysViewModel.passphrase.isNotEmpty) const SizedBox(height: 20),
189 Expanded(
190 child: SeedPhraseGridWidget(
191 list: isLegacySeed
192 ? widget.walletKeysViewModel.legacySeedSplit
193 : widget.walletKeysViewModel.seedSplit,
194 ),
195 ),
196 const SizedBox(height: 10),
197 if (_hasSeeds)
198 _buildBottomActionPanel(
199 titleForClipboard: S.of(context).wallet_seed.toLowerCase(),
200 dataToCopy: isLegacySeed
201 ? widget.walletKeysViewModel.legacySeed
202 : widget.walletKeysViewModel.seed,
203 onShowQR: () async => _showQR(context),
204 ),
205 ],
206 );
207 }
208
209 Widget _buildKeysTab(BuildContext context, List<StandartListItem> items) {
210 return Column(
211 children: [
212 Expanded(
213 child: ListView.separated(
214 controller: ModalScrollController.of(context),
215 shrinkWrap: true,
216 itemCount: items.length,
217 itemBuilder: (context, index) {
218 final item = items[index];
219 return TextInfoBox(
220 key: item.key,
221 title: item.title,
222 value: item.value,
223 onCopy: (context) => _onCopy(item.title, item.value, context),
224 );
225 },
226 separatorBuilder: (context, index) => const SizedBox(height: 20),
227 ),
228 ),
229 SizedBox(height: 20),
230 ],
231 );
232 }
233
234 Widget _buildHeightBox() {
235 return Container(
236 padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 14),
237 decoration: BoxDecoration(
238 borderRadius: BorderRadius.circular(18),
239 color: Theme.of(context).colorScheme.surfaceContainer,
240 ),
241 child: Row(
242 mainAxisAlignment: MainAxisAlignment.center,
243 children: [
244 Text(
245 S.of(context).block_height,
246 textAlign: TextAlign.center,
247 style: Theme.of(context).textTheme.bodyMedium!.copyWith(
248 fontWeight: FontWeight.w500,
249 color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.5),
250 ),
251 ),
252 const SizedBox(width: 6),
253 Expanded(
254 child: FutureBuilder<String?>(
255 future: widget.walletKeysViewModel.restoreHeight,
256 builder: (context, snapshot) {
257 final textToDisplay = snapshot.connectionState == ConnectionState.waiting
258 ? 'Fetching...'
259 : (snapshot.data ?? '---');
260
261 return Row(
262 mainAxisAlignment: MainAxisAlignment.spaceBetween,
263 children: [
264 Text(
265 textToDisplay,
266 style: Theme.of(context).textTheme.bodyMedium!.copyWith(
267 fontWeight: FontWeight.w500,
268 color: Theme.of(context).colorScheme.onSurfaceVariant,
269 ),
270 ),
271 if (snapshot.connectionState == ConnectionState.done && snapshot.data != null)
272 GestureDetector(
273 onTap: () => _onCopy(S.of(context).block_height, snapshot.data!, context),
274 child: Icon(
275 Icons.copy,
276 size: 16,
277 color: Theme.of(context).textTheme.bodyLarge?.color?.withOpacity(0.7),
278 ),
279 ),
280 ],
281 );
282 },
283 ),
284 ),
285 ],
286 ),
287 );
288 }
289
290 Widget? _buildPassphraseBox() {
291 if (widget.walletKeysViewModel.passphrase.isEmpty) return null;
292 return Container(
293 padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 14),
294 decoration: BoxDecoration(
295 borderRadius: BorderRadius.circular(18),
296 color: Theme.of(context).colorScheme.surfaceContainer,
297 ),
298 child: Row(
299 crossAxisAlignment: CrossAxisAlignment.start,
300 mainAxisAlignment: MainAxisAlignment.center,
301 children: [
302 Text(
303 S.of(context).passphrase_view_keys,
304 textAlign: TextAlign.center,
305 style: Theme.of(context).textTheme.bodyMedium!.copyWith(
306 fontWeight: FontWeight.w500,
307 color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.5),
308 ),
309 ),
310 const SizedBox(width: 6),
311 Expanded(
312 child: Row(
313 crossAxisAlignment: CrossAxisAlignment.start,
314 mainAxisAlignment: MainAxisAlignment.spaceBetween,
315 children: [
316 Expanded(
317 child: Observer(
318 builder: (BuildContext context) {
319 return Text(
320 (widget.walletKeysViewModel.obscurePassphrase)
321 ? "*****"
322 : widget.walletKeysViewModel.passphrase,
323 style: Theme.of(context).textTheme.bodyMedium!.copyWith(
324 fontWeight: FontWeight.w500,
325 color: Theme.of(context).colorScheme.onSurfaceVariant,
326 ),
327 );
328 },
329 ),
330 ),
331 const SizedBox(width: 6),
332 Observer(
333 builder: (BuildContext context) {
334 return GestureDetector(
335 onTap: () {
336 widget.walletKeysViewModel.obscurePassphrase =
337 !widget.walletKeysViewModel.obscurePassphrase;
338 },
339 child: Icon(
340 widget.walletKeysViewModel.obscurePassphrase
341 ? Icons.visibility_off
342 : Icons.visibility,
343 size: 16,
344 color: Theme.of(context).textTheme.bodyLarge?.color?.withOpacity(0.7),
345 ),
346 );
347 },
348 ),
349 ],
350 ),
351 ),
352 ],
353 ),
354 );
355 }
356
357 Widget _buildBottomActionPanel({
358 required String titleForClipboard,
359 required String dataToCopy,
360 required VoidCallback onShowQR,
361 }) {
362 return Column(
363 children: [
364 Padding(
365 padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
366 child: Row(
367 mainAxisSize: MainAxisSize.max,
368 children: [
369 Flexible(
370 child: Container(
371 padding: const EdgeInsets.only(right: 8.0, top: 8.0),
372 child: PrimaryButton(
373 key: const ValueKey('wallet_keys_page_copy_seeds_button_key'),
374 onPressed: () => _onCopy(titleForClipboard, dataToCopy, context),
375 text: S.of(context).copy,
376 color: Theme.of(context).colorScheme.surfaceContainer,
377 textColor: Theme.of(context).colorScheme.onSecondaryContainer,
378 ),
379 ),
380 ),
381 Flexible(
382 child: Container(
383 padding: const EdgeInsets.only(left: 8.0, top: 8.0),
384 child: PrimaryButton(
385 key: const ValueKey('wallet_keys_page_show_qr_seeds_button_key'),
386 onPressed: onShowQR,
387 text: S.current.show + ' QR',
388 color: Theme.of(context).colorScheme.primary,
389 textColor: Theme.of(context).colorScheme.onPrimary,
390 ),
391 ),
392 ),
393 ],
394 ),
395 ),
396 const SizedBox(height: 12),
397 ],
398 );
399 }
400
401 Future<void> _onCopy(String title, String text, BuildContext context) async {
402 await ClipboardUtil.setSensitiveDataToClipboard(ClipboardData(text: text));
403 showBar<void>(context, S.of(context).copied_key_to_clipboard(title));
404 }
405
406 Future<void> _showQR(BuildContext context) async {
407 final url = await widget.walletKeysViewModel.getUrl(false);
408
409 BrightnessUtil.changeBrightnessForFunction(() async {
410 await Navigator.pushNamed(
411 context,
412 Routes.fullscreenQR,
413 arguments: QrViewData(data: url.toString(), version: QrVersions.auto),
414 );
415 });
416 }
417 }