diff --git a/lib/app/router/routs/add_seed/add_seed.dart b/lib/app/router/routs/add_seed/add_seed.dart index 5d1859f1f..8b65436c9 100644 --- a/lib/app/router/routs/add_seed/add_seed.dart +++ b/lib/app/router/routs/add_seed/add_seed.dart @@ -4,7 +4,7 @@ import 'package:app/feature/add_seed/add_existing_wallet/view/add_existing_walle import 'package:app/feature/add_seed/add_seed_enable_biometry/add_seed_enable_biometry_page.dart'; import 'package:app/feature/add_seed/create_password/screens/create_seed_password/create_seed_password_screen.dart'; import 'package:app/feature/add_seed/create_password/view/create_seed_password_page.dart'; -import 'package:app/feature/add_seed/enter_seed_phrase/view/enter_seed_phrase_page.dart'; +import 'package:app/feature/add_seed/enter_seed_phrase/enter_seed_phrase.dart'; import 'package:app/feature/add_seed/import_wallet/import_wallet_screen.dart'; import 'package:app/v1/feature/add_seed/check_seed_phrase/check_seed_phrase.dart'; import 'package:app/v1/feature/add_seed/create_seed/create_seed.dart'; @@ -54,7 +54,7 @@ GoRoute createSeedNoNamedRoute(GoRoute passwordRoute) { GoRoute enterSeedNoNamedRoute(GoRoute passwordRoute) { return GoRoute( path: AppRoute.enterSeed.path, - builder: (_, __) => const EnterSeedPhrasePage(), + builder: (_, __) => const EnterSeedPhrase(), routes: [ passwordRoute, ], @@ -186,7 +186,7 @@ GoRoute get createSeedNamedProfileRoute { GoRoute get enterSeedNamedProfileRoute { return GoRoute( path: AppRoute.enterSeedNamed.path, - builder: (_, state) => const EnterSeedPhrasePage(), + builder: (_, state) => const EnterSeedPhrase(), routes: [ GoRoute( path: AppRoute.createSeedPassword.path, diff --git a/lib/core/error_handler_factory.dart b/lib/core/error_handler_factory.dart index a37a346d1..82b6e6fea 100644 --- a/lib/core/error_handler_factory.dart +++ b/lib/core/error_handler_factory.dart @@ -1,6 +1,10 @@ +import 'package:app/di/di.dart'; import 'package:app/utils/factories/error_handler/standard_error_handler.dart'; import 'package:flutter/cupertino.dart'; PrimaryErrorHandler createPrimaryErrorHandler(BuildContext context) { - return PrimaryErrorHandler(); + return PrimaryErrorHandler( + context, + inject(), + ); } diff --git a/lib/feature/add_seed/enter_seed_phrase/cubit/cubit.dart b/lib/feature/add_seed/enter_seed_phrase/cubit/cubit.dart deleted file mode 100644 index b9ec124dd..000000000 --- a/lib/feature/add_seed/enter_seed_phrase/cubit/cubit.dart +++ /dev/null @@ -1,3 +0,0 @@ -//GENERATED BARREL FILE -export 'enter_seed_phrase_cubit.dart'; -export 'enter_seed_phrase_input_state.dart'; diff --git a/lib/feature/add_seed/enter_seed_phrase/cubit/enter_seed_phrase_cubit.dart b/lib/feature/add_seed/enter_seed_phrase/cubit/enter_seed_phrase_cubit.dart index c610dfd2a..e69de29bb 100644 --- a/lib/feature/add_seed/enter_seed_phrase/cubit/enter_seed_phrase_cubit.dart +++ b/lib/feature/add_seed/enter_seed_phrase/cubit/enter_seed_phrase_cubit.dart @@ -1,442 +0,0 @@ -import 'package:app/app/service/messenger/message.dart'; -import 'package:app/app/service/messenger/service/messenger_service.dart'; -import 'package:app/app/service/network_connection/network_connection_service.dart'; -import 'package:app/core/bloc/bloc_mixin.dart'; -import 'package:app/di/di.dart'; -import 'package:app/feature/add_seed/enter_seed_phrase/cubit/enter_seed_phrase_input_state.dart'; -import 'package:app/feature/constants.dart'; -import 'package:app/generated/locale_keys.g.dart'; -import 'package:app/utils/mixins/connection_mixin.dart'; -import 'package:app/utils/seed_utils.dart'; -import 'package:bloc/bloc.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/widgets.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:logging/logging.dart'; -import 'package:nekoton_repository/nekoton_repository.dart' hide Message; -import 'package:ui_components_lib/ui_components_lib.dart'; - -part 'enter_seed_phrase_cubit.freezed.dart'; - -part 'enter_seed_phrase_state.dart'; - -/// Regexp that helps splitting seed phrase into words. -final seedSplitRegExp = RegExp(r'[ |;,:\n.]'); - -const _actualSeedPhraseLength = 12; -const _legacySeedPhraseLength = 24; - -const _autoNavigationDelay = Duration(milliseconds: 500); - -/// Callback that will be called when user correctly enter seed phrase. -typedef EnterSeedPhraseConfirmCallback = void Function(String phrase); - -/// Cubit that manages the state of the seed phrase entering page. -class EnterSeedPhraseCubit extends Cubit - with ConnectionMixin, BlocBaseMixin { - EnterSeedPhraseCubit(this.context, this.confirmCallback) - : super(const EnterSeedPhraseState.initial()); - - final _log = Logger('EnterSeedPhraseCubit'); - - final BuildContext context; - final EnterSeedPhraseConfirmCallback confirmCallback; - - final formKey = GlobalKey(); - - late List _controllers; - late List _focuses; - late List _allowedValues; - late int _currentValue; - - /// Models of input - late List _inputModels; - - @override - @protected - final messengerService = inject(); - - @override - @protected - final networkConnectionService = inject(); - - @override - Future close() { - for (final c in _controllers) { - c.dispose(); - } - for (final f in _focuses) { - f.dispose(); - } - - return super.close(); - } - - void init() { - _allowedValues = - inject().currentTransport.seedPhraseWordsCount; - _currentValue = _allowedValues.first; - final max = _allowedValues.max; - _controllers = List.generate(max, (_) => TextEditingController()); - _focuses = List.generate(max, (_) => FocusNode()); - - _inputModels = List.generate( - max, - (index) => EnterSeedPhraseInputState.input( - controller: _controllers[index], - focus: _focuses[index], - index: index, - hasError: false, - ), - ); - - _controllers.forEachIndexed((index, c) { - c.addListener(() { - final hasText = - _controllers.any((controller) => controller.text.isNotEmpty); - final st = state; - // hasText means paste button must not be shown - if (st is _Tab && st.displayPasteButton == hasText) { - emitSafe(st.copyWith(displayPasteButton: !hasText)); - } - - if (hasText) { - _checkDebugPhraseGenerating(); - } - - _checkInputCompletion(index); - }); - }); - _focuses.forEachIndexed((index, f) { - f.addListener(() => _checkInputCompletion(index)); - }); - final inputs = _inputModels.take(_currentValue).toList(); - emitSafe( - EnterSeedPhraseState.tab( - allowedValues: _allowedValues, - currentValue: _currentValue.clamp(0, inputs.length), - inputs: inputs, - displayPasteButton: true, - ), - ); - } - - /// Callback for UI TextField widget - Future> suggestionsCallback( - TextEditingController controller, - ) async { - final text = controller.value.text; - if (text.isEmpty) return []; - final hints = await getHints(input: text); - if (hints.length == 1 && hints[0] == text) { - return []; - } - - return hints; - } - - void changeTab(int value) { - if (value == _currentValue) return; - - _clearAllInputs(); - - _currentValue = value; - formKey.currentState?.reset(); - - final st = state; - if (st is _Tab) { - final inputs = _inputModels.take(value).toList(); - emitSafe( - st.copyWith( - currentValue: value.clamp(0, inputs.length), - allowedValues: _allowedValues, - inputs: inputs, - displayPasteButton: true, - ), - ); - } - } - - Future confirmAction() async { - if (!await checkConnection(context)) { - return; - } - - if (await _validateFormWithError()) { - String? error; - try { - FocusManager.instance.primaryFocus?.unfocus(); - - final buffer = StringBuffer(); - - for (var i = 0; i < _currentValue; i++) { - buffer - ..write(_controllers[i].text.trim()) - ..write(' '); - } - - final phrase = buffer.toString().trimRight(); - - final mnemonicType = _currentValue == _legacySeedPhraseLength - ? const MnemonicType.legacy() - : defaultMnemonicType; - - await deriveFromPhrase( - phrase: phrase, - mnemonicType: mnemonicType, - ); - confirmCallback(phrase); - } on FrbException catch (e, s) { - _log.severe('confirmAction FrbException', e, s); - error = LocaleKeys.wrongSeed.tr(); - } on Exception catch (e, s) { - _log.severe('confirmAction', e, s); - error = e.toString(); - } - if (error != null) { - _showValidateError(error); - } - } - } - - /// [index] starts with 0 - void nextOrConfirm(int index) { - if (index == _currentValue - 1) { - confirmAction(); - } else { - _focuses[index + 1].requestFocus(); - } - } - - void clearFields() { - for (final c in _controllers) { - c - ..text = '' - ..selection = const TextSelection.collapsed(offset: 0); - } - _resetFormAndError(); - } - - /// [index] start with 0 - void onSuggestionSelected( - String suggestion, - int index, - ) { - _controllers[index] - ..text = suggestion - ..selection = TextSelection.fromPosition( - TextPosition(offset: suggestion.length), - ); - - nextOrConfirm(index); - } - - /// Reset text of controller that triggers [_checkInputCompletion] - void clearInputModel(int index) => _controllers[index].clear(); - - Future pastePhrase() async { - final words = await getSeedListFromClipboard(); - - switch (words.length) { - case _actualSeedPhraseLength: - changeTab(_actualSeedPhraseLength); - case _legacySeedPhraseLength: - changeTab(_legacySeedPhraseLength); - default: - } - - Future.delayed(const Duration(milliseconds: 100), () async { - if (words.isNotEmpty && words.length == _currentValue) { - for (final word in words) { - if (!await _checkIsWordValid(word)) { - words.clear(); - break; - } - } - } else { - words.clear(); - } - - if (words.isEmpty) { - _resetFormAndError(); - - _showValidateError(LocaleKeys.incorrectWordsFormat.tr()); - - return; - } - - _canAutoNavigate = false; - - try { - // TODO(knightforce): check why error "Not in inclusive range 0..11: 12" - words.asMap().forEach((index, word) { - _controllers[index].value = TextEditingValue( - text: word, - selection: TextSelection.fromPosition( - TextPosition(offset: word.length), - ), - ); - }); - } catch (_) {} - - await _validateFormWithError(); - - _canAutoNavigate = true; - _checkAutoNavigate(); - }); - } - - /// Check if debug phrase is entered in any text field - Future _checkDebugPhraseGenerating() async { - if (_controllers.any((e) => e.text == 'speakfriendandenter')) { - final key = await generateKey( - accountType: _currentValue == _legacySeedPhraseLength - ? const MnemonicType.legacy() - : defaultMnemonicType, - ); - - for (var i = 0; i < _controllers.take(_currentValue).length; i++) { - final text = key.words[i]; - _controllers[i].text = text; - _controllers[i].selection = - TextSelection.fromPosition(TextPosition(offset: text.length)); - } - - await _validateFormWithError(); - } - } - - void _clearAllInputs() { - for (final controller in _controllers) { - controller.clear(); - } - } - - /// [word] is valid if it is in list of hints for this word. - Future _checkIsWordValid(String word) async { - final hints = await getHints(input: word); - if (hints.contains(word)) { - return true; - } - - return false; - } - - /// Validate form and return its status. - /// It also updates state of cubit if there was some errors. - /// - /// Returns true if there was no any error, false otherwise. - Future _validateFormWithError() async { - final hasEmptyFields = !(formKey.currentState?.validate() ?? false); - var hasWrongWords = false; - - for (var index = 0; index < _currentValue; index++) { - final input = _inputModels[index]; - if (input is EnterSeedPhraseEntered && - !await _checkIsWordValid(input.text)) { - hasWrongWords = true; - _inputModels[index] = _inputModels[index].copyWith(hasError: true); - } - } - - final st = state; - if (hasWrongWords && st is _Tab) { - emitSafe(st.copyWith(inputs: _inputModels.take(_currentValue).toList())); - } - - if (hasEmptyFields) { - _showValidateError(LocaleKeys.fillMissingWords.tr()); - } else if (hasWrongWords) { - _showValidateError(LocaleKeys.incorrectWordsFormat.tr()); - } - - return !hasEmptyFields && !hasWrongWords; - } - - void _showValidateError(String message) { - inject().show( - Message.error( - context: context, - message: message, - debounceTime: defaultInfoMessageDebounceDuration, - ), - ); - } - - /// Drop form validation state - void _resetFormAndError() { - formKey.currentState?.reset(); - var changedModels = false; - - try { - for (var index = 0; index < _currentValue; index++) { - if (_inputModels[index].hasError) { - changedModels = true; - _inputModels[index] = _inputModels[index].copyWith(hasError: false); - } - } - } finally { - final st = state; - if (changedModels && st is _Tab) { - emitSafe( - st.copyWith(inputs: _inputModels.take(_currentValue).toList()), - ); - } - } - } - - /// If input with [index] has any text and it's not in focus - void _checkInputCompletion(int index) { - final controller = _controllers[index]; - final focus = _focuses[index]; - final inputModel = _inputModels[index]; - - void update() { - final st = state; - if (st is _Tab) { - emitSafe( - st.copyWith(inputs: _inputModels.take(_currentValue).toList()), - ); - } - } - - if (controller.text.isNotEmpty && - !focus.hasFocus && - inputModel is EnterSeedPhraseInput) { - // if input entered, not focused and not completed yet - _inputModels[index] = - _inputModels[index] = EnterSeedPhraseInputState.input( - controller: controller, - focus: focus, - index: index, - hasError: false, - ); - update(); - } else if (controller.text.isEmpty && - inputModel is EnterSeedPhraseEntered) { - // if input is empty but still completed - _inputModels[index] = EnterSeedPhraseInputState.input( - controller: controller, - focus: focus, - index: index, - hasError: false, - ); - update(); - } - } - - /// If true, then [_checkAutoNavigate] will work, else otherwise. - /// This can be helpful to ignore this check, for example during paste phase - bool _canAutoNavigate = true; - - /// if all inputs are completed, go next automatically - void _checkAutoNavigate() { - if (_canAutoNavigate && - _inputModels - .take(_currentValue) - .every((i) => i is EnterSeedPhraseEntered)) { - Future.delayed(_autoNavigationDelay, confirmAction); - } - } -} diff --git a/lib/feature/add_seed/enter_seed_phrase/cubit/enter_seed_phrase_cubit.freezed.dart b/lib/feature/add_seed/enter_seed_phrase/cubit/enter_seed_phrase_cubit.freezed.dart deleted file mode 100644 index 1921931e7..000000000 --- a/lib/feature/add_seed/enter_seed_phrase/cubit/enter_seed_phrase_cubit.freezed.dart +++ /dev/null @@ -1,401 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'enter_seed_phrase_cubit.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - -/// @nodoc -mixin _$EnterSeedPhraseState { - @optionalTypeArgs - TResult when({ - required TResult Function() initial, - required TResult Function(List allowedValues, int currentValue, - List inputs, bool displayPasteButton) - tab, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult? whenOrNull({ - TResult? Function()? initial, - TResult? Function(List allowedValues, int currentValue, - List inputs, bool displayPasteButton)? - tab, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult maybeWhen({ - TResult Function()? initial, - TResult Function(List allowedValues, int currentValue, - List inputs, bool displayPasteButton)? - tab, - required TResult orElse(), - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult map({ - required TResult Function(_Initial value) initial, - required TResult Function(_Tab value) tab, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult? mapOrNull({ - TResult? Function(_Initial value)? initial, - TResult? Function(_Tab value)? tab, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult maybeMap({ - TResult Function(_Initial value)? initial, - TResult Function(_Tab value)? tab, - required TResult orElse(), - }) => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $EnterSeedPhraseStateCopyWith<$Res> { - factory $EnterSeedPhraseStateCopyWith(EnterSeedPhraseState value, - $Res Function(EnterSeedPhraseState) then) = - _$EnterSeedPhraseStateCopyWithImpl<$Res, EnterSeedPhraseState>; -} - -/// @nodoc -class _$EnterSeedPhraseStateCopyWithImpl<$Res, - $Val extends EnterSeedPhraseState> - implements $EnterSeedPhraseStateCopyWith<$Res> { - _$EnterSeedPhraseStateCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of EnterSeedPhraseState - /// with the given fields replaced by the non-null parameter values. -} - -/// @nodoc -abstract class _$$InitialImplCopyWith<$Res> { - factory _$$InitialImplCopyWith( - _$InitialImpl value, $Res Function(_$InitialImpl) then) = - __$$InitialImplCopyWithImpl<$Res>; -} - -/// @nodoc -class __$$InitialImplCopyWithImpl<$Res> - extends _$EnterSeedPhraseStateCopyWithImpl<$Res, _$InitialImpl> - implements _$$InitialImplCopyWith<$Res> { - __$$InitialImplCopyWithImpl( - _$InitialImpl _value, $Res Function(_$InitialImpl) _then) - : super(_value, _then); - - /// Create a copy of EnterSeedPhraseState - /// with the given fields replaced by the non-null parameter values. -} - -/// @nodoc - -class _$InitialImpl implements _Initial { - const _$InitialImpl(); - - @override - String toString() { - return 'EnterSeedPhraseState.initial()'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && other is _$InitialImpl); - } - - @override - int get hashCode => runtimeType.hashCode; - - @override - @optionalTypeArgs - TResult when({ - required TResult Function() initial, - required TResult Function(List allowedValues, int currentValue, - List inputs, bool displayPasteButton) - tab, - }) { - return initial(); - } - - @override - @optionalTypeArgs - TResult? whenOrNull({ - TResult? Function()? initial, - TResult? Function(List allowedValues, int currentValue, - List inputs, bool displayPasteButton)? - tab, - }) { - return initial?.call(); - } - - @override - @optionalTypeArgs - TResult maybeWhen({ - TResult Function()? initial, - TResult Function(List allowedValues, int currentValue, - List inputs, bool displayPasteButton)? - tab, - required TResult orElse(), - }) { - if (initial != null) { - return initial(); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map({ - required TResult Function(_Initial value) initial, - required TResult Function(_Tab value) tab, - }) { - return initial(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull({ - TResult? Function(_Initial value)? initial, - TResult? Function(_Tab value)? tab, - }) { - return initial?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap({ - TResult Function(_Initial value)? initial, - TResult Function(_Tab value)? tab, - required TResult orElse(), - }) { - if (initial != null) { - return initial(this); - } - return orElse(); - } -} - -abstract class _Initial implements EnterSeedPhraseState { - const factory _Initial() = _$InitialImpl; -} - -/// @nodoc -abstract class _$$TabImplCopyWith<$Res> { - factory _$$TabImplCopyWith(_$TabImpl value, $Res Function(_$TabImpl) then) = - __$$TabImplCopyWithImpl<$Res>; - @useResult - $Res call( - {List allowedValues, - int currentValue, - List inputs, - bool displayPasteButton}); -} - -/// @nodoc -class __$$TabImplCopyWithImpl<$Res> - extends _$EnterSeedPhraseStateCopyWithImpl<$Res, _$TabImpl> - implements _$$TabImplCopyWith<$Res> { - __$$TabImplCopyWithImpl(_$TabImpl _value, $Res Function(_$TabImpl) _then) - : super(_value, _then); - - /// Create a copy of EnterSeedPhraseState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? allowedValues = null, - Object? currentValue = null, - Object? inputs = null, - Object? displayPasteButton = null, - }) { - return _then(_$TabImpl( - allowedValues: null == allowedValues - ? _value._allowedValues - : allowedValues // ignore: cast_nullable_to_non_nullable - as List, - currentValue: null == currentValue - ? _value.currentValue - : currentValue // ignore: cast_nullable_to_non_nullable - as int, - inputs: null == inputs - ? _value._inputs - : inputs // ignore: cast_nullable_to_non_nullable - as List, - displayPasteButton: null == displayPasteButton - ? _value.displayPasteButton - : displayPasteButton // ignore: cast_nullable_to_non_nullable - as bool, - )); - } -} - -/// @nodoc - -class _$TabImpl implements _Tab { - const _$TabImpl( - {required final List allowedValues, - required this.currentValue, - required final List inputs, - required this.displayPasteButton}) - : _allowedValues = allowedValues, - _inputs = inputs; - - final List _allowedValues; - @override - List get allowedValues { - if (_allowedValues is EqualUnmodifiableListView) return _allowedValues; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_allowedValues); - } - - @override - final int currentValue; - final List _inputs; - @override - List get inputs { - if (_inputs is EqualUnmodifiableListView) return _inputs; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_inputs); - } - - @override - final bool displayPasteButton; - - @override - String toString() { - return 'EnterSeedPhraseState.tab(allowedValues: $allowedValues, currentValue: $currentValue, inputs: $inputs, displayPasteButton: $displayPasteButton)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$TabImpl && - const DeepCollectionEquality() - .equals(other._allowedValues, _allowedValues) && - (identical(other.currentValue, currentValue) || - other.currentValue == currentValue) && - const DeepCollectionEquality().equals(other._inputs, _inputs) && - (identical(other.displayPasteButton, displayPasteButton) || - other.displayPasteButton == displayPasteButton)); - } - - @override - int get hashCode => Object.hash( - runtimeType, - const DeepCollectionEquality().hash(_allowedValues), - currentValue, - const DeepCollectionEquality().hash(_inputs), - displayPasteButton); - - /// Create a copy of EnterSeedPhraseState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$TabImplCopyWith<_$TabImpl> get copyWith => - __$$TabImplCopyWithImpl<_$TabImpl>(this, _$identity); - - @override - @optionalTypeArgs - TResult when({ - required TResult Function() initial, - required TResult Function(List allowedValues, int currentValue, - List inputs, bool displayPasteButton) - tab, - }) { - return tab(allowedValues, currentValue, inputs, displayPasteButton); - } - - @override - @optionalTypeArgs - TResult? whenOrNull({ - TResult? Function()? initial, - TResult? Function(List allowedValues, int currentValue, - List inputs, bool displayPasteButton)? - tab, - }) { - return tab?.call(allowedValues, currentValue, inputs, displayPasteButton); - } - - @override - @optionalTypeArgs - TResult maybeWhen({ - TResult Function()? initial, - TResult Function(List allowedValues, int currentValue, - List inputs, bool displayPasteButton)? - tab, - required TResult orElse(), - }) { - if (tab != null) { - return tab(allowedValues, currentValue, inputs, displayPasteButton); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map({ - required TResult Function(_Initial value) initial, - required TResult Function(_Tab value) tab, - }) { - return tab(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull({ - TResult? Function(_Initial value)? initial, - TResult? Function(_Tab value)? tab, - }) { - return tab?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap({ - TResult Function(_Initial value)? initial, - TResult Function(_Tab value)? tab, - required TResult orElse(), - }) { - if (tab != null) { - return tab(this); - } - return orElse(); - } -} - -abstract class _Tab implements EnterSeedPhraseState { - const factory _Tab( - {required final List allowedValues, - required final int currentValue, - required final List inputs, - required final bool displayPasteButton}) = _$TabImpl; - - List get allowedValues; - int get currentValue; - List get inputs; - bool get displayPasteButton; - - /// Create a copy of EnterSeedPhraseState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - _$$TabImplCopyWith<_$TabImpl> get copyWith => - throw _privateConstructorUsedError; -} diff --git a/lib/feature/add_seed/enter_seed_phrase/cubit/enter_seed_phrase_input_state.dart b/lib/feature/add_seed/enter_seed_phrase/cubit/enter_seed_phrase_input_state.dart deleted file mode 100644 index baa375f22..000000000 --- a/lib/feature/add_seed/enter_seed_phrase/cubit/enter_seed_phrase_input_state.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'enter_seed_phrase_input_state.freezed.dart'; - -/// Model of input word in seed phrase. -/// This model allows display 2 different states of every input. -/// input - ready to enter seed word -/// entered - displayed card with entered (but not 100% correct word). -@freezed -class EnterSeedPhraseInputState with _$EnterSeedPhraseInputState { - const factory EnterSeedPhraseInputState.input({ - required TextEditingController controller, - required FocusNode focus, - required int index, - required bool hasError, - }) = EnterSeedPhraseInput; - - const factory EnterSeedPhraseInputState.entered({ - required String text, - required int index, - required bool hasError, - }) = EnterSeedPhraseEntered; -} diff --git a/lib/feature/add_seed/enter_seed_phrase/cubit/enter_seed_phrase_input_state.freezed.dart b/lib/feature/add_seed/enter_seed_phrase/cubit/enter_seed_phrase_input_state.freezed.dart deleted file mode 100644 index 232780b3a..000000000 --- a/lib/feature/add_seed/enter_seed_phrase/cubit/enter_seed_phrase_input_state.freezed.dart +++ /dev/null @@ -1,486 +0,0 @@ -// coverage:ignore-file -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'enter_seed_phrase_input_state.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -T _$identity(T value) => value; - -final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); - -/// @nodoc -mixin _$EnterSeedPhraseInputState { - int get index => throw _privateConstructorUsedError; - bool get hasError => throw _privateConstructorUsedError; - @optionalTypeArgs - TResult when({ - required TResult Function(TextEditingController controller, FocusNode focus, - int index, bool hasError) - input, - required TResult Function(String text, int index, bool hasError) entered, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult? whenOrNull({ - TResult? Function(TextEditingController controller, FocusNode focus, - int index, bool hasError)? - input, - TResult? Function(String text, int index, bool hasError)? entered, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult maybeWhen({ - TResult Function(TextEditingController controller, FocusNode focus, - int index, bool hasError)? - input, - TResult Function(String text, int index, bool hasError)? entered, - required TResult orElse(), - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult map({ - required TResult Function(EnterSeedPhraseInput value) input, - required TResult Function(EnterSeedPhraseEntered value) entered, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult? mapOrNull({ - TResult? Function(EnterSeedPhraseInput value)? input, - TResult? Function(EnterSeedPhraseEntered value)? entered, - }) => - throw _privateConstructorUsedError; - @optionalTypeArgs - TResult maybeMap({ - TResult Function(EnterSeedPhraseInput value)? input, - TResult Function(EnterSeedPhraseEntered value)? entered, - required TResult orElse(), - }) => - throw _privateConstructorUsedError; - - /// Create a copy of EnterSeedPhraseInputState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $EnterSeedPhraseInputStateCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $EnterSeedPhraseInputStateCopyWith<$Res> { - factory $EnterSeedPhraseInputStateCopyWith(EnterSeedPhraseInputState value, - $Res Function(EnterSeedPhraseInputState) then) = - _$EnterSeedPhraseInputStateCopyWithImpl<$Res, EnterSeedPhraseInputState>; - @useResult - $Res call({int index, bool hasError}); -} - -/// @nodoc -class _$EnterSeedPhraseInputStateCopyWithImpl<$Res, - $Val extends EnterSeedPhraseInputState> - implements $EnterSeedPhraseInputStateCopyWith<$Res> { - _$EnterSeedPhraseInputStateCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of EnterSeedPhraseInputState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? index = null, - Object? hasError = null, - }) { - return _then(_value.copyWith( - index: null == index - ? _value.index - : index // ignore: cast_nullable_to_non_nullable - as int, - hasError: null == hasError - ? _value.hasError - : hasError // ignore: cast_nullable_to_non_nullable - as bool, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$EnterSeedPhraseInputImplCopyWith<$Res> - implements $EnterSeedPhraseInputStateCopyWith<$Res> { - factory _$$EnterSeedPhraseInputImplCopyWith(_$EnterSeedPhraseInputImpl value, - $Res Function(_$EnterSeedPhraseInputImpl) then) = - __$$EnterSeedPhraseInputImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {TextEditingController controller, - FocusNode focus, - int index, - bool hasError}); -} - -/// @nodoc -class __$$EnterSeedPhraseInputImplCopyWithImpl<$Res> - extends _$EnterSeedPhraseInputStateCopyWithImpl<$Res, - _$EnterSeedPhraseInputImpl> - implements _$$EnterSeedPhraseInputImplCopyWith<$Res> { - __$$EnterSeedPhraseInputImplCopyWithImpl(_$EnterSeedPhraseInputImpl _value, - $Res Function(_$EnterSeedPhraseInputImpl) _then) - : super(_value, _then); - - /// Create a copy of EnterSeedPhraseInputState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? controller = null, - Object? focus = null, - Object? index = null, - Object? hasError = null, - }) { - return _then(_$EnterSeedPhraseInputImpl( - controller: null == controller - ? _value.controller - : controller // ignore: cast_nullable_to_non_nullable - as TextEditingController, - focus: null == focus - ? _value.focus - : focus // ignore: cast_nullable_to_non_nullable - as FocusNode, - index: null == index - ? _value.index - : index // ignore: cast_nullable_to_non_nullable - as int, - hasError: null == hasError - ? _value.hasError - : hasError // ignore: cast_nullable_to_non_nullable - as bool, - )); - } -} - -/// @nodoc - -class _$EnterSeedPhraseInputImpl implements EnterSeedPhraseInput { - const _$EnterSeedPhraseInputImpl( - {required this.controller, - required this.focus, - required this.index, - required this.hasError}); - - @override - final TextEditingController controller; - @override - final FocusNode focus; - @override - final int index; - @override - final bool hasError; - - @override - String toString() { - return 'EnterSeedPhraseInputState.input(controller: $controller, focus: $focus, index: $index, hasError: $hasError)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$EnterSeedPhraseInputImpl && - (identical(other.controller, controller) || - other.controller == controller) && - (identical(other.focus, focus) || other.focus == focus) && - (identical(other.index, index) || other.index == index) && - (identical(other.hasError, hasError) || - other.hasError == hasError)); - } - - @override - int get hashCode => - Object.hash(runtimeType, controller, focus, index, hasError); - - /// Create a copy of EnterSeedPhraseInputState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$EnterSeedPhraseInputImplCopyWith<_$EnterSeedPhraseInputImpl> - get copyWith => - __$$EnterSeedPhraseInputImplCopyWithImpl<_$EnterSeedPhraseInputImpl>( - this, _$identity); - - @override - @optionalTypeArgs - TResult when({ - required TResult Function(TextEditingController controller, FocusNode focus, - int index, bool hasError) - input, - required TResult Function(String text, int index, bool hasError) entered, - }) { - return input(controller, focus, index, hasError); - } - - @override - @optionalTypeArgs - TResult? whenOrNull({ - TResult? Function(TextEditingController controller, FocusNode focus, - int index, bool hasError)? - input, - TResult? Function(String text, int index, bool hasError)? entered, - }) { - return input?.call(controller, focus, index, hasError); - } - - @override - @optionalTypeArgs - TResult maybeWhen({ - TResult Function(TextEditingController controller, FocusNode focus, - int index, bool hasError)? - input, - TResult Function(String text, int index, bool hasError)? entered, - required TResult orElse(), - }) { - if (input != null) { - return input(controller, focus, index, hasError); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map({ - required TResult Function(EnterSeedPhraseInput value) input, - required TResult Function(EnterSeedPhraseEntered value) entered, - }) { - return input(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull({ - TResult? Function(EnterSeedPhraseInput value)? input, - TResult? Function(EnterSeedPhraseEntered value)? entered, - }) { - return input?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap({ - TResult Function(EnterSeedPhraseInput value)? input, - TResult Function(EnterSeedPhraseEntered value)? entered, - required TResult orElse(), - }) { - if (input != null) { - return input(this); - } - return orElse(); - } -} - -abstract class EnterSeedPhraseInput implements EnterSeedPhraseInputState { - const factory EnterSeedPhraseInput( - {required final TextEditingController controller, - required final FocusNode focus, - required final int index, - required final bool hasError}) = _$EnterSeedPhraseInputImpl; - - TextEditingController get controller; - FocusNode get focus; - @override - int get index; - @override - bool get hasError; - - /// Create a copy of EnterSeedPhraseInputState - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$EnterSeedPhraseInputImplCopyWith<_$EnterSeedPhraseInputImpl> - get copyWith => throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class _$$EnterSeedPhraseEnteredImplCopyWith<$Res> - implements $EnterSeedPhraseInputStateCopyWith<$Res> { - factory _$$EnterSeedPhraseEnteredImplCopyWith( - _$EnterSeedPhraseEnteredImpl value, - $Res Function(_$EnterSeedPhraseEnteredImpl) then) = - __$$EnterSeedPhraseEnteredImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({String text, int index, bool hasError}); -} - -/// @nodoc -class __$$EnterSeedPhraseEnteredImplCopyWithImpl<$Res> - extends _$EnterSeedPhraseInputStateCopyWithImpl<$Res, - _$EnterSeedPhraseEnteredImpl> - implements _$$EnterSeedPhraseEnteredImplCopyWith<$Res> { - __$$EnterSeedPhraseEnteredImplCopyWithImpl( - _$EnterSeedPhraseEnteredImpl _value, - $Res Function(_$EnterSeedPhraseEnteredImpl) _then) - : super(_value, _then); - - /// Create a copy of EnterSeedPhraseInputState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? text = null, - Object? index = null, - Object? hasError = null, - }) { - return _then(_$EnterSeedPhraseEnteredImpl( - text: null == text - ? _value.text - : text // ignore: cast_nullable_to_non_nullable - as String, - index: null == index - ? _value.index - : index // ignore: cast_nullable_to_non_nullable - as int, - hasError: null == hasError - ? _value.hasError - : hasError // ignore: cast_nullable_to_non_nullable - as bool, - )); - } -} - -/// @nodoc - -class _$EnterSeedPhraseEnteredImpl implements EnterSeedPhraseEntered { - const _$EnterSeedPhraseEnteredImpl( - {required this.text, required this.index, required this.hasError}); - - @override - final String text; - @override - final int index; - @override - final bool hasError; - - @override - String toString() { - return 'EnterSeedPhraseInputState.entered(text: $text, index: $index, hasError: $hasError)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$EnterSeedPhraseEnteredImpl && - (identical(other.text, text) || other.text == text) && - (identical(other.index, index) || other.index == index) && - (identical(other.hasError, hasError) || - other.hasError == hasError)); - } - - @override - int get hashCode => Object.hash(runtimeType, text, index, hasError); - - /// Create a copy of EnterSeedPhraseInputState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$EnterSeedPhraseEnteredImplCopyWith<_$EnterSeedPhraseEnteredImpl> - get copyWith => __$$EnterSeedPhraseEnteredImplCopyWithImpl< - _$EnterSeedPhraseEnteredImpl>(this, _$identity); - - @override - @optionalTypeArgs - TResult when({ - required TResult Function(TextEditingController controller, FocusNode focus, - int index, bool hasError) - input, - required TResult Function(String text, int index, bool hasError) entered, - }) { - return entered(text, index, hasError); - } - - @override - @optionalTypeArgs - TResult? whenOrNull({ - TResult? Function(TextEditingController controller, FocusNode focus, - int index, bool hasError)? - input, - TResult? Function(String text, int index, bool hasError)? entered, - }) { - return entered?.call(text, index, hasError); - } - - @override - @optionalTypeArgs - TResult maybeWhen({ - TResult Function(TextEditingController controller, FocusNode focus, - int index, bool hasError)? - input, - TResult Function(String text, int index, bool hasError)? entered, - required TResult orElse(), - }) { - if (entered != null) { - return entered(text, index, hasError); - } - return orElse(); - } - - @override - @optionalTypeArgs - TResult map({ - required TResult Function(EnterSeedPhraseInput value) input, - required TResult Function(EnterSeedPhraseEntered value) entered, - }) { - return entered(this); - } - - @override - @optionalTypeArgs - TResult? mapOrNull({ - TResult? Function(EnterSeedPhraseInput value)? input, - TResult? Function(EnterSeedPhraseEntered value)? entered, - }) { - return entered?.call(this); - } - - @override - @optionalTypeArgs - TResult maybeMap({ - TResult Function(EnterSeedPhraseInput value)? input, - TResult Function(EnterSeedPhraseEntered value)? entered, - required TResult orElse(), - }) { - if (entered != null) { - return entered(this); - } - return orElse(); - } -} - -abstract class EnterSeedPhraseEntered implements EnterSeedPhraseInputState { - const factory EnterSeedPhraseEntered( - {required final String text, - required final int index, - required final bool hasError}) = _$EnterSeedPhraseEnteredImpl; - - String get text; - @override - int get index; - @override - bool get hasError; - - /// Create a copy of EnterSeedPhraseInputState - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$EnterSeedPhraseEnteredImplCopyWith<_$EnterSeedPhraseEnteredImpl> - get copyWith => throw _privateConstructorUsedError; -} diff --git a/lib/feature/add_seed/enter_seed_phrase/cubit/enter_seed_phrase_state.dart b/lib/feature/add_seed/enter_seed_phrase/cubit/enter_seed_phrase_state.dart deleted file mode 100644 index 27223c2e8..000000000 --- a/lib/feature/add_seed/enter_seed_phrase/cubit/enter_seed_phrase_state.dart +++ /dev/null @@ -1,19 +0,0 @@ -part of 'enter_seed_phrase_cubit.dart'; - -/// State for -@freezed -class EnterSeedPhraseState with _$EnterSeedPhraseState { - /// Initial state - const factory EnterSeedPhraseState.initial() = _Initial; - - /// State that displays 12 or 24 words entering - /// [allowedValues] and [currentValue] used for tab widget. - /// If [allowedValues] contains only one item, tab widget must not be shown. - /// [displayPasteButton] = false if there are no text in fields else true - const factory EnterSeedPhraseState.tab({ - required List allowedValues, - required int currentValue, - required List inputs, - required bool displayPasteButton, - }) = _Tab; -} diff --git a/lib/feature/add_seed/enter_seed_phrase/data/input_data.dart b/lib/feature/add_seed/enter_seed_phrase/data/input_data.dart new file mode 100644 index 000000000..210425c3c --- /dev/null +++ b/lib/feature/add_seed/enter_seed_phrase/data/input_data.dart @@ -0,0 +1,43 @@ +import 'package:flutter/widgets.dart'; + +class EnterSeedPhraseInputData { + EnterSeedPhraseInputData({ + required this.index, + required this.controller, + required this.focusNode, + bool? isError, + }) : _isError = isError ?? false; + + EnterSeedPhraseInputData.init(int index) + : this( + index: index, + controller: TextEditingController(), + focusNode: FocusNode(), + ); + + final int index; + final TextEditingController controller; + final FocusNode focusNode; + bool _isError; + + set isError(bool? value) { + _isError = value ?? _isError; + } + + bool get isError => _isError; + + String get text => controller.text; + + bool get isFocused => focusNode.hasFocus; + + void dispose() { + controller.dispose(); + focusNode.dispose(); + } + + void clear() { + controller + ..text = '' + ..selection = const TextSelection.collapsed(offset: 0); + } +} diff --git a/lib/feature/add_seed/enter_seed_phrase/data/tab_data.dart b/lib/feature/add_seed/enter_seed_phrase/data/tab_data.dart new file mode 100644 index 000000000..6d550df35 --- /dev/null +++ b/lib/feature/add_seed/enter_seed_phrase/data/tab_data.dart @@ -0,0 +1,36 @@ +import 'package:app/feature/add_seed/enter_seed_phrase/data/input_data.dart'; + +class EnterSeedPhraseTabData { + EnterSeedPhraseTabData({ + required this.allowedValues, + required this.currentValue, + required this.inputs, + }); + + final List allowedValues; + final int currentValue; + final List inputs; + + late final fistInputsRange = inputs.getRange( + 0, + currentValue ~/ _gridColumnCount, + ); + + late final secondInputsRange = inputs.getRange( + currentValue ~/ _gridColumnCount, + currentValue, + ); + + static const _gridColumnCount = 2; + + EnterSeedPhraseTabData copyWith({ + int? currentValue, + List? inputs, + }) { + return EnterSeedPhraseTabData( + allowedValues: allowedValues, + currentValue: currentValue ?? this.currentValue, + inputs: inputs ?? this.inputs, + ); + } +} diff --git a/lib/feature/add_seed/enter_seed_phrase/enter_seed_phrase.dart b/lib/feature/add_seed/enter_seed_phrase/enter_seed_phrase.dart index 3fbdbb0e6..f951ba282 100644 --- a/lib/feature/add_seed/enter_seed_phrase/enter_seed_phrase.dart +++ b/lib/feature/add_seed/enter_seed_phrase/enter_seed_phrase.dart @@ -1,2 +1,107 @@ -export 'cubit/cubit.dart'; -export 'view/view.dart'; +import 'package:app/feature/add_seed/enter_seed_phrase/data/tab_data.dart'; +import 'package:app/feature/add_seed/enter_seed_phrase/enter_seed_phrase_wm.dart'; +import 'package:app/feature/add_seed/enter_seed_phrase/widgets/words.dart'; +import 'package:app/generated/generated.dart'; +import 'package:elementary/elementary.dart'; +import 'package:elementary_helper/elementary_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:ui_components_lib/ui_components_lib.dart'; +import 'package:ui_components_lib/v2/widgets/widgets.dart'; + +class EnterSeedPhrase extends ElementaryWidget { + const EnterSeedPhrase({ + Key? key, + WidgetModelFactory wmFactory = + defaultEnterSeedPhraseWidgetModelFactory, + }) : super( + wmFactory, + key: key, + ); + + @override + Widget build(EnterSeedPhraseWidgetModel wm) { + final theme = wm.themeStyleV2; + final colors = wm.colors; + + return GestureDetector( + onTap: wm.onPressedResetFocus, + child: Scaffold( + backgroundColor: theme.colors.background0, + resizeToAvoidBottomInset: false, + appBar: DefaultAppBar( + onClosePressed: wm.onClosePressed, + ), + body: SafeArea( + minimum: const EdgeInsets.only(bottom: DimensSizeV2.d16), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: DimensSizeV2.d16), + child: Column( + children: [ + Image.asset( + Assets.images.seedPhraseIcon.path, + width: DimensSizeV2.d56, + height: DimensSizeV2.d56, + ), + const SizedBox(height: DimensSizeV2.d16), + Text( + LocaleKeys.enterSeedPhrase.tr(), + style: theme.textStyles.headingLarge, + ), + Padding( + padding: const EdgeInsets.only( + top: DimensSizeV2.d8, + left: DimensSizeV2.d16, + right: DimensSizeV2.d16, + ), + child: PrimaryText( + LocaleKeys.pasteSeedIntoFirstBox.tr(), + ), + ), + if (wm.isExistBottomPadding) + Divider( + color: colors.strokePrimary, + height: DimensStroke.small, + thickness: DimensStroke.small, + ), + Expanded( + child: StateNotifierBuilder( + listenableState: wm.tabState, + builder: (_, EnterSeedPhraseTabData? tabData) { + if (tabData == null) { + return const SizedBox.shrink(); + } + return EnterSeedPhraseWords( + formKey: wm.formKey, + allowedValues: tabData.allowedValues, + currentValue: tabData.currentValue, + displayPasteButtonState: wm.displayPasteButtonState, + tabState: wm.tabState, + changeTab: wm.changeTab, + pastePhrase: wm.pastePhrase, + clearFields: wm.clearFields, + onSuggestions: wm.onSuggestions, + onSuggestionSelected: wm.onSuggestionSelected, + onNext: wm.nextOrConfirm, + ); + }, + ), + ), + SizedBox( + // subtract commonButtonHeight to avoid button above keyboard + height: wm.isExistBottomPadding + ? wm.bottomPadding - commonButtonHeight + : 0, + ), + AccentButton( + buttonShape: ButtonShape.pill, + title: LocaleKeys.confirm.tr(), + onPressed: wm.confirm, + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/feature/add_seed/enter_seed_phrase/enter_seed_phrase_model.dart b/lib/feature/add_seed/enter_seed_phrase/enter_seed_phrase_model.dart new file mode 100644 index 000000000..16b38db8b --- /dev/null +++ b/lib/feature/add_seed/enter_seed_phrase/enter_seed_phrase_model.dart @@ -0,0 +1,51 @@ +import 'package:app/app/service/messenger/service/messenger_service.dart'; +import 'package:app/app/service/network_connection/network_connection_service.dart'; +import 'package:app/feature/add_seed/enter_seed_phrase/enter_seed_phrase.dart'; +import 'package:app/feature/constants.dart'; +import 'package:app/utils/mixins/connection_mixin.dart'; +import 'package:elementary/elementary.dart'; +import 'package:nekoton_repository/nekoton_repository.dart' as nekoton; + +/// [ElementaryModel] for [EnterSeedPhrase] +class EnterSeedPhraseModel extends ElementaryModel with ConnectionMixin { + EnterSeedPhraseModel( + ErrorHandler errorHandler, + this.networkConnectionService, + this.messengerService, + this._nekotonRepository, + ) : super(errorHandler: errorHandler); + + final actualSeedPhraseLength = 12; + final legacySeedPhraseLength = 24; + + @override + final NetworkConnectionService networkConnectionService; + @override + final MessengerService messengerService; + final nekoton.NekotonRepository _nekotonRepository; + + late final List seedPhraseWordsCount = + _nekotonRepository.currentTransport.seedPhraseWordsCount; + + Future getKey(int currentValue) { + return nekoton.generateKey( + accountType: currentValue == legacySeedPhraseLength + ? const nekoton.MnemonicType.legacy() + : defaultMnemonicType, + ); + } + + Future> getHints(String text) => nekoton.getHints(input: text); + + /// [text] is valid if it is in list of hints for this word. + Future checkIsWordValid(String text) async { + final hints = await getHints(text); + if (hints.contains(text)) { + return true; + } + + return false; + } + + void showError(String text) => handleError(text); +} diff --git a/lib/feature/add_seed/enter_seed_phrase/enter_seed_phrase_wm.dart b/lib/feature/add_seed/enter_seed_phrase/enter_seed_phrase_wm.dart new file mode 100644 index 000000000..bb85c231a --- /dev/null +++ b/lib/feature/add_seed/enter_seed_phrase/enter_seed_phrase_wm.dart @@ -0,0 +1,398 @@ +import 'package:app/app/router/app_route.dart'; +import 'package:app/app/router/routs/add_seed/add_seed.dart'; +import 'package:app/core/error_handler_factory.dart'; +import 'package:app/core/wm/custom_wm.dart'; +import 'package:app/di/di.dart'; +import 'package:app/feature/add_seed/enter_seed_phrase/data/input_data.dart'; +import 'package:app/feature/add_seed/enter_seed_phrase/data/tab_data.dart'; +import 'package:app/feature/add_seed/enter_seed_phrase/enter_seed_phrase.dart'; +import 'package:app/feature/add_seed/enter_seed_phrase/enter_seed_phrase_model.dart'; +import 'package:app/feature/constants.dart'; +import 'package:app/generated/generated.dart'; +import 'package:app/utils/focus_utils.dart'; +import 'package:app/utils/seed_utils.dart'; +import 'package:collection/collection.dart'; +import 'package:elementary/elementary.dart'; +import 'package:elementary_helper/elementary_helper.dart'; +import 'package:flutter/widgets.dart'; +import 'package:go_router/go_router.dart'; +import 'package:logging/logging.dart'; +import 'package:nekoton_repository/nekoton_repository.dart'; +import 'package:ui_components_lib/ui_components_lib.dart'; + +typedef SuggestionSelectedCallback = void Function( + String suggestion, + int index, +); + +/// Factory method for creating [EnterSeedPhraseWidgetModel] +EnterSeedPhraseWidgetModel defaultEnterSeedPhraseWidgetModelFactory( + BuildContext context, +) { + return EnterSeedPhraseWidgetModel( + EnterSeedPhraseModel( + createPrimaryErrorHandler(context), + inject(), + inject(), + inject(), + ), + ); +} + +/// [WidgetModel] для [EnterSeedPhrase] +class EnterSeedPhraseWidgetModel + extends CustomWidgetModel { + EnterSeedPhraseWidgetModel( + super.model, + ); + + final formKey = GlobalKey(); + + final _log = Logger('EnterSeedPhraseCubit'); + + late final List _inputDataList = List.unmodifiable( + [ + for (int i = 0; i < model.seedPhraseWordsCount.max; i++) + EnterSeedPhraseInputData.init(i) + ..controller.addListener(() { + final isExistText = _inputDataList.any( + (data) => data.text.isNotEmpty, + ); + + if (displayPasteButtonState.value == isExistText) { + _displayPasteButtonState.accept(!isExistText); + } + + if (isExistText) { + _checkDebugPhraseGenerating(); + } + + _checkInputCompletion(i); + }) + ..focusNode.addListener(() => _checkInputCompletion(i)), + ], + ); + + late final _displayPasteButtonState = createNotifier(true); + late final _tabState = createNotifier( + () { + final currentValue = model.seedPhraseWordsCount.first; + + return EnterSeedPhraseTabData( + allowedValues: model.seedPhraseWordsCount, + currentValue: currentValue, + inputs: _inputDataList.take(currentValue).toList(), + ); + }(), + ); + + ColorsPalette get colors => context.themeStyle.colors; + + ThemeStyleV2 get themeStyleV2 => context.themeStyleV2; + + double get bottomPadding => MediaQuery.of(context).viewInsets.bottom; + + bool get isExistBottomPadding => bottomPadding >= commonButtonHeight; + + ListenableState get displayPasteButtonState => _displayPasteButtonState; + + ListenableState get tabState => _tabState; + + int get _currentValue => + _tabState.value?.currentValue ?? model.seedPhraseWordsCount.first; + + EnterSeedPhraseTabData? get _tabData => _tabState.value; + + @override + void dispose() { + for (final c in _inputDataList) { + c.dispose(); + } + + super.dispose(); + } + + void onPressedResetFocus() => resetFocus(contextSafe); + + void onClosePressed(BuildContext context) => context.maybePop(); + + /// Callback for UI TextField widget + Future> onSuggestions(String text) async { + if (text.isEmpty) return []; + final hints = await model.getHints(text); + if (hints.length == 1 && hints[0] == text) { + return []; + } + + return hints; + } + + void changeTab(int value) { + if (value == _currentValue) return; + + final tabData = _tabData; + + if (tabData == null) { + return; + } + + _clearAllInputs(); + _resetErrors(); + + formKey.currentState?.reset(); + + _tabState.accept( + tabData.copyWith( + currentValue: value, + inputs: _inputDataList.take(value).toList(), + ), + ); + + _displayPasteButtonState.accept(true); + } + + Future confirm() async { + if (!await model.checkConnection(context)) { + return; + } + + if (await _validateFormWithError()) { + try { + FocusManager.instance.primaryFocus?.unfocus(); + + final buffer = StringBuffer(); + + for (var i = 0; i < _currentValue; i++) { + buffer + ..write(_inputDataList[i].text.trim()) + ..write(' '); + } + + final phrase = buffer.toString().trimRight(); + + final mnemonicType = _currentValue == model.legacySeedPhraseLength + ? const MnemonicType.legacy() + : defaultMnemonicType; + + await deriveFromPhrase( + phrase: phrase, + mnemonicType: mnemonicType, + ); + _next(phrase); + } on AnyhowException catch (e, s) { + _log.severe('confirmAction AnyhowException', e, s); + model.showError(LocaleKeys.wrongSeed.tr()); + } on Exception catch (e, s) { + _log.severe('confirmAction', e, s); + model.showError(e.toString()); + } + } + } + + /// [index] starts with 0 + void nextOrConfirm(int index) { + if (index == _currentValue - 1) { + confirm(); + } else if (index + 1 < _inputDataList.length) { + _inputDataList[index + 1].focusNode.requestFocus(); + } + } + + void clearFields() { + for (final data in _inputDataList) { + data.clear(); + } + _resetFormAndError(); + } + + /// [index] start with 0 + void onSuggestionSelected( + String suggestion, + int index, + ) { + _inputDataList[index].controller + ..text = suggestion + ..selection = TextSelection.fromPosition( + TextPosition(offset: suggestion.length), + ); + + nextOrConfirm(index); + } + + /// Reset text of controller that triggers [_checkInputCompletion] + void clearField(int index) => _inputDataList[index].clear(); + + Future pastePhrase() async { + final words = await getSeedListFromClipboard(); + + final count = words.length; + if (count == model.actualSeedPhraseLength || + count == model.legacySeedPhraseLength) { + changeTab(count); + } + + Future.delayed(const Duration(milliseconds: 100), () async { + if (words.isNotEmpty && words.length == _currentValue) { + for (final word in words) { + if (!await model.checkIsWordValid(word)) { + words.clear(); + break; + } + } + } else { + words.clear(); + } + + if (words.isEmpty) { + _resetFormAndError(); + + model.showError(LocaleKeys.incorrectWordsFormat.tr()); + + return; + } + + try { + if (words.length > _inputDataList.length) { + words.length = _inputDataList.length; + } + + words.asMap().forEach((index, word) { + _inputDataList[index].controller.value = TextEditingValue( + text: word, + selection: TextSelection.fromPosition( + TextPosition(offset: word.length), + ), + ); + }); + } catch (_) {} + + await _validateFormWithError(); + }); + } + + /// Check if debug phrase is entered in any text field + Future _checkDebugPhraseGenerating() async { + if (!_inputDataList.any((data) => data.text == 'speakfriendandenter')) { + return; + } + + final key = await model.getKey(_currentValue); + + final count = _inputDataList.take(_currentValue).length; + + for (var i = 0; i < count; i++) { + final text = key.words[i]; + _inputDataList[i].controller + ..text = text + ..selection = TextSelection.fromPosition( + TextPosition(offset: text.length), + ); + } + + await _validateFormWithError(); + } + + /// If input with [index] has any text and it's not in focus + void _checkInputCompletion(int index) { + final data = _inputDataList[index]; + + if (data.text.isNotEmpty && !data.isFocused) { + _inputDataList[index].isError = false; + + _updateTab(); + } + } + + void _next(String phrase) { + final path = + GoRouter.of(context).routerDelegate.currentConfiguration.fullPath; + final route = getCurrentAppRoute(fullPath: path); + + if (route != AppRoute.createSeedPassword) { + context.goFurther( + AppRoute.createSeedPassword.pathWithData( + queryParameters: { + addSeedPhraseQueryParam: phrase, + }, + ), + preserveQueryParams: true, + ); + } + } + + /// Validate form and return its status. + /// It also updates state of cubit if there was some errors. + /// + /// Returns true if there was no any error, false otherwise. + + Future _validateFormWithError() async { + final isEmptyFields = !(formKey.currentState?.validate() ?? false); + var isWrongWords = false; + + for (var index = 0; index < _currentValue; index++) { + final input = _inputDataList[index]; + if (!await model.checkIsWordValid(input.text)) { + isWrongWords = true; + _inputDataList[index].isError = true; + } + } + + _updateTab(); + + if (isEmptyFields) { + model.showError(LocaleKeys.fillMissingWords.tr()); + } else if (isWrongWords) { + model.showError(LocaleKeys.incorrectWordsFormat.tr()); + } + + return !isEmptyFields && !isWrongWords; + } + + /// Drop form validation state + void _resetFormAndError() { + formKey.currentState?.reset(); + var isChanged = false; + + try { + for (var index = 0; index < _currentValue; index++) { + if (_inputDataList[index].isError) { + isChanged = true; + _inputDataList[index].isError = false; + } + } + } finally { + if (isChanged) { + _updateTab(); + } + } + } + + void _resetErrors() { + for (final data in _inputDataList) { + data.isError = false; + } + } + + void _updateTab() { + final data = _tabData; + if (data == null) { + return; + } + + var inputs = data.inputs; + + if (data.inputs.length != _currentValue) { + inputs = _inputDataList.take(_currentValue).toList(); + } + + _tabState.accept( + data.copyWith(inputs: inputs), + ); + } + + void _clearAllInputs() { + for (final data in _inputDataList) { + data.controller.clear(); + } + } +} diff --git a/lib/feature/add_seed/enter_seed_phrase/view/enter_seed_phrase_page.dart b/lib/feature/add_seed/enter_seed_phrase/view/enter_seed_phrase_page.dart deleted file mode 100644 index ed93ff362..000000000 --- a/lib/feature/add_seed/enter_seed_phrase/view/enter_seed_phrase_page.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:app/app/router/router.dart'; -import 'package:app/feature/add_seed/enter_seed_phrase/cubit/enter_seed_phrase_cubit.dart'; -import 'package:app/feature/add_seed/enter_seed_phrase/view/enter_seed_phrase_view.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; -import 'package:ui_components_lib/ui_components_lib.dart'; - -/// {@template enter_seed_phrase_page} -/// Starting page for seed phrase entering. -/// {@endtemplate} -class EnterSeedPhrasePage extends StatelessWidget { - /// {@macro enter_seed_phrase_page} - const EnterSeedPhrasePage({super.key}); - - @override - Widget build(BuildContext context) { - final theme = context.themeStyleV2; - return BlocProvider( - create: (context) => EnterSeedPhraseCubit( - context, - // ignore: prefer-extracting-callbacks - (phrase) { - final path = - GoRouter.of(context).routerDelegate.currentConfiguration.fullPath; - final route = getCurrentAppRoute(fullPath: path); - - // because of automatic navigation, we may face problem with double - // navigation here. - if (route != AppRoute.createSeedPassword) { - context.goFurther( - AppRoute.createSeedPassword.pathWithData( - queryParameters: { - addSeedPhraseQueryParam: phrase, - }, - ), - preserveQueryParams: true, - ); - } - }, - )..init(), - child: GestureDetector( - onTap: () => FocusScope.of(context).unfocus(), - child: Scaffold( - backgroundColor: theme.colors.background0, - resizeToAvoidBottomInset: false, - appBar: DefaultAppBar( - onClosePressed: (context) => context.maybePop(), - ), - body: const EnterSeedPhraseView(), - ), - ), - ); - } -} diff --git a/lib/feature/add_seed/enter_seed_phrase/view/enter_seed_phrase_view.dart b/lib/feature/add_seed/enter_seed_phrase/view/enter_seed_phrase_view.dart deleted file mode 100644 index 222537673..000000000 --- a/lib/feature/add_seed/enter_seed_phrase/view/enter_seed_phrase_view.dart +++ /dev/null @@ -1,285 +0,0 @@ -import 'package:app/feature/add_seed/enter_seed_phrase/cubit/cubit.dart'; -import 'package:app/generated/generated.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:lucide_icons_flutter/lucide_icons.dart'; -import 'package:ui_components_lib/components/input/common_input_style_v2.dart'; -import 'package:ui_components_lib/ui_components_lib.dart'; -import 'package:ui_components_lib/v2/ui_components_lib_v2.dart'; - -const _gridColumnCount = 2; - -/// {@template enter_seed_phrase_view} -/// Screen that allows user to enter seed phrase with 12 or 24 words. -/// {@endtemplate} -class EnterSeedPhraseView extends StatelessWidget { - /// {@macro enter_seed_phrase_view} - const EnterSeedPhraseView({super.key}); - - @override - Widget build(BuildContext context) { - final colors = context.themeStyle.colors; - final theme = context.themeStyleV2; - final bottomPadding = MediaQuery.of(context).viewInsets.bottom; - final hasBottomPadding = bottomPadding >= commonButtonHeight; - - return SafeArea( - minimum: const EdgeInsets.only(bottom: DimensSizeV2.d16), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: DimensSizeV2.d16), - child: Column( - children: [ - Image.asset( - Assets.images.seedPhraseIcon.path, - width: DimensSizeV2.d56, - height: DimensSizeV2.d56, - ), - const SizedBox(height: DimensSizeV2.d16), - Text( - LocaleKeys.enterSeedPhrase.tr(), - style: theme.textStyles.headingLarge, - ), - Padding( - padding: const EdgeInsets.only( - top: DimensSizeV2.d8, - left: DimensSizeV2.d16, - right: DimensSizeV2.d16, - ), - child: PrimaryText( - LocaleKeys.pasteSeedIntoFirstBox.tr(), - ), - ), - if (hasBottomPadding) - Divider( - color: colors.strokePrimary, - height: DimensStroke.small, - thickness: DimensStroke.small, - ), - Expanded( - child: _buildPhrasesList(theme), - ), - SizedBox( - // subtract commonButtonHeight to avoid button above keyboard - height: hasBottomPadding ? bottomPadding - commonButtonHeight : 0, - ), - AccentButton( - buttonShape: ButtonShape.pill, - title: LocaleKeys.confirm.tr(), - onPressed: () => - context.read().confirmAction(), - ), - ], - ), - ), - ); - } - - // ignore:long-method - Widget _buildPhrasesList(ThemeStyleV2 theme) { - return BlocBuilder( - builder: (context, state) { - final cubit = context.read(); - final bottomPadding = MediaQuery.of(context).viewInsets.bottom; - final isKeyboardOpen = bottomPadding >= commonButtonHeight; - - return state.when( - initial: () => const SizedBox.shrink(), - tab: ( - allowedValues, - currentValue, - inputsModels, - displayPasteButton, - ) => - SingleChildScrollView( - child: Form( - key: cubit.formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!isKeyboardOpen) ...[ - const SizedBox(height: DimensSize.d24), - _tabs( - allowedValues, - cubit, - currentValue, - displayPasteButton, - ), - ], - _inputs(theme, inputsModels, currentValue), - const SizedBox(height: DimensSize.d16), - ], - ), - ), - ), - ); - }, - ); - } - - /// [currentValue] starts with 0 - // ignore: long-method - Widget _inputBuild( - ThemeStyleV2 themeStyleV2, - EnterSeedPhraseInputState input, - int currentValue, - ) { - final displayIndex = input.index + 1; - final indexText = NumberFormat('00').format(displayIndex); - - return Builder( - builder: (context) { - final cubit = context.read(); - final colors = context.themeStyle.colors; - - return input.when( - entered: (text, index, hasError) { - return PressScaleWidget( - onPressed: () => cubit.clearInputModel(index), - child: CommonCard( - width: double.infinity, - alignment: Alignment.center, - leadingText: indexText, - titleText: text, - trailingChild: - CommonIconWidget.svg(svg: Assets.images.trash.path), - borderColor: hasError ? colors.alert : null, - ), - ); - }, - input: (controller, focus, index, hasError) { - return CommonInput( - autocorrect: false, - hintStyle: themeStyleV2.textStyles.labelSmall, - inactiveBorderColor: themeStyleV2.colors.border0, - textStyle: themeStyleV2.textStyles.labelSmall, - suggestionBackground: themeStyleV2.colors.background1, - keyboardType: TextInputType.visiblePassword, - key: Key('SeedInput-$index'), - height: DimensSize.d48, - controller: controller, - suggestionsCallback: (_) => cubit.suggestionsCallback(controller), - itemBuilder: _itemBuilder, - onSuggestionSelected: (suggestion) => - cubit.onSuggestionSelected(suggestion, index), - focusNode: focus, - // show error border if field is empty - validator: (v) => v?.isEmpty ?? true ? '' : null, - needClearButton: false, - // IntrinsicWidth to force Center match prefixIconConstraints - prefixIcon: IntrinsicWidth( - child: Center( - child: Text( - indexText, - style: StyleRes.addRegular.copyWith( - color: colors.textSecondary, - ), - ), - ), - ), - onSubmitted: (_) => cubit.nextOrConfirm(index), - textInputAction: index == currentValue - 1 - ? TextInputAction.done - : TextInputAction.next, - v2Style: CommonInputStyleV2(themeStyleV2), - ); - }, - ); - }, - ); - } - - Widget _itemBuilder(BuildContext context, String suggestion) { - final theme = context.themeStyleV2; - - return ListTile( - tileColor: Colors.transparent, - title: Text( - suggestion, - style: theme.textStyles.labelSmall, - ), - ); - } - - Widget _tabs( - List allowedValues, - EnterSeedPhraseCubit cubit, - int currentValue, - bool displayPasteButton, - ) { - return Builder( - builder: (context) { - return Row( - children: [ - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - for (final value in allowedValues) - GestureDetector( - onTap: () => cubit.changeTab(value), - child: PrimarySegmentControl( - size: SegmentControlSize.small, - state: value == currentValue - ? SegmentControlState.selected - : SegmentControlState.normal, - title: LocaleKeys.wordsCount.plural(value), - value: value, - ), - ), - ], - ), - ), - ), - GhostButton( - buttonShape: ButtonShape.pill, - buttonSize: ButtonSize.small, - onPressed: - displayPasteButton ? cubit.pastePhrase : cubit.clearFields, - title: displayPasteButton - ? LocaleKeys.pasteAll.tr() - : LocaleKeys.clearAll.tr(), - icon: displayPasteButton - ? LucideIcons.arrowDownToDot - : LucideIcons.trash2, - ), - ], - ); - }, - ); - } - - Widget _inputs( - ThemeStyleV2 themeStyleV2, - List inputs, - int currentValue, - ) { - return ContainerRow( - padding: const EdgeInsets.symmetric(vertical: DimensSize.d16), - children: [ - Expanded( - child: SeparatedColumn( - children: [ - for (final input in inputs.getRange( - 0, - currentValue ~/ _gridColumnCount, - )) - _inputBuild(themeStyleV2, input, currentValue), - ], - ), - ), - Expanded( - child: SeparatedColumn( - children: [ - for (final input in inputs.getRange( - currentValue ~/ _gridColumnCount, - currentValue, - )) - _inputBuild(themeStyleV2, input, currentValue), - ], - ), - ), - ], - ); - } -} diff --git a/lib/feature/add_seed/enter_seed_phrase/view/view.dart b/lib/feature/add_seed/enter_seed_phrase/view/view.dart deleted file mode 100644 index 06a9fdd25..000000000 --- a/lib/feature/add_seed/enter_seed_phrase/view/view.dart +++ /dev/null @@ -1,3 +0,0 @@ -//GENERATED BARREL FILE -export 'enter_seed_phrase_page.dart'; -export 'enter_seed_phrase_view.dart'; diff --git a/lib/feature/add_seed/enter_seed_phrase/widgets/tabs.dart b/lib/feature/add_seed/enter_seed_phrase/widgets/tabs.dart new file mode 100644 index 000000000..c48720269 --- /dev/null +++ b/lib/feature/add_seed/enter_seed_phrase/widgets/tabs.dart @@ -0,0 +1,68 @@ +import 'package:app/generated/generated.dart'; +import 'package:elementary_helper/elementary_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; +import 'package:ui_components_lib/v2/ui_components_lib_v2.dart'; + +class EnterSeedPhraseTabs extends StatelessWidget { + const EnterSeedPhraseTabs({ + required this.allowedValues, + required this.currentValue, + required this.displayPasteButtonState, + required this.changeTab, + required this.pastePhrase, + required this.clearFields, + super.key, + }); + + final List allowedValues; + final int currentValue; + final ListenableState displayPasteButtonState; + final ValueChanged changeTab; + final VoidCallback pastePhrase; + final VoidCallback clearFields; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + for (final value in allowedValues) + GestureDetector( + onTap: () => changeTab(value), + child: PrimarySegmentControl( + size: SegmentControlSize.small, + state: value == currentValue + ? SegmentControlState.selected + : SegmentControlState.normal, + title: LocaleKeys.wordsCount.plural(value), + value: value, + ), + ), + ], + ), + ), + ), + StateNotifierBuilder( + listenableState: displayPasteButtonState, + builder: (_, bool? isDisplay) { + isDisplay ??= true; + return GhostButton( + buttonShape: ButtonShape.pill, + buttonSize: ButtonSize.small, + onPressed: isDisplay ? pastePhrase : clearFields, + title: isDisplay + ? LocaleKeys.pasteAll.tr() + : LocaleKeys.clearAll.tr(), + icon: isDisplay ? LucideIcons.arrowDownToDot : LucideIcons.trash2, + ); + }, + ), + ], + ); + } +} diff --git a/lib/feature/add_seed/enter_seed_phrase/widgets/words.dart b/lib/feature/add_seed/enter_seed_phrase/widgets/words.dart new file mode 100644 index 000000000..55ff84f75 --- /dev/null +++ b/lib/feature/add_seed/enter_seed_phrase/widgets/words.dart @@ -0,0 +1,185 @@ +import 'package:app/feature/add_seed/enter_seed_phrase/data/input_data.dart'; +import 'package:app/feature/add_seed/enter_seed_phrase/data/tab_data.dart'; +import 'package:app/feature/add_seed/enter_seed_phrase/enter_seed_phrase_wm.dart'; +import 'package:app/feature/add_seed/enter_seed_phrase/widgets/tabs.dart'; +import 'package:app/generated/generated.dart'; +import 'package:elementary_helper/elementary_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:ui_components_lib/components/input/common_input_style_v2.dart'; +import 'package:ui_components_lib/ui_components_lib.dart'; + +class EnterSeedPhraseWords extends StatelessWidget { + const EnterSeedPhraseWords({ + required this.formKey, + required this.allowedValues, + required this.currentValue, + required this.displayPasteButtonState, + required this.tabState, + required this.changeTab, + required this.pastePhrase, + required this.clearFields, + required this.onSuggestions, + required this.onSuggestionSelected, + required this.onNext, + super.key, + }); + + final GlobalKey formKey; + final List allowedValues; + final int currentValue; + final ListenableState displayPasteButtonState; + final ListenableState tabState; + final ValueChanged changeTab; + final VoidCallback pastePhrase; + final VoidCallback clearFields; + final SuggestionsCallback onSuggestions; + final SuggestionSelectedCallback onSuggestionSelected; + final ValueChanged onNext; + + @override + Widget build(BuildContext context) { + final themeStyleV2 = context.themeStyleV2; + final bottomPadding = MediaQuery.of(context).viewInsets.bottom; + final isKeyboardOpen = bottomPadding >= commonButtonHeight; + + return SingleChildScrollView( + child: Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isKeyboardOpen) const SizedBox(height: DimensSize.d24), + if (!isKeyboardOpen) + EnterSeedPhraseTabs( + allowedValues: allowedValues, + currentValue: currentValue, + displayPasteButtonState: displayPasteButtonState, + changeTab: changeTab, + pastePhrase: pastePhrase, + clearFields: clearFields, + ), + StateNotifierBuilder( + listenableState: tabState, + builder: (_, EnterSeedPhraseTabData? data) { + if (data == null) { + return const SizedBox.shrink(); + } + + return ContainerRow( + padding: const EdgeInsets.symmetric(vertical: DimensSize.d16), + children: [ + Expanded( + child: SeparatedColumn( + children: [ + for (final input in data.fistInputsRange) + _Input( + key: ValueKey(input.index), + theme: themeStyleV2, + data: input, + currentValue: currentValue, + onSuggestions: onSuggestions, + onSuggestionSelected: onSuggestionSelected, + onNext: onNext, + ), + ], + ), + ), + Expanded( + child: SeparatedColumn( + children: [ + for (final input in data.secondInputsRange) + _Input( + key: ValueKey(input.index), + theme: themeStyleV2, + data: input, + currentValue: currentValue, + onSuggestions: onSuggestions, + onSuggestionSelected: onSuggestionSelected, + onNext: onNext, + ), + ], + ), + ), + ], + ); + }, + ), + const SizedBox(height: DimensSize.d16), + ], + ), + ), + ); + } +} + +class _Input extends StatelessWidget { + const _Input({ + required this.theme, + required this.data, + required this.currentValue, + required this.onSuggestions, + required this.onSuggestionSelected, + required this.onNext, + super.key, + }); + + final ThemeStyleV2 theme; + final EnterSeedPhraseInputData data; + final int currentValue; + final SuggestionsCallback onSuggestions; + final SuggestionSelectedCallback onSuggestionSelected; + final ValueChanged onNext; + + @override + Widget build(BuildContext context) { + final colors = context.themeStyle.colors; + final themeStyleV2 = context.themeStyleV2; + final displayIndex = data.index + 1; + final indexText = NumberFormat('00').format(displayIndex); + + return CommonInput( + autocorrect: false, + hintStyle: themeStyleV2.textStyles.labelSmall, + inactiveBorderColor: themeStyleV2.colors.border0, + textStyle: themeStyleV2.textStyles.labelSmall, + suggestionBackground: themeStyleV2.colors.background1, + keyboardType: TextInputType.visiblePassword, + height: DimensSize.d48, + controller: data.controller, + suggestionsCallback: (_) => onSuggestions(data.controller.text), + itemBuilder: _itemBuilder, + onSuggestionSelected: (suggestion) => + onSuggestionSelected(suggestion, data.index), + focusNode: data.focusNode, + // show error border if field is empty + validator: (v) => v?.isEmpty ?? true ? '' : null, + needClearButton: false, + // IntrinsicWidth to force Center match prefixIconConstraints + prefixIcon: IntrinsicWidth( + child: Center( + child: Text( + indexText, + style: StyleRes.addRegular.copyWith( + color: colors.textSecondary, + ), + ), + ), + ), + onSubmitted: (_) => onNext(data.index), + textInputAction: data.index == currentValue - 1 + ? TextInputAction.done + : TextInputAction.next, + v2Style: CommonInputStyleV2(themeStyleV2), + ); + } + + Widget _itemBuilder(BuildContext context, String suggestion) { + return ListTile( + tileColor: Colors.transparent, + title: Text( + suggestion, + style: theme.textStyles.labelSmall, + ), + ); + } +} diff --git a/lib/utils/factories/error_handler/standard_error_handler.dart b/lib/utils/factories/error_handler/standard_error_handler.dart index 562321b63..6a2e57968 100644 --- a/lib/utils/factories/error_handler/standard_error_handler.dart +++ b/lib/utils/factories/error_handler/standard_error_handler.dart @@ -1,4 +1,35 @@ +import 'package:app/app/service/messenger/message.dart'; +import 'package:app/app/service/messenger/service/messenger_service.dart'; import 'package:elementary/elementary.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:ui_components_lib/ui_components_lib.dart'; /// Customize if need -class PrimaryErrorHandler extends DefaultDebugErrorHandler {} +class PrimaryErrorHandler extends DefaultDebugErrorHandler { + PrimaryErrorHandler( + this._context, + this._messengerService, + ); + + final BuildContext _context; + final MessengerService _messengerService; + + @override + void handleError(Object error, {StackTrace? stackTrace}) { + showError(error.toString()); + super.handleError(error, stackTrace: stackTrace); + } + + void showError( + String message, { + Duration debounceTime = defaultInfoMessageDebounceDuration, + }) { + _messengerService.show( + Message.error( + context: _context, + message: message, + debounceTime: debounceTime, + ), + ); + } +} diff --git a/packages/ui_components_lib/lib/components/input/common_input.dart b/packages/ui_components_lib/lib/components/input/common_input.dart index fb5e71959..4c490f7eb 100644 --- a/packages/ui_components_lib/lib/components/input/common_input.dart +++ b/packages/ui_components_lib/lib/components/input/common_input.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:ui_components_lib/components/input/common_input_style_v2.dart'; import 'package:ui_components_lib/ui_components_lib.dart'; diff --git a/packages/ui_components_lib/lib/ui_components_lib.dart b/packages/ui_components_lib/lib/ui_components_lib.dart index 9da692ff4..7747d62fa 100644 --- a/packages/ui_components_lib/lib/ui_components_lib.dart +++ b/packages/ui_components_lib/lib/ui_components_lib.dart @@ -1,4 +1,5 @@ export 'package:flutter_svg/flutter_svg.dart'; +export 'package:flutter_typeahead/flutter_typeahead.dart'; export 'colors.dart'; export 'components/button/button.dart';