feat: totp linux flow + fixes for wallet switching

This commit is contained in:
Rafael Saes 2023-08-25 18:28:33 -03:00
parent 2bce18d240
commit feb385835a
22 changed files with 566 additions and 761 deletions

View file

@ -1,6 +1,6 @@
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
import 'package:cake_wallet/core/authentication_request_data.dart';
import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart';
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/core/secure_storage.dart';
@ -10,8 +10,6 @@ import 'package:cake_wallet/entities/secret_store_key.dart';
import 'package:cake_wallet/entities/encrypt.dart';
import 'package:cake_wallet/store/settings_store.dart';
import '../src/screens/setup_2fa/setup_2fa_enter_code_page.dart';
class AuthService with Store {
AuthService({
required this.secureStorage,
@ -86,62 +84,55 @@ class AuthService with Store {
Future<void> authenticateAction(BuildContext context,
{Function(bool)? onAuthSuccess,
String? authRoute,
Object? authArguments,
String? route,
Object? arguments,
bool? alwaysRequireAuth,
required bool conditionToDetermineIfToUse2FA}) async {
assert(route != null || onAuthSuccess != null,
'Either route or onAuthSuccess param must be passed.');
if (!conditionToDetermineIfToUse2FA) {
if (!requireAuth() && !_alwaysAuthenticateRoutes.contains(route)) {
if (alwaysRequireAuth != true &&
!requireAuth() &&
!_alwaysAuthenticateRoutes.contains(route)) {
if (onAuthSuccess != null) {
onAuthSuccess(true);
} else {
Navigator.of(context).pushNamed(
route ?? '',
arguments: arguments,
);
}
return;
}
}
Navigator.of(context).pushNamed(Routes.auth,
arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) async {
if (!isAuthenticatedSuccessfully) {
onAuthSuccess?.call(false);
return;
} else {
if (settingsStore.useTOTP2FA && conditionToDetermineIfToUse2FA) {
auth.close(
route: Routes.totpAuthCodePage,
arguments: TotpAuthArgumentsModel(
isForSetup: !settingsStore.useTOTP2FA,
onTotpAuthenticationFinished:
(bool isAuthenticatedSuccessfully, TotpAuthCodePageState totpAuth) async {
if (!isAuthenticatedSuccessfully) {
onAuthSuccess?.call(false);
return;
}
if (onAuthSuccess != null) {
totpAuth.close().then((value) => onAuthSuccess.call(true));
} else {
totpAuth.close(route: route, arguments: arguments);
}
},
),
);
} else {
if (onAuthSuccess != null) {
auth.close().then((value) => onAuthSuccess.call(true));
} else {
auth.close(route: route, arguments: arguments);
}
Navigator.of(context).pushNamed(route ?? '', arguments: arguments);
}
}
}
});
Navigator.of(context).pushNamed(authRoute ?? Routes.auth,
arguments: authArguments ??
(SettingsStoreBase.walletPasswordDirectInput
? WalletUnlockArguments(
useTotp: conditionToDetermineIfToUse2FA,
callback: (AuthResponse auth) async {
if (!auth.success) {
onAuthSuccess?.call(false);
return;
}
if (onAuthSuccess != null) {
auth.close().then((value) => onAuthSuccess.call(true));
} else {
auth.close(route: route, arguments: arguments);
}
})
: (AuthResponse auth) {
if (!auth.success) {
onAuthSuccess?.call(false);
return;
}
if (onAuthSuccess != null) {
auth.close().then((value) => onAuthSuccess.call(true));
} else {
auth.close(route: route, arguments: arguments);
}
}));
}
}

View file

@ -0,0 +1,14 @@
typedef CloseAuth = Future<void> Function({String? route, dynamic arguments});
class AuthResponse {
AuthResponse({bool success = false, this.payload, required this.close, String? error})
: this.success = success,
this.error = success == false ? error ?? '' : null;
final bool success;
final dynamic payload;
final String? error;
final CloseAuth close;
}
typedef OnAuthenticationFinished = void Function(AuthResponse);

View file

@ -1,13 +1,27 @@
import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_enter_code_page.dart';
import 'package:cake_wallet/core/authentication_request_data.dart';
class TotpResponse {
TotpResponse({bool success = false, required this.close, String? error})
: this.success = success,
this.error = success == false ? error ?? '' : null;
final bool success;
final String? error;
final CloseAuth close;
}
typedef OnTotpAuthenticationFinished = void Function(TotpResponse);
class TotpAuthArgumentsModel {
final bool? isForSetup;
final bool? isClosable;
final bool? showPopup;
final OnTotpAuthenticationFinished? onTotpAuthenticationFinished;
TotpAuthArgumentsModel({
this.isForSetup,
this.isClosable,
this.showPopup,
this.onTotpAuthenticationFinished,
});
}

View file

@ -1,5 +1,5 @@
import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
import 'package:cake_wallet/core/authentication_request_data.dart';
import 'package:cake_wallet/core/yat_service.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
@ -7,7 +7,6 @@ import 'package:cake_wallet/anonpay/anonpay_api.dart';
import 'package:cake_wallet/anonpay/anonpay_info_base.dart';
import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/buy/payfura/payfura_buy_provider.dart';
import 'package:cake_wallet/core/yat_service.dart';
import 'package:cake_wallet/entities/background_tasks.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/receive_page_option.dart';
@ -22,10 +21,8 @@ import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet
import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart';
import 'package:cake_wallet/src/screens/dashboard/desktop_dashboard_page.dart';
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart';
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart';
import 'package:cake_wallet/src/screens/dashboard/edit_token_page.dart';
import 'package:cake_wallet/src/screens/dashboard/home_settings_page.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart';
import 'package:cake_wallet/src/screens/settings/display_settings_page.dart';
@ -299,8 +296,7 @@ Future setup({
getIt.registerSingleton<ExchangeTemplateStore>(
ExchangeTemplateStore(templateSource: _exchangeTemplates));
getIt.registerSingleton<YatStore>(
YatStore(appStore: getIt.get<AppStore>(), secureStorage: getIt.get<SecureStorage>())
..init());
YatStore(appStore: getIt.get<AppStore>(), secureStorage: getIt.get<SecureStorage>())..init());
getIt.registerSingleton<AnonpayTransactionsStore>(
AnonpayTransactionsStore(anonpayInvoiceInfoSource: _anonpayInvoiceInfoSource));
@ -382,8 +378,8 @@ Future setup({
getIt.registerFactory<AuthViewModel>(() => AuthViewModel(getIt.get<AuthService>(),
getIt.get<SharedPreferences>(), getIt.get<SettingsStore>(), BiometricAuth()));
getIt.registerFactoryParam<AuthPage, void Function(bool, AuthPageState), bool>(
(onAuthFinished, closable) => AuthPage(getIt.get<AuthViewModel>(),
getIt.registerFactoryParam<AuthPage, OnAuthenticationFinished, bool>((onAuthFinished, closable) =>
AuthPage(getIt.get<AuthViewModel>(),
onAuthenticationFinished: onAuthFinished, closable: closable));
getIt.registerFactory<Setup2FAViewModel>(
@ -395,16 +391,12 @@ Future setup({
);
getIt.registerFactoryParam<TotpAuthCodePage, TotpAuthArgumentsModel, void>(
(totpAuthPageArguments, _) => TotpAuthCodePage(
getIt.get<Setup2FAViewModel>(),
totpArguments: totpAuthPageArguments,
),
);
(totpAuthPageArguments, _) => TotpAuthCodePage(
setup2FAViewModel: getIt.get<Setup2FAViewModel>(), totpArguments: totpAuthPageArguments));
getIt.registerFactory<AuthPage>(() {
return AuthPage(getIt.get<AuthViewModel>(),
onAuthenticationFinished: (isAuthenticated, AuthPageState authPageState) {
if (!isAuthenticated) {
return AuthPage(getIt.get<AuthViewModel>(), onAuthenticationFinished: (auth) {
if (!auth.success) {
return;
} else {
final authStore = getIt.get<AuthenticationStore>();
@ -413,14 +405,13 @@ Future setup({
final shouldUseTotp2FAToAccessWallets =
appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet;
if (useTotp && shouldUseTotp2FAToAccessWallets) {
authPageState.close(
auth.close(
route: Routes.totpAuthCodePage,
arguments: TotpAuthArgumentsModel(
isForSetup: false,
isClosable: false,
onTotpAuthenticationFinished: (bool isAuthenticatedSuccessfully,
TotpAuthCodePageState totpAuthPageState) async {
if (!isAuthenticatedSuccessfully) {
onTotpAuthenticationFinished: (totpAuth) async {
if (!totpAuth.success) {
return;
}
if (appStore.wallet != null) {
@ -428,12 +419,6 @@ Future setup({
return;
}
totpAuthPageState.changeProcessText('Loading the wallet');
if (loginError != null) {
totpAuthPageState.changeProcessText('ERROR: ${loginError.toString()}');
}
ReactionDisposer? _reaction;
_reaction = reaction((_) => appStore.wallet, (Object? _) {
_reaction?.reaction.dispose();
@ -448,12 +433,6 @@ Future setup({
return;
}
authPageState.changeProcessText('Loading the wallet');
if (loginError != null) {
authPageState.changeProcessText('ERROR: ${loginError.toString()}');
}
ReactionDisposer? _reaction;
_reaction = reaction((_) => appStore.wallet, (Object? _) {
_reaction?.reaction.dispose();
@ -760,17 +739,14 @@ Future setup({
case WalletType.monero:
return monero!.createMoneroWalletService(_walletInfoSource);
case WalletType.bitcoin:
return bitcoin!.createBitcoinWalletService(
_walletInfoSource, _unspentCoinsInfoSource!,
return bitcoin!.createBitcoinWalletService(_walletInfoSource, _unspentCoinsInfoSource!,
SettingsStoreBase.walletPasswordDirectInput);
case WalletType.litecoin:
return bitcoin!.createLitecoinWalletService(
_walletInfoSource, _unspentCoinsInfoSource!,
return bitcoin!.createLitecoinWalletService(_walletInfoSource, _unspentCoinsInfoSource!,
SettingsStoreBase.walletPasswordDirectInput);
case WalletType.ethereum:
return ethereum!.createEthereumWalletService(
_walletInfoSource,
SettingsStoreBase.walletPasswordDirectInput);
_walletInfoSource, SettingsStoreBase.walletPasswordDirectInput);
default:
throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService');
}
@ -825,19 +801,16 @@ Future setup({
trades: _tradesSource,
settingsStore: getIt.get<SettingsStore>()));
getIt.registerFactory(() => BackupService(
getIt.get<SecureStorage>(),
_walletInfoSource,
getIt.get<KeyService>(),
getIt.get<SharedPreferences>()));
getIt.registerFactory(() => BackupService(getIt.get<SecureStorage>(), _walletInfoSource,
getIt.get<KeyService>(), getIt.get<SharedPreferences>()));
getIt.registerFactory(() => BackupViewModel(
getIt.get<SecureStorage>(), getIt.get<SecretStore>(), getIt.get<BackupService>()));
getIt.registerFactory(() => BackupPage(getIt.get<BackupViewModel>()));
getIt.registerFactory(() =>
EditBackupPasswordViewModel(getIt.get<SecureStorage>(), getIt.get<SecretStore>()));
getIt.registerFactory(
() => EditBackupPasswordViewModel(getIt.get<SecureStorage>(), getIt.get<SecretStore>()));
getIt.registerFactory(() => EditBackupPasswordPage(getIt.get<EditBackupPasswordViewModel>()));
@ -1063,63 +1036,73 @@ Future setup({
getIt.registerFactoryParam<AdvancedPrivacySettingsViewModel, WalletType, void>(
(type, _) => AdvancedPrivacySettingsViewModel(type, getIt.get<SettingsStore>()));
getIt.registerFactoryParam<WalletUnlockLoadableViewModel, WalletUnlockArguments, void>((args, _) {
final currentWalletName = getIt
.get<SharedPreferences>()
.getString(PreferencesKey.currentWalletName) ?? '';
final currentWalletTypeRaw =
getIt.get<SharedPreferences>()
.getInt(PreferencesKey.currentWalletType) ?? 0;
final currentWalletType = deserializeFromInt(currentWalletTypeRaw);
String currentWalletName() {
return getIt.get<SharedPreferences>().getString(PreferencesKey.currentWalletName) ?? '';
}
return WalletUnlockLoadableViewModel(
getIt.get<AppStore>(),
getIt.get<WalletLoadingService>(),
walletName: args.walletName ?? currentWalletName,
walletType: args.walletType ?? currentWalletType);
});
WalletType currentWalletType() {
return deserializeFromInt(
getIt.get<SharedPreferences>().getInt(PreferencesKey.currentWalletType) ?? 0);
}
getIt.registerFactoryParam<WalletUnlockVerifiableViewModel, WalletUnlockArguments, void>((args, _) {
final currentWalletName = getIt
.get<SharedPreferences>()
.getString(PreferencesKey.currentWalletName) ?? '';
final currentWalletTypeRaw =
getIt.get<SharedPreferences>()
.getInt(PreferencesKey.currentWalletType) ?? 0;
final currentWalletType = deserializeFromInt(currentWalletTypeRaw);
getIt.registerFactoryParam<WalletUnlockLoadableViewModel, WalletUnlockArguments, void>(
(args, _) => WalletUnlockLoadableViewModel(
getIt.get<AppStore>(), getIt.get<WalletLoadingService>(),
useTotp: args.useTotp && getIt.get<Setup2FAViewModel>().useTOTP2FA,
walletName: args.walletName ?? currentWalletName(),
walletType: args.walletType ?? currentWalletType()));
return WalletUnlockVerifiableViewModel(
getIt.get<AppStore>(),
walletName: args.walletName ?? currentWalletName,
walletType: args.walletType ?? currentWalletType);
});
getIt.registerFactoryParam<WalletUnlockVerifiableViewModel, WalletUnlockArguments, void>(
(args, _) => WalletUnlockVerifiableViewModel(getIt.get<AppStore>(),
useTotp: args.useTotp && getIt.get<Setup2FAViewModel>().useTOTP2FA,
walletName: currentWalletName(),
walletType: currentWalletType()));
getIt.registerFactoryParam<WalletUnlockPage, WalletUnlockArguments, bool>((args, closable) {
final walletPasswordAuthViewModel = getIt.get<WalletUnlockLoadableViewModel>(param1: args);
return WalletUnlockPage(
getIt.get<WalletUnlockLoadableViewModel>(param1: args),
args.callback,
args.authPasswordHandler,
closable: closable);
walletPasswordAuthViewModel: walletPasswordAuthViewModel,
onAuthenticationFinished: args.callback,
authPasswordHandler: (String walletName, WalletType walletType, String password) async {
if (args.authPasswordHandler != null) {
// verify password matches (load wallet)
final wallet = await walletPasswordAuthViewModel.load();
// do custom password verification (eg. renaming wallet files)
await args.authPasswordHandler!(walletName, walletType, password);
return wallet;
}
// not closable -> login auth, unlocks wallet and wait for totp for authstore.allowed
if (!closable) {
return await walletPasswordAuthViewModel.unlock();
} else {
// closable -> only load to verify match
return await walletPasswordAuthViewModel.load();
}
},
closable: closable);
}, instanceName: 'wallet_unlock_loadable');
// Verifiable == simply uses the current loaded wallet and verifies the password matches - always used for current wallet auth methods after initial load
getIt.registerFactoryParam<WalletUnlockPage, WalletUnlockArguments, bool>((args, closable) {
final walletPasswordAuthViewModel = getIt.get<WalletUnlockVerifiableViewModel>(param1: args);
return WalletUnlockPage(
getIt.get<WalletUnlockVerifiableViewModel>(param1: args),
args.callback,
args.authPasswordHandler,
closable: closable);
walletPasswordAuthViewModel: walletPasswordAuthViewModel,
onAuthenticationFinished: args.callback,
authPasswordHandler: (_, __, ___) => walletPasswordAuthViewModel.verify(),
closable: closable,
isVerifiable: true);
}, instanceName: 'wallet_unlock_verifiable');
getIt.registerFactory<WalletUnlockPage>(
() => getIt.get<WalletUnlockPage>(
param1: WalletUnlockArguments(
callback: (bool successful, _) {
if (successful) {
final authStore = getIt.get<AuthenticationStore>();
authStore.allowed();
}}),
param2: false,
instanceName: 'wallet_unlock_loadable'),
param1: WalletUnlockArguments(
useTotp: getIt.get<AppStore>().settingsStore.shouldRequireTOTP2FAForAccessingWallet,
callback: (AuthResponse auth) =>
auth.success ? getIt.get<AuthenticationStore>().allowed() : null),
param2: false,
instanceName: 'wallet_unlock_loadable'),
instanceName: 'wallet_password_login');
getIt.registerFactoryParam<HomeSettingsPage, BalanceViewModel, void>((balanceViewModel, _) =>

View file

@ -6,6 +6,7 @@ class Cake2FAPresetsOptions extends EnumerableItem<int> with Serializable<int> {
static const narrow = Cake2FAPresetsOptions(title: 'Narrow', raw: 0);
static const normal = Cake2FAPresetsOptions(title: 'Normal', raw: 1);
static const aggressive = Cake2FAPresetsOptions(title: 'Aggressive', raw: 2);
static const unselected = Cake2FAPresetsOptions(title: 'Unselected', raw: 3);
static Cake2FAPresetsOptions deserialize({required int raw}) {
switch (raw) {
@ -15,6 +16,8 @@ class Cake2FAPresetsOptions extends EnumerableItem<int> with Serializable<int> {
return Cake2FAPresetsOptions.normal;
case 2:
return Cake2FAPresetsOptions.aggressive;
case 3:
return Cake2FAPresetsOptions.unselected;
default:
throw Exception(
'Incorrect Cake 2FA Preset $raw for Cake2FAPresetOptions deserialize',

View file

@ -1,5 +1,6 @@
import 'package:cake_wallet/anonpay/anonpay_info_base.dart';
import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/core/authentication_request_data.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/buy/order.dart';
@ -289,25 +290,19 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.auth:
return MaterialPageRoute<void>(
fullscreenDialog: true,
builder: (_)
=> SettingsStoreBase.walletPasswordDirectInput
? getIt.get<WalletUnlockPage>(
param1: WalletUnlockArguments(
callback: settings.arguments as OnAuthenticationFinished),
instanceName: 'wallet_unlock_verifiable',
param2: true)
: getIt.get<AuthPage>(
param1: settings.arguments as OnAuthenticationFinished,
param2: true));
builder: (_) => SettingsStoreBase.walletPasswordDirectInput
? getIt.get<WalletUnlockPage>(
param1: settings.arguments as WalletUnlockArguments,
instanceName: 'wallet_unlock_verifiable',
param2: true)
: getIt.get<AuthPage>(
param1: settings.arguments as OnAuthenticationFinished, param2: true));
case Routes.totpAuthCodePage:
final args = settings.arguments as TotpAuthArgumentsModel;
return MaterialPageRoute<void>(
fullscreenDialog: true,
builder: (_) => getIt.get<TotpAuthCodePage>(
param1: args,
),
);
fullscreenDialog: true,
builder: (_) =>
getIt.get<TotpAuthCodePage>(param1: settings.arguments as TotpAuthArgumentsModel));
case Routes.walletUnlockLoadable:
return MaterialPageRoute<void>(
@ -321,19 +316,16 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.unlock:
return MaterialPageRoute<void>(
fullscreenDialog: true,
builder: (_)
=> SettingsStoreBase.walletPasswordDirectInput
? WillPopScope(
child: getIt.get<WalletUnlockPage>(
param1: WalletUnlockArguments(
callback: settings.arguments as OnAuthenticationFinished),
builder: (_) => SettingsStoreBase.walletPasswordDirectInput
? WillPopScope(
child: getIt.get<WalletUnlockPage>(
param1: settings.arguments as WalletUnlockArguments,
param2: false,
instanceName: 'wallet_unlock_verifiable'),
onWillPop: () async => false)
: WillPopScope(
child: getIt.get<AuthPage>(
param1: settings.arguments as OnAuthenticationFinished,
param2: false),
onWillPop: () async => false)
: WillPopScope(
child: getIt.get<AuthPage>(
param1: settings.arguments as OnAuthenticationFinished, param2: false),
onWillPop: () async => false));
case Routes.connectionSync:

View file

@ -1,4 +1,5 @@
import 'package:another_flushbar/flushbar.dart';
import 'package:cake_wallet/core/authentication_request_data.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:mobx/mobx.dart';
import 'package:flutter/material.dart';
@ -10,8 +11,6 @@ import 'package:cake_wallet/src/screens/pin_code/pin_code.dart';
import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
import 'package:cake_wallet/core/execution_state.dart';
typedef OnAuthenticationFinished = void Function(bool, AuthPageState);
abstract class AuthPageState<T extends StatefulWidget> extends State<T> {
void changeProcessText(String text);
void hideProgressText();
@ -19,9 +18,7 @@ abstract class AuthPageState<T extends StatefulWidget> extends State<T> {
}
class AuthPage extends StatefulWidget {
AuthPage(this.authViewModel,
{required this.onAuthenticationFinished,
this.closable = true});
AuthPage(this.authViewModel, {required this.onAuthenticationFinished, this.closable = true});
final AuthViewModel authViewModel;
final OnAuthenticationFinished onAuthenticationFinished;
@ -34,19 +31,17 @@ class AuthPage extends StatefulWidget {
class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
final _key = GlobalKey<ScaffoldState>();
final _pinCodeKey = GlobalKey<PinCodeState>();
final _backArrowImageDarkTheme =
Image.asset('assets/images/close_button.png');
final _backArrowImageDarkTheme = Image.asset('assets/images/close_button.png');
ReactionDisposer? _reaction;
Flushbar<void>? _authBar;
Flushbar<void>? _progressBar;
@override
void initState() {
_reaction ??=
reaction((_) => widget.authViewModel.state, (ExecutionState state) {
_reaction ??= reaction((_) => widget.authViewModel.state, (ExecutionState state) {
if (state is ExecutedSuccessfullyState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onAuthenticationFinished(true, this);
widget.onAuthenticationFinished(AuthResponse(success: true, close: close));
});
setState(() {});
}
@ -54,9 +49,7 @@ class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
if (state is IsExecutingState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
// null duration to make it indefinite until its disposed
_authBar =
createBar<void>(S.of(context).authentication, duration: null)
..show(context);
_authBar = createBar<void>(S.of(context).authentication, duration: null)..show(context);
});
}
@ -64,10 +57,10 @@ class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
WidgetsBinding.instance.addPostFrameCallback((_) async {
_pinCodeKey.currentState?.clear();
dismissFlushBar(_authBar);
showBar<void>(
context, S.of(context).failed_authentication(state.error));
showBar<void>(context, S.of(context).failed_authentication(state.error));
widget.onAuthenticationFinished(false, this);
widget.onAuthenticationFinished(
AuthResponse(error: S.of(context).failed_authentication(state.error), close: close));
});
}
@ -75,10 +68,10 @@ class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
WidgetsBinding.instance.addPostFrameCallback((_) async {
_pinCodeKey.currentState?.clear();
dismissFlushBar(_authBar);
showBar<void>(
context, S.of(context).failed_authentication(state.error));
showBar<void>(context, S.of(context).failed_authentication(state.error));
widget.onAuthenticationFinished(false, this);
widget.onAuthenticationFinished(
AuthResponse(error: S.of(context).failed_authentication(state.error), close: close));
});
}
});
@ -102,8 +95,7 @@ class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
@override
void changeProcessText(String text) {
dismissFlushBar(_authBar);
_progressBar = createBar<void>(text, duration: null)
..show(_key.currentContext!);
_progressBar = createBar<void>(text, duration: null)..show(_key.currentContext!);
}
@override
@ -144,15 +136,15 @@ class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
width: 37,
child: InkWell(
onTap: () => Navigator.of(context).pop(),
child: _backArrowImageDarkTheme,
child: _backArrowImageDarkTheme,
),
))
: Container(),
backgroundColor: Theme.of(context).colorScheme.background,
border: null),
resizeToAvoidBottomInset: false,
body: PinCode((pin, _) => widget.authViewModel.auth(password: pin),
(_) => null, widget.authViewModel.pinLength, false, _pinCodeKey));
body: PinCode((pin, _) => widget.authViewModel.auth(password: pin), (_) => null,
widget.authViewModel.pinLength, false, _pinCodeKey));
}
void dismissFlushBar(Flushbar<dynamic>? bar) {

View file

@ -1,14 +1,15 @@
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_dashboard_navbar.dart';
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu.dart';
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu_item.dart';
import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator.dart';
import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart';
import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@ -40,14 +41,11 @@ class DesktopSidebarWrapper extends BasePage {
),
trailing: InkWell(
onTap: () {
Navigator.of(context).pushNamed(
Routes.unlock,
arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) {
if (isAuthenticatedSuccessfully) {
auth.close();
}
},
);
Navigator.of(context).pushNamed(Routes.unlock,
arguments: WalletUnlockArguments(
useTotp: getIt.get<Setup2FAViewModel>().useTOTP2FA &&
getIt.get<Setup2FAViewModel>().shouldRequireTOTP2FAForAccessingWallet,
callback: (auth) => auth.success ? auth.close() : null));
},
child: Icon(Icons.lock_outline),
),

View file

@ -148,38 +148,7 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
}
Future<void> _loadWallet(WalletListItem wallet) async {
if (SettingsStoreBase.walletPasswordDirectInput) {
Navigator.of(context).pushNamed(
Routes.walletUnlockLoadable,
arguments: WalletUnlockArguments(
callback: (bool isAuthenticatedSuccessfully, AuthPageState auth) async {
if (isAuthenticatedSuccessfully) {
auth.close();
setState(() {});
}
}, walletName: wallet.name,
walletType: wallet.type));
return;
}
widget._authService.authenticateAction(context,
onAuthSuccess: (isAuthenticatedSuccessfully) async {
if (!isAuthenticatedSuccessfully) {
return;
}
try {
changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name));
await widget.walletListViewModel.loadWallet(wallet);
hideProgressText();
setState(() {});
} catch (e) {
changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString()));
}
},
conditionToDetermineIfToUse2FA:
widget.walletListViewModel.shouldRequireTOTP2FAForAccessingWallet,
);
await widget.walletListViewModel.authWallet(context, wallet);
}
void _navigateToCreateWallet() {
@ -188,17 +157,16 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
context,
route: Routes.newWallet,
arguments: widget.walletListViewModel.currentWalletType,
conditionToDetermineIfToUse2FA: widget
.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
conditionToDetermineIfToUse2FA:
widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
);
} else {
widget._authService.authenticateAction(
context,
route: Routes.newWalletType,
conditionToDetermineIfToUse2FA: widget
.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
conditionToDetermineIfToUse2FA:
widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
);
}
}

View file

@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/utils/device_info.dart';
@ -12,8 +11,6 @@ import 'package:cake_wallet/store/authentication_store.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:uni_links/uni_links.dart';
import '../setup_2fa/setup_2fa_enter_code_page.dart';
class Root extends StatefulWidget {
Root({
required Key key,
@ -98,8 +95,7 @@ class RootState extends State<Root> with WidgetsBindingObserver {
return;
}
if (!_isInactive &&
widget.authenticationStore.state == AuthenticationState.allowed) {
if (!_isInactive && widget.authenticationStore.state == AuthenticationState.allowed) {
setState(() => _setInactive(true));
}
@ -126,17 +122,15 @@ class RootState extends State<Root> with WidgetsBindingObserver {
return;
} else {
final useTotp = widget.appStore.settingsStore.useTOTP2FA;
final shouldUseTotp2FAToAccessWallets = widget.appStore
.settingsStore.shouldRequireTOTP2FAForAccessingWallet;
final shouldUseTotp2FAToAccessWallets =
widget.appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet;
if (useTotp && shouldUseTotp2FAToAccessWallets) {
_reset();
auth.close(
route: Routes.totpAuthCodePage,
arguments: TotpAuthArgumentsModel(
onTotpAuthenticationFinished:
(bool isAuthenticatedSuccessfully,
TotpAuthCodePageState totpAuth) {
if (!isAuthenticatedSuccessfully) {
onTotpAuthenticationFinished: (totpAuth) {
if (!totpAuth.success) {
return;
}
_reset();

View file

@ -1,10 +1,7 @@
import 'package:another_flushbar/flushbar.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/view_model/auth_state.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/setup_2fa/widgets/popup_cancellable_alert.dart';
@ -13,135 +10,49 @@ import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import '../../../palette.dart';
import '../../../routes.dart';
typedef OnTotpAuthenticationFinished = void Function(
bool, TotpAuthCodePageState);
class TotpAuthCodePage extends StatefulWidget {
TotpAuthCodePage(
this.setup2FAViewModel, {
required this.totpArguments,
});
final Setup2FAViewModel setup2FAViewModel;
final TotpAuthArgumentsModel totpArguments;
@override
TotpAuthCodePageState createState() => TotpAuthCodePageState();
}
class TotpAuthCodePageState extends State<TotpAuthCodePage> {
final _key = GlobalKey<ScaffoldState>();
ReactionDisposer? _reaction;
Flushbar<void>? _authBar;
Flushbar<void>? _progressBar;
@override
void initState() {
if (widget.totpArguments.onTotpAuthenticationFinished != null) {
_reaction ??= reaction((_) => widget.setup2FAViewModel.state,
(ExecutionState state) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (state is ExecutedSuccessfullyState) {
widget.totpArguments.onTotpAuthenticationFinished!(true, this);
}
if (state is FailureState) {
print(state.error);
widget.totpArguments.onTotpAuthenticationFinished!(false, this);
}
if (state is AuthenticationBanned) {
widget.totpArguments.onTotpAuthenticationFinished!(false, this);
}
});
});
}
super.initState();
}
@override
void dispose() {
_reaction?.reaction.dispose();
super.dispose();
}
void changeProcessText(String text) {
dismissFlushBar(_authBar);
_progressBar = createBar<void>(text, duration: null)
..show(_key.currentContext!);
}
Future<void> close({String? route, dynamic arguments}) async {
if (_key.currentContext == null) {
throw Exception('Key context is null. Should be not happened');
}
await Future<void>.delayed(Duration(milliseconds: 50));
if (route != null) {
Navigator.of(_key.currentContext!)
.pushReplacementNamed(route, arguments: arguments);
} else {
Navigator.of(_key.currentContext!).pop();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _key,
resizeToAvoidBottomInset: false,
body: TOTPEnterCode(
setup2FAViewModel: widget.setup2FAViewModel,
isForSetup: widget.totpArguments.isForSetup ?? false,
isClosable: widget.totpArguments.isClosable ?? true,
),
);
}
void dismissFlushBar(Flushbar<dynamic>? bar) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await bar?.dismiss();
});
}
}
class TOTPEnterCode extends BasePage {
TOTPEnterCode({
required this.setup2FAViewModel,
required this.isForSetup,
required this.isClosable,
}) : totpController = TextEditingController() {
class TotpAuthCodePage extends BasePage {
TotpAuthCodePage({required this.setup2FAViewModel, required TotpAuthArgumentsModel totpArguments})
: isForSetup = totpArguments.isForSetup ?? false,
isClosable = totpArguments.isClosable ?? true,
showPopup = totpArguments.isForSetup ?? totpArguments.showPopup ?? false,
onTotpAuthenticationFinished = totpArguments.onTotpAuthenticationFinished,
totpController = TextEditingController() {
totpController.addListener(() {
setup2FAViewModel.enteredOTPCode = totpController.text;
});
}
@override
String get title =>
isForSetup ? S.current.setup_2fa : S.current.verify_with_2fa;
Widget? leading(BuildContext context) {
return isClosable ? super.leading(context) : null;
}
final TextEditingController totpController;
final Setup2FAViewModel setup2FAViewModel;
final bool isForSetup;
final bool isClosable;
final bool showPopup;
final Function(TotpResponse)? onTotpAuthenticationFinished;
@override
String get title => isForSetup ? S.current.setup_2fa : S.current.verify_with_2fa;
@override
Widget? leading(BuildContext context) {
return isClosable ? super.leading(context) : null;
}
@override
Widget body(BuildContext context) {
Future<void> close({String? route, dynamic arguments}) async {
if (route != null) {
Navigator.of(context).pushReplacementNamed(route, arguments: arguments);
} else {
Navigator.of(context).pop();
}
}
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24,
),
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
BaseTextFormField(
@ -167,40 +78,47 @@ class TOTPEnterCode extends BasePage {
),
Spacer(),
Observer(
builder: (context) {
return PrimaryButton(
isDisabled: setup2FAViewModel.enteredOTPCode.length != 8,
builder: (_) {
return LoadingPrimaryButton(
isDisabled: setup2FAViewModel.enteredOTPCode.length != 8 ||
setup2FAViewModel.state is IsExecutingState,
isLoading: setup2FAViewModel.state is IsExecutingState,
onPressed: () async {
final result = await setup2FAViewModel.totp2FAAuth(
totpController.text, isForSetup);
final bannedState =
setup2FAViewModel.state is AuthenticationBanned;
final result =
await setup2FAViewModel.totp2FAAuth(totpController.text, isForSetup);
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return PopUpCancellableAlertDialog(
contentText: _textDisplayedInPopupOnResult(
result, bannedState, context),
actionButtonText: S.of(context).ok,
buttonAction: () {
result ? setup2FAViewModel.success() : null;
if (isForSetup && result) {
Navigator.pop(context);
// Navigator.of(context)
// .popAndPushNamed(Routes.modify2FAPage);
} else {
Navigator.of(context).pop(result);
}
},
);
},
);
if (isForSetup && result) {
Navigator.pushReplacementNamed(
context, Routes.modify2FAPage);
if (showPopup || result == false) {
final bannedState = setup2FAViewModel.state is AuthenticationBanned;
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return PopUpCancellableAlertDialog(
contentText: _textDisplayedInPopupOnResult(result, bannedState, context),
actionButtonText: S.of(context).ok,
buttonAction: () {
result
? onTotpAuthenticationFinished
?.call(TotpResponse(success: true, close: close))
: null;
if (isForSetup && result) {
Navigator.pushNamedAndRemoveUntil(
context, Routes.dashboard, (route) => false);
} else {
Navigator.of(context).pop(result);
}
},
);
},
);
}
if (showPopup == false) {
result
? onTotpAuthenticationFinished
?.call(TotpResponse(success: true, close: close))
: null;
}
},
text: S.of(context).continue_text,
color: Theme.of(context).primaryColor,
@ -214,13 +132,10 @@ class TOTPEnterCode extends BasePage {
);
}
String _textDisplayedInPopupOnResult(
bool result, bool bannedState, BuildContext context) {
String _textDisplayedInPopupOnResult(bool result, bool bannedState, BuildContext context) {
switch (result) {
case true:
return isForSetup
? S.current.totp_2fa_success
: S.current.totp_verification_success;
return isForSetup ? S.current.totp_2fa_success : S.current.totp_verification_success;
case false:
if (bannedState) {
final state = setup2FAViewModel.state as AuthenticationBanned;

View file

@ -1,9 +1,9 @@
import 'package:another_flushbar/flushbar.dart';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/authentication_request_data.dart';
import 'package:cake_wallet/core/wallet_name_validator.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
@ -100,21 +100,17 @@ class WalletEditPage extends BasePage {
try {
bool confirmed = false;
if (SettingsStoreBase
.walletPasswordDirectInput) {
if (SettingsStoreBase.walletPasswordDirectInput) {
await Navigator.of(context).pushNamed(
Routes.walletUnlockLoadable,
arguments: WalletUnlockArguments(
authPasswordHandler:
(String password) async {
await walletEditViewModel
.changeName(editingWallet,
password: password);
useTotp: true,
authPasswordHandler: (_, __, String password) async {
await walletEditViewModel.changeName(editingWallet,
password: password);
},
callback: (bool
isAuthenticatedSuccessfully,
AuthPageState auth) async {
if (isAuthenticatedSuccessfully) {
callback: (AuthResponse auth) async {
if (auth.success) {
auth.close();
confirmed = true;
}
@ -122,8 +118,7 @@ class WalletEditPage extends BasePage {
walletName: editingWallet.name,
walletType: editingWallet.type));
} else {
await walletEditViewModel
.changeName(editingWallet);
await walletEditViewModel.changeName(editingWallet);
confirmed = true;
}
@ -150,12 +145,14 @@ class WalletEditPage extends BasePage {
}
Future<void> _removeWallet(BuildContext context) async {
authService.authenticateAction(context, onAuthSuccess: (isAuthenticatedSuccessfully) async {
if (!isAuthenticatedSuccessfully) {
return;
}
authService.authenticateAction(
context,
onAuthSuccess: (isAuthenticatedSuccessfully) async {
if (!isAuthenticatedSuccessfully) {
return;
}
_onSuccessfulAuth(context);
_onSuccessfulAuth(context);
},
conditionToDetermineIfToUse2FA: false,
);

View file

@ -252,45 +252,7 @@ class WalletListBodyState extends State<WalletListBody> {
}
Future<void> _loadWallet(WalletListItem wallet) async {
if (SettingsStoreBase.walletPasswordDirectInput) {
Navigator.of(context).pushNamed(
Routes.walletUnlockLoadable,
arguments: WalletUnlockArguments(
callback: (bool isAuthenticatedSuccessfully, AuthPageState auth) async {
if (isAuthenticatedSuccessfully) {
auth.close();
setState(() {});
}
}, walletName: wallet.name,
walletType: wallet.type));
return;
}
await widget.authService.authenticateAction(
context,
onAuthSuccess: (isAuthenticatedSuccessfully) async {
if (!isAuthenticatedSuccessfully) {
return;
}
try {
changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name));
await widget.walletListViewModel.loadWallet(wallet);
await hideProgressText();
// only pop the wallets route in mobile as it will go back to dashboard page
// in desktop platforms the navigation tree is different
if (ResponsiveLayoutUtil.instance.shouldRenderMobileUI()) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).pop();
});
}
} catch (e) {
changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString()));
}
},
conditionToDetermineIfToUse2FA:
widget.walletListViewModel.shouldRequireTOTP2FAForAccessingWallet,
);
widget.walletListViewModel.authWallet(context, wallet);
}
void changeProcessText(String text) {

View file

@ -1,17 +1,36 @@
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
import 'package:cake_wallet/core/authentication_request_data.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/core/execution_state.dart';
typedef AuthPasswordHandler = Future<void> Function(String);
typedef AuthPasswordHandler = Future<dynamic> Function(
String walletName, WalletType walletType, String password);
class WalletUnlockArguments {
WalletUnlockArguments(
{required this.callback,
required this.useTotp,
this.walletName,
this.walletType,
this.authPasswordHandler});
this.authPasswordHandler})
: state = InitialExecutionState();
final OnAuthenticationFinished callback;
final AuthPasswordHandler? authPasswordHandler;
final String? walletName;
final WalletType? walletType;
final bool useTotp;
@observable
ExecutionState state;
@action
void success() {
state = ExecutedSuccessfullyState();
}
@action
void failure(e) {
state = FailureState(e.toString());
}
}

View file

@ -1,228 +1,174 @@
import 'package:another_flushbar/flushbar.dart';
import 'package:cake_wallet/core/authentication_request_data.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/new_wallet_theme.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:cake_wallet/utils/exception_handler.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/view_model/wallet_unlock_view_model.dart';
import 'package:cake_wallet/view_model/wallet_password_auth_view_model.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
class WalletUnlockPage extends StatefulWidget {
class WalletUnlockPage extends BasePage {
WalletUnlockPage(
this.walletUnlockViewModel,
this.onAuthenticationFinished,
this.authPasswordHandler,
{required this.closable});
{required this.walletPasswordAuthViewModel,
required this.onAuthenticationFinished,
required this.authPasswordHandler,
bool closable = false,
bool isVerifiable = false})
: this.isClosable = closable,
this.isVerifiable = isVerifiable,
_passwordController = TextEditingController() {
_passwordController.text = walletPasswordAuthViewModel.password;
_passwordController
.addListener(() => walletPasswordAuthViewModel.password = _passwordController.text);
}
final WalletUnlockViewModel walletUnlockViewModel;
final WalletPasswordAuthViewModel walletPasswordAuthViewModel;
final OnAuthenticationFinished onAuthenticationFinished;
final AuthPasswordHandler? authPasswordHandler;
final bool closable;
@override
State<StatefulWidget> createState() => WalletUnlockPageState();
}
class WalletUnlockPageState extends AuthPageState<WalletUnlockPage> {
WalletUnlockPageState()
: _passwordController = TextEditingController();
final AuthPasswordHandler authPasswordHandler;
final bool isClosable;
final bool isVerifiable;
final TextEditingController _passwordController;
final _key = GlobalKey<ScaffoldState>();
final _backArrowImageDarkTheme =
Image.asset('assets/images/close_button.png');
ReactionDisposer? _reaction;
Flushbar<void>? _authBar;
Flushbar<void>? _progressBar;
void Function()? _passwordControllerListener;
@override
void initState() {
_reaction ??=
reaction((_) => widget.walletUnlockViewModel.state, (ExecutionState state) {
if (state is ExecutedSuccessfullyState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onAuthenticationFinished(true, this);
});
setState(() {});
Widget? leading(BuildContext context) {
return isClosable ? super.leading(context) : null;
}
@override
Widget body(BuildContext context) {
Future<void> close({String? route, arguments}) async {
if (route != null) {
Navigator.of(context).pushReplacementNamed(route, arguments: arguments);
} else {
Navigator.of(context).pop();
}
if (state is IsExecutingState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
// null duration to make it indefinite until its disposed
_authBar =
createBar<void>(S.of(context).authentication, duration: null)
..show(context);
});
}
if (state is FailureState) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
dismissFlushBar(_authBar);
showBar<void>(
context, S.of(context).failed_authentication(state.error));
widget.onAuthenticationFinished(false, this);
});
}
});
_passwordControllerListener = () => widget.walletUnlockViewModel.setPassword(_passwordController.text);
if (_passwordControllerListener != null) {
_passwordController.addListener(_passwordControllerListener!);
}
super.initState();
}
@override
void dispose() {
_reaction?.reaction.dispose();
if (_passwordControllerListener != null) {
_passwordController.removeListener(_passwordControllerListener!);
}
super.dispose();
}
@override
void changeProcessText(String text) {
dismissFlushBar(_authBar);
_progressBar = createBar<void>(text, duration: null)
..show(_key.currentContext!);
}
@override
void hideProgressText() {
dismissFlushBar(_progressBar);
_progressBar = null;
}
void dismissFlushBar(Flushbar<dynamic>? bar) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await bar?.dismiss();
});
}
@override
Future<void> close({String? route, arguments}) async {
if (_key.currentContext == null) {
throw Exception('Key context is null. Should be not happened');
void onFailure(String error) {
walletPasswordAuthViewModel.state = FailureState(error);
showBar<void>(context, S.of(context).failed_authentication(error),
duration: Duration(seconds: 3));
onAuthenticationFinished(
AuthResponse(error: S.of(context).failed_authentication(error), close: close));
}
/// not the best scenario, but WidgetsBinding is not behaving correctly on Android
await Future<void>.delayed(Duration(milliseconds: 50));
await _authBar?.dismiss();
await Future<void>.delayed(Duration(milliseconds: 50));
await _progressBar?.dismiss();
await Future<void>.delayed(Duration(milliseconds: 50));
if (route != null) {
Navigator.of(_key.currentContext!).pushReplacementNamed(route, arguments: arguments);
} else {
Navigator.of(_key.currentContext!).pop();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _key,
appBar: CupertinoNavigationBar(
leading: widget.closable
? Container(
padding: EdgeInsets.only(top: 10),
child: SizedBox(
height: 37,
width: 37,
child: InkWell(
onTap: () => Navigator.of(context).pop(),
child: _backArrowImageDarkTheme,
),
))
: Container(),
backgroundColor: Theme.of(context).colorScheme.background,
border: null),
resizeToAvoidBottomInset: false,
body: Center(
child: ConstrainedBox(
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtil.kDesktopMaxWidthConstraint),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(widget.walletUnlockViewModel.walletName,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor)),
SizedBox(height: 24),
Form(
child: TextFormField(
onChanged: (value) => null,
controller: _passwordController,
textAlign: TextAlign.center,
obscureText: true,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.w600,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor),
decoration: InputDecoration(
hintStyle: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<NewWalletTheme>()!.hintTextColor),
hintText: S.of(context).enter_wallet_password,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).extension<NewWalletTheme>()!.underlineColor,
width: 1.0)),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).extension<NewWalletTheme>()!.underlineColor,
width: 1.0),
)
)))])),
Padding(
padding: EdgeInsets.only(bottom: 24),
child: Observer(
builder: (_) =>
LoadingPrimaryButton(
onPressed: () async {
if (widget.authPasswordHandler != null) {
try {
await widget.authPasswordHandler!(widget
.walletUnlockViewModel.password);
widget.walletUnlockViewModel.success();
} catch (e) {
widget.walletUnlockViewModel.failure(e);
}
return;
}
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(walletPasswordAuthViewModel.walletName,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor)),
SizedBox(height: 24),
Form(
child: TextFormField(
onChanged: (value) => null,
controller: _passwordController,
textAlign: TextAlign.center,
obscureText: true,
style: TextStyle(
fontSize: 20.0,
fontWeight: FontWeight.w600,
color:
Theme.of(context).extension<CakeTextTheme>()!.titleColor),
decoration: InputDecoration(
hintStyle: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.extension<NewWalletTheme>()!
.hintTextColor),
hintText: S.of(context).enter_wallet_password,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context)
.extension<NewWalletTheme>()!
.underlineColor,
width: 1.0)),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context)
.extension<NewWalletTheme>()!
.underlineColor,
width: 1.0)))))
])),
Observer(builder: (_) {
return Padding(
padding: EdgeInsets.only(bottom: 24),
child: LoadingPrimaryButton(
onPressed: () async {
Flushbar<void>? loadingBar;
if (!isVerifiable)
loadingBar = createBar<void>(S.of(context).loading_your_wallet)
..show(context);
widget.walletUnlockViewModel.unlock();
},
text: S.of(context).unlock,
color: Colors.green,
textColor: Colors.white,
isLoading: widget.walletUnlockViewModel.state is IsExecutingState,
isDisabled: widget.walletUnlockViewModel.state is IsExecutingState)))
]))
));
walletPasswordAuthViewModel.state = IsExecutingState();
dynamic payload;
try {
payload = await authPasswordHandler(
walletPasswordAuthViewModel.walletName,
walletPasswordAuthViewModel.walletType,
walletPasswordAuthViewModel.password);
} catch (err, stack) {
loadingBar?.dismiss();
onFailure(S.of(context).invalid_password);
ExceptionHandler.onError(
FlutterErrorDetails(exception: err, stack: stack));
return;
}
loadingBar?.dismiss();
if (!walletPasswordAuthViewModel.useTotp) {
onAuthenticationFinished(
AuthResponse(success: true, close: close, payload: payload));
} else {
Navigator.of(context).pushReplacementNamed(
Routes.totpAuthCodePage,
arguments: TotpAuthArgumentsModel(
isForSetup: false,
isClosable: isClosable,
onTotpAuthenticationFinished: (totpAuth) async {
if (!totpAuth.success) {
onFailure(totpAuth.error!);
return;
}
onAuthenticationFinished(AuthResponse(
success: true, close: totpAuth.close, payload: payload));
},
),
);
}
},
text: S.of(context).unlock,
color: Colors.green,
textColor: Colors.white,
isLoading: walletPasswordAuthViewModel.state is IsExecutingState,
isDisabled: walletPasswordAuthViewModel.state is IsExecutingState ||
walletPasswordAuthViewModel.password.length == 0));
})
])));
}
}

View file

@ -532,7 +532,7 @@ abstract class SettingsStoreBase with Store {
sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey) ?? false;
final selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize(
raw: sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset) ??
Cake2FAPresetsOptions.normal.raw);
Cake2FAPresetsOptions.unselected.raw);
final shouldRequireTOTP2FAForAccessingWallet =
sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet) ?? false;
final shouldRequireTOTP2FAForSendsToContact =
@ -736,7 +736,7 @@ abstract class SettingsStoreBase with Store {
allowBiometricalAuthentication;
selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize(
raw: sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset) ??
Cake2FAPresetsOptions.normal.raw);
Cake2FAPresetsOptions.unselected.raw);
shouldRequireTOTP2FAForAccessingWallet =
sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet) ?? false;
shouldRequireTOTP2FAForSendsToContact =
@ -762,7 +762,7 @@ abstract class SettingsStoreBase with Store {
shouldShowMarketPlaceInDashboard;
selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize(
raw: sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset) ??
Cake2FAPresetsOptions.narrow.raw);
Cake2FAPresetsOptions.unselected.raw);
exchangeStatus = ExchangeApiMode.deserialize(
raw: sharedPreferences.getInt(PreferencesKey.exchangeStatusKey) ??
ExchangeApiMode.enabled.raw);

View file

@ -28,7 +28,9 @@ abstract class Setup2FAViewModelBase with Store {
selected2FASettings = ObservableList<VerboseControlSettings>(),
state = InitialExecutionState() {
_getRandomBase32SecretKey();
selectCakePreset(selectedCake2FAPreset);
if (selectedCake2FAPreset == Cake2FAPresetsOptions.unselected) {
selectCakePreset(Cake2FAPresetsOptions.normal);
}
reaction((_) => state, _saveLastAuthTime);
}
@ -92,6 +94,7 @@ abstract class Setup2FAViewModelBase with Store {
@action
void setUseTOTP2FA(bool value) {
_settingsStore.useTOTP2FA = value;
if (!value) _settingsStore.selectedCake2FAPreset = Cake2FAPresetsOptions.unselected;
}
@action
@ -134,7 +137,7 @@ abstract class Setup2FAViewModelBase with Store {
@action
Future<bool> totp2FAAuth(String otpText, bool isForSetup) async {
state = InitialExecutionState();
state = IsExecutingState();
_failureCounter = _settingsStore.numberOfFailedTokenTrials;
final _banDuration = banDuration();
@ -154,11 +157,11 @@ abstract class Setup2FAViewModelBase with Store {
isForSetup ? setUseTOTP2FA(result) : null;
if (result) {
state = ExecutedSuccessfullyState();
return true;
} else {
final value = _settingsStore.numberOfFailedTokenTrials + 1;
adjustTokenTrialNumber(value);
print(value);
if (_failureCounter >= maxFailedTrials) {
final banDuration = await ban();
state = AuthenticationBanned(

View file

@ -1,5 +1,10 @@
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/authentication_request_data.dart';
import 'package:cake_wallet/core/wallet_loading_service.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/store/app_store.dart';
@ -7,6 +12,8 @@ import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/wallet_types.g.dart';
import 'package:cake_wallet/routes.dart';
import 'package:flutter/material.dart';
part 'wallet_list_view_model.g.dart';
@ -43,8 +50,7 @@ abstract class WalletListViewModelBase with Store {
@action
Future<void> loadWallet(WalletListItem walletItem) async {
final wallet =
await _walletLoadingService.load(walletItem.type, walletItem.name);
final wallet = await _walletLoadingService.load(walletItem.type, walletItem.name);
_appStore.changeCurrentWallet(wallet);
}
@ -57,15 +63,53 @@ abstract class WalletListViewModelBase with Store {
name: info.name,
type: info.type,
key: info.key,
isCurrent: info.name == _appStore.wallet?.name &&
info.type == _appStore.wallet?.type,
isCurrent: info.name == _appStore.wallet!.name && info.type == _appStore.wallet!.type,
isEnabled: availableWalletTypes.contains(info.type),
),
),
);
}
bool checkIfAuthRequired() {
return _authService.requireAuth();
@action
Future<void> authWallet(BuildContext context, WalletListItem wallet) async {
void onAuthAndTotpSuccess(bool isAuthenticatedSuccessfully, {AuthPageState? auth}) async {
if (isAuthenticatedSuccessfully) {
auth != null ? auth.close() : null;
await loadWallet(wallet);
return;
}
}
// Desktop/password auth -> walletUnlockLoadable Route with wallet values as params
if (SettingsStoreBase.walletPasswordDirectInput) {
_authService.authenticateAction(
context,
// Desktop: every wallet file has a different password,
// so, when switching, always require auth
alwaysRequireAuth: true,
authRoute: Routes.walletUnlockLoadable,
authArguments: WalletUnlockArguments(
useTotp: shouldRequireTOTP2FAForAccessingWallet,
callback: (AuthResponse auth) async {
if (auth.success) {
auth.close();
_appStore.changeCurrentWallet(auth.payload as WalletBase);
return;
}
},
walletName: wallet.name,
walletType: wallet.type),
onAuthSuccess: onAuthAndTotpSuccess,
conditionToDetermineIfToUse2FA: shouldRequireTOTP2FAForAccessingWallet,
);
return;
}
// Mobile/PIN auth -> regular Auth Route
_authService.authenticateAction(context,
onAuthSuccess: onAuthAndTotpSuccess,
conditionToDetermineIfToUse2FA: shouldRequireTOTP2FAForAccessingWallet);
}
}

View file

@ -0,0 +1,40 @@
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
part 'wallet_password_auth_view_model.g.dart';
class WalletPasswordAuthViewModel = WalletPasswordAuthViewModelBase
with _$WalletPasswordAuthViewModel;
abstract class WalletPasswordAuthViewModelBase with Store {
WalletPasswordAuthViewModelBase(
{required this.useTotp, required this.walletName, required this.walletType})
: password = '',
state = InitialExecutionState();
@observable
String walletName;
@observable
WalletType walletType;
@observable
String password;
@observable
bool useTotp;
@action
void setPassword(String password) => this.password = password;
@observable
ExecutionState state;
@action
void success() {
state = ExecutedSuccessfullyState();
}
@action
void failure(e) {
state = FailureState(e.toString());
}
}

View file

@ -1,65 +1,38 @@
import 'package:cw_core/wallet_base.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/core/wallet_loading_service.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/view_model/wallet_unlock_view_model.dart';
import 'package:cake_wallet/view_model/wallet_password_auth_view_model.dart';
part 'wallet_unlock_loadable_view_model.g.dart';
class WalletUnlockLoadableViewModel = WalletUnlockLoadableViewModelBase
with _$WalletUnlockLoadableViewModel;
abstract class WalletUnlockLoadableViewModelBase extends WalletUnlockViewModel
with Store {
abstract class WalletUnlockLoadableViewModelBase extends WalletPasswordAuthViewModel with Store {
WalletUnlockLoadableViewModelBase(this._appStore, this._walletLoadingService,
{required this.walletName, required this.walletType})
: password = '',
state = InitialExecutionState();
{required this.useTotp, required this.walletName, required this.walletType})
: super(useTotp: useTotp, walletName: walletName, walletType: walletType);
final String walletName;
final WalletType walletType;
@override
@observable
String password;
@override
@observable
ExecutionState state;
final WalletLoadingService _walletLoadingService;
final AppStore _appStore;
@override
@action
void setPassword(String password) => this.password = password;
@observable
bool useTotp;
@override
@action
Future<void> unlock() async {
try {
state = InitialExecutionState();
final wallet = await _walletLoadingService.load(walletType, walletName,
password: password);
_appStore.changeCurrentWallet(wallet);
success();
} catch (e) {
failure(e.toString());
}
Future<dynamic> load() async {
final wallet = await _walletLoadingService.load(walletType, walletName, password: password);
return wallet;
}
@override
@action
void success() {
state = ExecutedSuccessfullyState();
}
@override
@action
void failure(e) {
state = FailureState(e.toString());
Future<dynamic> unlock() async {
final wallet = await load();
_appStore.changeCurrentWallet(wallet as WalletBase);
return wallet;
}
}

View file

@ -2,60 +2,28 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/view_model/wallet_unlock_view_model.dart';
import 'package:cake_wallet/view_model/wallet_password_auth_view_model.dart';
part 'wallet_unlock_verifiable_view_model.g.dart';
class WalletUnlockVerifiableViewModel = WalletUnlockVerifiableViewModelBase
with _$WalletUnlockVerifiableViewModel;
abstract class WalletUnlockVerifiableViewModelBase extends WalletUnlockViewModel
with Store {
WalletUnlockVerifiableViewModelBase(this.appStore,
{required this.walletName, required this.walletType})
: password = '',
state = InitialExecutionState();
abstract class WalletUnlockVerifiableViewModelBase extends WalletPasswordAuthViewModel with Store {
WalletUnlockVerifiableViewModelBase(this._appStore,
{required this.useTotp, required this.walletName, required this.walletType})
: super(useTotp: useTotp, walletName: walletName, walletType: walletType);
final String walletName;
final WalletType walletType;
final AppStore _appStore;
final AppStore appStore;
@override
@observable
String password;
bool useTotp;
@override
@observable
ExecutionState state;
@override
@action
void setPassword(String password) => this.password = password;
@override
@action
Future<void> unlock() async {
try {
state = appStore.wallet!.password == password
? ExecutedSuccessfullyState()
: FailureState(S.current.invalid_password);
} catch (e) {
failure('${S.current.invalid_password}\n${e.toString()}');
}
}
@override
@action
void success() {
state = ExecutedSuccessfullyState();
}
@override
@action
void failure(e) {
state = FailureState(e.toString());
Future<void> verify() async {
final valid = _appStore.wallet!.password == password;
if (!valid) throw Exception('${S.current.invalid_password}');
}
}

View file

@ -1,11 +0,0 @@
import 'package:cake_wallet/core/execution_state.dart';
abstract class WalletUnlockViewModel {
String get walletName;
String get password;
void setPassword(String password);
ExecutionState get state;
Future<void> unlock();
void success();
void failure(dynamic e);
}