diff --git a/assets/images/icons/1.5x/icon_field_email.png b/assets/images/icons/1.5x/icon_field_email.png new file mode 100644 index 000000000..cc2abfb22 Binary files /dev/null and b/assets/images/icons/1.5x/icon_field_email.png differ diff --git a/assets/images/icons/1.5x/icon_login_authcode.png b/assets/images/icons/1.5x/icon_login_authcode.png new file mode 100644 index 000000000..d980fb371 Binary files /dev/null and b/assets/images/icons/1.5x/icon_login_authcode.png differ diff --git a/assets/images/icons/1.5x/icon_login_smscode.png b/assets/images/icons/1.5x/icon_login_smscode.png new file mode 100644 index 000000000..5689867cd Binary files /dev/null and b/assets/images/icons/1.5x/icon_login_smscode.png differ diff --git a/assets/images/icons/1.5x/icon_select2.png b/assets/images/icons/1.5x/icon_select2.png new file mode 100644 index 000000000..0c24d1723 Binary files /dev/null and b/assets/images/icons/1.5x/icon_select2.png differ diff --git a/assets/images/icons/2.0x/icon_field_email.png b/assets/images/icons/2.0x/icon_field_email.png new file mode 100644 index 000000000..47081a544 Binary files /dev/null and b/assets/images/icons/2.0x/icon_field_email.png differ diff --git a/assets/images/icons/2.0x/icon_login_authcode.png b/assets/images/icons/2.0x/icon_login_authcode.png new file mode 100644 index 000000000..93a08785b Binary files /dev/null and b/assets/images/icons/2.0x/icon_login_authcode.png differ diff --git a/assets/images/icons/2.0x/icon_login_smscode.png b/assets/images/icons/2.0x/icon_login_smscode.png new file mode 100644 index 000000000..3f2d1a569 Binary files /dev/null and b/assets/images/icons/2.0x/icon_login_smscode.png differ diff --git a/assets/images/icons/2.0x/icon_select2.png b/assets/images/icons/2.0x/icon_select2.png new file mode 100644 index 000000000..17db3c8a7 Binary files /dev/null and b/assets/images/icons/2.0x/icon_select2.png differ diff --git a/assets/images/icons/3.0x/icon_field_email.png b/assets/images/icons/3.0x/icon_field_email.png new file mode 100644 index 000000000..f4c0b52b8 Binary files /dev/null and b/assets/images/icons/3.0x/icon_field_email.png differ diff --git a/assets/images/icons/3.0x/icon_login_authcode.png b/assets/images/icons/3.0x/icon_login_authcode.png new file mode 100644 index 000000000..dbacb46f7 Binary files /dev/null and b/assets/images/icons/3.0x/icon_login_authcode.png differ diff --git a/assets/images/icons/3.0x/icon_login_smscode.png b/assets/images/icons/3.0x/icon_login_smscode.png new file mode 100644 index 000000000..234ab7efe Binary files /dev/null and b/assets/images/icons/3.0x/icon_login_smscode.png differ diff --git a/assets/images/icons/3.0x/icon_select2.png b/assets/images/icons/3.0x/icon_select2.png new file mode 100644 index 000000000..1dcd38651 Binary files /dev/null and b/assets/images/icons/3.0x/icon_select2.png differ diff --git a/assets/images/icons/4.0x/icon_field_email.png b/assets/images/icons/4.0x/icon_field_email.png new file mode 100644 index 000000000..9cc0e5918 Binary files /dev/null and b/assets/images/icons/4.0x/icon_field_email.png differ diff --git a/assets/images/icons/4.0x/icon_login_authcode.png b/assets/images/icons/4.0x/icon_login_authcode.png new file mode 100644 index 000000000..8f9ae2fe2 Binary files /dev/null and b/assets/images/icons/4.0x/icon_login_authcode.png differ diff --git a/assets/images/icons/4.0x/icon_login_smscode.png b/assets/images/icons/4.0x/icon_login_smscode.png new file mode 100644 index 000000000..6f98c8d13 Binary files /dev/null and b/assets/images/icons/4.0x/icon_login_smscode.png differ diff --git a/assets/images/icons/4.0x/icon_select2.png b/assets/images/icons/4.0x/icon_select2.png new file mode 100644 index 000000000..d6a27b240 Binary files /dev/null and b/assets/images/icons/4.0x/icon_select2.png differ diff --git a/assets/images/icons/icon_field_email.png b/assets/images/icons/icon_field_email.png new file mode 100644 index 000000000..8129eedf5 Binary files /dev/null and b/assets/images/icons/icon_field_email.png differ diff --git a/assets/images/icons/icon_login_authcode.png b/assets/images/icons/icon_login_authcode.png new file mode 100644 index 000000000..d394c95a1 Binary files /dev/null and b/assets/images/icons/icon_login_authcode.png differ diff --git a/assets/images/icons/icon_login_smscode.png b/assets/images/icons/icon_login_smscode.png new file mode 100644 index 000000000..ed6ee471f Binary files /dev/null and b/assets/images/icons/icon_login_smscode.png differ diff --git a/assets/images/icons/icon_select2.png b/assets/images/icons/icon_select2.png new file mode 100644 index 000000000..0c26d2922 Binary files /dev/null and b/assets/images/icons/icon_select2.png differ diff --git a/lib/app/components/button/button.dart b/lib/app/components/button/button.dart index 917d8c360..219b4582c 100644 --- a/lib/app/components/button/button.dart +++ b/lib/app/components/button/button.dart @@ -93,8 +93,10 @@ class Button extends StatelessWidget { ButtonStyle style, Color? borderColor, Color? backgroundColor, - double? leadingButtonOffset, + double? leadingIconOffset, double? trailingIconOffset, + bool useDefaultBorderRadius, + bool useDefaultPaddings, bool disabled, bool opened, }) = _ButtonDropdown; @@ -236,10 +238,12 @@ class ButtonIconFrame extends StatelessWidget { required this.icon, super.key, this.color, + this.border, }); final Color? color; final Widget icon; + final Border? border; @override Widget build(BuildContext context) { @@ -250,6 +254,7 @@ class ButtonIconFrame extends StatelessWidget { decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(9.0.s), + border: border, ), child: icon, ); diff --git a/lib/app/components/button/variants/button_dropdown.dart b/lib/app/components/button/variants/button_dropdown.dart index 32a9fd88a..7fd351fd7 100644 --- a/lib/app/components/button/variants/button_dropdown.dart +++ b/lib/app/components/button/variants/button_dropdown.dart @@ -9,27 +9,33 @@ class _ButtonDropdown extends Button { super.disabled, super.backgroundColor, super.borderColor, - double? leadingButtonOffset, + double? leadingIconOffset, double? trailingIconOffset, bool opened = false, ButtonStyle style = const ButtonStyle(), + bool useDefaultBorderRadius = false, + bool useDefaultPaddings = false, }) : super( type: ButtonType.dropdown, style: style.merge( OutlinedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(12.0.s)), - ), + shape: useDefaultBorderRadius == true + ? null + : RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0.s)), + ), minimumSize: Size.square(40.0.s), - padding: leadingIcon != null - ? EdgeInsets.only( - left: 4.0.s, - right: 10.0.s, - ) - : EdgeInsets.symmetric(horizontal: 14.0.s), + padding: useDefaultPaddings == true + ? null + : leadingIcon != null + ? EdgeInsets.only( + left: 4.0.s, + right: 10.0.s, + ) + : EdgeInsets.symmetric(horizontal: 14.0.s), ), ), - leadingIconOffset: leadingButtonOffset ?? 10.0.s, + leadingIconOffset: leadingIconOffset ?? 10.0.s, trailingIcon: (opened ? Assets.images.icons.iconArrowUp : Assets.images.icons.iconArrowDown).icon(), trailingIconOffset: trailingIconOffset ?? 8.0.s, diff --git a/lib/app/components/button/widgetbook.dart b/lib/app/components/button/widgetbook.dart index 187862a71..59680007b 100644 --- a/lib/app/components/button/widgetbook.dart +++ b/lib/app/components/button/widgetbook.dart @@ -261,7 +261,7 @@ Widget dropdownButtonUseCase(BuildContext context) { leadingIcon: ButtonIconFrame( icon: Assets.images.icons.iconBadgeIcelogo.icon(size: 26.0.s), ), - leadingButtonOffset: 4.0.s, + leadingIconOffset: 4.0.s, backgroundColor: context.theme.appColors.tertararyBackground, label: Text( 'ice.wallet', diff --git a/lib/app/features/auth/data/models/twofa_type.dart b/lib/app/features/auth/data/models/twofa_type.dart new file mode 100644 index 000000000..8b473fa34 --- /dev/null +++ b/lib/app/features/auth/data/models/twofa_type.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:ice/app/extensions/build_context.dart'; +import 'package:ice/generated/assets.gen.dart'; + +enum TwoFaType { + auth, + email, + sms; + + String getDisplayName(BuildContext context) { + return switch (this) { + TwoFaType.auth => context.i18n.two_fa_auth, + TwoFaType.email => context.i18n.two_fa_email, + TwoFaType.sms => context.i18n.two_fa_sms, + }; + } + + AssetGenImage get iconAsset { + return switch (this) { + TwoFaType.auth => Assets.images.icons.iconLoginAuthcode, + TwoFaType.email => Assets.images.icons.iconFieldEmail, + TwoFaType.sms => Assets.images.icons.iconLoginSmscode, + }; + } +} diff --git a/lib/app/features/auth/views/pages/restore_menu/restore_menu.dart b/lib/app/features/auth/views/pages/restore_menu/restore_menu.dart index aef0b6948..0b8010c32 100644 --- a/lib/app/features/auth/views/pages/restore_menu/restore_menu.dart +++ b/lib/app/features/auth/views/pages/restore_menu/restore_menu.dart @@ -42,7 +42,7 @@ class RestoreMenuPage extends HookWidget { description: context.i18n.restore_identity_type_credentials_description, onPressed: () { hideKeyboardAndCallOnce( - callback: () => RestoreCredsRoute().push(context), + callback: () => TwoFaOptionsRoute().push(context), ); }, ), diff --git a/lib/app/features/auth/views/pages/twofa_codes/twofa_code_input.dart b/lib/app/features/auth/views/pages/twofa_codes/twofa_code_input.dart new file mode 100644 index 000000000..543326da2 --- /dev/null +++ b/lib/app/features/auth/views/pages/twofa_codes/twofa_code_input.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:ice/app/components/inputs/text_input/components/text_input_icons.dart'; +import 'package:ice/app/components/inputs/text_input/components/text_input_text_button.dart'; +import 'package:ice/app/components/inputs/text_input/text_input.dart'; +import 'package:ice/app/extensions/extensions.dart'; +import 'package:ice/app/features/auth/data/models/twofa_type.dart'; +import 'package:ice/app/hooks/use_countdown.dart'; +import 'package:ice/app/utils/validators.dart'; + +class TwoFaCodeInput extends HookWidget { + const TwoFaCodeInput({required this.controller, required this.twoFaType, super.key}); + + final TextEditingController controller; + final TwoFaType twoFaType; + + @override + Widget build(BuildContext context) { + final isSent = useState(false); + final countdownState = useCountdown(60); + final countdown = countdownState.countdown; + final startCountdown = countdownState.startCountdown; + + return TextInput( + prefixIcon: TextInputIcons( + hasRightDivider: true, + icons: [twoFaType.iconAsset.icon()], + ), + labelText: twoFaType.getDisplayName(context), + controller: controller, + validator: (String? value) { + if (Validators.isEmpty(value)) return ''; + return null; + }, + textInputAction: TextInputAction.next, + scrollPadding: EdgeInsets.only(bottom: 200.0.s), + suffixIcon: countdown.value > 0 + ? Padding( + padding: EdgeInsets.all(14.0.s), + child: Text( + context.i18n.common_seconds(countdown.value), + style: context.theme.appTextThemes.caption.copyWith( + color: context.theme.appColors.tertararyText, + ), + ), + ) + : TextInputTextButton( + onPressed: () { + isSent.value = true; + startCountdown(); + }, + label: isSent.value ? context.i18n.button_retry : context.i18n.button_send, + ), + ); + } +} diff --git a/lib/app/features/auth/views/pages/twofa_codes/twofa_codes_page.dart b/lib/app/features/auth/views/pages/twofa_codes/twofa_codes_page.dart new file mode 100644 index 000000000..54d89c81f --- /dev/null +++ b/lib/app/features/auth/views/pages/twofa_codes/twofa_codes_page.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:ice/app/components/button/button.dart'; +import 'package:ice/app/components/screen_offset/screen_bottom_offset.dart'; +import 'package:ice/app/components/screen_offset/screen_side_offset.dart'; +import 'package:ice/app/extensions/asset_gen_image.dart'; +import 'package:ice/app/extensions/build_context.dart'; +import 'package:ice/app/extensions/num.dart'; +import 'package:ice/app/features/auth/data/models/twofa_type.dart'; +import 'package:ice/app/features/auth/views/components/auth_footer/auth_footer.dart'; +import 'package:ice/app/features/auth/views/components/auth_scrolled_body/auth_scrolled_body.dart'; +import 'package:ice/app/features/auth/views/pages/twofa_codes/twofa_code_input.dart'; +import 'package:ice/app/router/components/sheet_content/sheet_content.dart'; +import 'package:ice/generated/assets.gen.dart'; + +class TwoFaCodesPage extends HookWidget { + const TwoFaCodesPage({ + super.key, + required this.twoFaTypes, + }); + + final Set twoFaTypes; + + @override + Widget build(BuildContext context) { + final formKey = useRef(GlobalKey()); + final controllers = { + for (final type in twoFaTypes) type: useTextEditingController(), + }; + + return SheetContent( + body: AuthScrollContainer( + title: context.i18n.two_fa_title, + description: context.i18n.two_fa_desc, + icon: Assets.images.icons.iconWalletProtectVar1.icon(size: 36.0.s), + children: [ + Column( + children: [ + ScreenSideOffset.large( + child: Form( + key: formKey.value, + child: Column( + children: [ + SizedBox(height: 16.0.s), + ...twoFaTypes.map((twoFaType) { + return Padding( + padding: EdgeInsets.only(bottom: 16.0.s), + child: TwoFaCodeInput( + controller: controllers[twoFaType]!, + twoFaType: twoFaType, + ), + ); + }).toList(), + Button( + onPressed: () { + if (!formKey.value.currentState!.validate()) {} + }, + label: Text(context.i18n.button_confirm), + mainAxisSize: MainAxisSize.max, + ), + SizedBox(height: 16.0.s), + ], + ), + ), + ) + ], + ), + ScreenBottomOffset( + child: const AuthFooter(), + ), + ], + ), + ); + } +} diff --git a/lib/app/features/auth/views/pages/twofa_options/mock_data/mock.dart b/lib/app/features/auth/views/pages/twofa_options/mock_data/mock.dart new file mode 100644 index 000000000..74a795f3a --- /dev/null +++ b/lib/app/features/auth/views/pages/twofa_options/mock_data/mock.dart @@ -0,0 +1,5 @@ +import 'dart:math'; + +int get2FAOptionsNumber() { + return Random().nextInt(3) + 1; +} diff --git a/lib/app/features/auth/views/pages/twofa_options/twofa_option_selector.dart b/lib/app/features/auth/views/pages/twofa_options/twofa_option_selector.dart new file mode 100644 index 000000000..2e528eb05 --- /dev/null +++ b/lib/app/features/auth/views/pages/twofa_options/twofa_option_selector.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:ice/app/components/button/button.dart'; +import 'package:ice/app/components/drop_down_menu/drop_down_menu.dart'; +import 'package:ice/app/components/screen_offset/screen_side_offset.dart'; +import 'package:ice/app/extensions/extensions.dart'; +import 'package:ice/app/features/auth/data/models/twofa_type.dart'; +import 'package:ice/app/features/auth/views/pages/twofa_options/twofa_options_selector_button.dart'; + +class TwoFaOptionSelector extends HookWidget { + const TwoFaOptionSelector({ + required this.availableOptions, + required this.optionIndex, + required this.onSaved, + super.key, + }); + + final Set availableOptions; + final int optionIndex; + final FormFieldSetter onSaved; + + static double get height => 58.0.s; + + @override + Widget build(BuildContext context) { + final isOpened = useState(false); + final iconBorderSize = Border.fromBorderSide( + BorderSide(color: context.theme.appColors.onTerararyFill, width: 1.0.s), + ); + + return FormField( + validator: (option) => option == null ? '' : null, + onSaved: onSaved, + builder: (state) { + return SizedBox( + width: double.infinity, + height: height, + child: DropDownMenu( + style: MenuStyle( + elevation: WidgetStateProperty.all(0), + side: WidgetStateProperty.all( + BorderSide( + color: context.theme.appColors.strokeElements, + width: 1.0.s, + ), + ), + minimumSize: WidgetStateProperty.all( + Size( + MediaQuery.of(context).size.width - ScreenSideOffset.defaultLargeMargin * 2, + height, + ), + ), + ), + crossAxisUnconstrained: false, + builder: ( + BuildContext context, + MenuController controller, + Widget? child, + ) { + return TwoFaOptionsSelectorButton( + state: state, + controller: controller, + isOpened: isOpened, + optionIndex: optionIndex, + ); + }, + menuChildren: [ + for (final TwoFaType option in availableOptions) + MenuItemButton( + onPressed: () { + state.didChange(option); + state.save(); + state.validate(); + isOpened.value = false; + }, + leadingIcon: ButtonIconFrame( + color: context.theme.appColors.secondaryBackground, + icon: option.iconAsset.icon( + size: 20.0.s, + color: context.theme.appColors.secondaryText, + ), + border: iconBorderSize, + ), + child: Text( + option.getDisplayName(context), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/app/features/auth/views/pages/twofa_options/twofa_options_page.dart b/lib/app/features/auth/views/pages/twofa_options/twofa_options_page.dart new file mode 100644 index 000000000..1d83caf3e --- /dev/null +++ b/lib/app/features/auth/views/pages/twofa_options/twofa_options_page.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:ice/app/components/button/button.dart'; +import 'package:ice/app/components/screen_offset/screen_bottom_offset.dart'; +import 'package:ice/app/components/screen_offset/screen_side_offset.dart'; +import 'package:ice/app/extensions/asset_gen_image.dart'; +import 'package:ice/app/extensions/build_context.dart'; +import 'package:ice/app/extensions/num.dart'; +import 'package:ice/app/features/auth/data/models/twofa_type.dart'; +import 'package:ice/app/features/auth/views/components/auth_footer/auth_footer.dart'; +import 'package:ice/app/features/auth/views/components/auth_scrolled_body/auth_scrolled_body.dart'; +import 'package:ice/app/features/auth/views/pages/twofa_options/mock_data/mock.dart'; +import 'package:ice/app/features/auth/views/pages/twofa_options/twofa_option_selector.dart'; +import 'package:ice/app/router/app_routes.dart'; +import 'package:ice/app/router/components/sheet_content/sheet_content.dart'; +import 'package:ice/generated/assets.gen.dart'; + +class TwoFaOptionsPage extends HookWidget { + const TwoFaOptionsPage({super.key}); + + @override + Widget build(BuildContext context) { + final optionsNumber = useState(get2FAOptionsNumber()); + final formKey = useRef(GlobalKey()); + final selectedValues = { + for (int i = 0; i < optionsNumber.value; i++) i: useState(null) + }; + final availableOptions = useState>(TwoFaType.values.toSet()); + + return SheetContent( + body: AuthScrollContainer( + title: context.i18n.two_fa_title, + description: context.i18n.two_fa_desc, + icon: Assets.images.icons.iconWalletProtectVar1.icon(size: 36.0.s), + children: [ + Column( + children: [ + ScreenSideOffset.large( + child: Form( + key: formKey.value, + child: Column( + children: [ + SizedBox(height: 16.0.s), + ...List.generate(optionsNumber.value, (i) { + return Padding( + padding: EdgeInsets.only(bottom: 16.0.s), + child: TwoFaOptionSelector( + availableOptions: selectedValues[i]?.value != null + ? {selectedValues[i]!.value!, ...availableOptions.value} + : availableOptions.value, + optionIndex: i + 1, + onSaved: (value) { + if (selectedValues[i]?.value != null) { + availableOptions.value = { + ...availableOptions.value, + selectedValues[i]!.value! + }; + } + selectedValues[i]?.value = value; + availableOptions.value = {...availableOptions.value}..remove(value); + }, + ), + ); + }), + Button( + onPressed: () { + if (formKey.value.currentState!.validate()) { + final Set extra = selectedValues.values + .map((selectedValue) => selectedValue.value) + .where((TwoFaType? value) => value != null) + .cast() + .toSet(); + TwoFaCodesRoute($extra: extra).push(context); + } + }, + label: Text(context.i18n.button_confirm), + mainAxisSize: MainAxisSize.max, + ), + SizedBox(height: 16.0.s), + ], + ), + ), + ) + ], + ), + ScreenBottomOffset( + child: const AuthFooter(), + ), + ], + ), + ); + } +} diff --git a/lib/app/features/auth/views/pages/twofa_options/twofa_options_selector_button.dart b/lib/app/features/auth/views/pages/twofa_options/twofa_options_selector_button.dart new file mode 100644 index 000000000..1128a47ae --- /dev/null +++ b/lib/app/features/auth/views/pages/twofa_options/twofa_options_selector_button.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:ice/app/components/button/button.dart'; +import 'package:ice/app/extensions/extensions.dart'; +import 'package:ice/app/features/auth/data/models/twofa_type.dart'; +import 'package:ice/generated/assets.gen.dart'; + +class TwoFaOptionsSelectorButton extends StatelessWidget { + const TwoFaOptionsSelectorButton({ + required this.state, + required this.isOpened, + required this.controller, + required this.optionIndex, + super.key, + }); + + final FormFieldState state; + final ValueNotifier isOpened; + final MenuController controller; + final int optionIndex; + + @override + Widget build(BuildContext context) { + final iconBorderSize = Border.fromBorderSide( + BorderSide(color: context.theme.appColors.onTerararyFill, width: 1.0.s), + ); + + return Button.dropdown( + useDefaultBorderRadius: true, + useDefaultPaddings: true, + backgroundColor: context.theme.appColors.secondaryBackground, + borderColor: state.hasError + ? context.theme.appColors.attentionRed + : context.theme.appColors.strokeElements, + leadingIcon: ButtonIconFrame( + color: context.theme.appColors.tertararyBackground, + icon: (state.value?.iconAsset ?? Assets.images.icons.iconSelect2).icon( + size: 20.0.s, + color: context.theme.appColors.secondaryText, + ), + border: iconBorderSize, + ), + label: Container( + width: double.infinity, + child: Text( + state.value?.getDisplayName(context) ?? context.i18n.two_fa_select(optionIndex), + ), + ), + opened: isOpened.value, + onPressed: () { + isOpened.value = !isOpened.value; + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + ); + } +} diff --git a/lib/app/features/components/wallet_switcher/wallet_switcher.dart b/lib/app/features/components/wallet_switcher/wallet_switcher.dart index 3b5fbeb92..6645d8a67 100644 --- a/lib/app/features/components/wallet_switcher/wallet_switcher.dart +++ b/lib/app/features/components/wallet_switcher/wallet_switcher.dart @@ -26,7 +26,7 @@ class WalletSwitcher extends ConsumerWidget { imageUrl: walletData.icon, borderRadius: BorderRadius.circular(10.0.s), ), - leadingButtonOffset: 11.0.s, + leadingIconOffset: 11.0.s, trailingIconOffset: 0.0.s, backgroundColor: context.theme.appColors.tertararyBackground, label: Text( diff --git a/lib/app/hooks/use_countdown.dart b/lib/app/hooks/use_countdown.dart new file mode 100644 index 000000000..ed407c6a3 --- /dev/null +++ b/lib/app/hooks/use_countdown.dart @@ -0,0 +1,32 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +({ValueNotifier countdown, void Function() startCountdown}) useCountdown(int initialCount) { + final countdown = useState(0); + Timer? timer; + + void startCountdown() { + countdown.value = initialCount; + timer?.cancel(); + timer = Timer.periodic( + const Duration(seconds: 1), + (timer) { + if (countdown.value > 0) { + countdown.value--; + } else { + timer.cancel(); + } + }, + ); + } + + useEffect(() { + return () { + timer?.cancel(); + }; + }, []); + + return (countdown: countdown, startCountdown: startCountdown); +} diff --git a/lib/app/router/app_routes.dart b/lib/app/router/app_routes.dart index 82a6efca5..6c4581290 100644 --- a/lib/app/router/app_routes.dart +++ b/lib/app/router/app_routes.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:ice/app/features/auth/data/models/twofa_type.dart'; import 'package:ice/app/features/auth/views/pages/discover_creators/discover_creators.dart'; import 'package:ice/app/features/auth/views/pages/fill_profile/fill_profile.dart'; import 'package:ice/app/features/auth/views/pages/get_started/get_started.dart'; @@ -10,6 +11,8 @@ import 'package:ice/app/features/auth/views/pages/select_languages/select_langua import 'package:ice/app/features/auth/views/pages/sign_up_passkey/sign_up_passkey.dart'; import 'package:ice/app/features/auth/views/pages/sign_up_password/sign_up_password.dart'; import 'package:ice/app/features/auth/views/pages/turn_on_notifications/turn_on_notifications.dart'; +import 'package:ice/app/features/auth/views/pages/twofa_codes/twofa_codes_page.dart'; +import 'package:ice/app/features/auth/views/pages/twofa_options/twofa_options_page.dart'; import 'package:ice/app/features/chat/views/pages/chat_main_modal/chat_main_modal_page.dart'; import 'package:ice/app/features/chat/views/pages/chat_page/chat_page.dart'; import 'package:ice/app/features/core/views/pages/error_page.dart'; @@ -27,7 +30,6 @@ import 'package:ice/app/features/feed/views/pages/quote_post_modal_page/quote_po import 'package:ice/app/features/feed/views/pages/reply_expanded_page/reply_expanded_page.dart'; import 'package:ice/app/features/feed/views/pages/share_options_modal/share_options_modal_page.dart'; import 'package:ice/app/features/feed/views/pages/share_type_modal_page/share_type_modal_page.dart'; -import 'package:ice/app/features/wallet/views/pages/send_nft/views/pages/nft_details/nft_details_page.dart'; import 'package:ice/app/features/user/pages/pull_right_menu_page/pull_right_menu_page.dart'; import 'package:ice/app/features/user/pages/switch_account_page/switch_account_page.dart'; import 'package:ice/app/features/wallet/model/coin_data.dart'; @@ -50,6 +52,7 @@ import 'package:ice/app/features/wallet/views/pages/contact_modal_page/contact_m import 'package:ice/app/features/wallet/views/pages/manage_coins/manage_coins_page.dart'; import 'package:ice/app/features/wallet/views/pages/nfts_sorting_modal/nfts_sorting_modal.dart'; import 'package:ice/app/features/wallet/views/pages/request_contacts_access_modal/request_contacts_access_modal.dart'; +import 'package:ice/app/features/wallet/views/pages/send_nft/views/pages/nft_details/nft_details_page.dart'; import 'package:ice/app/features/wallet/views/pages/wallet_main_modal/wallet_main_modal_page.dart'; import 'package:ice/app/features/wallet/views/pages/wallet_page/wallet_page.dart'; import 'package:ice/app/features/wallet/views/pages/wallet_scan/wallet_scan_modal_page.dart'; diff --git a/lib/app/router/auth_routes.dart b/lib/app/router/auth_routes.dart index 6ab89028c..e15366d90 100644 --- a/lib/app/router/auth_routes.dart +++ b/lib/app/router/auth_routes.dart @@ -9,6 +9,8 @@ class AuthRoutes { TypedGoRoute(path: 'sign-up-password'), TypedGoRoute(path: 'restore-menu'), TypedGoRoute(path: 'restore-creds'), + TypedGoRoute(path: 'twofa-codes'), + TypedGoRoute(path: 'twofa-options'), TypedGoRoute(path: 'select-languages'), TypedGoRoute(path: 'fill-profile'), TypedGoRoute(path: 'discover-creators'), @@ -58,6 +60,23 @@ class RestoreCredsRoute extends BaseRouteData { ); } +class TwoFaCodesRoute extends BaseRouteData { + TwoFaCodesRoute({required this.$extra}) + : super( + child: TwoFaCodesPage(twoFaTypes: $extra), + type: IceRouteType.bottomSheet, + ); + final Set $extra; +} + +class TwoFaOptionsRoute extends BaseRouteData { + TwoFaOptionsRoute() + : super( + child: TwoFaOptionsPage(), + type: IceRouteType.bottomSheet, + ); +} + class SelectLanguagesRoute extends BaseRouteData { SelectLanguagesRoute() : super( diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index f35f11e44..386bf3712 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -8,17 +8,25 @@ "button_close": "Close", "button_register": "Register", "button_send": "Send", + "button_retry": "Retry", "button_confirm": "Confirm", "button_restore": "Restore", "common_show_more": "Show more", "common_show_less": "Show less", "common_identity_key_name": "Identity key name", "common_password": "Password", + "common_seconds": "{seconds}s", "auth_secured_by": "Secured by", "auth_privacy": "By continuing, you are agreeing to our [[:link]]terms_of_service[[/:link]] & [[:link]]privacy_policy[[/:link]]", "auth_terms_of_service": "Terms of Service", "auth_privacy_policy": "Privacy Policy", "auth_identity_io": "Identity.io", + "two_fa_title": "2FA Verification", + "two_fa_desc": "Please enter your confirmation code below", + "two_fa_select": "Select option {number}", + "two_fa_email": "Email code", + "two_fa_sms": "SMS code", + "two_fa_auth": "Authenticator code", "sign_up_passkey_title": "Passkeys are a better way to sign in", "sign_up_passkey_advantage_1_title": "No password to remember", "sign_up_passkey_advantage_1_description": "With passkey, you can use things like your fingerprint or face to login", diff --git a/scripts/silence_non_flutter_logs.sh b/scripts/silence_non_flutter_logs.sh new file mode 100755 index 000000000..002aa84cc --- /dev/null +++ b/scripts/silence_non_flutter_logs.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +adb shell setprop log.tag.EGL_emulation "ERROR" +adb shell setprop log.tag.OpenGLRenderer "ERROR" +adb shell setprop log.tag.ViewRootImpl "ERROR" +adb shell setprop log.tag.PipelineWatcher "ERROR" +adb shell setprop log.tag.BufferPoolAccessor2 "ERROR" +adb shell setprop log.tag.BufferPoolAccessor2.0 "ERROR" +adb shell setprop log.tag.Codec2Client "ERROR" +adb shell setprop log.tag.CCodecBuffers "ERROR" +adb shell setprop log.tag.CCodec "ERROR" +adb shell setprop log.tag.ReflectedParamUpdater "ERROR" +adb shell setprop log.tag.MediaCodecUtil "ERROR" +adb shell setprop log.tag.WindowOnBackDispatcher "ERROR" +adb shell setprop log.tag.MediaCodec "ERROR" +adb shell setprop log.tag.CCodecConfig "ERROR" +adb shell setprop log.tag.ImeTracker "ERROR" +adb shell setprop log.tag.InputMethodManager "ERROR" +adb shell setprop log.tag.InputConnectionAdaptor "ERROR" +adb shell setprop log.tag.InsetsController "ERROR" +adb shell setprop log.tag.CCodecBufferChannel "ERROR" +adb shell setprop log.tag.DMCodecAdapterFactory "ERROR" +adb shell setprop log.tag.SurfaceUtils "ERROR" +adb shell setprop log.tag.ColorUtils "ERROR" +adb shell setprop log.tag.ExoPlayerImplInternal "ERROR"