diff --git a/assets/icons/passphrase.png b/assets/icons/passphrase.png new file mode 100644 index 0000000..f72d1e1 Binary files /dev/null and b/assets/icons/passphrase.png differ diff --git a/lib/coins/bitcoin/creation/common.dart b/lib/coins/bitcoin/creation/common.dart index 3beee9e..138ddd3 100644 --- a/lib/coins/bitcoin/creation/common.dart +++ b/lib/coins/bitcoin/creation/common.dart @@ -37,8 +37,8 @@ class BitcoinWalletCreation extends WalletCreation { ); late StringFormElement passphrase = StringFormElement( - L.wallet_passphrase, - password: false, + L.seed_offset, + password: true, validator: nonEmptyValidator( L, extra: (final input) => null, @@ -49,8 +49,8 @@ class BitcoinWalletCreation extends WalletCreation { ); late StringFormElement passphraseConfirm = StringFormElement( - L.wallet_passphrase, - password: false, + L.seed_offset_confirm, + password: true, validator: nonEmptyValidator( L, extra: (final input) => input != passphrase.ctrl.text ? L.seed_passphrase_mismatch : null, diff --git a/lib/coins/litecoin/creation/common.dart b/lib/coins/litecoin/creation/common.dart index 9bca97f..2437631 100644 --- a/lib/coins/litecoin/creation/common.dart +++ b/lib/coins/litecoin/creation/common.dart @@ -37,8 +37,8 @@ class LitecoinWalletCreation extends WalletCreation { ); late StringFormElement passphrase = StringFormElement( - L.wallet_passphrase, - password: false, + L.seed_offset, + password: true, validator: nonEmptyValidator( L, extra: (final input) => null, @@ -49,8 +49,8 @@ class LitecoinWalletCreation extends WalletCreation { ); late StringFormElement passphraseConfirm = StringFormElement( - L.wallet_passphrase, - password: false, + L.seed_offset_confirm, + password: true, validator: nonEmptyValidator( L, extra: (final input) => input != passphrase.ctrl.text ? L.seed_passphrase_mismatch : null, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 60d9f5b..183c058 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -24,6 +24,7 @@ "copied": "Copied {what}", "copy": "Copy", "create_new_wallet": "Create new wallet", + "create_passphrase_warning_text": "Make sure this passphrase is saved. Without this passphrase you will not be able to restore your funds. It is recommended to save your passphrase in a different location from your seed for optimal security.", "create_wallet": "Create Wallet", "create_your_pin": "Create your PIN", "creating_wallet": "Creating wallet", @@ -96,6 +97,7 @@ "rename": "Rename", "rename_wallet": "Rename wallet", "restore_height": "Restore height", + "restore_passphrase_warning_text": "Only enter a passphrase if you have used one for thie wallet in the past. If you enter the wrong passphrase or have not used a passphrase before on this wallet, you won't see any of existing funds or history.", "restore_wallet": "Restore wallet", "restoring_wallet": "Restoring wallet", "save": "Save", @@ -162,6 +164,7 @@ "wallet_password_repeat": "Wallet password (repeat)", "wallet_seed": "Wallet seed", "wallets": "Wallets", + "warning": "Warning", "warning_input_cannot_be_empty": "Input cannot be empty", "warning_input_cannot_be_null": "Input cannot be null", "warning_input_seed_length_invalid": "Incorrect number of words present in the seed", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index c6eda02..fbd7283 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -8,6 +8,7 @@ "confirming": "Potwierdzanie", "copy": "Kopiuj", "create_new_wallet": "Stwórz nowy portfel", + "create_passphrase_warning_text": "Upewnij się, że to hasło zostało zapisane. Bez tego hasła nie będziesz w stanie odzyskać swoich środków. Zaleca się przechowywanie hasła w innym miejscu niż fraza odzyskiwania, aby zapewnić optymalne bezpieczeństwo.", "create_wallet": "Stwórz Portfel", "creating_wallet": "Tworzenie portfela", "cupcake_slogan": "Bezpieczeństwo - jeszcze łatwiejsze", @@ -28,6 +29,7 @@ "primary_address_label": "Adres główny", "receive": "Wpłać", "restore_height": "Wysokość przywracania", + "restore_passphrase_warning_text": "Wprowadź hasło tylko wtedy, gdy było ono wcześniej używane w tym portfelu. Jeśli wprowadzisz nieprawidłowe hasło lub nie było ono wcześniej używane w tym portfelu, nie zobaczysz żadnych istniejących środków ani historii.", "restore_wallet": "Przywróć portfel", "save": "Zapisz", "scan": "Skanuj", @@ -55,6 +57,7 @@ "wallet_password_repeat": "Hasło portfela (ponownie)", "wallet_seed": "Fraza odzyskiwania", "wallets": "Portfele", + "warning": "Warning", "warning_input_cannot_be_empty": "Pole nie może być puste", "warning_input_cannot_be_null": "Pole nie może być puste", "warning_must_be_a_number": "W pole należy wprowadzić liczbę", diff --git a/lib/view_model/create_wallet_view_model.dart b/lib/view_model/create_wallet_view_model.dart index 44a4181..6d5a567 100644 --- a/lib/view_model/create_wallet_view_model.dart +++ b/lib/view_model/create_wallet_view_model.dart @@ -263,7 +263,9 @@ abstract class CreateWalletViewModelBase extends ViewModel with Store { // verify that password match when confirming, ignore otherwise if (walletPassword1 != walletPassword2 && walletPassword2.isNotEmpty) { if (kDebugMode) { - throw Exception("${L.password_doesnt_match} /$walletPassword1/$walletPassword2/"); + throw Exception( + "${L.password_doesnt_match} /$walletPassword1/$walletPassword2/", + ); } throw Exception(L.password_doesnt_match); } diff --git a/lib/views/create_wallet.dart b/lib/views/create_wallet.dart index d1b70f6..31ed60a 100644 --- a/lib/views/create_wallet.dart +++ b/lib/views/create_wallet.dart @@ -1,7 +1,11 @@ +import 'dart:ui'; + import 'package:cupcake/utils/text_span_markdown.dart'; import 'package:cupcake/utils/types.dart'; import 'package:cupcake/gen/assets.gen.dart'; import 'package:cupcake/view_model/create_wallet_view_model.dart'; +import 'package:cupcake/utils/alerts/basic.dart'; +import 'package:cupcake/utils/form/string_form_element.dart'; import 'package:cupcake/view_model/form_builder_view_model.dart'; import 'package:cupcake/views/abstract.dart'; import 'package:cupcake/views/widgets/buttons/long_primary.dart'; @@ -153,7 +157,7 @@ class CreateWallet extends AbstractView { } } final form = FormBuilder( - showExtra: viewModel.showExtra, + showExtra: false, viewModel: viewModel.formBuilderViewModelList[viewModel.formIndex] as FormBuilderViewModel, ); return Column( @@ -241,12 +245,154 @@ class CreateWallet extends AbstractView { LongPrimaryButton( text: L.continue_, icon: null, - onPressed: viewModel.createWallet, + onPressed: () async { + if (viewModel.showExtra) { + await _showBottomSheet(context); + } else { + await viewModel.createWallet(); + } + }, ), ], ); } + String? _extraFieldsError() { + final elements = viewModel.formBuilderViewModelList[viewModel.formIndex].formElements + .where((final e) => e.isExtra) + .whereType(); + for (final e in elements) { + final error = e.validator(e.ctrl.text); + if (error != null) return error; + } + return null; + } + + Future _showBottomSheet(final BuildContext context) async { + await showModalBottomSheet( + context: context, + isDismissible: false, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (final context) => BackdropFilter( + filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8), + child: Container( + decoration: const BoxDecoration( + color: Color(0xFF273765), + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: _buildBottomSheet(context), + ), + ), + ); + } + + Widget _buildBottomSheet(final BuildContext context) { + return Builder( + builder: (final context) { + return Padding( + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: Container( + decoration: const BoxDecoration( + color: Color(0xFF273765), + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 40, + height: 4, + margin: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: Colors.white.withAlpha(76), + borderRadius: BorderRadius.circular(2), + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Column( + spacing: 16, + children: [ + Text(L.add_passphrase, style: Theme.of(context).textTheme.titleLarge), + Assets.icons.passphrase.image(width: 200), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: '${L.warning.toUpperCase()}: ', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Theme.of(context).colorScheme.errorContainer, + decoration: TextDecoration.none, + ), + ), + TextSpan( + text: viewModel.createMethod == CreateMethod.restore + ? L.restore_passphrase_warning_text + : L.create_passphrase_warning_text, + ), + ], + ), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7), + decoration: TextDecoration.none, + ), + ), + FormBuilder( + showExtra: true, + extraOnly: true, + viewModel: viewModel.formBuilderViewModelList[viewModel.formIndex] + as FormBuilderViewModel, + ), + Row( + children: [ + Expanded( + child: LongSecondaryButton( + T, + onPressed: () => Navigator.of(context).pop(), + padding: EdgeInsets.only(right: 10, left: 12), + text: "Cancel", + ), + ), + Expanded( + child: LongPrimaryButton( + onPressed: () async { + final error = _extraFieldsError(); + if (error != null) { + await showAlert( + context: context, + title: L.warning, + body: [error], + ); + return; + } + await viewModel.createWallet(); + }, + padding: EdgeInsets.only(left: 10), + text: "Confirm", + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ); + }, + ); + } + @override Widget build(final BuildContext context) { viewModel.register(context); diff --git a/lib/views/widgets/form_builder.dart b/lib/views/widgets/form_builder.dart index c08128b..4adee18 100644 --- a/lib/views/widgets/form_builder.dart +++ b/lib/views/widgets/form_builder.dart @@ -16,11 +16,18 @@ import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; class FormBuilder extends StatefulWidget { - const FormBuilder({super.key, required this.viewModel, required this.showExtra}); + const FormBuilder({ + super.key, + required this.viewModel, + required this.showExtra, + this.extraOnly = false, + }); final FormBuilderViewModel viewModel; final bool showExtra; + final bool extraOnly; + @override State createState() => _FormBuilderState(); } @@ -176,7 +183,9 @@ class _FormBuilderState extends State { _onLabelChange(null); final List children = []; for (final e in widget.viewModel.formElements) { - if (e.isExtra && !widget.showExtra) { + if (widget.extraOnly) { + if (!e.isExtra) continue; + } else if (e.isExtra && !widget.showExtra) { // If we return Container() some stuff happens on flutter render cache // and it doesn't render properly. continue; diff --git a/pubspec.yaml b/pubspec.yaml index 16ca8d4..300b898 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -190,6 +190,7 @@ flutter: - assets/icons/secure_storage.webp - assets/icons/tiny_cake_labs.svg - assets/icons/cupcake_about.svg + - assets/icons/passphrase.png fonts: - family: Lato