diff --git a/lib/screen/onboarding/import_address/import_seeds.dart b/lib/screen/onboarding/import_address/import_seeds.dart index 34c1ab883..ef8af2147 100644 --- a/lib/screen/onboarding/import_address/import_seeds.dart +++ b/lib/screen/onboarding/import_address/import_seeds.dart @@ -1,20 +1,20 @@ import 'dart:async'; +import 'dart:math'; import 'package:autonomy_flutter/common/injector.dart'; import 'package:autonomy_flutter/screen/app_router.dart'; import 'package:autonomy_flutter/screen/onboarding/import_address/select_addresses.dart'; import 'package:autonomy_flutter/service/account_service.dart'; import 'package:autonomy_flutter/util/log.dart'; -import 'package:autonomy_flutter/util/style.dart'; import 'package:autonomy_flutter/util/ui_helper.dart'; import 'package:autonomy_flutter/view/au_text_field.dart'; +import 'package:autonomy_flutter/view/au_toggle.dart'; import 'package:autonomy_flutter/view/back_appbar.dart'; import 'package:autonomy_flutter/view/primary_button.dart'; import 'package:autonomy_flutter/view/responsive.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:feralfile_app_theme/feral_file_app_theme.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:sentry/sentry.dart'; @@ -26,14 +26,31 @@ class ImportSeedsPage extends StatefulWidget { } class _ImportSeedsPageState extends State { - bool isError = false; - final TextEditingController _phraseTextController = TextEditingController(); + bool _isError = false; + static const _rowNumber = 12; + static const _maxWords = 24; + final List _mnemonicControllers = + List.generate(_maxWords, (_) => TextEditingController(), growable: false); + final List _focusNodes = + List.generate(_maxWords + 1, (_) => FocusNode(), growable: false); final TextEditingController _passphraseTextController = TextEditingController(); bool _isSubmissionEnabled = false; bool _obscureText = true; bool _passphraseObscureText = true; + @override + void dispose() { + for (var element in _mnemonicControllers) { + element.dispose(); + } + for (var element in _focusNodes) { + element.dispose(); + } + _passphraseTextController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) => Scaffold( appBar: getBackAppBar( @@ -53,43 +70,39 @@ class _ImportSeedsPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - addTitleSpace(), + const SizedBox(height: 34), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'reveal_secret_phrase'.tr(), + style: Theme.of(context).textTheme.ppMori400Black14, + ), + AuToggle( + value: !_obscureText, + onToggle: (value) { + setState(() { + _obscureText = !value; + }); + }, + ), + ], + ), + const SizedBox(height: 30), Text( - 'input_your_mnemonic'.tr(), + 'enter_your_mnemonic'.tr(), style: Theme.of(context).textTheme.ppMori400Black14, ), const SizedBox(height: 5), - AuTextField( - labelSemantics: 'enter_seed', - title: '', - enableSuggestions: false, - obscureText: _obscureText, - placeholder: 'enter_recovery_phrase'.tr(), - hintMaxLines: 1, - controller: _phraseTextController, - isError: isError, - onChanged: (value) { - final numberOfWords = value.trim().split(' ').length; - setState(() { - _isSubmissionEnabled = - [12, 15, 18, 21, 24].contains(numberOfWords); - isError = false; - }); - }, - suffix: IconButton( - icon: SvgPicture.asset( - _obscureText - ? 'assets/images/unhide.svg' - : 'assets/images/hide.svg', - colorFilter: const ColorFilter.mode( - AppColor.primaryBlack, BlendMode.srcIn), - ), - onPressed: () { - setState(() { - _obscureText = !_obscureText; - }); - }, + Table( + children: List.generate( + _rowNumber, + (index) => _tableRow(context, index, _rowNumber), ), + border: TableBorder.all( + color: + _isError ? AppColor.red : AppColor.auLightGrey, + borderRadius: BorderRadius.circular(10)), ), const SizedBox(height: 20), RichText( @@ -121,6 +134,7 @@ class _ImportSeedsPageState extends State { placeholder: 'enter_passphrase_placeholder'.tr(), hintMaxLines: 1, controller: _passphraseTextController, + focusNode: _focusNodes[_maxWords], suffix: IconButton( icon: SvgPicture.asset( _passphraseObscureText @@ -150,7 +164,7 @@ class _ImportSeedsPageState extends State { const SizedBox(height: 20), PrimaryAsyncButton( text: 'continue'.tr(), - enabled: _isSubmissionEnabled && !isError, + enabled: _isSubmissionEnabled && !_isError, onTap: () async => _import(), ), ], @@ -161,12 +175,12 @@ class _ImportSeedsPageState extends State { Future _import() async { try { setState(() { - isError = false; + _isError = false; }); final accountService = injector(); final persona = await accountService.importPersona( - _phraseTextController.text.trim(), + _getMnemonic(), _passphraseTextController.text.trim(), ); if (!mounted) { @@ -183,8 +197,88 @@ class _ImportSeedsPageState extends State { unawaited(Sentry.captureException(exception)); UIHelper.hideInfoDialog(context); setState(() { - isError = true; + _isError = true; }); } } + + TableRow _tableRow(BuildContext context, int index, int itemsEachCol) => + TableRow(children: [ + _rowItem(context, index), + _rowItem(context, index + itemsEachCol), + ]); + + Widget _rowItem(BuildContext context, int index) { + final theme = Theme.of(context); + NumberFormat formatter = NumberFormat('00'); + final controller = _mnemonicControllers[index]; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + children: [ + Container( + width: 32, + alignment: Alignment.centerRight, + child: Text(formatter.format(index + 1), + style: theme.textTheme.ppMori400Grey14), + ), + const SizedBox(width: 16), + Expanded( + child: TextField( + enableSuggestions: false, + focusNode: _focusNodes[index], + autocorrect: false, + obscureText: _obscureText, + controller: controller, + decoration: InputDecoration( + contentPadding: const EdgeInsets.fromLTRB(0, 3, 0, 0), + isDense: true, + border: InputBorder.none, + hintStyle: ResponsiveLayout.isMobile + ? theme.textTheme.ppMori400Black14 + .copyWith(color: AppColor.auQuickSilver) + : theme.textTheme.ppMori400Black16 + .copyWith(color: AppColor.auQuickSilver, fontSize: 20), + ), + onSubmitted: (value) { + if (index < _maxWords) { + FocusScope.of(context).requestFocus(_focusNodes[index + 1]); + } + }, + style: theme.textTheme.ppMori400Black14 + .copyWith(color: _isError ? AppColor.red : null), + onChanged: (value) { + if (value.contains(' ')) { + final words = value.split(' '); + if (words.last.isEmpty) { + words.removeLast(); + } + final wordsLeft = _maxWords - index; + final wordsToInsertNum = min(wordsLeft, words.length); + for (var i = 0; i < wordsToInsertNum; i++) { + if (i != wordsToInsertNum || words[i].isNotEmpty) { + _mnemonicControllers[index + i].text = words[i]; + } + } + FocusScope.of(context) + .requestFocus(_focusNodes[index + wordsToInsertNum]); + } + + final numberOfWords = _getMnemonic().split(' ').length; + setState(() { + _isSubmissionEnabled = + [12, 15, 18, 21, 24].contains(numberOfWords); + _isError = false; + }); + }, + ), + ), + ], + ), + ); + } + + String _getMnemonic() => + _mnemonicControllers.map((e) => e.text.trim()).join(' ').trim(); }