CakeWallet/lib/src/screens/new_wallet/new_wallet_page.dart
Konstantin Ullrich f58a5fb8fd
CW-723-Add-Monero-support-to-the-Shared-Seed-feature-in-Cake (#2131)
* feat: add exodus style bip39 to monero legacy seed

* feat: restore monero wallet from bip39 and add test

* bug: fix wrong naming in CI

* feat: add monero bip39 UI flow

* fix: monero.dart generation

* fix: skip monero_wallet_service tests till CI is fixed

* ci: copy monero_libwallet2_api_c.so to /usr/lib for testing
ci: reduce timeout for cw_monero tests

* fix: monero wallet creation credentials default to bip39 if mnemonic are set

* fix: do not skip monero wallets services test

* fix: Include non bip39 monero wallets on Wallet Group

* fix: null pointer stemming from missing language selector if seed is selected

* fix: Fixes to Bip39 Creation and restore

- Do not restore from 0 for fresh bip39 wallet
- disallow restoring bip39 wallet without date or height

* fix: Fixes to Bip39 restore

- Refresh height is now getting set correctly
- Add new create monero wallet tests
- Add seed-language English for Bip39 Monero wallets
- Fix seed-type naming

* feat (cw_monero): Store monero wallet after bip39 creation

* feat (cw_monero): remove prints from monero_wallet_service_test.dart

* fix: exception during seed language autodetect

* feat (cw_monero): Add support for passphrases on bip39 seeds

* feat (cw_monero): Add support for passphrases on bip39 seeds

* fix: seed language selection for recovering bip39 wallets

* style: improve readability of isLegacySeedOnly in wallet_keys_view_model.dart

* feat: hide monero seed type selector from advanced settings when creating a child wallet

* fix(cw_monero): use named arguments for bip39_seed tests

---------

Co-authored-by: cyan <cyjan@mrcyjanek.net>
2025-04-10 03:31:26 +02:00

424 lines
19 KiB
Dart

import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/core/wallet_name_validator.dart';
import 'package:cake_wallet/entities/generate_name.dart';
import 'package:cake_wallet/entities/seed_type.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/main.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/picker.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/widgets/seed_language_picker.dart';
import 'package:cake_wallet/src/widgets/seed_language_selector.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/new_wallet_theme.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/seed_settings_view_model.dart';
import 'package:cake_wallet/view_model/wallet_new_vm.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
class NewWalletPage extends BasePage {
NewWalletPage(
this._walletNewVM,
this._seedSettingsViewModel, {
this.isChildWallet = false,
});
final WalletNewVM _walletNewVM;
final SeedSettingsViewModel _seedSettingsViewModel;
final bool isChildWallet;
final walletNameImage = Image.asset('assets/images/wallet_name.png');
final walletNameLightImage = Image.asset('assets/images/wallet_name_light.png');
@override
String get title => S.current.new_wallet;
@override
Function(BuildContext)? get pushToNextWidget => (context) {
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) {
currentFocus.focusedChild?.unfocus();
}
};
@override
Widget body(BuildContext context) => WalletNameForm(
_walletNewVM,
currentTheme.type == ThemeType.dark ? walletNameImage : walletNameLightImage,
_seedSettingsViewModel,
isChildWallet,
);
}
class WalletNameForm extends StatefulWidget {
WalletNameForm(
this._walletNewVM,
this.walletImage,
this._seedSettingsViewModel,
this.isChildWallet,
);
final WalletNewVM _walletNewVM;
final bool isChildWallet;
final Image walletImage;
final SeedSettingsViewModel _seedSettingsViewModel;
@override
_WalletNameFormState createState() => _WalletNameFormState(_walletNewVM);
}
class _WalletNameFormState extends State<WalletNameForm> {
_WalletNameFormState(this._walletNewVM)
: _formKey = GlobalKey<FormState>(),
_languageSelectorKey = GlobalKey<SeedLanguageSelectorState>(),
_nameController = TextEditingController(),
_passwordController = _walletNewVM.hasWalletPassword ? TextEditingController() : null,
_repeatedPasswordController =
_walletNewVM.hasWalletPassword ? TextEditingController() : null;
static const aspectRatioImage = 1.22;
bool _formProcessing = false;
final GlobalKey<FormState> _formKey;
final GlobalKey<SeedLanguageSelectorState> _languageSelectorKey;
final WalletNewVM _walletNewVM;
final TextEditingController _nameController;
final TextEditingController? _passwordController;
final TextEditingController? _repeatedPasswordController;
ReactionDisposer? _stateReaction;
@override
void initState() {
_stateReaction ??= reaction((_) => _walletNewVM.state, (ExecutionState state) async {
if (state is ExecutedSuccessfullyState) {
if (widget.isChildWallet) {
Navigator.of(navigatorKey.currentContext ?? context)
.pushNamed(Routes.walletGroupExistingSeedDescriptionPage,
arguments: _walletNewVM.seedPhraseWordsLength);
} else {
Navigator.of(navigatorKey.currentContext ?? context)
.pushNamed(Routes.preSeedPage, arguments: _walletNewVM.seedPhraseWordsLength);
}
}
if (state is FailureState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
showPopUp<void>(
context: context,
builder: (_) {
return AlertWithOneAction(
key: ValueKey('new_wallet_page_failure_dialog_key'),
buttonKey: ValueKey('new_wallet_page_failure_dialog_button_key'),
alertTitle: S.current.new_wallet,
alertContent: state.error,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
);
});
}
});
}
});
_setSeedType(MoneroSeedType.defaultSeedType);
super.initState();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(top: 24),
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
content: Center(
child: ConstrainedBox(
constraints:
BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.only(left: 12, right: 12),
child: AspectRatio(
aspectRatio: aspectRatioImage,
child: FittedBox(child: widget.walletImage, fit: BoxFit.fill)),
),
Padding(
padding: EdgeInsets.only(top: 24),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
key: ValueKey('new_wallet_page_wallet_name_textformfield_key'),
onChanged: (value) => _walletNewVM.name = value,
controller: _nameController,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.w600,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor),
decoration: InputDecoration(
hintStyle: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.w500,
color:
Theme.of(context).extension<NewWalletTheme>()!.hintTextColor),
hintText: S.of(context).wallet_name,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context)
.extension<NewWalletTheme>()!
.underlineColor,
width: 1.0)),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context)
.extension<NewWalletTheme>()!
.underlineColor,
width: 1.0),
),
suffixIcon: Semantics(
label: S.of(context).generate_name,
child: IconButton(
key: ValueKey(
'new_wallet_page_wallet_name_textformfield_generate_name_button_key'),
onPressed: () async {
final rName = await generateName();
FocusManager.instance.primaryFocus?.unfocus();
setState(() {
_nameController.text = rName;
_walletNewVM.name = rName;
_nameController.selection = TextSelection.fromPosition(
TextPosition(offset: _nameController.text.length));
});
},
icon: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
color: Theme.of(context).hintColor,
),
width: 34,
height: 34,
child: Image.asset(
'assets/images/refresh_icon.png',
color: Theme.of(context)
.extension<SendPageTheme>()!
.textFieldButtonIconColor,
),
),
),
),
),
validator: WalletNameValidator(),
),
if (_walletNewVM.hasWalletPassword) ...[
TextFormField(
key: ValueKey('password'),
onChanged: (value) => _walletNewVM.walletPassword = value,
controller: _passwordController,
textAlign: TextAlign.center,
obscureText: true,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.w600,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
decoration: InputDecoration(
hintStyle: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.w500,
color:
Theme.of(context).extension<NewWalletTheme>()!.hintTextColor,
),
hintText: S.of(context).password,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context)
.extension<NewWalletTheme>()!
.underlineColor,
width: 1.0,
),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context)
.extension<NewWalletTheme>()!
.underlineColor,
width: 1.0,
),
),
),
),
TextFormField(
key: ValueKey('repeat_wallet_password'),
onChanged: (value) => _walletNewVM.repeatedWalletPassword = value,
controller: _repeatedPasswordController,
textAlign: TextAlign.center,
obscureText: true,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.w600,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
decoration: InputDecoration(
hintStyle: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.w500,
color:
Theme.of(context).extension<NewWalletTheme>()!.hintTextColor,
),
hintText: S.of(context).repeat_wallet_password,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context)
.extension<NewWalletTheme>()!
.underlineColor,
width: 1.0,
),
),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context)
.extension<NewWalletTheme>()!
.underlineColor,
width: 1.0,
),
),
),
),
],
],
),
),
),
if (_walletNewVM.showLanguageSelector) ...[
if (_walletNewVM.hasSeedType) ...[
Observer(
builder: (BuildContext build) => Padding(
padding: EdgeInsets.only(top: 24),
child: SelectButton(
key: ValueKey('new_wallet_page_monero_seed_type_button_key'),
text: widget._seedSettingsViewModel.moneroSeedType.title,
onTap: () async {
await showPopUp<void>(
context: context,
builder: (_) => Picker(
items: MoneroSeedType.all,
selectedAtIndex: isPolyseed ? 1 : 0,
onItemSelected: _setSeedType,
isSeparated: false,
),
);
},
),
),
),
],
Observer(
builder: (BuildContext build) => Padding(
padding: EdgeInsets.only(top: 10),
child: SeedLanguageSelector(
key: _languageSelectorKey,
buttonKey: ValueKey('new_wallet_page_seed_language_selector_button_key'),
initialSelected: defaultSeedLanguage,
seedType: _walletNewVM.hasSeedType
? widget._seedSettingsViewModel.moneroSeedType
: MoneroSeedType.legacy,
),
),
)
],
],
),
),
),
bottomSectionPadding: EdgeInsets.all(24),
bottomSection: Column(
children: [
Observer(
builder: (context) {
return LoadingPrimaryButton(
key: ValueKey('new_wallet_page_confirm_button_key'),
onPressed: _confirmForm,
text: S.of(context).seed_language_next,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
isLoading: _walletNewVM.state is IsExecutingState,
isDisabled: _walletNewVM.name.isEmpty,
);
},
),
const SizedBox(height: 25),
GestureDetector(
key: ValueKey('new_wallet_page_advanced_settings_button_key'),
onTap: () {
Navigator.of(context).pushNamed(Routes.advancedPrivacySettings, arguments: {
"type": _walletNewVM.type,
"useTestnet": _walletNewVM.useTestnet,
"toggleTestnet": _walletNewVM.toggleUseTestnet,
"isChildWallet": widget.isChildWallet,
});
},
child: Text(S.of(context).advanced_settings),
),
],
)),
);
}
void _confirmForm() async {
if (_formProcessing) return;
_formProcessing = true;
try {
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
_formProcessing = false;
return;
}
if (_walletNewVM.nameExists(_walletNewVM.name)) {
await showPopUp<void>(
context: context,
builder: (_) {
return AlertWithOneAction(
alertTitle: '',
alertContent: S.of(context).wallet_name_exists,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop());
});
} else {
await _walletNewVM.create(
options: _walletNewVM.hasLanguageSelector
? [
_languageSelectorKey.currentState?.selected ??
defaultSeedLanguage,
widget._seedSettingsViewModel.moneroSeedType
]
: null);
}
} catch (e) {
_formProcessing = false;
rethrow;
}
_formProcessing = false;
}
bool get isPolyseed => widget._seedSettingsViewModel.moneroSeedType == MoneroSeedType.polyseed;
void _setSeedType(MoneroSeedType item) {
widget._seedSettingsViewModel.setMoneroSeedType(item);
_languageSelectorKey.currentState?.selected = defaultSeedLanguage; // Reset Seed language
}
}