From cbe62f8e4146ea566c33580527caba8415e8b82c Mon Sep 17 00:00:00 2001 From: reasje Date: Mon, 16 Oct 2023 10:04:01 +0330 Subject: [PATCH 01/26] feat: Added findAccountsLastIndex to account use case --- lib/features/common/account/account_use_case.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/features/common/account/account_use_case.dart b/lib/features/common/account/account_use_case.dart index a700f976..6e976730 100644 --- a/lib/features/common/account/account_use_case.dart +++ b/lib/features/common/account/account_use_case.dart @@ -44,6 +44,17 @@ class AccountUseCase extends ReactiveUseCase { update(account, item); } + int findAccountsLastIndex() { + int lastIndex = 0; + for (Account account in accounts.value.reversed) { + if (!account.isCustom) { + lastIndex = int.parse(account.name); + break; + } + } + return lastIndex; + } + void resetXsdConversionRate(double value) { _accountCacheRepository.setXsdConversionRate(value); update(xsdConversionRate, value); From afb6572c439f1571d8e69b75cef385c6827dc984 Mon Sep 17 00:00:00 2001 From: reasje Date: Mon, 16 Oct 2023 10:04:24 +0330 Subject: [PATCH 02/26] feat: Added private key validation --- lib/common/utils/validation.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/common/utils/validation.dart b/lib/common/utils/validation.dart index 0b099670..f765e2c5 100644 --- a/lib/common/utils/validation.dart +++ b/lib/common/utils/validation.dart @@ -53,6 +53,17 @@ class Validation { return null; } + static String? checkEthereumPrivateKey(BuildContext context, String value) { + String ethereumPrivateKeyPattern = r'^[0-9a-fA-F]{64}$'; + + if (!RegExp(ethereumPrivateKeyPattern, caseSensitive: false) + .hasMatch(value)) { + return FlutterI18n.translate(context, 'invalid_format'); + } + + return null; + } + static String? checkMnsValidation(BuildContext context, String value) { if (!((value.endsWith('.mxc') || value.endsWith('.MXC')) && value.length > 4)) { From 0279c24cabf10005d45d32d6edfd1bf524196178 Mon Sep 17 00:00:00 2001 From: reasje Date: Mon, 16 Oct 2023 10:06:53 +0330 Subject: [PATCH 03/26] impv: Add new account settings page --- .../presentation/settings_page_presenter.dart | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/lib/features/settings/presentation/settings_page_presenter.dart b/lib/features/settings/presentation/settings_page_presenter.dart index a9c6f66e..7def881b 100644 --- a/lib/features/settings/presentation/settings_page_presenter.dart +++ b/lib/features/settings/presentation/settings_page_presenter.dart @@ -48,31 +48,21 @@ class SettingsPresenter extends CompletePresenter { notify(() => state.isLoading = true); try { - final index = findAccountLastIndex(state.accounts); - // final index = state.accounts.length; + final index = _accountUserCase.findAccountsLastIndex(); + final newAccount = await _authUseCase.addNewAccount(index); - // final newAccount = await _authUseCase.addCustomAccount('index' ,'6373f6b31ccb382ea61f02a89c28d88972bdc8a45ea0d817826c097188832b3c'); _accountUserCase.addAccount(newAccount); loadCache(); notify(() => state.isLoading = false); - navigator?.pop(); + navigator?.popUntil((route) { + return route.settings.name?.contains('SettingsPage') ?? false; + }); } catch (e, s) { addError(e, s); } } - int findAccountLastIndex(List accounts) { - int lastIndex = 0; - for (Account account in accounts.reversed) { - if (!account.isCustom) { - lastIndex = int.parse(account.name); - break; - } - } - return lastIndex; - } - void changeAccount(Account item) { _accountUserCase.changeAccount(item); _authUseCase.changeAccount(item); From ecdbedd41d2ca28cad0bfe3e35545f6a21579c65 Mon Sep 17 00:00:00 2001 From: reasje Date: Mon, 16 Oct 2023 10:10:22 +0330 Subject: [PATCH 04/26] feat: Added imported chip to account item & limit accounts list size --- assets/flutter_i18n/en.json | 14 ++++++++-- .../subfeatures/accounts/account_item.dart | 22 ++++++++++----- .../subfeatures/accounts/portrait.dart | 2 +- .../accounts/show_accounts_dialog.dart | 27 +++++++++++-------- 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/assets/flutter_i18n/en.json b/assets/flutter_i18n/en.json index a571d04a..7a259164 100644 --- a/assets/flutter_i18n/en.json +++ b/assets/flutter_i18n/en.json @@ -280,7 +280,7 @@ "checking_balance": "Checking balance...", "no_balance": "You have no balance", "no_balance_tip": "You currently have no MXC in your wallet.\nYou can either receive tokens or choose to claim a username later.", - "signature_request": "Signature request", + "signature_request": "Signature request", "sign": "Sign", "insufficient_balance": "Insufficient balance", "unregistered_mns_notice": "Oops! Sending tokens to a username as elusive as a unicorn in a forest. Check if they're registered!", @@ -288,5 +288,15 @@ "send_&_receive": "Send & Receive", "contract": "Contract", "decimals": "Decimals", - "add_token_success_message" : "Hooray! The token has been successfully added to your AXS wallet! 🎉" + "add_token_success_message": "Hooray! The token has been successfully added to your AXS wallet! 🎉", + "import_account": "Import account", + "add_account": "Add account", + "private_key": "Private key", + "import_notice": "Imported accounts won’t be associated with your AXS wallet Secret Recovery Phrase.", + "imported": "Imported", + "show_private_key": "Show private key", + "show_private_key_notice": "Warning: Never disclose this key. Anyone with your private keys can steal any assets held in your account.", + "confirm_show_private_key_notice_1": "Your Private Key provides full access to your wallet and funds.", + "confirm_show_private_key_notice_2": "Do not share this with anyone. MetaMask Support will not request this, but phishers might.", + "confirm_show_private_key_title": "Keep your private key safe" } \ No newline at end of file diff --git a/lib/features/settings/subfeatures/accounts/account_item.dart b/lib/features/settings/subfeatures/accounts/account_item.dart index b135c802..841087d5 100644 --- a/lib/features/settings/subfeatures/accounts/account_item.dart +++ b/lib/features/settings/subfeatures/accounts/account_item.dart @@ -7,17 +7,20 @@ import 'package:mxc_ui/mxc_ui.dart'; import 'portrait.dart'; class AccountItem extends StatelessWidget { - const AccountItem({ - super.key, - required this.account, - this.isSelected = false, - this.onSelect, - }); + const AccountItem( + {super.key, + required this.account, + this.isSelected = false, + this.onSelect, + required this.isCustom}); final Account account; final bool isSelected; final VoidCallback? onSelect; + /// Imported + final bool isCustom; + @override Widget build(BuildContext context) { return InkWell( @@ -35,12 +38,19 @@ class AccountItem extends StatelessWidget { '${FlutterI18n.translate(context, 'account')} ${account.name}', style: FontTheme.of(context).body2.secondary(), ), + const SizedBox(height: Sizes.space2XSmall), Text( account.mns ?? Formatter.formatWalletAddress(account.address, nCharacters: 10), style: FontTheme.of(context).body1.primary(), ), + const SizedBox(height: Sizes.space2XSmall), + if (isCustom) + MxcChipButton( + key: const Key('importedChip'), + onTap: () {}, + title: FlutterI18n.translate(context, 'imported')) ], ), const Spacer(), diff --git a/lib/features/settings/subfeatures/accounts/portrait.dart b/lib/features/settings/subfeatures/accounts/portrait.dart index 6dc4047d..7314e461 100644 --- a/lib/features/settings/subfeatures/accounts/portrait.dart +++ b/lib/features/settings/subfeatures/accounts/portrait.dart @@ -13,7 +13,7 @@ class Portrait extends StatelessWidget { @override Widget build(BuildContext context) { return CircleAvatar( - radius: 12, + radius: 16, child: SvgPicture.string( Jdenticon.toSvg(name), fit: BoxFit.contain, diff --git a/lib/features/settings/subfeatures/accounts/show_accounts_dialog.dart b/lib/features/settings/subfeatures/accounts/show_accounts_dialog.dart index fb490bff..95db2eb7 100644 --- a/lib/features/settings/subfeatures/accounts/show_accounts_dialog.dart +++ b/lib/features/settings/subfeatures/accounts/show_accounts_dialog.dart @@ -11,6 +11,7 @@ void showAccountsDialog({ required List accounts, bool isLoading = false, VoidCallback? onAdd, + VoidCallback? onImport, required Function(Account) onSelect, }) { showModalBottomSheet( @@ -41,17 +42,21 @@ void showAccountsDialog({ ), ), ), - ListView.builder( - padding: EdgeInsets.zero, - itemCount: accounts.length, - shrinkWrap: true, - itemBuilder: (ctx, index) { - return AccountItem( - account: accounts[index], - isSelected: currentAccount.address == accounts[index].address, - onSelect: () => onSelect(accounts[index]), - ); - }, + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 400), + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: accounts.length, + shrinkWrap: true, + itemBuilder: (ctx, index) { + return AccountItem( + account: accounts[index], + isSelected: currentAccount.address == accounts[index].address, + onSelect: () => onSelect(accounts[index]), + isCustom: accounts[index].isCustom, + ); + }, + ), ), const SizedBox(height: Sizes.spaceXSmall), MxcButton.primary( From 8901eb1eab992555919e6b36f75b6dc9ec03e0ce Mon Sep 17 00:00:00 2001 From: reasje Date: Mon, 16 Oct 2023 10:13:06 +0330 Subject: [PATCH 05/26] feat: Added add accounts dialog --- .../account_managment_panel.dart | 12 +++- .../subfeatures/accounts/row_item.dart | 60 +++++++++++++++++++ .../accounts/show_add_accounts_dialog.dart | 50 ++++++++++++++++ 3 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 lib/features/settings/subfeatures/accounts/row_item.dart create mode 100644 lib/features/settings/subfeatures/accounts/show_add_accounts_dialog.dart diff --git a/lib/features/settings/presentation/widgets/account_managment/account_managment_panel.dart b/lib/features/settings/presentation/widgets/account_managment/account_managment_panel.dart index ed75986d..00fd07f4 100644 --- a/lib/features/settings/presentation/widgets/account_managment/account_managment_panel.dart +++ b/lib/features/settings/presentation/widgets/account_managment/account_managment_panel.dart @@ -2,11 +2,13 @@ import 'package:datadashwallet/common/common.dart'; import 'package:datadashwallet/core/core.dart'; import 'package:datadashwallet/features/settings/settings.dart'; import 'package:datadashwallet/features/settings/subfeatures/accounts/show_accounts_dialog.dart'; +import 'package:datadashwallet/features/settings/subfeatures/accounts/subfeatures/account_details/import_account_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mxc_ui/mxc_ui.dart'; +import '../../../subfeatures/accounts/show_add_accounts_dialog.dart'; import 'copyable_item.dart'; class AccountManagementPanel extends HookConsumerWidget { @@ -34,7 +36,15 @@ class AccountManagementPanel extends HookConsumerWidget { currentAccount: state.account!, accounts: state.accounts, isLoading: state.isLoading, - onAdd: () => presenter.addNewAccount(), + onAdd: () => showAddAccountsDialog( + context: context, + isLoading: state.isLoading, + onAdd: presenter.addNewAccount, + onImport: () => Navigator.of(context).push( + route.featureDialog( + const ImportAccountPage(), + ), + ),), onSelect: (item) => presenter.changeAccount(item)), child: Row( children: [ diff --git a/lib/features/settings/subfeatures/accounts/row_item.dart b/lib/features/settings/subfeatures/accounts/row_item.dart new file mode 100644 index 00000000..514fd48c --- /dev/null +++ b/lib/features/settings/subfeatures/accounts/row_item.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:mxc_ui/mxc_ui.dart'; + +class RowItem extends HookConsumerWidget { + const RowItem( + this.title, + this.icon, + this.onTap, { + super.key, + }); + + final String title; + final IconData icon; + final void Function() onTap; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Container( + margin: const EdgeInsets.only(top: Sizes.spaceNormal), + child: InkWell( + onTap: () => onTap(), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: Sizes.spaceSmall), + child: Row( + children: [ + Icon( + icon, + size: 24, + color: ColorsTheme.of(context).iconPrimary, + ), + const SizedBox( + width: 24, + ), + Text( + title, + style: FontTheme.of(context).body2.primary(), + ), + const Spacer(), + const SizedBox( + width: 16, + ), + // trailingIcon != null + // ? Icon( + // MxcIcons.external_link, + // size: 24, + // color: ColorsTheme.of(context).iconPrimary, + // ) + // : Icon( + // Icons.arrow_forward_ios, + // size: 16, + // color: ColorsTheme.of(context).iconWhite32, + // ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/settings/subfeatures/accounts/show_add_accounts_dialog.dart b/lib/features/settings/subfeatures/accounts/show_add_accounts_dialog.dart new file mode 100644 index 00000000..80f7ff4b --- /dev/null +++ b/lib/features/settings/subfeatures/accounts/show_add_accounts_dialog.dart @@ -0,0 +1,50 @@ +import 'package:datadashwallet/features/settings/subfeatures/accounts/row_item.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; +import 'package:mxc_ui/mxc_ui.dart'; + +import 'account_item.dart'; + +void showAddAccountsDialog({ + required BuildContext context, + bool isLoading = false, + required VoidCallback onAdd, + required VoidCallback onImport, +}) { + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + useSafeArea: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) => Container( + padding: const EdgeInsets.only(left: 16, right: 16, top: 0, bottom: 44), + decoration: BoxDecoration( + color: ColorsTheme.of(context).screenBackground, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + MxcAppBarEvenly.title( + titleText: FlutterI18n.translate(context, 'add_account'), + action: Container( + alignment: Alignment.centerRight, + child: InkWell( + child: const Icon(Icons.close), + onTap: () => Navigator.of(context).pop(false), + ), + ), + ), + RowItem(FlutterI18n.translate(context, 'add_new_account'), + Icons.add_rounded, onAdd), + RowItem(FlutterI18n.translate(context, 'import_account'), + Icons.file_download_outlined, onImport), + ], + ), + ), + ); +} From a476ad25e38b88893c26eb4abdb0f1ffc37270b0 Mon Sep 17 00:00:00 2001 From: reasje Date: Mon, 16 Oct 2023 10:14:04 +0330 Subject: [PATCH 06/26] feat: Added import account page --- .../account_details/import_account_page.dart | 90 +++++++++++++++++++ .../import_account_presenter.dart | 51 +++++++++++ .../account_details/import_account_state.dart | 10 +++ 3 files changed, 151 insertions(+) create mode 100644 lib/features/settings/subfeatures/accounts/subfeatures/account_details/import_account_page.dart create mode 100644 lib/features/settings/subfeatures/accounts/subfeatures/account_details/import_account_presenter.dart create mode 100644 lib/features/settings/subfeatures/accounts/subfeatures/account_details/import_account_state.dart diff --git a/lib/features/settings/subfeatures/accounts/subfeatures/account_details/import_account_page.dart b/lib/features/settings/subfeatures/accounts/subfeatures/account_details/import_account_page.dart new file mode 100644 index 00000000..c3c9750b --- /dev/null +++ b/lib/features/settings/subfeatures/accounts/subfeatures/account_details/import_account_page.dart @@ -0,0 +1,90 @@ +import 'package:datadashwallet/common/common.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:mxc_ui/mxc_ui.dart'; + +import 'import_account_presenter.dart'; + +class ImportAccountPage extends HookConsumerWidget { + const ImportAccountPage({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final formKey = useMemoized(() => GlobalKey()); + String translate(String text) => FlutterI18n.translate(context, text); + final presenter = ref.read(importAccountContainer.actions); + final state = ref.watch(importAccountContainer.state); + return MxcPage.layer( + presenter: presenter, + crossAxisAlignment: CrossAxisAlignment.start, + layout: LayoutType.scrollable, + children: [ + MxcAppBarEvenly.text( + titleText: translate('import_account'), + actionText: translate('save'), + onActionTap: () { + if (!formKey.currentState!.validate()) return; + presenter.onSave(); + }, + isActionTap: state.ableToSave, + ), + Form( + key: formKey, + child: Column( + children: [ + MxcTextField( + key: const ValueKey('privateKeyTextField'), + label: translate('private_key'), + hint: translate('private_key'), + controller: presenter.privateKeyController, + action: TextInputAction.next, + validator: (value) { + final res = Validation.notEmpty( + context, + value, + translate('x_not_empty') + .replaceFirst('{0}', translate('private_key'))); + if (res != null) return res; + return Validation.checkEthereumPrivateKey( + context, value ?? ''); + }, + onChanged: (value) { + presenter.changeAbleToSave( + formKey.currentState!.validate() ? true : false); + }, + ), + // MxcTextField( + // key: const ValueKey('accountNameTextField'), + // label: translate('account_name'), + // hint: translate('account_name'), + // controller: presenter.rpcUrlController, + // action: TextInputAction.next, + // validator: (value) { + // final res = Validation.notEmpty( + // context, + // value, + // translate('x_not_empty') + // .replaceFirst('{0}', translate('account_name'))); + // if (res != null) return res; + // return Validation.checkHttps(context, value!, + // errorText: translate('invalid_url_format_notice')); + // }, + // onChanged: (value) { + // presenter.changeAbleToSave( + // formKey.currentState!.validate() ? true : false); + // presenter.onRpcUrlChange(value); + // }, + // onFocused: (focused) => + // focused ? null : formKey.currentState!.validate(), + // ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/features/settings/subfeatures/accounts/subfeatures/account_details/import_account_presenter.dart b/lib/features/settings/subfeatures/accounts/subfeatures/account_details/import_account_presenter.dart new file mode 100644 index 00000000..7d7001e2 --- /dev/null +++ b/lib/features/settings/subfeatures/accounts/subfeatures/account_details/import_account_presenter.dart @@ -0,0 +1,51 @@ +import 'package:datadashwallet/core/core.dart'; +import 'package:datadashwallet/features/settings/domain/webview_use_case.dart'; +import 'package:flutter/material.dart'; +import 'package:mxc_ui/mxc_ui.dart'; + +import 'import_account_state.dart'; + +final importAccountContainer = + PresenterContainer( + () => ImportAccountPresenter()); + +class ImportAccountPresenter extends CompletePresenter { + ImportAccountPresenter() : super(ImportAccountState()); + + late final _accountUserCase = ref.read(accountUseCaseProvider); + late final _authUseCase = ref.read(authUseCaseProvider); + late final _webviewUseCase = WebviewUseCase(); + + final TextEditingController privateKeyController = TextEditingController(); + + void onSave() async { + loading = true; + try { + final index = _accountUserCase.findAccountsLastIndex(); + final privateKey = privateKeyController.text; + + final newAccount = + await _authUseCase.addCustomAccount('${index + 1}', privateKey); + _accountUserCase.addAccount(newAccount); + loadCache(); + + notify(() => state.isLoading = false); + BottomFlowDialog.of(context!).close(); + navigator?.popUntil((route) { + return route.settings.name?.contains('SettingsPage') ?? false; + }); + } catch (error, stackTrace) { + addError(error, stackTrace); + } finally { + loading = false; + } + } + + void changeAbleToSave(bool value) { + notify(() => state.ableToSave = value); + } + + void loadCache() { + _webviewUseCase.clearCache(); + } +} diff --git a/lib/features/settings/subfeatures/accounts/subfeatures/account_details/import_account_state.dart b/lib/features/settings/subfeatures/accounts/subfeatures/account_details/import_account_state.dart new file mode 100644 index 00000000..6052c416 --- /dev/null +++ b/lib/features/settings/subfeatures/accounts/subfeatures/account_details/import_account_state.dart @@ -0,0 +1,10 @@ +import 'package:equatable/equatable.dart'; +import 'package:mxc_logic/mxc_logic.dart'; + +class ImportAccountState with EquatableMixin { + bool ableToSave = false; + bool isLoading = false; + + @override + List get props => [ableToSave, isLoading]; +} From d5073793f8950dcf8ba11413d94f5d0f906dbf88 Mon Sep 17 00:00:00 2001 From: reasje Date: Mon, 16 Oct 2023 13:22:43 +0330 Subject: [PATCH 07/26] feat: Added remove account functionality --- .../common/account/account_cache_repository.dart | 2 +- lib/features/common/account/account_use_case.dart | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/features/common/account/account_cache_repository.dart b/lib/features/common/account/account_cache_repository.dart index 70878aea..7f9267fb 100644 --- a/lib/features/common/account/account_cache_repository.dart +++ b/lib/features/common/account/account_cache_repository.dart @@ -56,7 +56,7 @@ class AccountCacheRepository extends GlobalCacheRepository { void addAccount(Account item) => accounts.value = [...accounts.value, item]; void removeAccount(Account item) => accounts.value = - accounts.value.where((e) => e.name != item.name).toList(); + accounts.value.where((e) => e.address != item.address).toList(); void updateAccount(Account item) => accounts.value = accounts.value.map((e) { if (item.address == account.value!.address) { account.value = item; diff --git a/lib/features/common/account/account_use_case.dart b/lib/features/common/account/account_use_case.dart index 6e976730..35611e3f 100644 --- a/lib/features/common/account/account_use_case.dart +++ b/lib/features/common/account/account_use_case.dart @@ -40,6 +40,15 @@ class AccountUseCase extends ReactiveUseCase { getAccountsNames(); } + /// Deletes the given account, If the account is selected will select the index 0 account + /// This is only used to delete the imported accounts. + void removeAccount(Account item) async { + _accountCacheRepository.removeAccount(item); + final items = _accountCacheRepository.accountItems; + if (item.address == account.value!.address) update(account, items[0]); + update(accounts, items); + } + void changeAccount(Account item) { update(account, item); } From 32fe801e85c0dfa1237724cc3bbebbd8ae98195a Mon Sep 17 00:00:00 2001 From: reasje Date: Mon, 16 Oct 2023 16:45:43 +0330 Subject: [PATCH 08/26] fix: fee exceed in send crypto & tx recording number conversion --- lib/common/utils/formatter.dart | 2 +- .../send_crypto/send_crypto_presenter.dart | 55 +++++++++++-------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/lib/common/utils/formatter.dart b/lib/common/utils/formatter.dart index 0da396a4..4d8d3f24 100644 --- a/lib/common/utils/formatter.dart +++ b/lib/common/utils/formatter.dart @@ -44,7 +44,7 @@ class Formatter { static String convertWeiToEth(String inputString, int tokenDecimal) { // 10^18 = 1000000000000000000 but we want to have up to 2 digits accuracy - if (double.parse(inputString).toDouble() < 10000000000000000) { + if (double.parse(inputString).toDouble() < 1000000000000000) { return '0'; } final valueDouble = double.parse(inputString).toDouble() / pow(10, 18); diff --git a/lib/features/portfolio/subfeatures/token/send_token/send_crypto/send_crypto_presenter.dart b/lib/features/portfolio/subfeatures/token/send_token/send_crypto/send_crypto_presenter.dart index ba936ab4..f5fec1b5 100644 --- a/lib/features/portfolio/subfeatures/token/send_token/send_crypto/send_crypto_presenter.dart +++ b/lib/features/portfolio/subfeatures/token/send_token/send_crypto/send_crypto_presenter.dart @@ -150,23 +150,25 @@ class SendCryptoPresenter extends CompletePresenter { double sumBalance = token.balance! - double.parse(amount); estimatedGasFee = await _estimatedFee(recipientAddress); - sumBalance -= estimatedGasFee?.gasFee ?? 0.0; - final estimatedFee = estimatedGasFee == null - ? '--' - : Validation.isExpoNumber(estimatedGasFee.gasFee.toString()) - ? '0.000' - : estimatedGasFee.gasFee.toString(); - - final result = await showTransactionDialog(context!, - amount: amount, - balance: sumBalance.toString(), - token: token, - network: state.network?.label ?? '--', - from: state.account!.address, - to: recipient, - estimatedFee: estimatedFee, - onTap: (transactionType) => _nextTransactionStep(transactionType), - networkSymbol: state.network?.symbol ?? '--'); + if (estimatedGasFee != null) { + sumBalance -= estimatedGasFee.gasFee; + final estimatedFee = estimatedGasFee == null + ? '--' + : Validation.isExpoNumber(estimatedGasFee.gasFee.toString()) + ? '0.000' + : estimatedGasFee.gasFee.toString(); + + final result = await showTransactionDialog(context!, + amount: amount, + balance: sumBalance.toString(), + token: token, + network: state.network?.label ?? '--', + from: state.account!.address, + to: recipient, + estimatedFee: estimatedFee, + onTap: (transactionType) => _nextTransactionStep(transactionType), + networkSymbol: state.network?.symbol ?? '--'); + } } String? checkAmountCeiling() { @@ -203,7 +205,11 @@ class SendCryptoPresenter extends CompletePresenter { return gasFee; } catch (e, s) { - addError(e, s); + if (e is RPCError) { + String errorMessage = e.message; + errorMessage = changeErrorMessage(errorMessage); + addError(errorMessage); + } } finally { loading = false; } @@ -229,7 +235,7 @@ class SendCryptoPresenter extends CompletePresenter { hash: res, status: TransactionStatus.pending, type: TransactionType.sent, - value: amount.getValueInUnit(EtherUnit.wei).toString(), + value: amount.getValueInUnitBI(EtherUnit.wei).toString(), token: token, timeStamp: DateTime.now()); @@ -249,9 +255,7 @@ class SendCryptoPresenter extends CompletePresenter { BottomFlowDialog.of(context!).close(); } String errorMessage = e.message; - if (e.message.contains('gas required exceeds allowance')) { - errorMessage = translate('insufficient_balance_for_fee') ?? e.message; - } + errorMessage = changeErrorMessage(errorMessage); addError(errorMessage); } } finally { @@ -259,6 +263,13 @@ class SendCryptoPresenter extends CompletePresenter { } } + String changeErrorMessage(String message) { + if (message.contains('gas required exceeds allowance')) { + return translate('insufficient_balance_for_fee') ?? message; + } + return message; + } + @override Future dispose() async { super.dispose(); From 63ebe97e1bcc7e195ac33c5244c53a839573d58f Mon Sep 17 00:00:00 2001 From: reasje Date: Tue, 17 Oct 2023 13:25:19 +0330 Subject: [PATCH 09/26] fix: Removed add accounts for better UX --- .../accounts/show_add_accounts_dialog.dart | 50 ------------------- 1 file changed, 50 deletions(-) delete mode 100644 lib/features/settings/subfeatures/accounts/show_add_accounts_dialog.dart diff --git a/lib/features/settings/subfeatures/accounts/show_add_accounts_dialog.dart b/lib/features/settings/subfeatures/accounts/show_add_accounts_dialog.dart deleted file mode 100644 index 80f7ff4b..00000000 --- a/lib/features/settings/subfeatures/accounts/show_add_accounts_dialog.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:datadashwallet/features/settings/subfeatures/accounts/row_item.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_i18n/flutter_i18n.dart'; -import 'package:mxc_ui/mxc_ui.dart'; - -import 'account_item.dart'; - -void showAddAccountsDialog({ - required BuildContext context, - bool isLoading = false, - required VoidCallback onAdd, - required VoidCallback onImport, -}) { - showModalBottomSheet( - context: context, - useRootNavigator: true, - isScrollControlled: true, - useSafeArea: true, - backgroundColor: Colors.transparent, - builder: (BuildContext context) => Container( - padding: const EdgeInsets.only(left: 16, right: 16, top: 0, bottom: 44), - decoration: BoxDecoration( - color: ColorsTheme.of(context).screenBackground, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - MxcAppBarEvenly.title( - titleText: FlutterI18n.translate(context, 'add_account'), - action: Container( - alignment: Alignment.centerRight, - child: InkWell( - child: const Icon(Icons.close), - onTap: () => Navigator.of(context).pop(false), - ), - ), - ), - RowItem(FlutterI18n.translate(context, 'add_new_account'), - Icons.add_rounded, onAdd), - RowItem(FlutterI18n.translate(context, 'import_account'), - Icons.file_download_outlined, onImport), - ], - ), - ), - ); -} From a189f4f0a1be88cd7056d2ed2837fd59810d722b Mon Sep 17 00:00:00 2001 From: reasje Date: Tue, 17 Oct 2023 16:27:05 +0330 Subject: [PATCH 10/26] feat: Added private key validation & moved pub key validation to validation --- lib/common/utils/validation.dart | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/common/utils/validation.dart b/lib/common/utils/validation.dart index f765e2c5..ef9f1db0 100644 --- a/lib/common/utils/validation.dart +++ b/lib/common/utils/validation.dart @@ -1,6 +1,8 @@ import 'package:datadashwallet/common/config.dart'; import 'package:flutter/material.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; +import 'package:web3dart/web3dart.dart'; + class Validation { static String? notEmpty(BuildContext context, String? value, @@ -129,4 +131,23 @@ class Validation { return regex.hasMatch(value); } + + + static bool isAddress(String address) { + try { + EthereumAddress.fromHex(address); + return true; + } catch (e) { + return false; + } + } + + static bool isPrivateKey(String privateKey) { + try { + EthPrivateKey.fromHex(privateKey); + return true; + } catch (e) { + return false; + } + } } From b7049adb9abad1d7f8be91d17c3f0c3a37d6bf9f Mon Sep 17 00:00:00 2001 From: reasje Date: Tue, 17 Oct 2023 16:35:15 +0330 Subject: [PATCH 11/26] feat: Updated single line info for custom icons & functions --- .../list/single_line_info_item.dart | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/lib/common/components/list/single_line_info_item.dart b/lib/common/components/list/single_line_info_item.dart index 47a55441..f3bf98f1 100644 --- a/lib/common/components/list/single_line_info_item.dart +++ b/lib/common/components/list/single_line_info_item.dart @@ -1,23 +1,29 @@ -import 'package:datadashwallet/features/dapps/subfeatures/open_dapp/open_dapp_presenter.dart'; +import 'package:datadashwallet/common/common.dart'; +import 'package:datadashwallet/core/src/providers/providers_use_cases.dart'; import 'package:flutter/material.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mxc_ui/mxc_ui.dart'; class SingleLineInfoItem extends HookConsumerWidget { - const SingleLineInfoItem({ - super.key, - required this.title, - required this.value, - this.hint, - }); + const SingleLineInfoItem( + {super.key, + required this.title, + required this.value, + this.hint, + this.valueActionIcon}); + final String title; final String value; final String? hint; + final Widget? valueActionIcon; + @override Widget build(BuildContext context, WidgetRef ref) { - final presenter = ref.read(openDAppPageContainer.actions); - final isAddress = presenter.isAddress(value); + late final _chainConfigurationUseCase = + ref.read(chainConfigurationUseCaseProvider); + + final isAddress = Validation.isAddress(value); return Padding( padding: const EdgeInsets.symmetric(vertical: Sizes.spaceXSmall), child: Row( @@ -34,7 +40,9 @@ class SingleLineInfoItem extends HookConsumerWidget { ), Expanded( child: InkWell( - onTap: isAddress ? () => presenter.launchAddress(value) : null, + onTap: isAddress + ? () => _chainConfigurationUseCase.launchAddress(value) + : null, child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -46,7 +54,9 @@ class SingleLineInfoItem extends HookConsumerWidget { textAlign: TextAlign.end, ), ), - if (isAddress) ...[ + if (valueActionIcon != null) + valueActionIcon! + else if (isAddress) ...[ const SizedBox(width: 8), Icon( MxcIcons.external_link, From 7ed6fc45a337a4a3416f66fcea2a63942cda37a6 Mon Sep 17 00:00:00 2001 From: reasje Date: Tue, 17 Oct 2023 16:36:59 +0330 Subject: [PATCH 12/26] feat: Added passcode athentication page for verifying user --- .../open_dapp/widgets/add_asset_info.dart | 4 +- .../passcode_authenticate_user_page.dart | 67 +++++++++++++++++++ .../passcode_authenticate_user_presenter.dart | 41 ++++++++++++ .../passcode_authenticate_user_state.dart | 11 +++ 4 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 lib/features/security/presentation/passcode_authenticate/passcode_authenticate_user_page.dart create mode 100644 lib/features/security/presentation/passcode_authenticate/passcode_authenticate_user_presenter.dart create mode 100644 lib/features/security/presentation/passcode_authenticate/passcode_authenticate_user_state.dart diff --git a/lib/features/dapps/subfeatures/open_dapp/widgets/add_asset_info.dart b/lib/features/dapps/subfeatures/open_dapp/widgets/add_asset_info.dart index 2c784dbd..250a966d 100644 --- a/lib/features/dapps/subfeatures/open_dapp/widgets/add_asset_info.dart +++ b/lib/features/dapps/subfeatures/open_dapp/widgets/add_asset_info.dart @@ -65,7 +65,9 @@ class AddAssetInfo extends ConsumerWidget { title: FlutterI18n.translate(context, 'contract'), value: contractAddress ?? '')); infoList.add(SingleLineInfoItem( - title: FlutterI18n.translate(context, 'symbol'), value: symbol ?? '')); + title: FlutterI18n.translate(context, 'symbol'), + value: symbol ?? '', + )); infoList.add(SingleLineInfoItem( title: FlutterI18n.translate(context, 'decimals'), value: decimalsString)); diff --git a/lib/features/security/presentation/passcode_authenticate/passcode_authenticate_user_page.dart b/lib/features/security/presentation/passcode_authenticate/passcode_authenticate_user_page.dart new file mode 100644 index 00000000..d8a3ce30 --- /dev/null +++ b/lib/features/security/presentation/passcode_authenticate/passcode_authenticate_user_page.dart @@ -0,0 +1,67 @@ +import 'package:datadashwallet/features/security/security.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:mxc_ui/mxc_ui.dart'; + +import 'passcode_authenticate_user_presenter.dart'; +import 'passcode_authenticate_user_state.dart'; + +class PasscodeAuthenticateUserPage extends PasscodeBasePage { + const PasscodeAuthenticateUserPage( + {Key? key, this.change = false, this.dismissedDest}) + : super(key: key); + + final bool change; + final String? dismissedDest; + + @override + String title(BuildContext context, WidgetRef ref) => + FlutterI18n.translate(context, 'view_private_key'); + + @override + String hint(BuildContext context, WidgetRef ref) => + FlutterI18n.translate(context, 'enter_your_passcode'); + + @override + String secondHint(BuildContext context, WidgetRef ref) => + FlutterI18n.translate(context, 'view_private_key_notice'); + + @override + String? dismissedPage() => dismissedDest; + + @override + ProviderBase get presenter => + passcodeAuthenticateUserContainer.actions; + + @override + ProviderBase get state => + passcodeAuthenticateUserContainer.state; + + @override + Widget buildErrorMessage(BuildContext context, WidgetRef ref) => SizedBox( + height: 90, + width: 280, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (ref.watch(state).errorText != null) ...[ + Text( + ref.watch(state).errorText!, + textAlign: TextAlign.center, + style: FontTheme.of(context).subtitle2.error(), + ), + if (ref.watch(state).wrongInputCounter == 2) ...[ + const SizedBox(height: 6), + Text( + FlutterI18n.translate(context, 'app_will_be_locked_alert'), + textAlign: TextAlign.center, + style: FontTheme.of(context).subtitle1.error(), + ), + ], + ] + ], + ), + ); +} diff --git a/lib/features/security/presentation/passcode_authenticate/passcode_authenticate_user_presenter.dart b/lib/features/security/presentation/passcode_authenticate/passcode_authenticate_user_presenter.dart new file mode 100644 index 00000000..fc71b572 --- /dev/null +++ b/lib/features/security/presentation/passcode_authenticate/passcode_authenticate_user_presenter.dart @@ -0,0 +1,41 @@ +import 'package:datadashwallet/core/core.dart'; +import 'package:datadashwallet/features/security/security.dart'; +import 'passcode_authenticate_user_state.dart'; + +final passcodeAuthenticateUserContainer = PresenterContainer< + PasscodeAuthenticateUserPresenter, + PasscodeAuthenticateUserState>( + () => PasscodeAuthenticateUserPresenter()); + +class PasscodeAuthenticateUserPresenter + extends PasscodeBasePagePresenter { + PasscodeAuthenticateUserPresenter() + : super(PasscodeAuthenticateUserState()); + + late final PasscodeUseCase _passcodeUseCase = + ref.read(passcodeUseCaseProvider); + + @override + void onAllNumbersEntered(String? dismissedPage) async { + if (state.enteredNumbers.join('') != _passcodeUseCase.passcode.value) { + if (state.wrongInputCounter < 2) { + state.errorText = translate('incorrect_passcode')!; + state.wrongInputCounter++; + } else { + state.errorText = null; + state.wrongInputCounter = 0; + ref.read(passcodeUseCaseProvider).penaltyLock(); + } + state.enteredNumbers = []; + notify(); + return; + } + + Future.delayed( + passcodeTransitionDuration, + () => notify(() => state.enteredNumbers = []), + ); + + navigator?.pop(true); + } +} diff --git a/lib/features/security/presentation/passcode_authenticate/passcode_authenticate_user_state.dart b/lib/features/security/presentation/passcode_authenticate/passcode_authenticate_user_state.dart new file mode 100644 index 00000000..e1c5a093 --- /dev/null +++ b/lib/features/security/presentation/passcode_authenticate/passcode_authenticate_user_state.dart @@ -0,0 +1,11 @@ +import 'package:datadashwallet/features/security/security.dart'; + +class PasscodeAuthenticateUserState extends PasscodeBasePageState { + int wrongInputCounter = 0; + + @override + List get props => [ + ...super.props, + wrongInputCounter, + ]; +} From 157638db744cdc7ef3a1400a15fe1642ac96f573 Mon Sep 17 00:00:00 2001 From: reasje Date: Tue, 17 Oct 2023 16:37:37 +0330 Subject: [PATCH 13/26] feat: Added second hint for passcode pages --- .../presentation/passcode_base/passcode_base_page.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/features/security/presentation/passcode_base/passcode_base_page.dart b/lib/features/security/presentation/passcode_base/passcode_base_page.dart index a1db5786..13242570 100644 --- a/lib/features/security/presentation/passcode_base/passcode_base_page.dart +++ b/lib/features/security/presentation/passcode_base/passcode_base_page.dart @@ -22,6 +22,8 @@ abstract class PasscodeBasePage extends HookConsumerWidget { String hint(BuildContext context, WidgetRef ref); + String? secondHint(BuildContext context, WidgetRef ref) => null; + String description(BuildContext context, WidgetRef ref) => ''; String? dismissedPage() => null; @@ -178,6 +180,13 @@ abstract class PasscodeBasePage extends HookConsumerWidget { hint(context, ref), style: FontTheme.of(context).body1.white(), ), + if (secondHint(context, ref) != null) ...[ + const SizedBox(height: 16), + Text( + secondHint(context, ref)!, + style: FontTheme.of(context).body1.white(), + ), + ], const SizedBox(height: 64), SizedBox( height: 57.5, From b236cf8ac7449ab21b245a604b0e2b09ee3dbe17 Mon Sep 17 00:00:00 2001 From: reasje Date: Tue, 17 Oct 2023 16:40:01 +0330 Subject: [PATCH 14/26] feat: Added remove account functionality --- assets/flutter_i18n/en.json | 10 +++--- .../common/account/account_use_case.dart | 5 ++- .../open_dapp/open_dapp_presenter.dart | 7 +--- .../presentation/settings_page_presenter.dart | 34 +++++++++++++++++-- .../account_managment/copyable_item.dart | 3 -- .../subfeatures/accounts/account_item.dart | 15 +++++--- 6 files changed, 53 insertions(+), 21 deletions(-) diff --git a/assets/flutter_i18n/en.json b/assets/flutter_i18n/en.json index 7a259164..334f3b21 100644 --- a/assets/flutter_i18n/en.json +++ b/assets/flutter_i18n/en.json @@ -294,9 +294,9 @@ "private_key": "Private key", "import_notice": "Imported accounts won’t be associated with your AXS wallet Secret Recovery Phrase.", "imported": "Imported", - "show_private_key": "Show private key", - "show_private_key_notice": "Warning: Never disclose this key. Anyone with your private keys can steal any assets held in your account.", - "confirm_show_private_key_notice_1": "Your Private Key provides full access to your wallet and funds.", - "confirm_show_private_key_notice_2": "Do not share this with anyone. MetaMask Support will not request this, but phishers might.", - "confirm_show_private_key_title": "Keep your private key safe" + "view_private_key": "View private key", + "view_private_key_notice": "Warning: Never disclose this key. Anyone with your private keys can steal any assets held in your account.", + "removing_account": "Removing account", + "removing_account_warning": "Are you sure you want to remove this account?", + "remove": "Remove" } \ No newline at end of file diff --git a/lib/features/common/account/account_use_case.dart b/lib/features/common/account/account_use_case.dart index 35611e3f..3b6dae30 100644 --- a/lib/features/common/account/account_use_case.dart +++ b/lib/features/common/account/account_use_case.dart @@ -45,9 +45,12 @@ class AccountUseCase extends ReactiveUseCase { void removeAccount(Account item) async { _accountCacheRepository.removeAccount(item); final items = _accountCacheRepository.accountItems; - if (item.address == account.value!.address) update(account, items[0]); update(accounts, items); } + + bool isAccountSelected(Account item) { + return (item.address == account.value!.address); + } void changeAccount(Account item) { update(account, item); diff --git a/lib/features/dapps/subfeatures/open_dapp/open_dapp_presenter.dart b/lib/features/dapps/subfeatures/open_dapp/open_dapp_presenter.dart index f863657e..41a75575 100644 --- a/lib/features/dapps/subfeatures/open_dapp/open_dapp_presenter.dart +++ b/lib/features/dapps/subfeatures/open_dapp/open_dapp_presenter.dart @@ -350,12 +350,7 @@ class OpenDAppPresenter extends CompletePresenter { } bool isAddress(String address) { - try { - EthereumAddress.fromHex(address); - return true; - } catch (e) { - return false; - } + return Validation.isAddress(address); } void addAsset(int id, Map data, diff --git a/lib/features/settings/presentation/settings_page_presenter.dart b/lib/features/settings/presentation/settings_page_presenter.dart index 7def881b..2c96bf35 100644 --- a/lib/features/settings/presentation/settings_page_presenter.dart +++ b/lib/features/settings/presentation/settings_page_presenter.dart @@ -1,6 +1,9 @@ import 'package:clipboard/clipboard.dart'; +import 'package:datadashwallet/common/components/snack_bar.dart'; +import 'package:datadashwallet/common/dialogs/alert_dialog.dart'; import 'package:datadashwallet/core/core.dart'; import 'package:datadashwallet/features/settings/settings.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:mxc_logic/mxc_logic.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'settings_page_state.dart'; @@ -33,6 +36,9 @@ class SettingsPresenter extends CompletePresenter { void copyToClipboard(String text) async { FlutterClipboard.copy(text).then((value) => null); + + showSnackBar( + context: context!, content: FlutterI18n.translate(context!, 'copied')); } void getAppVersion() async { @@ -63,12 +69,36 @@ class SettingsPresenter extends CompletePresenter { } } - void changeAccount(Account item) { + void changeAccount(Account item, {bool shouldPop = true}) { _accountUserCase.changeAccount(item); _authUseCase.changeAccount(item); loadCache(); - navigator?.pop(); + if (shouldPop) navigator?.pop(); + } + + void removeAccount(Account item) async { + try { + final result = await showAlertDialog( + context: context!, + title: translate('removing_account')!, + content: translate('removing_account_warning')!, + ok: translate('remove')!, + ); + + if (result != null && result) { + _accountUserCase.removeAccount(item); + + final isSelected = _accountUserCase.isAccountSelected(item); + if (isSelected) { + changeAccount(state.accounts[0], shouldPop: false); + } + + navigator?.pop(); + } + } catch (e, s) { + addError(e, s); + } } void loadCache() { diff --git a/lib/features/settings/presentation/widgets/account_managment/copyable_item.dart b/lib/features/settings/presentation/widgets/account_managment/copyable_item.dart index 1ac44a16..dc0cd530 100644 --- a/lib/features/settings/presentation/widgets/account_managment/copyable_item.dart +++ b/lib/features/settings/presentation/widgets/account_managment/copyable_item.dart @@ -19,9 +19,6 @@ class CopyableItem extends HookConsumerWidget { return InkWell( onTap: () { presenter.copyToClipboard(copyableText); - showSnackBar( - context: context, - content: FlutterI18n.translate(context, 'copied')); }, child: Row( mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/features/settings/subfeatures/accounts/account_item.dart b/lib/features/settings/subfeatures/accounts/account_item.dart index 841087d5..7f9b1d77 100644 --- a/lib/features/settings/subfeatures/accounts/account_item.dart +++ b/lib/features/settings/subfeatures/accounts/account_item.dart @@ -12,11 +12,13 @@ class AccountItem extends StatelessWidget { required this.account, this.isSelected = false, this.onSelect, - required this.isCustom}); + required this.isCustom, + this.onRemove}); final Account account; final bool isSelected; final VoidCallback? onSelect; + final Function(Account)? onRemove; /// Imported final bool isCustom; @@ -54,9 +56,14 @@ class AccountItem extends StatelessWidget { ], ), const Spacer(), - if (isSelected) ...[ - const Icon(Icons.check_rounded), - ] + if (isSelected) const Icon(Icons.check_rounded), + if (isCustom) + IconButton( + key: const Key('removeImportedAccountButton'), + icon: Icon(Icons.delete, + size: 24, color: ColorsTheme.of(context).iconPrimary), + onPressed:() => onRemove!(account), + ) ], ), ), From c16446a103b9a1e12be0b38972fbb073c70d3759 Mon Sep 17 00:00:00 2001 From: reasje Date: Tue, 17 Oct 2023 16:40:32 +0330 Subject: [PATCH 15/26] feat: Added view private key button to qr code page --- .../qr_code/show_qa_code/qr_code_page.dart | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/lib/features/settings/subfeatures/qr_code/show_qa_code/qr_code_page.dart b/lib/features/settings/subfeatures/qr_code/show_qa_code/qr_code_page.dart index 0f3f7c23..b2a7c649 100644 --- a/lib/features/settings/subfeatures/qr_code/show_qa_code/qr_code_page.dart +++ b/lib/features/settings/subfeatures/qr_code/show_qa_code/qr_code_page.dart @@ -1,6 +1,8 @@ import 'package:datadashwallet/common/common.dart'; import 'package:datadashwallet/core/core.dart'; +import 'package:datadashwallet/features/security/presentation/passcode_authenticate/passcode_authenticate_user_page.dart'; import 'package:datadashwallet/features/settings/presentation/widgets/account_managment/copyable_item.dart'; +import 'package:datadashwallet/features/settings/subfeatures/accounts/show_view_private_key_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; @@ -8,20 +10,21 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mxc_ui/mxc_ui.dart'; import 'package:qr_flutter/qr_flutter.dart'; +import '../../../presentation/settings_page_presenter.dart'; import '../qr_scanner/qr_scanner_page.dart'; class QrCodePage extends HookConsumerWidget { - const QrCodePage({ - Key? key, - this.name, - this.address, - }) : super(key: key); + const QrCodePage( + {Key? key, this.name, this.address, required this.privateKey}) + : super(key: key); final String? name; final String? address; + final String privateKey; @override Widget build(BuildContext context, WidgetRef ref) { + final presenter = ref.read(settingsContainer.actions); String translate(String text) => FlutterI18n.translate(context, text); return MxcPage( @@ -68,6 +71,25 @@ class QrCodePage extends HookConsumerWidget { ], ), ), + MxcButton.secondary( + key: const ValueKey('viewPrivateKeyButton'), + title: FlutterI18n.translate(context, 'view_private_key'), + onTap: () => Navigator.of(context) + .push( + route.featureDialog( + const PasscodeAuthenticateUserPage( + dismissedDest: 'QrCodePage', + )), + ) + .then((value) { + if (value == true) { + showViewPrivateKeyDialog( + context: context, + privateKey: privateKey, + onCopy: presenter.copyToClipboard); + } + }), + ), const SizedBox(height: Sizes.space5XLarge), MxcButton.primary( key: const ValueKey('scanQrCodeButton'), From 3af7afde051169d5ab93b65cb311bdc6e87f9aa5 Mon Sep 17 00:00:00 2001 From: reasje Date: Tue, 17 Oct 2023 16:40:56 +0330 Subject: [PATCH 16/26] feat: Added show private key dialog --- .../show_view_private_key_dialog.dart | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 lib/features/settings/subfeatures/accounts/show_view_private_key_dialog.dart diff --git a/lib/features/settings/subfeatures/accounts/show_view_private_key_dialog.dart b/lib/features/settings/subfeatures/accounts/show_view_private_key_dialog.dart new file mode 100644 index 00000000..78a25247 --- /dev/null +++ b/lib/features/settings/subfeatures/accounts/show_view_private_key_dialog.dart @@ -0,0 +1,63 @@ +import 'package:datadashwallet/common/common.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_i18n/flutter_i18n.dart'; +import 'package:mxc_ui/mxc_ui.dart'; + +void showViewPrivateKeyDialog( + {required BuildContext context, + required String privateKey, + required Function(String) onCopy}) { + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + useSafeArea: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) => Container( + padding: const EdgeInsets.only(left: 16, right: 16, top: 0, bottom: 44), + decoration: BoxDecoration( + color: ColorsTheme.of(context).screenBackground, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + MxcAppBarEvenly.title( + titleText: FlutterI18n.translate(context, 'private_key'), + action: Container( + alignment: Alignment.centerRight, + child: InkWell( + child: const Icon(Icons.close), + onTap: () => Navigator.of(context).pop(false), + ), + ), + ), + SingleLineInfoItem( + title: 'private_key', + value: privateKey, + valueActionIcon: IconButton( + icon: Icon( + MxcIcons.copy, + size: 20, + color: ColorsTheme.of(context).iconGrey1, + ), + onPressed: () { + onCopy(privateKey); + Navigator.of(context).pop(); + }), + ), + const SizedBox(height: Sizes.spaceXSmall), + MxcButton.primary( + key: const ValueKey('doneButton'), + title: FlutterI18n.translate(context, 'done'), + onTap: () => Navigator.of(context).pop(false), + size: AxsButtonSize.xl, + ), + ], + ), + ), + ); +} From 91bdf2b1a6bd2bc4002bd27f89e5c3390dd10635 Mon Sep 17 00:00:00 2001 From: reasje Date: Tue, 17 Oct 2023 16:41:53 +0330 Subject: [PATCH 17/26] feat: Updated show accounts dialog & added import button --- .../account_managment_panel.dart | 27 ++-- .../accounts/show_accounts_dialog.dart | 120 ++++++++++-------- 2 files changed, 81 insertions(+), 66 deletions(-) diff --git a/lib/features/settings/presentation/widgets/account_managment/account_managment_panel.dart b/lib/features/settings/presentation/widgets/account_managment/account_managment_panel.dart index 00fd07f4..a65dab97 100644 --- a/lib/features/settings/presentation/widgets/account_managment/account_managment_panel.dart +++ b/lib/features/settings/presentation/widgets/account_managment/account_managment_panel.dart @@ -8,7 +8,6 @@ import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mxc_ui/mxc_ui.dart'; -import '../../../subfeatures/accounts/show_add_accounts_dialog.dart'; import 'copyable_item.dart'; class AccountManagementPanel extends HookConsumerWidget { @@ -32,20 +31,19 @@ class AccountManagementPanel extends HookConsumerWidget { children: [ InkWell( onTap: () => showAccountsDialog( - context: context, - currentAccount: state.account!, - accounts: state.accounts, - isLoading: state.isLoading, - onAdd: () => showAddAccountsDialog( - context: context, - isLoading: state.isLoading, - onAdd: presenter.addNewAccount, - onImport: () => Navigator.of(context).push( - route.featureDialog( - const ImportAccountPage(), + context: context, + currentAccount: state.account!, + accounts: state.accounts, + isLoading: state.isLoading, + onImport: () => Navigator.of(context).push( + route.featureDialog( + const ImportAccountPage(), + ), ), - ),), - onSelect: (item) => presenter.changeAccount(item)), + onAdd: () => presenter.addNewAccount(), + onSelect: (item) => presenter.changeAccount(item), + onRemove: (item) => presenter.removeAccount(item), + ), child: Row( children: [ Portrait( @@ -95,6 +93,7 @@ class AccountManagementPanel extends HookConsumerWidget { onTap: () => Navigator.of(context).push(route(QrCodePage( name: account.mns, address: account.address, + privateKey: state.account?.privateKey ?? '', ))), child: Container( padding: const EdgeInsets.all(Sizes.spaceXSmall), diff --git a/lib/features/settings/subfeatures/accounts/show_accounts_dialog.dart b/lib/features/settings/subfeatures/accounts/show_accounts_dialog.dart index 95db2eb7..c321eb4e 100644 --- a/lib/features/settings/subfeatures/accounts/show_accounts_dialog.dart +++ b/lib/features/settings/subfeatures/accounts/show_accounts_dialog.dart @@ -1,72 +1,88 @@ import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:mxc_logic/mxc_logic.dart'; import 'package:mxc_ui/mxc_ui.dart'; import 'account_item.dart'; -void showAccountsDialog({ - required BuildContext context, - required Account currentAccount, - required List accounts, - bool isLoading = false, - VoidCallback? onAdd, - VoidCallback? onImport, - required Function(Account) onSelect, -}) { +void showAccountsDialog( + {required BuildContext context, + required Account currentAccount, + required List accounts, + bool isLoading = false, + VoidCallback? onAdd, + VoidCallback? onImport, + required Function(Account) onSelect, + required Function(Account) onRemove}) { showModalBottomSheet( context: context, useRootNavigator: true, isScrollControlled: true, useSafeArea: true, backgroundColor: Colors.transparent, - builder: (BuildContext context) => Container( - padding: const EdgeInsets.only(left: 16, right: 16, top: 0, bottom: 44), - decoration: BoxDecoration( - color: ColorsTheme.of(context).screenBackground, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), + builder: (BuildContext context) => ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.95, ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - MxcAppBarEvenly.title( - titleText: FlutterI18n.translate(context, 'accounts'), - action: Container( - alignment: Alignment.centerRight, - child: InkWell( - child: const Icon(Icons.close), - onTap: () => Navigator.of(context).pop(false), + child: Container( + padding: const EdgeInsets.only( + left: 16, right: 16, top: 0, bottom: Sizes.space3XLarge), + decoration: BoxDecoration( + color: ColorsTheme.of(context).screenBackground, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + MxcAppBarEvenly.title( + titleText: FlutterI18n.translate(context, 'accounts'), + action: Container( + alignment: Alignment.centerRight, + child: InkWell( + child: const Icon(Icons.close), + onTap: () => Navigator.of(context).pop(false), + ), ), ), - ), - ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 400), - child: ListView.builder( - padding: EdgeInsets.zero, - itemCount: accounts.length, - shrinkWrap: true, - itemBuilder: (ctx, index) { - return AccountItem( - account: accounts[index], - isSelected: currentAccount.address == accounts[index].address, - onSelect: () => onSelect(accounts[index]), - isCustom: accounts[index].isCustom, - ); - }, + Flexible( + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: accounts.length, + shrinkWrap: true, + itemBuilder: (ctx, index) { + return AccountItem( + account: accounts[index], + isSelected: + currentAccount.address == accounts[index].address, + onSelect: () => onSelect(accounts[index]), + isCustom: accounts[index].isCustom, + onRemove: onRemove, + ); + }, + ), ), - ), - const SizedBox(height: Sizes.spaceXSmall), - MxcButton.primary( - key: const ValueKey('addAccountButton'), - title: FlutterI18n.translate( - context, isLoading ? 'adding_account' : 'add_new_account'), - onTap: onAdd, - size: AxsButtonSize.xl, - ), - ], + const SizedBox(height: Sizes.spaceXSmall), + MxcButton.primary( + key: const ValueKey('addAccountButton'), + title: FlutterI18n.translate( + context, isLoading ? 'adding_account' : 'add_new_account'), + onTap: onAdd, + size: AxsButtonSize.xl, + ), + const SizedBox(height: Sizes.spaceXSmall), + MxcButton.plainWhite( + key: const ValueKey('importAccountButton'), + title: FlutterI18n.translate(context, 'import_account'), + onTap: onImport, + size: AxsButtonSize.xl, + titleColor: ColorsTheme.of(context).primary, + ), + ], + ), ), ), ); From 197143fb3b1f2bca69b75890f13abf066c67b622 Mon Sep 17 00:00:00 2001 From: reasje Date: Tue, 17 Oct 2023 17:33:49 +0330 Subject: [PATCH 18/26] fix: UI fixes on qr code view private key button --- .../subfeatures/qr_code/show_qa_code/qr_code_page.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/features/settings/subfeatures/qr_code/show_qa_code/qr_code_page.dart b/lib/features/settings/subfeatures/qr_code/show_qa_code/qr_code_page.dart index b2a7c649..685b6fc1 100644 --- a/lib/features/settings/subfeatures/qr_code/show_qa_code/qr_code_page.dart +++ b/lib/features/settings/subfeatures/qr_code/show_qa_code/qr_code_page.dart @@ -71,6 +71,9 @@ class QrCodePage extends HookConsumerWidget { ], ), ), + const SizedBox( + height: Sizes.space6XLarge, + ), MxcButton.secondary( key: const ValueKey('viewPrivateKeyButton'), title: FlutterI18n.translate(context, 'view_private_key'), @@ -90,7 +93,7 @@ class QrCodePage extends HookConsumerWidget { } }), ), - const SizedBox(height: Sizes.space5XLarge), + const SizedBox(height: Sizes.spaceNormal), MxcButton.primary( key: const ValueKey('scanQrCodeButton'), title: FlutterI18n.translate(context, 'scan_qr_code'), From 655b25ff6d24503acd3522ab92a595d16de61469 Mon Sep 17 00:00:00 2001 From: reasje Date: Wed, 18 Oct 2023 10:00:31 +0330 Subject: [PATCH 19/26] fix: Imported accounts numbering & separation --- .../widgets/account_managment/account_managment_panel.dart | 2 +- .../import_account_page.dart | 0 .../import_account_presenter.dart | 2 +- .../import_account_state.dart | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename lib/features/settings/subfeatures/accounts/subfeatures/{account_details => import_account}/import_account_page.dart (100%) rename lib/features/settings/subfeatures/accounts/subfeatures/{account_details => import_account}/import_account_presenter.dart (96%) rename lib/features/settings/subfeatures/accounts/subfeatures/{account_details => import_account}/import_account_state.dart (100%) diff --git a/lib/features/settings/presentation/widgets/account_managment/account_managment_panel.dart b/lib/features/settings/presentation/widgets/account_managment/account_managment_panel.dart index a65dab97..1ce94b5e 100644 --- a/lib/features/settings/presentation/widgets/account_managment/account_managment_panel.dart +++ b/lib/features/settings/presentation/widgets/account_managment/account_managment_panel.dart @@ -2,7 +2,7 @@ import 'package:datadashwallet/common/common.dart'; import 'package:datadashwallet/core/core.dart'; import 'package:datadashwallet/features/settings/settings.dart'; import 'package:datadashwallet/features/settings/subfeatures/accounts/show_accounts_dialog.dart'; -import 'package:datadashwallet/features/settings/subfeatures/accounts/subfeatures/account_details/import_account_page.dart'; +import 'package:datadashwallet/features/settings/subfeatures/accounts/subfeatures/import_account/import_account_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; diff --git a/lib/features/settings/subfeatures/accounts/subfeatures/account_details/import_account_page.dart b/lib/features/settings/subfeatures/accounts/subfeatures/import_account/import_account_page.dart similarity index 100% rename from lib/features/settings/subfeatures/accounts/subfeatures/account_details/import_account_page.dart rename to lib/features/settings/subfeatures/accounts/subfeatures/import_account/import_account_page.dart diff --git a/lib/features/settings/subfeatures/accounts/subfeatures/account_details/import_account_presenter.dart b/lib/features/settings/subfeatures/accounts/subfeatures/import_account/import_account_presenter.dart similarity index 96% rename from lib/features/settings/subfeatures/accounts/subfeatures/account_details/import_account_presenter.dart rename to lib/features/settings/subfeatures/accounts/subfeatures/import_account/import_account_presenter.dart index 7d7001e2..b8f807ed 100644 --- a/lib/features/settings/subfeatures/accounts/subfeatures/account_details/import_account_presenter.dart +++ b/lib/features/settings/subfeatures/accounts/subfeatures/import_account/import_account_presenter.dart @@ -21,7 +21,7 @@ class ImportAccountPresenter extends CompletePresenter { void onSave() async { loading = true; try { - final index = _accountUserCase.findAccountsLastIndex(); + final index = _accountUserCase.accounts.value.length; final privateKey = privateKeyController.text; final newAccount = diff --git a/lib/features/settings/subfeatures/accounts/subfeatures/account_details/import_account_state.dart b/lib/features/settings/subfeatures/accounts/subfeatures/import_account/import_account_state.dart similarity index 100% rename from lib/features/settings/subfeatures/accounts/subfeatures/account_details/import_account_state.dart rename to lib/features/settings/subfeatures/accounts/subfeatures/import_account/import_account_state.dart From 187f634cc648f49244f9bdcf0fba3aaa78784f97 Mon Sep 17 00:00:00 2001 From: reasje Date: Wed, 18 Oct 2023 10:01:20 +0330 Subject: [PATCH 20/26] fix: Added accounts numbering & separation --- .../common/account/account_cache_repository.dart | 13 ++++++++++++- lib/features/common/account/account_use_case.dart | 6 +++--- .../presentation/settings_page_presenter.dart | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/features/common/account/account_cache_repository.dart b/lib/features/common/account/account_cache_repository.dart index 7f9267fb..08f9ec6b 100644 --- a/lib/features/common/account/account_cache_repository.dart +++ b/lib/features/common/account/account_cache_repository.dart @@ -54,9 +54,19 @@ class AccountCacheRepository extends GlobalCacheRepository { List get accountItems => accounts.value; Account get accountItem => account.value!; - void addAccount(Account item) => accounts.value = [...accounts.value, item]; + void addAccount(Account item, {int? index}) { + if (index == null) { + accounts.value = [...accounts.value, item]; + } else { + final newList = accounts.value; + newList.insert(index, item); + accounts.value = newList; + } + } + void removeAccount(Account item) => accounts.value = accounts.value.where((e) => e.address != item.address).toList(); + void updateAccount(Account item) => accounts.value = accounts.value.map((e) { if (item.address == account.value!.address) { account.value = item; @@ -67,6 +77,7 @@ class AccountCacheRepository extends GlobalCacheRepository { } return e; }).toList(); + void resetAccounts() => accounts.value = []; void setXsdConversionRate(double value) => xsdConversionRate.value = value; diff --git a/lib/features/common/account/account_use_case.dart b/lib/features/common/account/account_use_case.dart index 3b6dae30..d9696f8d 100644 --- a/lib/features/common/account/account_use_case.dart +++ b/lib/features/common/account/account_use_case.dart @@ -32,8 +32,8 @@ class AccountUseCase extends ReactiveUseCase { update(account, item); } - void addAccount(Account item) async { - _accountCacheRepository.addAccount(item); + void addAccount(Account item, {int? index}) async { + _accountCacheRepository.addAccount(item, index: index); final items = _accountCacheRepository.accountItems; update(account, item); update(accounts, items); @@ -47,7 +47,7 @@ class AccountUseCase extends ReactiveUseCase { final items = _accountCacheRepository.accountItems; update(accounts, items); } - + bool isAccountSelected(Account item) { return (item.address == account.value!.address); } diff --git a/lib/features/settings/presentation/settings_page_presenter.dart b/lib/features/settings/presentation/settings_page_presenter.dart index 2c96bf35..61789a22 100644 --- a/lib/features/settings/presentation/settings_page_presenter.dart +++ b/lib/features/settings/presentation/settings_page_presenter.dart @@ -57,7 +57,7 @@ class SettingsPresenter extends CompletePresenter { final index = _accountUserCase.findAccountsLastIndex(); final newAccount = await _authUseCase.addNewAccount(index); - _accountUserCase.addAccount(newAccount); + _accountUserCase.addAccount(newAccount, index: index); loadCache(); notify(() => state.isLoading = false); From 49327f75667511d541949244de09f0578811e304 Mon Sep 17 00:00:00 2001 From: reasje Date: Wed, 18 Oct 2023 14:06:00 +0330 Subject: [PATCH 21/26] fix: Added duplicate account check in import --- assets/flutter_i18n/en.json | 3 ++- .../import_account/import_account_page.dart | 8 +++++-- .../import_account_presenter.dart | 23 ++++++++++++++++++- .../import_account/import_account_state.dart | 3 ++- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/assets/flutter_i18n/en.json b/assets/flutter_i18n/en.json index 334f3b21..97bf7f83 100644 --- a/assets/flutter_i18n/en.json +++ b/assets/flutter_i18n/en.json @@ -298,5 +298,6 @@ "view_private_key_notice": "Warning: Never disclose this key. Anyone with your private keys can steal any assets held in your account.", "removing_account": "Removing account", "removing_account_warning": "Are you sure you want to remove this account?", - "remove": "Remove" + "remove": "Remove", + "duplicate_account_import_notice": "The account you are trying to import is a duplicate" } \ No newline at end of file diff --git a/lib/features/settings/subfeatures/accounts/subfeatures/import_account/import_account_page.dart b/lib/features/settings/subfeatures/accounts/subfeatures/import_account/import_account_page.dart index c3c9750b..282c9de7 100644 --- a/lib/features/settings/subfeatures/accounts/subfeatures/import_account/import_account_page.dart +++ b/lib/features/settings/subfeatures/accounts/subfeatures/import_account/import_account_page.dart @@ -49,8 +49,12 @@ class ImportAccountPage extends HookConsumerWidget { translate('x_not_empty') .replaceFirst('{0}', translate('private_key'))); if (res != null) return res; - return Validation.checkEthereumPrivateKey( - context, value ?? ''); + + final isPrivateKey = + Validation.checkEthereumPrivateKey(context, value ?? ''); + if (isPrivateKey != null) return isPrivateKey; + + return presenter.checkDuplicate(value ?? ''); }, onChanged: (value) { presenter.changeAbleToSave( diff --git a/lib/features/settings/subfeatures/accounts/subfeatures/import_account/import_account_presenter.dart b/lib/features/settings/subfeatures/accounts/subfeatures/import_account/import_account_presenter.dart index b8f807ed..7854ee7d 100644 --- a/lib/features/settings/subfeatures/accounts/subfeatures/import_account/import_account_presenter.dart +++ b/lib/features/settings/subfeatures/accounts/subfeatures/import_account/import_account_presenter.dart @@ -18,10 +18,19 @@ class ImportAccountPresenter extends CompletePresenter { final TextEditingController privateKeyController = TextEditingController(); + @override + void initState() { + super.initState(); + + listen(_accountUserCase.accounts, (value) { + notify(() => state.accounts = value); + }); + } + void onSave() async { loading = true; try { - final index = _accountUserCase.accounts.value.length; + final index = state.accounts.length; final privateKey = privateKeyController.text; final newAccount = @@ -41,6 +50,18 @@ class ImportAccountPresenter extends CompletePresenter { } } + String? checkDuplicate(String privateKey) { + if (privateKey.isEmpty) return translate('invalid_format'); + + final foundIndex = state.accounts + .indexWhere((element) => element.privateKey == privateKey); + + if (foundIndex != -1) { + return translate('duplicate_account_import_notice')!; + } + return null; + } + void changeAbleToSave(bool value) { notify(() => state.ableToSave = value); } diff --git a/lib/features/settings/subfeatures/accounts/subfeatures/import_account/import_account_state.dart b/lib/features/settings/subfeatures/accounts/subfeatures/import_account/import_account_state.dart index 6052c416..62dba5ba 100644 --- a/lib/features/settings/subfeatures/accounts/subfeatures/import_account/import_account_state.dart +++ b/lib/features/settings/subfeatures/accounts/subfeatures/import_account/import_account_state.dart @@ -4,7 +4,8 @@ import 'package:mxc_logic/mxc_logic.dart'; class ImportAccountState with EquatableMixin { bool ableToSave = false; bool isLoading = false; + List accounts = []; @override - List get props => [ableToSave, isLoading]; + List get props => [ableToSave, isLoading, accounts]; } From 2cf1d2e1b2d4495929399d4f6f4047d28e14d880 Mon Sep 17 00:00:00 2001 From: reasje Date: Thu, 19 Oct 2023 10:39:10 +0330 Subject: [PATCH 22/26] feat: Added utils with email app launch & check --- lib/common/utils/utils.dart | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/common/utils/utils.dart b/lib/common/utils/utils.dart index 069b264f..dcc80ba4 100644 --- a/lib/common/utils/utils.dart +++ b/lib/common/utils/utils.dart @@ -1,3 +1,24 @@ +import 'package:datadashwallet/common/common.dart'; +import 'package:url_launcher/url_launcher.dart'; + export 'formatter.dart'; export 'permission.dart'; export 'validation.dart'; + +class Utils { + static Future isEmailAppAvailable() async { + final url = Uri.parse(Urls.emailApp); + + return await canLaunchUrl(url); + } + + static Future launchEmailApp() async { + final url = Uri.parse(Urls.emailApp); + + if (await canLaunchUrl(url)) { + await launchUrl(url); + } else { + throw 'unable_to_launch_email_app'; + } + } +} From bafa7ec465bc484db6757c3b0a513291e0ecefb5 Mon Sep 17 00:00:00 2001 From: reasje Date: Thu, 19 Oct 2023 10:39:31 +0330 Subject: [PATCH 23/26] feat: Added email app url --- lib/common/urls.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/common/urls.dart b/lib/common/urls.dart index 2d98a0af..ef9d7ea2 100644 --- a/lib/common/urls.dart +++ b/lib/common/urls.dart @@ -8,4 +8,6 @@ class Urls { static const String iOSUrl = 'https://apps.apple.com/us/app/axs-decentralized-wallet/id6460891587'; + + static const String emailApp = 'mailto:'; } From 16bb77b6b826af82ef760eb23ee28fa76789945d Mon Sep 17 00:00:00 2001 From: reasje Date: Thu, 19 Oct 2023 10:44:17 +0330 Subject: [PATCH 24/26] fix: Disable email button if there is no email app --- .../presentation/create_storage_page.dart | 16 +++++++++------- .../presentation/create_storage_presenter.dart | 2 +- .../splash_base/splash_base_presenter.dart | 9 ++++----- .../splash/splash_base/splash_base_state.dart | 5 +++-- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/lib/features/splash/create_storage/presentation/create_storage_page.dart b/lib/features/splash/create_storage/presentation/create_storage_page.dart index 856e17cd..c374630e 100644 --- a/lib/features/splash/create_storage/presentation/create_storage_page.dart +++ b/lib/features/splash/create_storage/presentation/create_storage_page.dart @@ -65,13 +65,15 @@ class SplashStoragePage extends SplashBasePage { icon: MxcIcons.email, iconSize: 18, title: FlutterI18n.translate(context, 'email_secured_storage'), - onTap: () => Navigator.of(context).push( - route.featureDialog( - EmailRecoveryPhrasePage( - settingsFlow: settingsFlow, - ), - ), - ), + onTap: ref.watch(state).isEmailAppAvailable == true + ? () => Navigator.of(context).push( + route.featureDialog( + EmailRecoveryPhrasePage( + settingsFlow: settingsFlow, + ), + ), + ) + : null, ), ]; } diff --git a/lib/features/splash/create_storage/presentation/create_storage_presenter.dart b/lib/features/splash/create_storage/presentation/create_storage_presenter.dart index 677a2624..60012c93 100644 --- a/lib/features/splash/create_storage/presentation/create_storage_presenter.dart +++ b/lib/features/splash/create_storage/presentation/create_storage_presenter.dart @@ -11,7 +11,7 @@ class SplashStoragePresenter extends SplashBasePresenter { @override void initState() { super.initState(); - isInstallApps(); + checkEmailAppAvailability(); } } diff --git a/lib/features/splash/splash_base/splash_base_presenter.dart b/lib/features/splash/splash_base/splash_base_presenter.dart index 910438c3..885b9f56 100644 --- a/lib/features/splash/splash_base/splash_base_presenter.dart +++ b/lib/features/splash/splash_base/splash_base_presenter.dart @@ -1,3 +1,4 @@ +import '../../../common/common.dart'; import '../../../core/core.dart'; import 'package:appinio_social_share/appinio_social_share.dart'; import 'package:flutter_mailer/flutter_mailer.dart'; @@ -16,11 +17,9 @@ abstract class SplashBasePresenter notify(() => state.applist = applist); } - Future isInstallEmail() async { - final result = await FlutterMailer.canSendMail() || - await FlutterMailer.isAppInstalled('mailto:'); - - notify(() => state.isInstallEmail = result); + void checkEmailAppAvailability() async { + final isEmailAppAvailable = await Utils.isEmailAppAvailable(); + notify(() => state.isEmailAppAvailable = isEmailAppAvailable); } } diff --git a/lib/features/splash/splash_base/splash_base_state.dart b/lib/features/splash/splash_base/splash_base_state.dart index 84117503..719b6294 100644 --- a/lib/features/splash/splash_base/splash_base_state.dart +++ b/lib/features/splash/splash_base/splash_base_state.dart @@ -2,10 +2,11 @@ import 'package:equatable/equatable.dart'; class SplashBaseState with EquatableMixin { Map applist = {}; - bool isInstallEmail = false; + bool isEmailAppAvailable = false; bool animate = false; @override - List get props => [applist, isInstallEmail, animate]; + List get props => + [applist, animate, isEmailAppAvailable]; } From 6a4a74db7729d91b2185c2bfe3192431a01aecb1 Mon Sep 17 00:00:00 2001 From: reasje Date: Thu, 19 Oct 2023 10:46:00 +0330 Subject: [PATCH 25/26] fix: Launch email app if not logged in IOS --- assets/flutter_i18n/en.json | 3 ++- .../recovery_phrase_base_presenter.dart | 24 +++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/assets/flutter_i18n/en.json b/assets/flutter_i18n/en.json index 97bf7f83..0add92cf 100644 --- a/assets/flutter_i18n/en.json +++ b/assets/flutter_i18n/en.json @@ -299,5 +299,6 @@ "removing_account": "Removing account", "removing_account_warning": "Are you sure you want to remove this account?", "remove": "Remove", - "duplicate_account_import_notice": "The account you are trying to import is a duplicate" + "duplicate_account_import_notice": "The account you are trying to import is a duplicate", + "unable_to_launch_email_app": "Unable to launch email app" } \ No newline at end of file diff --git a/lib/features/splash/secure_recovery_phrase/presentation/recovery_phrase_base/recovery_phrase_base_presenter.dart b/lib/features/splash/secure_recovery_phrase/presentation/recovery_phrase_base/recovery_phrase_base_presenter.dart index 6a46efc4..742a1756 100644 --- a/lib/features/splash/secure_recovery_phrase/presentation/recovery_phrase_base/recovery_phrase_base_presenter.dart +++ b/lib/features/splash/secure_recovery_phrase/presentation/recovery_phrase_base/recovery_phrase_base_presenter.dart @@ -114,15 +114,25 @@ abstract class RecoveryPhraseBasePresenter ); try { - MailerResponse sendResult = await FlutterMailer.send(email); - // only [ios] can return sent | saved | cancelled - // [android] will return android there is no way of knowing on android - // if the intent was sent saved or even cancelled. - if (MailerResponse.cancelled != sendResult) { - nextProcess(settingsFlow, res['phrases']); + bool canSend = await FlutterMailer.canSendMail(); + + if (Platform.isIOS && !canSend) { + await Utils.launchEmailApp(); + } else { + MailerResponse sendResult = await FlutterMailer.send(email); + // only [ios] can return sent | saved | cancelled + // [android] will return android there is no way of knowing on android + // if the intent was sent saved or even cancelled. + if (MailerResponse.cancelled != sendResult) { + nextProcess(settingsFlow, res['phrases']); + } } } catch (e, s) { - addError(e, s); + if (e == 'unable_to_launch_email_app') { + addError(translate('unable_to_launch_email_app')); + } else { + addError(e, s); + } } } } From a60f3cf516b8e565270a3959e8eb9f84efeff757 Mon Sep 17 00:00:00 2001 From: reasje Date: Thu, 19 Oct 2023 12:45:48 +0330 Subject: [PATCH 26/26] feat: Updated app version to 1.5.2 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 6b6573db..d824f12b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.4.0 +version: 1.5.2 environment: sdk: ">=2.19.0 <3.0.0"