Qr code passphrase restoration flow fix (#1694)
Some checks are pending
Cache Dependencies / test (push) Waiting to run

* 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>
This commit is contained in:
Serhii 2024-09-24 06:40:33 +03:00 committed by GitHub
parent 094b5ec82e
commit cf1e8a306c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 100 additions and 42 deletions

View file

@ -31,30 +31,45 @@ class RestoreOptionsPage extends BasePage {
final bool isNewInstall; final bool isNewInstall;
@override
Widget body(BuildContext context) {
return _RestoreOptionsBody(isNewInstall: isNewInstall, themeType: currentTheme.type);
}
}
class _RestoreOptionsBody extends StatefulWidget {
const _RestoreOptionsBody({required this.isNewInstall, required this.themeType});
final bool isNewInstall;
final ThemeType themeType;
@override
_RestoreOptionsBodyState createState() => _RestoreOptionsBodyState();
}
class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> {
bool isRestoring = false;
bool get _doesSupportHardwareWallets { bool get _doesSupportHardwareWallets {
if (!DeviceInfo.instance.isMobile) { if (!DeviceInfo.instance.isMobile) {
return false; return false;
} }
if (isMoneroOnly) { if (isMoneroOnly) {
return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS) return DeviceConnectionType.supportedConnectionTypes(WalletType.monero, Platform.isIOS).isNotEmpty;
.isNotEmpty;
} }
return true; return true;
} }
@override @override
Widget body(BuildContext context) { Widget build(BuildContext context) {
final mainImageColor = Theme.of(context).extension<DashboardPageTheme>()!.pageTitleTextColor; final mainImageColor = Theme.of(context).extension<DashboardPageTheme>()!.pageTitleTextColor;
final brightImageColor = Theme.of(context).extension<InfoTheme>()!.textColor; final brightImageColor = Theme.of(context).extension<InfoTheme>()!.textColor;
final imageColor = currentTheme.type == ThemeType.bright ? brightImageColor : mainImageColor; final imageColor = widget.themeType == ThemeType.bright ? brightImageColor : mainImageColor;
final imageLedger = Image.asset('assets/images/ledger_nano.png', width: 40, color: imageColor); final imageLedger = Image.asset('assets/images/ledger_nano.png', width: 40, color: imageColor);
final imageSeedKeys = Image.asset('assets/images/restore_wallet_image.png', color: imageColor); final imageSeedKeys = Image.asset('assets/images/restore_wallet_image.png', color: imageColor);
final imageBackup = Image.asset('assets/images/backup.png', color: imageColor); final imageBackup = Image.asset('assets/images/backup.png', color: imageColor);
final qrCode = Image.asset('assets/images/restore_qr.png', color: imageColor);
return Center( return Center(
child: Container( child: Container(
@ -66,16 +81,17 @@ class RestoreOptionsPage extends BasePage {
children: <Widget>[ children: <Widget>[
OptionTile( OptionTile(
key: ValueKey('restore_options_from_seeds_button_key'), key: ValueKey('restore_options_from_seeds_button_key'),
onPressed: () => Navigator.pushNamed( onPressed: () =>
context, Navigator.pushNamed(
Routes.restoreWalletFromSeedKeys, context,
arguments: isNewInstall, Routes.restoreWalletFromSeedKeys,
), arguments: widget.isNewInstall,
),
image: imageSeedKeys, image: imageSeedKeys,
title: S.of(context).restore_title_from_seed_keys, title: S.of(context).restore_title_from_seed_keys,
description: S.of(context).restore_description_from_seed_keys, description: S.of(context).restore_description_from_seed_keys,
), ),
if (isNewInstall) if (widget.isNewInstall)
Padding( Padding(
padding: EdgeInsets.only(top: 24), padding: EdgeInsets.only(top: 24),
child: OptionTile( child: OptionTile(
@ -91,9 +107,8 @@ class RestoreOptionsPage extends BasePage {
padding: EdgeInsets.only(top: 24), padding: EdgeInsets.only(top: 24),
child: OptionTile( child: OptionTile(
key: ValueKey('restore_options_from_hardware_wallet_button_key'), key: ValueKey('restore_options_from_hardware_wallet_button_key'),
onPressed: () => Navigator.pushNamed( onPressed: () => Navigator.pushNamed(context, Routes.restoreWalletFromHardwareWallet,
context, Routes.restoreWalletFromHardwareWallet, arguments: widget.isNewInstall),
arguments: isNewInstall),
image: imageLedger, image: imageLedger,
title: S.of(context).restore_title_from_hardware_wallet, title: S.of(context).restore_title_from_hardware_wallet,
description: S.of(context).restore_description_from_hardware_wallet, description: S.of(context).restore_description_from_hardware_wallet,
@ -119,36 +134,47 @@ class RestoreOptionsPage extends BasePage {
} }
void _onWalletCreateFailure(BuildContext context, String error) { void _onWalletCreateFailure(BuildContext context, String error) {
showPopUp<void>( setState(() {
context: context, isRestoring = false;
builder: (BuildContext context) { });
return AlertWithOneAction(
alertTitle: S.current.error, WidgetsBinding.instance.addPostFrameCallback((_) {
alertContent: error, showPopUp<void>(
buttonText: S.of(context).ok, context: context,
buttonAction: () => Navigator.of(context).pop()); builder: (BuildContext context) {
}); return AlertWithOneAction(
alertTitle: S.current.error,
alertContent: error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
});
} }
Future<void> _onScanQRCode(BuildContext context) async { Future<void> _onScanQRCode(BuildContext context) async {
final isCameraPermissionGranted = final isCameraPermissionGranted = await PermissionHandler.checkPermission(Permission.camera, context);
await PermissionHandler.checkPermission(Permission.camera, context);
if (!isCameraPermissionGranted) return; if (!isCameraPermissionGranted) return;
bool isPinSet = false; bool isPinSet = false;
if (isNewInstall) { if (widget.isNewInstall) {
await Navigator.pushNamed(context, Routes.setupPin, await Navigator.pushNamed(context, Routes.setupPin,
arguments: (PinCodeState<PinCodeWidget> setupPinContext, String _) { arguments: (PinCodeState<PinCodeWidget> setupPinContext, String _) {
setupPinContext.close(); setupPinContext.close();
isPinSet = true; isPinSet = true;
}); });
} }
if (!isNewInstall || isPinSet) { if (!widget.isNewInstall || isPinSet) {
try { try {
if (isRestoring) {
return;
}
setState(() {
isRestoring = true;
});
final restoreWallet = await WalletRestoreFromQRCode.scanQRCodeForRestoring(context); final restoreWallet = await WalletRestoreFromQRCode.scanQRCodeForRestoring(context);
final restoreFromQRViewModel = final restoreFromQRViewModel = getIt.get<WalletRestorationFromQRVM>(param1: restoreWallet.type);
getIt.get<WalletRestorationFromQRVM>(param1: restoreWallet.type);
await restoreFromQRViewModel.create(restoreWallet: restoreWallet); await restoreFromQRViewModel.create(restoreWallet: restoreWallet);
if (restoreFromQRViewModel.state is FailureState) { if (restoreFromQRViewModel.state is FailureState) {
@ -160,4 +186,4 @@ class RestoreOptionsPage extends BasePage {
} }
} }
} }
} }

View file

@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:cake_wallet/src/widgets/base_alert_dialog.dart';
class AlertWithNoAction extends BaseAlertDialog {
AlertWithNoAction({
required this.alertTitle,
required this.alertContent,
this.alertBarrierDismissible = true,
Key? key,
});
final String alertTitle;
final String alertContent;
final bool alertBarrierDismissible;
@override
String get titleText => alertTitle;
@override
String get contentText => alertContent;
@override
bool get barrierDismissible => alertBarrierDismissible;
@override
bool get isBottomDividerExists => false;
@override
Widget actionButtons(BuildContext context) => Container(height: 60);
}

View file

@ -17,6 +17,8 @@ class BaseAlertDialog extends StatelessWidget {
bool get isDividerExists => false; bool get isDividerExists => false;
bool get isBottomDividerExists => true;
VoidCallback get actionLeft => () {}; VoidCallback get actionLeft => () {};
VoidCallback get actionRight => () {}; VoidCallback get actionRight => () {};
@ -205,7 +207,7 @@ class BaseAlertDialog extends StatelessWidget {
) )
], ],
), ),
const HorizontalSectionDivider(), if (isBottomDividerExists) const HorizontalSectionDivider(),
ClipRRect( ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(30)), borderRadius: BorderRadius.all(Radius.circular(30)),
child: actionButtons(context)) child: actionButtons(context))

View file

@ -56,12 +56,8 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store
WalletCredentials getCredentialsFromRestoredWallet( WalletCredentials getCredentialsFromRestoredWallet(
dynamic options, RestoredWallet restoreWallet) { dynamic options, RestoredWallet restoreWallet) {
final password = generateWalletPassword(); final password = generateWalletPassword();
String? passphrase;
DerivationInfo? derivationInfo; DerivationInfo? derivationInfo;
if (options != null) {
derivationInfo = options["derivationInfo"] as DerivationInfo?;
passphrase = options["passphrase"] as String?;
}
derivationInfo ??= getDefaultCreateDerivation(); derivationInfo ??= getDefaultCreateDerivation();
switch (restoreWallet.restoreMode) { switch (restoreWallet.restoreMode) {
@ -119,7 +115,7 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store
name: name, name: name,
mnemonic: restoreWallet.mnemonicSeed ?? '', mnemonic: restoreWallet.mnemonicSeed ?? '',
password: password, password: password,
passphrase: passphrase, passphrase: restoreWallet.passphrase,
derivationType: derivationInfo!.derivationType!, derivationType: derivationInfo!.derivationType!,
derivationPath: derivationInfo.derivationPath!, derivationPath: derivationInfo.derivationPath!,
); );

View file

@ -10,6 +10,7 @@ class RestoredWallet {
this.spendKey, this.spendKey,
this.viewKey, this.viewKey,
this.mnemonicSeed, this.mnemonicSeed,
this.passphrase,
this.txAmount, this.txAmount,
this.txDescription, this.txDescription,
this.recipientName, this.recipientName,
@ -23,6 +24,7 @@ class RestoredWallet {
final String? spendKey; final String? spendKey;
final String? viewKey; final String? viewKey;
final String? mnemonicSeed; final String? mnemonicSeed;
final String? passphrase;
final String? txAmount; final String? txAmount;
final String? txDescription; final String? txDescription;
final String? recipientName; final String? recipientName;
@ -46,11 +48,13 @@ class RestoredWallet {
final height = json['height'] as String?; final height = json['height'] as String?;
final mnemonic_seed = json['mnemonic_seed'] as String?; final mnemonic_seed = json['mnemonic_seed'] as String?;
final seed = json['seed'] as String? ?? json['hexSeed'] as String?; final seed = json['seed'] as String? ?? json['hexSeed'] as String?;
final passphrase = json['passphrase'] as String?;
return RestoredWallet( return RestoredWallet(
restoreMode: json['mode'] as WalletRestoreMode, restoreMode: json['mode'] as WalletRestoreMode,
type: json['type'] as WalletType, type: json['type'] as WalletType,
address: json['address'] as String?, address: json['address'] as String?,
mnemonicSeed: mnemonic_seed ?? seed, mnemonicSeed: mnemonic_seed ?? seed,
passphrase: passphrase,
height: height != null ? int.parse(height) : 0, height: height != null ? int.parse(height) : 0,
); );
} }