diff --git a/cw_monero/ios/Classes/monero_api.cpp b/cw_monero/ios/Classes/monero_api.cpp index 68cf99ee..aa96d765 100644 --- a/cw_monero/ios/Classes/monero_api.cpp +++ b/cw_monero/ios/Classes/monero_api.cpp @@ -219,6 +219,8 @@ extern "C" bool create_wallet(char *path, char *password, char *language, int32_t networkType, char *error) { + Monero::WalletManagerFactory::setLogLevel(4); + Monero::NetworkType _networkType = static_cast(networkType); Monero::WalletManager *walletManager = Monero::WalletManagerFactory::getWalletManager(); Monero::Wallet *wallet = walletManager->createWallet(path, password, language, _networkType); @@ -228,9 +230,9 @@ extern "C" wallet->statusWithErrorString(status, errorString); - if (status != Monero::Wallet::Status_Ok) + if (wallet->status() != Monero::Wallet::Status_Ok) { - error = strdup(errorString.c_str()); + error = strdup(wallet->errorString().c_str()); return false; } @@ -254,7 +256,7 @@ extern "C" wallet->statusWithErrorString(status, errorString); - if (status != Monero::Wallet::Status_Ok) + if (status != Monero::Wallet::Status_Ok || !errorString.empty()) { error = strdup(errorString.c_str()); return false; @@ -282,7 +284,7 @@ extern "C" wallet->statusWithErrorString(status, errorString); - if (status != Monero::Wallet::Status_Ok) + if (status != Monero::Wallet::Status_Ok || !errorString.empty()) { error = strdup(errorString.c_str()); return false; @@ -349,11 +351,13 @@ extern "C" uint64_t get_full_balance(uint32_t account_index) { +// return 0; return get_current_wallet()->balance(account_index); } uint64_t get_unlocked_balance(uint32_t account_index) { +// return 0; return get_current_wallet()->unlockedBalance(account_index); } @@ -681,6 +685,7 @@ extern "C" void on_startup() { Monero::Utils::onStartup(); + Monero::WalletManagerFactory::setLogLevel(4); } void rescan_blockchain() diff --git a/cw_monero/lib/exceptions/wallet_creation_exception.dart b/cw_monero/lib/exceptions/wallet_creation_exception.dart index a3555338..6b00445a 100644 --- a/cw_monero/lib/exceptions/wallet_creation_exception.dart +++ b/cw_monero/lib/exceptions/wallet_creation_exception.dart @@ -2,4 +2,7 @@ class WalletCreationException implements Exception { WalletCreationException({this.message}); final String message; + + @override + String toString() => message; } \ No newline at end of file diff --git a/cw_monero/lib/wallet_manager.dart b/cw_monero/lib/wallet_manager.dart index c5c1dd22..07f534c9 100644 --- a/cw_monero/lib/wallet_manager.dart +++ b/cw_monero/lib/wallet_manager.dart @@ -1,4 +1,5 @@ import 'dart:ffi'; +import 'package:cw_monero/wallet.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; import 'package:cw_monero/convert_utf8_to_string.dart'; @@ -49,6 +50,8 @@ void createWalletSync( throw WalletCreationException( message: convertUTF8ToString(pointer: errorMessagePointer)); } + + // setupNodeSync(address: "node.moneroworld.com:18089"); } bool isWalletExistSync({String path}) { diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index a3b83428..95a7598f 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -354,7 +354,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -493,7 +493,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -526,7 +526,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 4; + CURRENT_PROJECT_VERSION = 5; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/lib/entities/fs_migration.dart b/lib/entities/fs_migration.dart index 1d3ae8d4..8225eae3 100644 --- a/lib/entities/fs_migration.dart +++ b/lib/entities/fs_migration.dart @@ -137,6 +137,11 @@ Future ios_migrate_wallet_passwords() async { final walletsDir = Directory('${appDocDir.path}/wallets'); final moneroWalletsDir = Directory('${walletsDir.path}/monero'); + if (!moneroWalletsDir.existsSync() || moneroWalletsDir.listSync().isEmpty) { + await prefs.setBool('ios_migration_wallet_passwords_completed', true); + return; + } + moneroWalletsDir.listSync().forEach((item) async { try { if (item is Directory) { diff --git a/lib/main.dart b/lib/main.dart index 6c73a5bc..e9145ddc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -95,7 +95,7 @@ Future initialSetup( tradesSource: tradesSource, templates: templates, exchangeTemplates: exchangeTemplates); - await bootstrap(navigatorKey); + bootstrap(navigatorKey); monero_wallet.onStartup(); } diff --git a/lib/monero/monero_wallet_service.dart b/lib/monero/monero_wallet_service.dart index 5ff0c032..5461d94d 100644 --- a/lib/monero/monero_wallet_service.dart +++ b/lib/monero/monero_wallet_service.dart @@ -67,7 +67,7 @@ class MoneroWalletService extends WalletService< return wallet; } catch (e) { // TODO: Implement Exception for wallet list service. - print('MoneroWalletsManager Error: $e'); + print('MoneroWalletsManager Error: ${e.toString()}'); rethrow; } } diff --git a/lib/reactions/on_authentication_state_change.dart b/lib/reactions/on_authentication_state_change.dart index 80f3b282..23b314e5 100644 --- a/lib/reactions/on_authentication_state_change.dart +++ b/lib/reactions/on_authentication_state_change.dart @@ -8,22 +8,25 @@ import 'package:cake_wallet/store/authentication_store.dart'; ReactionDisposer _onAuthenticationStateChange; void startAuthenticationStateChange(AuthenticationStore authenticationStore, - GlobalKey navigatorKey) { + @required GlobalKey navigatorKey) { _onAuthenticationStateChange ??= autorun((_) async { final state = authenticationStore.state; if (state == AuthenticationState.installed) { await loadCurrentWallet(); + return; } if (state == AuthenticationState.allowed) { await navigatorKey.currentState .pushNamedAndRemoveUntil(Routes.dashboard, (route) => false); + return; } if (state == AuthenticationState.denied) { await navigatorKey.currentState .pushNamedAndRemoveUntil(Routes.welcome, (_) => false); + return; } }); } diff --git a/lib/router.dart b/lib/router.dart index 1c579c70..466e6086 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -58,7 +58,7 @@ class Router { return CupertinoPageRoute( builder: (_) => getIt.get( param1: (BuildContext context, dynamic _) => - Navigator.pushNamed(context, Routes.newWalletType)), + Navigator.pushNamed(context, Routes.newWallet)), fullscreenDialog: true); case Routes.newWalletType: @@ -128,7 +128,7 @@ class Router { return CupertinoPageRoute( builder: (_) => getIt.get( param1: (BuildContext context, dynamic _) => - Navigator.pushNamed(context, Routes.restoreWalletType)), + Navigator.pushNamed(context, Routes.restoreWalletFromSeed)), fullscreenDialog: true); case Routes.seed: @@ -137,15 +137,15 @@ class Router { getIt.get(param1: settings.arguments as bool)); case Routes.restoreWalletFromSeed: - final args = settings.arguments as List; - final type = args.first as WalletType; - final language = type == WalletType.monero - ? args[1] as String - : LanguageList.english; + // final args = settings.arguments as List; + final type = WalletType.monero; //args.first as WalletType; + // final language = type == WalletType.monero + // ? args[1] as String + // : LanguageList.english; return CupertinoPageRoute( builder: (_) => - RestoreWalletFromSeedPage(type: type, language: language)); + RestoreWalletFromSeedPage(type: type)); case Routes.restoreWalletFromKeys: final args = settings.arguments as List; diff --git a/lib/src/screens/restore/restore_from_keys.dart b/lib/src/screens/restore/restore_from_keys.dart new file mode 100644 index 00000000..cd0116e5 --- /dev/null +++ b/lib/src/screens/restore/restore_from_keys.dart @@ -0,0 +1,83 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/blockchain_height_widget.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; + +class RestoreFromKeysFrom extends StatefulWidget { + @override + _RestoreFromKeysFromState createState() => _RestoreFromKeysFromState(); +} + +class _RestoreFromKeysFromState extends State { + final _formKey = GlobalKey(); + final _blockchainHeightKey = GlobalKey(); + final _nameController = TextEditingController(); + final _addressController = TextEditingController(); + final _viewKeyController = TextEditingController(); + final _spendKeyController = TextEditingController(); + final _wifController = TextEditingController(); + + @override + void initState() { + // _nameController.addListener(() => + // widget.walletRestorationFromKeysVM.name = _nameController.text); + // _addressController.addListener(() => + // widget.walletRestorationFromKeysVM.address = _addressController.text); + // _viewKeyController.addListener(() => + // widget.walletRestorationFromKeysVM.viewKey = _viewKeyController.text); + // _spendKeyController.addListener(() => + // widget.walletRestorationFromKeysVM.spendKey = _spendKeyController.text); + // _wifController.addListener(() => + // widget.walletRestorationFromKeysVM.wif = _wifController.text); + + super.initState(); + } + + @override + void dispose() { + _nameController.dispose(); + _addressController.dispose(); + _viewKeyController.dispose(); + _spendKeyController.dispose(); + _wifController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only(left: 24, right: 24), + child: Form( + key: _formKey, + child: Column(children: [ + BaseTextFormField( + controller: _addressController, + keyboardType: TextInputType.multiline, + maxLines: null, + hintText: S.of(context).restore_address, + ), + Container( + padding: EdgeInsets.only(top: 20.0), + child: BaseTextFormField( + controller: _viewKeyController, + hintText: S.of(context).restore_view_key_private, + )), + Container( + padding: EdgeInsets.only(top: 20.0), + child: BaseTextFormField( + controller: _spendKeyController, + hintText: S.of(context).restore_spend_key_private, + )), + BlockchainHeightWidget( + key: _blockchainHeightKey, + onHeightChange: (height) { + // widget.walletRestorationFromKeysVM.height = height; + print(height); + }), + ]), + ), + ); + } +} diff --git a/lib/src/screens/restore/restore_wallet_from_seed_page.dart b/lib/src/screens/restore/restore_wallet_from_seed_page.dart index ec60ea24..1c657501 100644 --- a/lib/src/screens/restore/restore_wallet_from_seed_page.dart +++ b/lib/src/screens/restore/restore_wallet_from_seed_page.dart @@ -1,3 +1,11 @@ +import 'package:cake_wallet/src/screens/restore/restore_from_keys.dart'; +import 'package:cake_wallet/src/screens/seed_language/widgets/seed_language_picker.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cake_wallet/src/widgets/blockchain_height_widget.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/utils/show_pop_up.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:cake_wallet/routes.dart'; @@ -7,48 +15,162 @@ import 'package:cake_wallet/src/widgets/seed_widget.dart'; import 'package:cake_wallet/entities/wallet_type.dart'; import 'package:cake_wallet/core/seed_validator.dart'; import 'package:cake_wallet/core/mnemonic_length.dart'; +import 'package:smooth_page_indicator/smooth_page_indicator.dart'; class RestoreWalletFromSeedPage extends BasePage { - RestoreWalletFromSeedPage({@required this.type, @required this.language}); + RestoreWalletFromSeedPage({@required this.type}); final WalletType type; - final String language; - final formKey = GlobalKey<_RestoreFromSeedFormState>(); + final String language = 'en'; + + // final formKey = GlobalKey<_RestoreFromSeedFormState>(); + // final formKey = GlobalKey<_RestoreFromSeedFormState>(); @override String get title => S.current.restore_title_from_seed; - @override - Color get titleColor => Colors.white; + final controller = PageController(initialPage: 0); - @override - Color get backgroundLightColor => Colors.transparent; + Widget _page(BuildContext context, int index) { + if (_pages == null || _pages.isEmpty) { + _setPages(context); + } - @override - Color get backgroundDarkColor => Colors.transparent; + return _pages[index]; + } - @override - bool get resizeToAvoidBottomPadding => false; + int _pageLength(BuildContext context) { + if (_pages == null || _pages.isEmpty) { + _setPages(context); + } - @override - Widget body(BuildContext context) => - RestoreFromSeedForm(key: formKey, type: type, language: language, - leading: leading(context), middle: middle(context)); + return _pages.length; + } + + void _setPages(BuildContext context) { + _pages = [ + Container( + padding: EdgeInsets.only(left: 25, right: 25), + child: Column(children: [ + SeedWidget( + maxLength: mnemonicLength(WalletType.monero), + onMnemonicChange: (seed) => null, + onFinish: () => null, + // Navigator.of(context).pushNamed( + // Routes.restoreWalletFromSeedDetails, + // arguments: [WalletType.monero, '', '']), + validator: SeedValidator(type: WalletType.monero, language: ''), + ), + GestureDetector( + onTap: () async { + final selected = await showPopUp( + context: context, + builder: (BuildContext context) => + SeedLanguagePicker(selected: 'English')); //key: _pickerKey + print('Seletec $selected'); + }, + child: Container( + color: Colors.transparent, + padding: EdgeInsets.only(top: 20.0), + child: IgnorePointer(child: BaseTextFormField( + enableInteractiveSelection: false, + readOnly: true, + hintText: 'Language', + initialValue: 'English (Seed language)')))), + BlockchainHeightWidget( + // key: _blockchainHeightKey, + onHeightChange: (height) { + // widget.walletRestorationFromKeysVM.height = height; + print(height); + }) + ])), + RestoreFromKeysFrom(), + // Container(color: Colors.yellow) + ]; + } + + List _pages; @override - Widget build(BuildContext context) { - return Scaffold( - resizeToAvoidBottomPadding: resizeToAvoidBottomPadding, - body: Container( - color: Theme.of(context).backgroundColor, - child: body(context) - ) - ); + Widget body(BuildContext context) { + return Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + Expanded( + child: PageView.builder( + onPageChanged: (page) { + print('Page index $page'); + }, + controller: controller, + itemCount: _pageLength(context), + itemBuilder: (context, index) => _page(context, index))), + Padding( + padding: EdgeInsets.only(top: 10), + child: SmoothPageIndicator( + controller: controller, + count: _pageLength(context), + effect: ColorTransitionEffect( + spacing: 6.0, + radius: 6.0, + dotWidth: 6.0, + dotHeight: 6.0, + dotColor: Theme.of(context).hintColor.withOpacity(0.5), + activeDotColor: Theme.of(context).hintColor), + )), + Padding( + padding: EdgeInsets.only(top: 20, bottom: 40, left: 25, right: 25), + child: PrimaryButton( + text: S.of(context).restore_recover, + isDisabled: false, + onPressed: () => null, + color: Theme.of(context).accentTextTheme.body2.color, + textColor: Colors.white)), + ]); + + // return GestureDetector( + // onTap: () => + // SystemChannels.textInput.invokeMethod('TextInput.hide'), + // child: ScrollableWithBottomSection( + // bottomSection: Column(children: [ + // GestureDetector( + // onTap: () {}, + // child: Text('Switch to restore from keys', + // style: TextStyle(fontSize: 15, color: Theme.of(context).hintColor))), + // SizedBox(height: 30), + // PrimaryButton( + // text: S.of(context).restore_next, + // isDisabled: false, + // onPressed: () => null, + // color: Theme.of(context).accentTextTheme.body2.color, + // textColor: Colors.white) + // ]), + // contentPadding: EdgeInsets.only(bottom: 24), + // content: Container( + // padding: EdgeInsets.only(left: 25, right: 25), + // child: Column(children: [ + // SeedWidget( + // maxLength: mnemonicLength(type), + // onMnemonicChange: (seed) => null, + // onFinish: () => Navigator.of(context).pushNamed( + // Routes.restoreWalletFromSeedDetails, + // arguments: [type, language, '']), + // validator: SeedValidator(type: type, language: language), + // ), + // // SizedBox(height: 15), + // // BaseTextFormField(hintText: 'Language', initialValue: 'English'), + // BlockchainHeightWidget( + // // key: _blockchainHeightKey, + // onHeightChange: (height) { + // // widget.walletRestorationFromKeysVM.height = height; + // print(height); + // }) + // ]))), + // ); } } class RestoreFromSeedForm extends StatefulWidget { - RestoreFromSeedForm({Key key, this.type, this.language, this.leading, this.middle}) : super(key: key); + RestoreFromSeedForm( + {Key key, this.type, this.language, this.leading, this.middle}) + : super(key: key); final WalletType type; final String language; final Widget leading; @@ -59,27 +181,46 @@ class RestoreFromSeedForm extends StatefulWidget { } class _RestoreFromSeedFormState extends State { - final _seedKey = GlobalKey(); + // final _seedKey = GlobalKey(); - String mnemonic() => _seedKey.currentState.items.map((e) => e.text).join(' '); + String mnemonic() => + ''; // _seedKey.currentState.items.map((e) => e.text).join(' '); @override Widget build(BuildContext context) { return GestureDetector( onTap: () => SystemChannels.textInput.invokeMethod('TextInput.hide'), - child: SeedWidget( - key: _seedKey, - maxLength: mnemonicLength(widget.type), - onMnemonicChange: (seed) => null, - onFinish: () => Navigator.of(context).pushNamed( - Routes.restoreWalletFromSeedDetails, - arguments: [widget.type, widget.language, mnemonic()]), - leading: widget.leading, - middle: widget.middle, - validator: - SeedValidator(type: widget.type, language: widget.language), - ), + child: Container( + padding: EdgeInsets.only(left: 25, right: 25), + // color: Colors.blue, + // height: 300, + child: Column(children: [ + SeedWidget( + // key: _seedKey, + maxLength: mnemonicLength(widget.type), + onMnemonicChange: (seed) => null, + onFinish: () => Navigator.of(context).pushNamed( + Routes.restoreWalletFromSeedDetails, + arguments: [widget.type, widget.language, mnemonic()]), + leading: widget.leading, + middle: widget.middle, + validator: + SeedValidator(type: widget.type, language: widget.language), + ), + BlockchainHeightWidget( + // key: _blockchainHeightKey, + onHeightChange: (height) { + // widget.walletRestorationFromKeysVM.height = height; + print(height); + }), + Container( + color: Colors.green, + width: 100, + height: 56, + child: BaseTextFormField( + hintText: 'Language', initialValue: 'English')), + ])), ); } } diff --git a/lib/src/screens/seed_language/seed_language_page.dart b/lib/src/screens/seed_language/seed_language_page.dart index ccee8aae..701b5dcc 100644 --- a/lib/src/screens/seed_language/seed_language_page.dart +++ b/lib/src/screens/seed_language/seed_language_page.dart @@ -35,13 +35,15 @@ class SeedLanguageFormState extends State { static const aspectRatioImage = 1.22; final walletNameImage = Image.asset('assets/images/wallet_name.png'); - final walletNameLightImage = Image.asset('assets/images/wallet_name_light.png'); + final walletNameLightImage = + Image.asset('assets/images/wallet_name_light.png'); final _languageSelectorKey = GlobalKey(); @override Widget build(BuildContext context) { final walletImage = getIt.get().isDarkTheme - ? walletNameImage : walletNameLightImage; + ? walletNameImage + : walletNameLightImage; return Container( padding: EdgeInsets.only(top: 24), @@ -78,8 +80,8 @@ class SeedLanguageFormState extends State { bottomSection: Observer( builder: (context) { return PrimaryButton( - onPressed: () => widget - .onConfirm(context, _languageSelectorKey.currentState.selected), + onPressed: () => widget.onConfirm( + context, _languageSelectorKey.currentState.selected), text: S.of(context).seed_language_next, color: Colors.green, textColor: Colors.white); diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index f6116b65..70bc56a4 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -175,7 +175,7 @@ class WalletListBodyState extends State { SizedBox(height: 10.0), PrimaryImageButton( onPressed: () => - Navigator.of(context).pushNamed(Routes.restoreWalletType), + Navigator.of(context).pushNamed(Routes.restoreWalletFromSeed), image: restoreWalletImage, text: S.of(context).wallet_list_restore_wallet, color: Theme.of(context).accentTextTheme.caption.color, diff --git a/lib/src/screens/welcome/welcome_page.dart b/lib/src/screens/welcome/welcome_page.dart index aa0d5b80..78cd0483 100644 --- a/lib/src/screens/welcome/welcome_page.dart +++ b/lib/src/screens/welcome/welcome_page.dart @@ -103,7 +103,7 @@ class WelcomePage extends BasePage { Padding( padding: EdgeInsets.only(top: 24), child: PrimaryImageButton( - onPressed: () => Navigator.pushNamed(context, Routes.newWallet), + onPressed: () => Navigator.pushNamed(context, Routes.newWalletFromWelcome), image: newWalletImage, text: S.of(context).create_new, color: Theme.of(context).accentTextTheme.subtitle.decorationColor, @@ -113,7 +113,7 @@ class WelcomePage extends BasePage { Padding( padding: EdgeInsets.only(top: 10), child: PrimaryImageButton( - onPressed: () => Navigator.pushNamed(context, Routes.restoreWalletOptions), + onPressed: () => Navigator.pushNamed(context, Routes.restoreWalletOptionsFromWelcome), image: restoreWalletImage, text: S.of(context).restore_wallet, color: Theme.of(context).accentTextTheme.caption.color, diff --git a/lib/src/widgets/base_text_form_field.dart b/lib/src/widgets/base_text_form_field.dart index e7a2d565..2c296df2 100644 --- a/lib/src/widgets/base_text_form_field.dart +++ b/lib/src/widgets/base_text_form_field.dart @@ -19,11 +19,14 @@ class BaseTextFormField extends StatelessWidget { this.suffix, this.suffixIcon, this.enabled = true, + this.readOnly = false, + this.enableInteractiveSelection = true, this.validator, this.textStyle, this.placeholderTextStyle, this.maxLength, - this.focusNode}); + this.focusNode, + this.initialValue}); final TextEditingController controller; final TextInputType keyboardType; @@ -46,10 +49,16 @@ class BaseTextFormField extends StatelessWidget { final TextStyle textStyle; final int maxLength; final FocusNode focusNode; + final bool readOnly; + final bool enableInteractiveSelection; + String initialValue; @override Widget build(BuildContext context) { return TextFormField( + enableInteractiveSelection: enableInteractiveSelection, + readOnly: readOnly, + initialValue: initialValue, focusNode: focusNode, controller: controller, keyboardType: keyboardType, @@ -60,9 +69,11 @@ class BaseTextFormField extends StatelessWidget { inputFormatters: inputFormatters, enabled: enabled, maxLength: maxLength, - style: textStyle ?? TextStyle( - fontSize: 16.0, - color: textColor ?? Theme.of(context).primaryTextTheme.title.color), + style: textStyle ?? + TextStyle( + fontSize: 16.0, + color: + textColor ?? Theme.of(context).primaryTextTheme.title.color), decoration: InputDecoration( prefix: prefix, prefixIcon: prefixIcon, @@ -75,15 +86,18 @@ class BaseTextFormField extends StatelessWidget { hintText: hintText, focusedBorder: UnderlineInputBorder( borderSide: BorderSide( - color: borderColor ?? Theme.of(context).primaryTextTheme.title.backgroundColor, + color: borderColor ?? + Theme.of(context).primaryTextTheme.title.backgroundColor, width: 1.0)), disabledBorder: UnderlineInputBorder( borderSide: BorderSide( - color: borderColor ?? Theme.of(context).primaryTextTheme.title.backgroundColor, + color: borderColor ?? + Theme.of(context).primaryTextTheme.title.backgroundColor, width: 1.0)), enabledBorder: UnderlineInputBorder( borderSide: BorderSide( - color: borderColor ?? Theme.of(context).primaryTextTheme.title.backgroundColor, + color: borderColor ?? + Theme.of(context).primaryTextTheme.title.backgroundColor, width: 1.0))), validator: validator, ); diff --git a/lib/src/widgets/seed_widget.dart b/lib/src/widgets/seed_widget.dart index 208fef82..b2e29789 100644 --- a/lib/src/widgets/seed_widget.dart +++ b/lib/src/widgets/seed_widget.dart @@ -1,3 +1,5 @@ +import 'package:cake_wallet/entities/wallet_type.dart'; +import 'package:cake_wallet/src/widgets/blockchain_height_widget.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -8,6 +10,166 @@ import 'package:cake_wallet/entities/mnemonic_item.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:flutter/widgets.dart'; +class Annotation extends Comparable { + Annotation({@required this.range, this.style}); + + final TextRange range; + final TextStyle style; + + @override + int compareTo(Annotation other) => range.start.compareTo(other.range.start); +} + +class TextAnnotation extends Comparable { + TextAnnotation({@required this.text, this.style}); + + final TextStyle style; + final String text; + + @override + int compareTo(TextAnnotation other) => text.compareTo(other.text); +} + +class AnnotatedEditableText extends EditableText { + AnnotatedEditableText({ + Key key, + FocusNode focusNode, + TextEditingController controller, + TextStyle style, + ValueChanged onChanged, + ValueChanged onSubmitted, + Color cursorColor, + Color selectionColor, + Color backgroundCursorColor, + TextSelectionControls selectionControls, + @required this.words, + }) : textAnnotations = words + .map((word) => TextAnnotation( + text: word, + style: TextStyle( + color: Colors.black, + backgroundColor: Colors.transparent, + fontWeight: FontWeight.normal, + fontSize: 20))) + .toList(), + super( + maxLines: null, + key: key, + focusNode: focusNode, + controller: controller, + cursorColor: cursorColor, + style: style, + keyboardType: TextInputType.text, + autocorrect: false, + autofocus: false, + selectionColor: selectionColor, + selectionControls: selectionControls, + backgroundCursorColor: backgroundCursorColor, + onChanged: onChanged, + onSubmitted: onSubmitted, + toolbarOptions: const ToolbarOptions( + copy: true, + cut: true, + paste: true, + selectAll: true, + ), + enableSuggestions: false, + enableInteractiveSelection: true, + showSelectionHandles: true, + showCursor: true, + ) { + textAnnotations.add(TextAnnotation( + text: ' ', style: TextStyle(backgroundColor: Colors.transparent))); + } + + final List words; + final List textAnnotations; + + @override + AnnotatedEditableTextState createState() => AnnotatedEditableTextState(); +} + +class AnnotatedEditableTextState extends EditableTextState { + @override + AnnotatedEditableText get widget => super.widget as AnnotatedEditableText; + + List getRanges() { + final source = widget.textAnnotations + .map((item) => range(item.text, textEditingValue.text) + .map((range) => Annotation(style: item.style, range: range))) + .expand((e) => e) + .toList(); + final result = List(); + final text = textEditingValue.text; + source.sort(); + Annotation prev; + + for (var item in source) { + if (prev == null) { + if (item.range.start > 0) { + result.add(Annotation( + range: TextRange(start: 0, end: item.range.start), + style: TextStyle( + color: Colors.black, backgroundColor: Colors.transparent))); + } + result.add(item); + prev = item; + continue; + } else { + if (prev.range.end > item.range.start) { + // throw StateError('Invalid (intersecting) ranges for annotated field'); + } else if (prev.range.end < item.range.start) { + result.add(Annotation( + range: TextRange(start: prev.range.end, end: item.range.start), + style: TextStyle( + color: Colors.red, backgroundColor: Colors.transparent))); + } + + result.add(item); + prev = item; + } + } + + if (result.length > 0 && result.last.range.end < text.length) { + result.add(Annotation( + range: TextRange(start: result.last.range.end, end: text.length), + style: TextStyle( backgroundColor: Colors.transparent))); + } + return result; + } + + List range(String pattern, String source) { + final result = List(); + + for (int index = source.indexOf(pattern); + index >= 0; + index = source.indexOf(pattern, index + 1)) { + final start = index; + final end = start + pattern.length; + result.add(TextRange(start: start, end: end)); + } + + return result; + } + + @override + TextSpan buildTextSpan() { + final text = textEditingValue.text; + final ranges = getRanges(); + + if (ranges.isNotEmpty) { + return TextSpan( + style: widget.style, + children: ranges + .map((item) => TextSpan( + style: item.style, text: item.range.textInside(text))) + .toList()); + } + + return TextSpan(style: widget.style, text: text); + } +} + class SeedWidget extends StatefulWidget { SeedWidget( {Key key, @@ -48,10 +210,13 @@ class SeedWidgetState extends State { @override void initState() { super.initState(); + showPlaceholder = true; isValid = false; isCurrentMnemonicValid = false; _seedController .addListener(() => changeCurrentMnemonic(_seedController.text)); + focusNode.addListener(() => setState(() => + showPlaceholder = !focusNode.hasFocus && controller.text.isEmpty)); } void addMnemonic(String text) { @@ -198,239 +363,308 @@ class SeedWidgetState extends State { return isValid; } + final controller = TextEditingController(); + final focusNode = FocusNode(); + + bool showPlaceholder; + + final words = + SeedValidator.getWordList(type: WalletType.monero, language: 'en'); + + Future _pasteAddress() async { + final value = await Clipboard.getData('text/plain'); + + if (value?.text?.isNotEmpty ?? false) { + controller.text = value.text; + } + } + @override Widget build(BuildContext context) { + print('build'); return Container( - child: Column(children: [ - Flexible( - fit: FlexFit.tight, - flex: 2, - child: Container( - width: double.infinity, - height: double.infinity, - padding: EdgeInsets.all(0), - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(24), - bottomRight: Radius.circular(24)), - gradient: LinearGradient(colors: [ - Theme.of(context).primaryTextTheme.subhead.color, - Theme.of(context).primaryTextTheme.subhead.decorationColor, - ], begin: Alignment.topLeft, end: Alignment.bottomRight)), - child: Column( - children: [ - CupertinoNavigationBar( - leading: widget.leading, - middle: widget.middle, - backgroundColor: Colors.transparent, - border: null, - ), - Expanded( - child: Container( - padding: EdgeInsets.all(24), - alignment: Alignment.topLeft, - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - S.of(context).restore_active_seed, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Theme.of(context) - .textTheme - .overline - .backgroundColor), - ), - Padding( - padding: EdgeInsets.only(top: 5), - child: Wrap( - children: items.map((item) { - final isValid = - widget.validator.isValid(item); - final isSelected = selectedItem == item; - - return InkWell( - onTap: () => onMnemonicTap(item), - child: Container( - decoration: BoxDecoration( - color: isValid - ? Colors.transparent - : Palette.red), - margin: EdgeInsets.only( - right: 7, bottom: 8), - child: Text( - item.toString(), - style: TextStyle( - color: isValid - ? Colors.white - : Colors.grey, - fontSize: 16, - fontWeight: isSelected - ? FontWeight.w900 - : FontWeight.w600, - decoration: isSelected - ? TextDecoration.underline - : TextDecoration.none), - )), - ); - }).toList(), - )) - ], - ), - ), - )) - ], - )), - ), - Flexible( - fit: FlexFit.tight, - flex: 3, - child: Padding( - padding: - EdgeInsets.only(left: 24, top: 48, right: 24, bottom: 24), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - S.of(context).restore_new_seed, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Stack(children: [ + SizedBox(height: 35), + if (showPlaceholder) + Positioned( + top: 10, + left: 0, + child: Text('Enter your seed', style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - color: - Theme.of(context).primaryTextTheme.title.color), - ), - Padding( - padding: EdgeInsets.only(top: 24), - child: TextFormField( - key: _seedTextFieldKey, - onFieldSubmitted: (text) => isCurrentMnemonicValid - ? saveCurrentMnemonicToItems() - : null, - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.normal, - color: - Theme.of(context).primaryTextTheme.title.color), - controller: _seedController, - textInputAction: TextInputAction.done, - decoration: InputDecoration( - suffixIcon: GestureDetector( - behavior: HitTestBehavior.opaque, - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: 145), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text('${items.length}/$maxLength', - style: TextStyle( - color: Theme.of(context) - .accentTextTheme - .display2 - .decorationColor, - fontWeight: FontWeight.normal, - fontSize: 16)), - SizedBox(width: 10), - InkWell( - onTap: () async => - Clipboard.getData('text/plain').then( - (clipboard) => - replaceText(clipboard.text)), - child: Container( - height: 35, - padding: EdgeInsets.all(7), - decoration: BoxDecoration( - color: Theme.of(context) - .accentTextTheme - .caption - .color, - borderRadius: - BorderRadius.circular(10.0)), - child: Text( - S.of(context).paste, - style: TextStyle( - color: Palette.blueCraiola), - )), - ) - ], - ), - ), - ), - hintStyle: TextStyle( - color: Theme.of(context) - .accentTextTheme - .display2 - .decorationColor, - fontWeight: FontWeight.normal, - fontSize: 16), - hintText: - S.of(context).restore_from_seed_placeholder, - errorText: _errorMessage, - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context) - .accentTextTheme - .subtitle - .backgroundColor, - width: 1.0)), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context) - .accentTextTheme - .subtitle - .backgroundColor, - width: 1.0))), - enableInteractiveSelection: false, - ), - ) - ]), - )), - Padding( - padding: EdgeInsets.only(left: 24, right: 24, bottom: 24), - child: Row( - children: [ - Flexible( - child: Padding( - padding: EdgeInsets.only(right: 8), - child: PrimaryButton( - onPressed: clear, - text: S.of(context).clear, - color: Colors.orange, - textColor: Colors.white, - isDisabled: items.isEmpty, - ), - )), - Flexible( - child: Padding( - padding: EdgeInsets.only(left: 8), - child: (selectedItem == null && items.length == maxLength) - ? PrimaryButton( - text: S.of(context).restore_next, - isDisabled: !isSeedValid(), - onPressed: () => widget.onFinish != null - ? widget.onFinish() - : null, - color: Theme.of(context).accentTextTheme.body2.color, - textColor: Colors.white) - : PrimaryButton( - text: selectedItem != null - ? S.of(context).save - : S.of(context).add_new_word, - onPressed: () => isCurrentMnemonicValid - ? saveCurrentMnemonicToItems() - : null, - onDisabledPressed: () => showErrorIfExist(), - isDisabled: !isCurrentMnemonicValid, - color: Theme.of(context).accentTextTheme.body2.color, - textColor: Colors.white), - ), - ) - ], - )) - ]), - ); + fontSize: 16.0, color: Theme.of(context).hintColor))), + Padding( + padding: EdgeInsets.only(right: 40, top: 10), + child: AnnotatedEditableText( + cursorColor: Colors.green, + backgroundCursorColor: Colors.blue, + style: TextStyle( + fontSize: 20, + color: Colors.red, + fontWeight: FontWeight.normal, + backgroundColor: Colors.transparent), + focusNode: focusNode, + controller: controller, + words: words)), + Positioned( + top: 0, + right: 0, + child: Container( + width: 34, + height: 34, + child: InkWell( + onTap: () async => _pasteAddress(), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).hintColor, + borderRadius: + BorderRadius.all(Radius.circular(6))), + child: Image.asset('assets/images/duplicate.png', + color: Theme.of(context) + .primaryTextTheme + .display1 + .decorationColor)), + ))) + ]), + Container( + margin: EdgeInsets.only(top: 15), + height: 1.0, + color: Theme.of(context).primaryTextTheme.title.backgroundColor), + ])); + // return Container( + // child: Column(children: [ + // Flexible( + // fit: FlexFit.tight, + // flex: 2, + // child: Container( + // width: double.infinity, + // height: double.infinity, + // padding: EdgeInsets.all(0), + // decoration: BoxDecoration( + // borderRadius: BorderRadius.only( + // bottomLeft: Radius.circular(24), + // bottomRight: Radius.circular(24)), + // gradient: LinearGradient(colors: [ + // Theme.of(context).primaryTextTheme.subhead.color, + // Theme.of(context).primaryTextTheme.subhead.decorationColor, + // ], begin: Alignment.topLeft, end: Alignment.bottomRight)), + // child: Column( + // children: [ + // CupertinoNavigationBar( + // leading: widget.leading, + // middle: widget.middle, + // backgroundColor: Colors.transparent, + // border: null, + // ), + // Expanded( + // child: Container( + // padding: EdgeInsets.all(24), + // alignment: Alignment.topLeft, + // child: SingleChildScrollView( + // child: Column( + // mainAxisAlignment: MainAxisAlignment.start, + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Text( + // S.of(context).restore_active_seed, + // style: TextStyle( + // fontSize: 14, + // fontWeight: FontWeight.w500, + // color: Theme.of(context) + // .textTheme + // .overline + // .backgroundColor), + // ), + // Padding( + // padding: EdgeInsets.only(top: 5), + // child: Wrap( + // children: items.map((item) { + // final isValid = + // widget.validator.isValid(item); + // final isSelected = selectedItem == item; + // + // return InkWell( + // onTap: () => onMnemonicTap(item), + // child: Container( + // decoration: BoxDecoration( + // color: isValid + // ? Colors.transparent + // : Palette.red), + // margin: EdgeInsets.only( + // right: 7, bottom: 8), + // child: Text( + // item.toString(), + // style: TextStyle( + // color: isValid + // ? Colors.white + // : Colors.grey, + // fontSize: 16, + // fontWeight: isSelected + // ? FontWeight.w900 + // : FontWeight.w600, + // decoration: isSelected + // ? TextDecoration.underline + // : TextDecoration.none), + // )), + // ); + // }).toList(), + // )) + // ], + // ), + // ), + // )) + // ], + // )), + // ), + // Flexible( + // fit: FlexFit.tight, + // flex: 3, + // child: Padding( + // padding: + // EdgeInsets.only(left: 24, top: 48, right: 24, bottom: 24), + // child: Column( + // mainAxisAlignment: MainAxisAlignment.start, + // crossAxisAlignment: CrossAxisAlignment.center, + // children: [ + // Text( + // S.of(context).restore_new_seed, + // style: TextStyle( + // fontSize: 20, + // fontWeight: FontWeight.w500, + // color: + // Theme.of(context).primaryTextTheme.title.color), + // ), + // Padding( + // padding: EdgeInsets.only(top: 24), + // child: TextFormField( + // key: _seedTextFieldKey, + // onFieldSubmitted: (text) => isCurrentMnemonicValid + // ? saveCurrentMnemonicToItems() + // : null, + // style: TextStyle( + // fontSize: 16.0, + // fontWeight: FontWeight.normal, + // color: + // Theme.of(context).primaryTextTheme.title.color), + // controller: _seedController, + // textInputAction: TextInputAction.done, + // decoration: InputDecoration( + // suffixIcon: GestureDetector( + // behavior: HitTestBehavior.opaque, + // child: ConstrainedBox( + // constraints: BoxConstraints(maxWidth: 145), + // child: Row( + // mainAxisAlignment: MainAxisAlignment.end, + // children: [ + // Text('${items.length}/$maxLength', + // style: TextStyle( + // color: Theme.of(context) + // .accentTextTheme + // .display2 + // .decorationColor, + // fontWeight: FontWeight.normal, + // fontSize: 16)), + // SizedBox(width: 10), + // InkWell( + // onTap: () async => + // Clipboard.getData('text/plain').then( + // (clipboard) => + // replaceText(clipboard.text)), + // child: Container( + // height: 35, + // padding: EdgeInsets.all(7), + // decoration: BoxDecoration( + // color: Theme.of(context) + // .accentTextTheme + // .caption + // .color, + // borderRadius: + // BorderRadius.circular(10.0)), + // child: Text( + // S.of(context).paste, + // style: TextStyle( + // color: Palette.blueCraiola), + // )), + // ) + // ], + // ), + // ), + // ), + // hintStyle: TextStyle( + // color: Theme.of(context) + // .accentTextTheme + // .display2 + // .decorationColor, + // fontWeight: FontWeight.normal, + // fontSize: 16), + // hintText: + // S.of(context).restore_from_seed_placeholder, + // errorText: _errorMessage, + // focusedBorder: UnderlineInputBorder( + // borderSide: BorderSide( + // color: Theme.of(context) + // .accentTextTheme + // .subtitle + // .backgroundColor, + // width: 1.0)), + // enabledBorder: UnderlineInputBorder( + // borderSide: BorderSide( + // color: Theme.of(context) + // .accentTextTheme + // .subtitle + // .backgroundColor, + // width: 1.0))), + // enableInteractiveSelection: false, + // ), + // ) + // ]), + // )), + // Padding( + // padding: EdgeInsets.only(left: 24, right: 24, bottom: 24), + // child: Row( + // children: [ + // Flexible( + // child: Padding( + // padding: EdgeInsets.only(right: 8), + // child: PrimaryButton( + // onPressed: clear, + // text: S.of(context).clear, + // color: Colors.orange, + // textColor: Colors.white, + // isDisabled: items.isEmpty, + // ), + // )), + // Flexible( + // child: Padding( + // padding: EdgeInsets.only(left: 8), + // child: (selectedItem == null && items.length == maxLength) + // ? PrimaryButton( + // text: S.of(context).restore_next, + // isDisabled: !isSeedValid(), + // onPressed: () => widget.onFinish != null + // ? widget.onFinish() + // : null, + // color: Theme.of(context).accentTextTheme.body2.color, + // textColor: Colors.white) + // : PrimaryButton( + // text: selectedItem != null + // ? S.of(context).save + // : S.of(context).add_new_word, + // onPressed: () => isCurrentMnemonicValid + // ? saveCurrentMnemonicToItems() + // : null, + // onDisabledPressed: () => showErrorIfExist(), + // isDisabled: !isCurrentMnemonicValid, + // color: Theme.of(context).accentTextTheme.body2.color, + // textColor: Colors.white), + // ), + // ) + // ], + // )) + // ]), + // ); } }