diff --git a/app/lib/l10n/arb/app_de.arb b/app/lib/l10n/arb/app_de.arb index 5abcc049f..1b1c453cd 100644 --- a/app/lib/l10n/arb/app_de.arb +++ b/app/lib/l10n/arb/app_de.arb @@ -21,6 +21,7 @@ "addToContactFromQrContact": "Kontakt per Qr hinzufügen", "alreadyEndorsedErrorBody": "Dieses Konto wurde bereits für diesen Key-Signing Cycle endorsed.", "alreadyEndorsedErrorTitle": "Bereits endorsed", + "authenticationNeeded": "Authentifizierung notwendig", "amountError": "Ungültiger Betrag", "amountToBeTransferred": "Betrag", "appSettings": "App-Einstellungen", diff --git a/app/lib/l10n/arb/app_en.arb b/app/lib/l10n/arb/app_en.arb index 54eabc695..039d3e7ba 100644 --- a/app/lib/l10n/arb/app_en.arb +++ b/app/lib/l10n/arb/app_en.arb @@ -21,6 +21,7 @@ "addToContactFromQrContact": "Add Contact-Qr", "alreadyEndorsedErrorBody": "This account has already been endorsed for this cycle.", "alreadyEndorsedErrorTitle": "Already Endorsed", + "authenticationNeeded": "Authentication Needed", "amountError": "Invalid amount", "amountToBeTransferred": "Send amount", "appSettings": "App settings", diff --git a/app/lib/l10n/arb/app_fr.arb b/app/lib/l10n/arb/app_fr.arb index fa3c6eb14..67241aeb1 100644 --- a/app/lib/l10n/arb/app_fr.arb +++ b/app/lib/l10n/arb/app_fr.arb @@ -21,6 +21,7 @@ "addToContactFromQrContact": "Add Contact-Qr", "alreadyEndorsedErrorBody": "Ce compte a déjà été endossé pour ce cycle", "alreadyEndorsedErrorTitle": "Déjà endossé", + "authenticationNeeded": "Authentification nécessaire", "amountError": "Solde non valide", "amountToBeTransferred": "Montant", "appSettings": "Paramètres de l'app", diff --git a/app/lib/l10n/arb/app_ru.arb b/app/lib/l10n/arb/app_ru.arb index 9e688e68f..68de9f931 100644 --- a/app/lib/l10n/arb/app_ru.arb +++ b/app/lib/l10n/arb/app_ru.arb @@ -21,6 +21,7 @@ "addToContactFromQrContact": "Add Contact-Qr", "alreadyEndorsedErrorBody": "Этот аккаунт уже был подтвержден в этом цикле.", "alreadyEndorsedErrorTitle": "Уже подтверждено", + "authenticationNeeded": "Необходима аутентификация", "amountError": "Недопустимая сумма", "amountToBeTransferred": "Отправить сумму", "appSettings": "Настройки приложения", diff --git a/app/lib/l10n/arb/app_sw.arb b/app/lib/l10n/arb/app_sw.arb index ba7a0dfca..4d71c5b85 100644 --- a/app/lib/l10n/arb/app_sw.arb +++ b/app/lib/l10n/arb/app_sw.arb @@ -21,6 +21,7 @@ "addToContactFromQrContact": "Ongeza Qr ya mawasiliano", "alreadyEndorsedErrorBody": "Akaunti hii tayari imeidhinishwa kwa ajili ya mzunguko huu", "alreadyEndorsedErrorTitle": "imeidhinishwa tayari", + "authenticationNeeded": "Uthibitishaji Unahitajika", "amountError": "Kiasi kisichokubalika", "amountToBeTransferred": "Tuma kiasi", "appSettings": "Mipangilio ya app", diff --git a/app/lib/modules/account/view/add_account_view.dart b/app/lib/modules/account/view/add_account_view.dart index 94c6e2b7a..5271a329f 100644 --- a/app/lib/modules/account/view/add_account_view.dart +++ b/app/lib/modules/account/view/add_account_view.dart @@ -1,3 +1,4 @@ +import 'package:encointer_wallet/utils/snack_bar.dart'; import 'package:ew_test_keys/ew_test_keys.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -108,14 +109,16 @@ class AddAccountForm extends StatelessWidget with HandleNewAccountResultMixin { final newAccount = context.read(); if (_formKey.currentState!.validate() && !newAccount.loading) { newAccount.setName(_nameCtrl.text.trim()); - final pin = await context.read().getPin(context); - if (pin != null) { + final authenticated = await context.read().ensureAuthenticated(context); + if (authenticated) { final res = await newAccount.generateAccount(); await navigate( context: context, type: res.operationResult, onOk: () => Navigator.of(context).popUntil((route) => route.isFirst), ); + } else { + RootSnackBar.showMsg(context.l10n.authenticationNeeded); } } }, diff --git a/app/lib/modules/account/view/import_account_view.dart b/app/lib/modules/account/view/import_account_view.dart index c001b6749..c34e048f6 100644 --- a/app/lib/modules/account/view/import_account_view.dart +++ b/app/lib/modules/account/view/import_account_view.dart @@ -1,3 +1,4 @@ +import 'package:encointer_wallet/utils/snack_bar.dart'; import 'package:ew_test_keys/ew_test_keys.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -118,8 +119,8 @@ class ImportAccountForm extends StatelessWidget with HandleNewAccountResultMixin ), ); } else { - final pin = await context.read().getPin(context); - if (pin != null) { + final authenticated = await context.read().ensureAuthenticated(context); + if (authenticated) { final res = await newAccount.importAccount(); await navigate( context: context, @@ -127,6 +128,8 @@ class ImportAccountForm extends StatelessWidget with HandleNewAccountResultMixin onOk: () => Navigator.of(context).popUntil((route) => route.isFirst), onDuplicateAccount: () => _onDuplicateAccount(context, res.duplicateAccountData), ); + } else { + RootSnackBar.showMsg(l10n.authenticationNeeded); } } } diff --git a/app/lib/modules/login/logic/login_store.dart b/app/lib/modules/login/logic/login_store.dart index 3af17a5d6..6a6b7f2c0 100644 --- a/app/lib/modules/login/logic/login_store.dart +++ b/app/lib/modules/login/logic/login_store.dart @@ -24,13 +24,13 @@ abstract class _LoginStoreBase with Store { @observable bool loading = false; - FutureOr getPin(BuildContext context) async { - if (cachedPin != null) return cachedPin!; - await LoginDialog.verifyPinOrBioAuth( - context, - onSuccess: (v) async => cachedPin = await loginService.getPin(), - ); - return cachedPin; + /// If the user has already authenticated this session, true will be returned. + /// if not, the user will be asked to authenticate with PIN or biometric depending on the + /// settings. + FutureOr ensureAuthenticated(BuildContext context) async { + if (cachedPin != null) return true; + await LoginDialog.verifyPinOrBioAuth(context); + return cachedPin != null; } /// Persists the new PIN in the secure storage. diff --git a/app/lib/modules/login/view/login_dialog.dart b/app/lib/modules/login/view/login_dialog.dart index e34bcafd9..6a35e44d5 100644 --- a/app/lib/modules/login/view/login_dialog.dart +++ b/app/lib/modules/login/view/login_dialog.dart @@ -39,21 +39,19 @@ final class LoginDialog { ? showLocalAuth( context, titleText: context.l10n.biometricAuthEnableDisableDescription, - onSuccess: (_) => context.read().setBiometricAuthState(BiometricAuthState.enabled), + onSuccess: () => context.read().setBiometricAuthState(BiometricAuthState.enabled), ) : showPasswordInputDialog( context, titleText: context.l10n.biometricAuthEnableDisableDescription, - onSuccess: (_) => context.read().setBiometricAuthState(BiometricAuthState.disabled), + onSuccess: () => context.read().setBiometricAuthState(BiometricAuthState.disabled), ); static Future verifyPinOrBioAuth( BuildContext context, { - required Future Function(String password) onSuccess, + Future Function()? onSuccess, bool barrierDismissible = true, - bool autoCloseOnSuccess = true, bool showCancelButton = true, - bool canPop = true, bool stickyAuth = false, String? titleText, }) async { @@ -72,9 +70,7 @@ final class LoginDialog { context, onSuccess: onSuccess, barrierDismissible: barrierDismissible, - autoCloseOnSuccess: autoCloseOnSuccess, showCancelButton: showCancelButton, - canPop: canPop, titleText: titleText, ); } @@ -83,9 +79,7 @@ final class LoginDialog { context, onSuccess: onSuccess, barrierDismissible: barrierDismissible, - autoCloseOnSuccess: autoCloseOnSuccess, showCancelButton: showCancelButton, - canPop: canPop, titleText: titleText, ); } @@ -93,14 +87,23 @@ final class LoginDialog { static Future showLocalAuth( BuildContext context, { - required Future Function(String password) onSuccess, + required Future Function()? onSuccess, String? titleText, bool stickyAuth = false, }) async { final loginStore = context.read(); try { - final value = await loginStore.localAuthenticate(titleText ?? context.l10n.verifyAuthTitle('true'), stickyAuth); - if (value) await onSuccess(loginStore.cachedPin ?? await loginStore.loginService.getPin() ?? ''); + final success = await loginStore.localAuthenticate(titleText ?? context.l10n.verifyAuthTitle('true'), stickyAuth); + + if (success) { + loginStore.cachedPin = await loginStore.loginService.getPin(); + + if (loginStore.cachedPin == null) { + throw Exception(['Pin retrieved from storage is null, fatal error']); + } + + if (onSuccess != null) await onSuccess(); + } } catch (e) { rethrow; } @@ -108,11 +111,9 @@ final class LoginDialog { static Future showPasswordInputDialog( BuildContext context, { - required Future Function(String password) onSuccess, + Future Function()? onSuccess, bool barrierDismissible = true, - bool autoCloseOnSuccess = true, bool showCancelButton = true, - bool canPop = true, String? titleText, }) async { final l10n = context.l10n; @@ -142,15 +143,17 @@ final class LoginDialog { child: Text(l10n.cancel), ), PopScope( - onPopInvoked: (bool didPop) async => canPop, child: CupertinoButton( key: const Key(EWTestKeys.passwordOk), onPressed: () async { loginStore.loading = true; - final value = await _onOk(context, passCtrl.text.trim()); - if (value) { - await onSuccess(passCtrl.text.trim()); - if (autoCloseOnSuccess) Navigator.of(context).pop(); + final success = await _checkPassword(context, passCtrl.text.trim()); + if (success) { + loginStore.cachedPin = passCtrl.text.trim(); + + if (onSuccess != null) await onSuccess(); + + Navigator.of(context).pop(); } else { AppAlert.showErrorDialog( context, @@ -170,7 +173,7 @@ final class LoginDialog { ); } - static Future _onOk(BuildContext context, String password) async { + static Future _checkPassword(BuildContext context, String password) async { final loginStore = context.read(); return loginStore.isValid(password); } diff --git a/app/lib/page/assets/transfer/payment_confirmation_page/index.dart b/app/lib/page/assets/transfer/payment_confirmation_page/index.dart index 2421d9e99..7c1e3581c 100644 --- a/app/lib/page/assets/transfer/payment_confirmation_page/index.dart +++ b/app/lib/page/assets/transfer/payment_confirmation_page/index.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:encointer_wallet/utils/snack_bar.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:iconsax/iconsax.dart'; @@ -141,8 +142,8 @@ class _PaymentConfirmationPageState extends State { Address recipientAddress, double amount, ) async { - final pin = await context.read().getPin(context); - if (pin != null) { + final authenticated = await context.read().ensureAuthenticated(context); + if (authenticated) { setState(() { _transferState = TransferState.submitting; }); @@ -164,6 +165,8 @@ class _PaymentConfirmationPageState extends State { Log.d('TransferState after callback: $_transferState', 'PaymentConfirmationPage'); // trigger rebuild after state update in callback setState(() {}); + } else { + RootSnackBar.showMsg(context.l10n.authenticationNeeded); } } diff --git a/app/lib/page/profile/account/account_manage_page.dart b/app/lib/page/profile/account/account_manage_page.dart index 3df4225c2..d8072a5b1 100644 --- a/app/lib/page/profile/account/account_manage_page.dart +++ b/app/lib/page/profile/account/account_manage_page.dart @@ -81,7 +81,7 @@ class _AccountManagePageState extends State { LoginDialog.verifyPinOrBioAuth( context, titleText: context.l10n.accountDelete, - onSuccess: (v) async { + onSuccess: () async { await _appStore.account .removeAccount(accountToBeEdited) .then((_) => _appStore.loadAccountCache()) @@ -142,9 +142,7 @@ class _AccountManagePageState extends State { await LoginDialog.verifyPinOrBioAuth( context, titleText: context.l10n.confirmPin, - autoCloseOnSuccess: false, - onSuccess: (password) async { - Navigator.of(context).pop(); + onSuccess: () async { final account = _appStore.account.getKeyringAccount(accountToBeEdited.pubKey); await Navigator.of(context).pushNamed(ExportResultPage.route, arguments: { 'key': account.uri, diff --git a/app/lib/page/profile/index.dart b/app/lib/page/profile/index.dart index c98cba489..b9e094a52 100644 --- a/app/lib/page/profile/index.dart +++ b/app/lib/page/profile/index.dart @@ -161,7 +161,7 @@ class _ProfileState extends State { LoginDialog.verifyPinOrBioAuth( context, titleText: l10n.accountsDelete, - onSuccess: (v) async { + onSuccess: () async { for (final acc in context.read().account.accountListAll) { await store.account.removeAccount(acc); } diff --git a/app/lib/service/tx/lib/src/submit_tx_wrappers.dart b/app/lib/service/tx/lib/src/submit_tx_wrappers.dart index ddc526145..62104f7e6 100644 --- a/app/lib/service/tx/lib/src/submit_tx_wrappers.dart +++ b/app/lib/service/tx/lib/src/submit_tx_wrappers.dart @@ -1,3 +1,4 @@ +import 'package:encointer_wallet/utils/snack_bar.dart'; import 'package:flutter/cupertino.dart'; import 'package:provider/provider.dart'; @@ -43,8 +44,8 @@ Future submitTx( dynamic Function(BuildContext txPageContext, ExtrinsicReport report)? onFinish, void Function(DispatchError report)? onError, }) async { - final pin = await context.read().getPin(context); - if (pin != null) { + final authenticated = await context.read().ensureAuthenticated(context); + if (authenticated) { return submitTxInner( context, store, @@ -54,6 +55,8 @@ Future submitTx( onError: onError, onFinish: onFinish, ); + } else { + RootSnackBar.showMsg(context.l10n.authenticationNeeded); } } diff --git a/app/test_driver/app/profile/profile_test.dart b/app/test_driver/app/profile/profile_test.dart index 76c29ad07..70cdac02d 100644 --- a/app/test_driver/app/profile/profile_test.dart +++ b/app/test_driver/app/profile/profile_test.dart @@ -21,7 +21,7 @@ Future getNextPhase(FlutterDriver driver) async { await tapNextPhase(driver); } -Future checkPeputationCount(FlutterDriver driver, int count) async { +Future checkReputationCount(FlutterDriver driver, int count) async { await driver.waitFor(find.text('$count')); } diff --git a/app/test_driver/app_test.dart b/app/test_driver/app_test.dart index 30fb4ed27..5b7c5d2c3 100644 --- a/app/test_driver/app_test.dart +++ b/app/test_driver/app_test.dart @@ -232,7 +232,7 @@ void main() async { test('Go to Profile Page and Check reputation count', () async { await goToProfileViewFromNavBar(driver); - await checkPeputationCount(driver, 2); + await checkReputationCount(driver, 2); }, timeout: timeout240); test('Get Registering phase', () async { @@ -253,7 +253,7 @@ void main() async { await sendEndorse(driver); }, timeout: timeout240); - test('send money to account from Bootstraper account', () async { + test('send money to account from Bootstrapper account', () async { await senMoneyToContact(driver); await sendMoneyToSelectedAccount(driver, '0.2'); await goToHomeViewFromNavBar(driver); @@ -270,7 +270,7 @@ void main() async { await verifyInputPin(driver); }, timeout: timeout240); - test('create niewbie Account', () async { + test('create newbie Account', () async { await goToHomeViewFromNavBar(driver); await goToAddAcoountViewFromPanel(driver); await createNewbieAccount(driver, 'Li'); @@ -353,7 +353,7 @@ void main() async { await verifyInputPin(driver); }, timeout: timeout240); - test('import account with menemonic phrase', () async { + test('import account with mnemonic phrase', () async { await goToHomeViewFromNavBar(driver); await goToAddAcoountViewFromPanel(driver); await importAccount(driver, 'Bob', menemonic);