Qr code passphrase restoration flow fix (#1694)

* add passphrase to credentials * prevent double restoring * fix conflict, simplify restricting user from tapping restore multiple times --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

Serhii committed Sep 24, 2024 at 06:40 UTC cf1e8a306c3117feb3be42c81c5abe0fd2fe7769
5 files changed +100 -42
lib/src/screens/restore/restore_options_page.dart
+61 -35
@@ -31,30 +31,45 @@ class RestoreOptionsPage extends BasePage {
31
32 final bool isNewInstall;
33
34 + @override
35 + Widget body(BuildContext context) {
36 + return _RestoreOptionsBody(isNewInstall: isNewInstall, themeType: currentTheme.type);
37 + }
38 +}
39 +
40 +class _RestoreOptionsBody extends StatefulWidget {
41 + const _RestoreOptionsBody({required this.isNewInstall, required this.themeType});
42 +
43 + final bool isNewInstall;
44 + final ThemeType themeType;
45 +
46 + @override
47 + _RestoreOptionsBodyState createState() => _RestoreOptionsBodyState();
48 +}
49 +
50 +class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> {
51 + bool isRestoring = false;
52 +
53 bool get _doesSupportHardwareWallets {
54 if (!DeviceInfo.instance.isMobile) {
55 return false;
56 }
57
58 if (isMoneroOnly) {
40 - return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS)
41 - .isNotEmpty;
59 + return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS).isNotEmpty;
60 }
61
62 return true;
63 }
64
65 @override
48 - Widget body(BuildContext context) {
66 + Widget build(BuildContext context) {
67 final mainImageColor = Theme.of(context).extension<DashboardPageTheme>()!.pageTitleTextColor;
68 final brightImageColor = Theme.of(context).extension<InfoTheme>()!.textColor;
51 - final imageColor = currentTheme.type == ThemeType.bright ? brightImageColor : mainImageColor;
69 + final imageColor = widget.themeType == ThemeType.bright ? brightImageColor : mainImageColor;
70 final imageLedger = Image.asset('assets/images/ledger_nano.png', width: 40, color: imageColor);
71 final imageSeedKeys = Image.asset('assets/images/restore_wallet_image.png', color: imageColor);
72 final imageBackup = Image.asset('assets/images/backup.png', color: imageColor);
55 - final qrCode = Image.asset('assets/images/restore_qr.png', color: imageColor);
56 -
57 -
73
74 return Center(
75 child: Container(
@@ -66,16 +81,17 @@ class RestoreOptionsPage extends BasePage {
81 children: <Widget>[
82 OptionTile(
83 key: ValueKey('restore_options_from_seeds_button_key'),
69 - onPressed: () => Navigator.pushNamed(
70 - context,
71 - Routes.restoreWalletFromSeedKeys,
72 - arguments: isNewInstall,
73 - ),
84 + onPressed: () =>
85 + Navigator.pushNamed(
86 + context,
87 + Routes.restoreWalletFromSeedKeys,
88 + arguments: widget.isNewInstall,
89 + ),
90 image: imageSeedKeys,
91 title: S.of(context).restore_title_from_seed_keys,
92 description: S.of(context).restore_description_from_seed_keys,
93 ),
78 - if (isNewInstall)
94 + if (widget.isNewInstall)
95 Padding(
96 padding: EdgeInsets.only(top: 24),
97 child: OptionTile(
@@ -91,9 +107,8 @@ class RestoreOptionsPage extends BasePage {
107 padding: EdgeInsets.only(top: 24),
108 child: OptionTile(
109 key: ValueKey('restore_options_from_hardware_wallet_button_key'),
94 - onPressed: () => Navigator.pushNamed(
95 - context, Routes.restoreWalletFromHardwareWallet,
96 - arguments: isNewInstall),
110 + onPressed: () => Navigator.pushNamed(context, Routes.restoreWalletFromHardwareWallet,
111 + arguments: widget.isNewInstall),
112 image: imageLedger,
113 title: S.of(context).restore_title_from_hardware_wallet,
114 description: S.of(context).restore_description_from_hardware_wallet,
@@ -119,36 +134,47 @@ class RestoreOptionsPage extends BasePage {
134 }
135
136 void _onWalletCreateFailure(BuildContext context, String error) {
122 - showPopUp<void>(
123 - context: context,
124 - builder: (BuildContext context) {
125 - return AlertWithOneAction(
126 - alertTitle: S.current.error,
127 - alertContent: error,
128 - buttonText: S.of(context).ok,
129 - buttonAction: () => Navigator.of(context).pop());
130 - });
137 + setState(() {
138 + isRestoring = false;
139 + });
140 +
141 + WidgetsBinding.instance.addPostFrameCallback((_) {
142 + showPopUp<void>(
143 + context: context,
144 + builder: (BuildContext context) {
145 + return AlertWithOneAction(
146 + alertTitle: S.current.error,
147 + alertContent: error,
148 + buttonText: S.of(context).ok,
149 + buttonAction: () => Navigator.of(context).pop());
150 + });
151 + });
152 +
153 }
154
155 Future<void> _onScanQRCode(BuildContext context) async {
134 - final isCameraPermissionGranted =
135 - await PermissionHandler.checkPermission(Permission.camera, context);
156 + final isCameraPermissionGranted = await PermissionHandler.checkPermission(Permission.camera, context);
157
158 if (!isCameraPermissionGranted) return;
159 bool isPinSet = false;
139 - if (isNewInstall) {
160 + if (widget.isNewInstall) {
161 await Navigator.pushNamed(context, Routes.setupPin,
162 arguments: (PinCodeState<PinCodeWidget> setupPinContext, String _) {
142 - setupPinContext.close();
143 - isPinSet = true;
144 - });
163 + setupPinContext.close();
164 + isPinSet = true;
165 + });
166 }
146 - if (!isNewInstall || isPinSet) {
167 + if (!widget.isNewInstall || isPinSet) {
168 try {
169 + if (isRestoring) {
170 + return;
171 + }
172 + setState(() {
173 + isRestoring = true;
174 + });
175 final restoreWallet = await WalletRestoreFromQRCode.scanQRCodeForRestoring(context);
176
150 - final restoreFromQRViewModel =
151 - getIt.get<WalletRestorationFromQRVM>(param1: restoreWallet.type);
177 + final restoreFromQRViewModel = getIt.get<WalletRestorationFromQRVM>(param1: restoreWallet.type);
178
179 await restoreFromQRViewModel.create(restoreWallet: restoreWallet);
180 if (restoreFromQRViewModel.state is FailureState) {
@@ -160,4 +186,4 @@ class RestoreOptionsPage extends BasePage {
186 }
187 }
188 }
163 -}
189 +}
\ No newline at end of file
lib/src/widgets/alert_with_no_action.dart.dart new
+30
@@ -0,0 +1,30 @@
1 +import 'package:flutter/material.dart';
2 +import 'package:cake_wallet/src/widgets/base_alert_dialog.dart';
3 +
4 +class AlertWithNoAction extends BaseAlertDialog {
5 + AlertWithNoAction({
6 + required this.alertTitle,
7 + required this.alertContent,
8 + this.alertBarrierDismissible = true,
9 + Key? key,
10 + });
11 +
12 + final String alertTitle;
13 + final String alertContent;
14 + final bool alertBarrierDismissible;
15 +
16 + @override
17 + String get titleText => alertTitle;
18 +
19 + @override
20 + String get contentText => alertContent;
21 +
22 + @override
23 + bool get barrierDismissible => alertBarrierDismissible;
24 +
25 + @override
26 + bool get isBottomDividerExists => false;
27 +
28 + @override
29 + Widget actionButtons(BuildContext context) => Container(height: 60);
30 +}
lib/src/widgets/base_alert_dialog.dart
+3 -1
@@ -17,6 +17,8 @@ class BaseAlertDialog extends StatelessWidget {
17
18 bool get isDividerExists => false;
19
20 + bool get isBottomDividerExists => true;
21 +
22 VoidCallback get actionLeft => () {};
23
24 VoidCallback get actionRight => () {};
@@ -205,7 +207,7 @@ class BaseAlertDialog extends StatelessWidget {
207 )
208 ],
209 ),
208 - const HorizontalSectionDivider(),
210 + if (isBottomDividerExists) const HorizontalSectionDivider(),
211 ClipRRect(
212 borderRadius: BorderRadius.all(Radius.circular(30)),
213 child: actionButtons(context))
lib/view_model/restore/restore_from_qr_vm.dart
+2 -6
@@ -56,12 +56,8 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store
56 WalletCredentials getCredentialsFromRestoredWallet(
57 dynamic options, RestoredWallet restoreWallet) {
58 final password = generateWalletPassword();
59 - String? passphrase;
59 +
60 DerivationInfo? derivationInfo;
61 - if (options != null) {
62 - derivationInfo = options["derivationInfo"] as DerivationInfo?;
63 - passphrase = options["passphrase"] as String?;
64 - }
61 derivationInfo ??= getDefaultCreateDerivation();
62
63 switch (restoreWallet.restoreMode) {
@@ -119,7 +115,7 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store
115 name: name,
116 mnemonic: restoreWallet.mnemonicSeed ?? '',
117 password: password,
122 - passphrase: passphrase,
118 + passphrase: restoreWallet.passphrase,
119 derivationType: derivationInfo!.derivationType!,
120 derivationPath: derivationInfo.derivationPath!,
121 );
lib/view_model/restore/restore_wallet.dart
+4
@@ -10,6 +10,7 @@ class RestoredWallet {
10 this.spendKey,
11 this.viewKey,
12 this.mnemonicSeed,
13 + this.passphrase,
14 this.txAmount,
15 this.txDescription,
16 this.recipientName,
@@ -23,6 +24,7 @@ class RestoredWallet {
24 final String? spendKey;
25 final String? viewKey;
26 final String? mnemonicSeed;
27 + final String? passphrase;
28 final String? txAmount;
29 final String? txDescription;
30 final String? recipientName;
@@ -46,11 +48,13 @@ class RestoredWallet {
48 final height = json['height'] as String?;
49 final mnemonic_seed = json['mnemonic_seed'] as String?;
50 final seed = json['seed'] as String? ?? json['hexSeed'] as String?;
51 + final passphrase = json['passphrase'] as String?;
52 return RestoredWallet(
53 restoreMode: json['mode'] as WalletRestoreMode,
54 type: json['type'] as WalletType,
55 address: json['address'] as String?,
56 mnemonicSeed: mnemonic_seed ?? seed,
57 + passphrase: passphrase,
58 height: height != null ? int.parse(height) : 0,
59 );
60 }