Merge pull request #649 from cake-tech/CW-225-pin-timeout

[CW-225] pin timeout
This commit is contained in:
Omar Hatem 2022-12-13 23:38:06 +02:00 committed by GitHub
commit 4337af022c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 332 additions and 118 deletions

View file

@ -4,12 +4,19 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/secret_store_key.dart';
import 'package:cake_wallet/entities/encrypt.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/store/settings_store.dart';
class AuthService with Store {
AuthService({required this.secureStorage, required this.sharedPreferences});
AuthService({
required this.secureStorage,
required this.sharedPreferences,
required this.settingsStore,
});
final FlutterSecureStorage secureStorage;
final SharedPreferences sharedPreferences;
final SettingsStore settingsStore;
Future<void> setPassword(String password) async {
final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
@ -19,8 +26,7 @@ class AuthService with Store {
Future<bool> canAuthenticate() async {
final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
final walletName =
sharedPreferences.getString(PreferencesKey.currentWalletName) ?? '';
final walletName = sharedPreferences.getString(PreferencesKey.currentWalletName) ?? '';
var password = '';
try {
@ -39,4 +45,25 @@ class AuthService with Store {
return decodedPin == pin;
}
void saveLastAuthTime() {
int timestamp = DateTime.now().millisecondsSinceEpoch;
sharedPreferences.setInt(PreferencesKey.lastAuthTimeMilliseconds, timestamp);
}
bool requireAuth() {
final timestamp = sharedPreferences.getInt(PreferencesKey.lastAuthTimeMilliseconds);
final duration = _durationToRequireAuth(timestamp ?? 0);
final requiredPinInterval = settingsStore.pinTimeOutDuration;
return duration >= requiredPinInterval.value;
}
int _durationToRequireAuth(int timestamp) {
DateTime before = DateTime.fromMillisecondsSinceEpoch(timestamp);
DateTime now = DateTime.now();
Duration timeDifference = now.difference(before);
return timeDifference.inMinutes;
}
}

View file

@ -309,7 +309,10 @@ Future setup(
getIt.registerFactory<AuthService>(() => AuthService(
secureStorage: getIt.get<FlutterSecureStorage>(),
sharedPreferences: getIt.get<SharedPreferences>()));
sharedPreferences: getIt.get<SharedPreferences>(),
settingsStore: getIt.get<SettingsStore>(),
),
);
getIt.registerFactory<AuthViewModel>(() => AuthViewModel(
getIt.get<AuthService>(),
@ -397,7 +400,10 @@ Future setup(
getIt.registerFactory(() => WalletListViewModel(
_walletInfoSource,
getIt.get<AppStore>(),
getIt.get<WalletLoadingService>()));
getIt.get<WalletLoadingService>(),
getIt.get<AuthService>(),
),
);
getIt.registerFactory(() =>
WalletListPage(walletListViewModel: getIt.get<WalletListViewModel>()));
@ -457,7 +463,7 @@ Future setup(
});
getIt.registerFactory(() {
return SecuritySettingsViewModel(getIt.get<SettingsStore>());
return SecuritySettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AuthService>());
});
getIt

View file

@ -0,0 +1,32 @@
import 'package:cake_wallet/generated/i18n.dart';
enum PinCodeRequiredDuration {
always(0),
tenminutes(10),
onehour(60);
const PinCodeRequiredDuration(this.value);
final int value;
static PinCodeRequiredDuration deserialize({required int raw}) =>
PinCodeRequiredDuration.values.firstWhere((e) => e.value == raw);
@override
String toString(){
String label = '';
switch (this) {
case PinCodeRequiredDuration.always:
label = S.current.always;
break;
case PinCodeRequiredDuration.tenminutes:
label = S.current.minutes_to_pin_code('10');
break;
case PinCodeRequiredDuration.onehour:
label = S.current.minutes_to_pin_code('60');
break;
}
return label;
}
}

View file

@ -27,6 +27,9 @@ class PreferencesKey {
static const shouldShowReceiveWarning = 'should_show_receive_warning';
static const shouldShowYatPopup = 'should_show_yat_popup';
static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1';
static const pinTimeOutDuration = 'pin_timeout_duration';
static const lastAuthTimeMilliseconds = 'last_auth_time_milliseconds';
static String moneroWalletUpdateV1Key(String name)
=> '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}';

View file

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/entities/language_service.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/ionia/ionia_category.dart';
@ -251,6 +252,7 @@ class AppState extends State<App> with SingleTickerProviderStateMixin {
Widget build(BuildContext context) {
return Observer(builder: (BuildContext context) {
final appStore = getIt.get<AppStore>();
final authService = getIt.get<AuthService>();
final settingsStore = appStore.settingsStore;
final statusBarColor = Colors.transparent;
final authenticationStore = getIt.get<AuthenticationStore>();
@ -275,6 +277,7 @@ class AppState extends State<App> with SingleTickerProviderStateMixin {
appStore: appStore,
authenticationStore: authenticationStore,
navigatorKey: navigatorKey,
authService: authService,
child: MaterialApp(
navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false,

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/routes.dart';
@ -9,17 +10,19 @@ import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:uni_links/uni_links.dart';
class Root extends StatefulWidget {
Root(
{required Key key,
Root({
required Key key,
required this.authenticationStore,
required this.appStore,
required this.child,
required this.navigatorKey})
: super(key: key);
required this.navigatorKey,
required this.authService,
}) : super(key: key);
final AuthenticationStore authenticationStore;
final AppStore appStore;
final GlobalKey<NavigatorState> navigatorKey;
final AuthService authService;
final Widget child;
@override
@ -30,18 +33,21 @@ class RootState extends State<Root> with WidgetsBindingObserver {
RootState()
: _isInactiveController = StreamController<bool>.broadcast(),
_isInactive = false,
_requestAuth = true,
_postFrameCallback = false;
Stream<bool> get isInactive => _isInactiveController.stream;
StreamController<bool> _isInactiveController;
bool _isInactive;
bool _postFrameCallback;
bool _requestAuth;
StreamSubscription<Uri?>? stream;
Uri? launchUri;
@override
void initState() {
_requestAuth = widget.authService.requireAuth();
_isInactiveController = StreamController<bool>.broadcast();
_isInactive = false;
_postFrameCallback = false;
@ -85,8 +91,11 @@ class RootState extends State<Root> with WidgetsBindingObserver {
return;
}
if (!_isInactive &&
widget.authenticationStore.state == AuthenticationState.allowed) {
setState(() {
_requestAuth = widget.authService.requireAuth();
});
if (!_isInactive && widget.authenticationStore.state == AuthenticationState.allowed) {
setState(() => _setInactive(true));
}
@ -98,7 +107,7 @@ class RootState extends State<Root> with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
if (_isInactive && !_postFrameCallback) {
if (_isInactive && !_postFrameCallback && _requestAuth) {
_postFrameCallback = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.navigatorKey.currentState?.pushNamed(Routes.unlock,

View file

@ -1,9 +1,11 @@
import 'package:cake_wallet/entities/pin_code_required_duration.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/generated/i18n.dart';
import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/view_model/settings/security_settings_view_model.dart';
@ -25,22 +27,26 @@ class SecurityBackupPage extends BasePage {
child: Column(mainAxisSize: MainAxisSize.min, children: [
SettingsCellWithArrow(
title: S.current.show_keys,
handler: (_) => Navigator.of(context).pushNamed(Routes.auth,
handler: (_) => _securitySettingsViewModel.checkPinCodeRiquired()
? Navigator.of(context).pushNamed(Routes.auth,
arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) {
if (isAuthenticatedSuccessfully) {
auth.close(route: Routes.showKeys);
}
}),
})
: Navigator.of(context).pushNamed(Routes.showKeys),
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
SettingsCellWithArrow(
title: S.current.create_backup,
handler: (_) => Navigator.of(context).pushNamed(Routes.auth,
handler: (_) => _securitySettingsViewModel.checkPinCodeRiquired()
? Navigator.of(context).pushNamed(Routes.auth,
arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) {
if (isAuthenticatedSuccessfully) {
auth.close(route: Routes.backup);
}
}),
})
: Navigator.of(context).pushNamed(Routes.backup),
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
SettingsCellWithArrow(
@ -65,10 +71,12 @@ class SecurityBackupPage extends BasePage {
arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) async {
if (isAuthenticatedSuccessfully) {
if (await _securitySettingsViewModel.biometricAuthenticated()) {
_securitySettingsViewModel.setAllowBiometricalAuthentication(isAuthenticatedSuccessfully);
_securitySettingsViewModel
.setAllowBiometricalAuthentication(isAuthenticatedSuccessfully);
}
} else {
_securitySettingsViewModel.setAllowBiometricalAuthentication(isAuthenticatedSuccessfully);
_securitySettingsViewModel
.setAllowBiometricalAuthentication(isAuthenticatedSuccessfully);
}
auth.close();
@ -78,6 +86,16 @@ class SecurityBackupPage extends BasePage {
}
});
}),
Observer(builder: (_) {
return SettingsPickerCell<PinCodeRequiredDuration>(
title: S.current.require_pin_after,
items: PinCodeRequiredDuration.values,
selectedItem: _securitySettingsViewModel.pinCodeRequiredDuration,
onItemSelected: (PinCodeRequiredDuration code) {
_securitySettingsViewModel.setPinCodeRequiredDuration(code);
},
);
}),
]),
);
}

View file

@ -220,15 +220,15 @@ class WalletListBodyState extends State<WalletListBody> {
}
Future<void> _loadWallet(WalletListItem wallet) async {
await Navigator.of(context).pushNamed(Routes.auth, arguments:
(bool isAuthenticatedSuccessfully, AuthPageState auth) async {
if (await widget.walletListViewModel.checkIfAuthRequired()) {
await Navigator.of(context).pushNamed(Routes.auth,
arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) async {
if (!isAuthenticatedSuccessfully) {
return;
}
try {
auth.changeProcessText(
S.of(context).wallet_list_loading_wallet(wallet.name));
auth.changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name));
await widget.walletListViewModel.loadWallet(wallet);
auth.hideProgressText();
auth.close();
@ -236,22 +236,38 @@ class WalletListBodyState extends State<WalletListBody> {
Navigator.of(context).pop();
});
} catch (e) {
auth.changeProcessText(S
.of(context)
.wallet_list_failed_to_load(wallet.name, e.toString()));
auth.changeProcessText(
S.of(context).wallet_list_failed_to_load(wallet.name, e.toString()));
}
});
} else {
try {
changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name));
await widget.walletListViewModel.loadWallet(wallet);
hideProgressText();
Navigator.of(context).pop();
} catch (e) {
changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString()));
}
}
}
Future<void> _removeWallet(WalletListItem wallet) async {
await Navigator.of(context).pushNamed(Routes.auth, arguments:
(bool isAuthenticatedSuccessfully, AuthPageState auth) async {
if (widget.walletListViewModel.checkIfAuthRequired()) {
await Navigator.of(context).pushNamed(Routes.auth,
arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) async {
if (!isAuthenticatedSuccessfully) {
return;
}
_onSuccessfulAuth(wallet, auth);
});
} else {
_onSuccessfulAuth(wallet, null);
}
}
void _onSuccessfulAuth(WalletListItem wallet, AuthPageState? auth) async {
bool confirmed = false;
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
@ -270,18 +286,22 @@ class WalletListBodyState extends State<WalletListBody> {
if (confirmed) {
try {
auth.changeProcessText(
S.of(context).wallet_list_removing_wallet(wallet.name));
auth != null
? auth.changeProcessText(S.of(context).wallet_list_removing_wallet(wallet.name))
: changeProcessText(S.of(context).wallet_list_removing_wallet(wallet.name));
await widget.walletListViewModel.remove(wallet);
} catch (e) {
auth.changeProcessText(S
.of(context)
.wallet_list_failed_to_remove(wallet.name, e.toString()));
auth != null
? auth.changeProcessText(
S.of(context).wallet_list_failed_to_remove(wallet.name, e.toString()),
)
: changeProcessText(
S.of(context).wallet_list_failed_to_remove(wallet.name, e.toString()),
);
}
}
auth.close();
});
auth?.close();
}
void changeProcessText(String text) {

View file

@ -1,4 +1,5 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/entities/pin_code_required_duration.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cake_wallet/themes/theme_base.dart';
@ -17,7 +18,6 @@ import 'package:cw_core/node.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/entities/action_list_display_mode.dart';
import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
part 'settings_store.g.dart';
@ -41,6 +41,7 @@ abstract class SettingsStoreBase with Store {
required this.shouldShowYatPopup,
required this.isBitcoinBuyEnabled,
required this.actionlistDisplayMode,
required this.pinTimeOutDuration,
TransactionPriority? initialBitcoinTransactionPriority,
TransactionPriority? initialMoneroTransactionPriority,
TransactionPriority? initialHavenTransactionPriority,
@ -141,6 +142,11 @@ abstract class SettingsStoreBase with Store {
(String languageCode) => sharedPreferences.setString(
PreferencesKey.currentLanguageCode, languageCode));
reaction(
(_) => pinTimeOutDuration,
(PinCodeRequiredDuration pinCodeInterval) => sharedPreferences.setInt(
PreferencesKey.pinTimeOutDuration, pinCodeInterval.value));
reaction(
(_) => balanceDisplayMode,
(BalanceDisplayMode mode) => sharedPreferences.setInt(
@ -162,6 +168,7 @@ abstract class SettingsStoreBase with Store {
static const defaultPinLength = 4;
static const defaultActionsMode = 11;
static const defaultPinCodeTimeOutDuration = PinCodeRequiredDuration.tenminutes;
@observable
FiatCurrency fiatCurrency;
@ -193,6 +200,9 @@ abstract class SettingsStoreBase with Store {
@observable
int pinCodeLength;
@observable
PinCodeRequiredDuration pinTimeOutDuration;
@computed
ThemeData get theme => currentTheme.themeData;
@ -288,6 +298,11 @@ abstract class SettingsStoreBase with Store {
sharedPreferences.getInt(PreferencesKey.displayActionListModeKey) ??
defaultActionsMode));
var pinLength = sharedPreferences.getInt(PreferencesKey.currentPinLength);
final timeOutDuration = sharedPreferences.getInt(PreferencesKey.pinTimeOutDuration);
final pinCodeTimeOutDuration = timeOutDuration != null
? PinCodeRequiredDuration.deserialize(raw: timeOutDuration)
: defaultPinCodeTimeOutDuration;
// If no value
if (pinLength == null || pinLength == 0) {
pinLength = defaultPinLength;
@ -343,6 +358,7 @@ abstract class SettingsStoreBase with Store {
initialTheme: savedTheme,
actionlistDisplayMode: actionListDisplayMode,
initialPinLength: pinLength,
pinTimeOutDuration: pinCodeTimeOutDuration,
initialLanguageCode: savedLanguageCode,
initialMoneroTransactionPriority: moneroTransactionPriority,
initialBitcoinTransactionPriority: bitcoinTransactionPriority,

View file

@ -14,10 +14,12 @@ part 'auth_view_model.g.dart';
class AuthViewModel = AuthViewModelBase with _$AuthViewModel;
abstract class AuthViewModelBase with Store {
AuthViewModelBase(this._authService, this._sharedPreferences,
this._settingsStore, this._biometricAuth)
AuthViewModelBase(
this._authService, this._sharedPreferences, this._settingsStore, this._biometricAuth)
: _failureCounter = 0,
state = InitialExecutionState();
state = InitialExecutionState() {
reaction((_) => state, _saveLastAuthTime);
}
static const maxFailedLogins = 3;
static const banTimeout = 180; // 3 minutes
@ -28,8 +30,7 @@ abstract class AuthViewModelBase with Store {
int get pinLength => _settingsStore.pinCodeLength;
bool get isBiometricalAuthenticationAllowed =>
_settingsStore.allowBiometricalAuthentication;
bool get isBiometricalAuthenticationAllowed => _settingsStore.allowBiometricalAuthentication;
@observable
int _failureCounter;
@ -114,8 +115,14 @@ abstract class AuthViewModelBase with Store {
state = ExecutedSuccessfullyState();
}
}
} catch(e) {
} catch (e) {
state = FailureState(e.toString());
}
}
void _saveLastAuthTime(ExecutionState state) {
if (state is ExecutedSuccessfullyState) {
_authService.saveLastAuthTime();
}
}
}

View file

@ -1,4 +1,6 @@
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/entities/biometric_auth.dart';
import 'package:cake_wallet/entities/pin_code_required_duration.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:mobx/mobx.dart';
@ -7,19 +9,33 @@ part 'security_settings_view_model.g.dart';
class SecuritySettingsViewModel = SecuritySettingsViewModelBase with _$SecuritySettingsViewModel;
abstract class SecuritySettingsViewModelBase with Store {
SecuritySettingsViewModelBase(this._settingsStore) : _biometricAuth = BiometricAuth();
SecuritySettingsViewModelBase(
this._settingsStore,
this._authService,
) : _biometricAuth = BiometricAuth();
final BiometricAuth _biometricAuth;
final SettingsStore _settingsStore;
final AuthService _authService;
@computed
bool get allowBiometricalAuthentication => _settingsStore.allowBiometricalAuthentication;
@computed
PinCodeRequiredDuration get pinCodeRequiredDuration => _settingsStore.pinTimeOutDuration;
@action
Future<bool> biometricAuthenticated() async {
return await _biometricAuth.canCheckBiometrics() && await _biometricAuth.isAuthenticated();
}
@action
void setAllowBiometricalAuthentication(bool value) => _settingsStore.allowBiometricalAuthentication = value;
void setAllowBiometricalAuthentication(bool value) =>
_settingsStore.allowBiometricalAuthentication = value;
@action
setPinCodeRequiredDuration(PinCodeRequiredDuration duration) =>
_settingsStore.pinTimeOutDuration = duration;
bool checkPinCodeRiquired() => _authService.requireAuth();
}

View file

@ -1,5 +1,5 @@
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/wallet_loading_service.dart';
import 'package:cake_wallet/view_model/wallet_new_vm.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/di.dart';
@ -15,9 +15,12 @@ part 'wallet_list_view_model.g.dart';
class WalletListViewModel = WalletListViewModelBase with _$WalletListViewModel;
abstract class WalletListViewModelBase with Store {
WalletListViewModelBase(this._walletInfoSource, this._appStore,
this._walletLoadingService)
: wallets = ObservableList<WalletListItem>() {
WalletListViewModelBase(
this._walletInfoSource,
this._appStore,
this._walletLoadingService,
this._authService,
) : wallets = ObservableList<WalletListItem>() {
_updateList();
}
@ -27,6 +30,7 @@ abstract class WalletListViewModelBase with Store {
final AppStore _appStore;
final Box<WalletInfo> _walletInfoSource;
final WalletLoadingService _walletLoadingService;
final AuthService _authService;
WalletType get currentWalletType => _appStore.wallet!.type;
@ -47,12 +51,20 @@ abstract class WalletListViewModelBase with Store {
void _updateList() {
wallets.clear();
wallets.addAll(_walletInfoSource.values.map((info) => WalletListItem(
wallets.addAll(
_walletInfoSource.values.map(
(info) => WalletListItem(
name: info.name,
type: info.type,
key: info.key,
isCurrent: info.name == _appStore.wallet!.name &&
info.type == _appStore.wallet!.type,
isEnabled: availableWalletTypes.contains(info.type))));
isCurrent: info.name == _appStore.wallet!.name && info.type == _appStore.wallet!.type,
isEnabled: availableWalletTypes.contains(info.type),
),
),
);
}
bool checkIfAuthRequired() {
return _authService.requireAuth();
}
}

View file

@ -661,6 +661,9 @@
"privacy": "Datenschutz",
"display_settings": "Anzeigeeinstellungen",
"other_settings": "Andere Einstellungen",
"require_pin_after": "PIN anfordern nach",
"always": "immer",
"minutes_to_pin_code": "${minute} Minuten",
"disable_exchange": "Exchange deaktivieren",
"advanced_privacy_settings": "Erweiterte Datenschutzeinstellungen",
"settings_can_be_changed_later": "Diese Einstellungen können später in den App-Einstellungen geändert werden",

View file

@ -661,6 +661,9 @@
"privacy": "Privacy",
"display_settings": "Display settings",
"other_settings": "Other settings",
"require_pin_after": "Require PIN after",
"always": "Always",
"minutes_to_pin_code": "${minute} minutes",
"disable_exchange": "Disable exchange",
"advanced_privacy_settings": "Advanced Privacy Settings",
"settings_can_be_changed_later": "These settings can be changed later in the app settings",

View file

@ -661,6 +661,9 @@
"privacy": "Privacidad",
"display_settings": "Configuración de pantalla",
"other_settings": "Otras configuraciones",
"require_pin_after": "Requerir PIN después de",
"always": "siempre",
"minutes_to_pin_code": "${minute} minutos",
"disable_exchange": "Deshabilitar intercambio",
"advanced_privacy_settings": "Configuración avanzada de privacidad",
"settings_can_be_changed_later": "Estas configuraciones se pueden cambiar más tarde en la configuración de la aplicación",

View file

@ -659,6 +659,9 @@
"privacy": "Confidentialité",
"display_settings": "Paramètres d'affichage",
"other_settings": "Autres paramètres",
"require_pin_after": "NIP requis après",
"always": "toujours",
"minutes_to_pin_code": "${minute} minutes",
"disable_exchange": "Désactiver l'échange",
"advanced_privacy_settings": "Paramètres de confidentialité avancés",
"settings_can_be_changed_later": "Ces paramètres peuvent être modifiés ultérieurement dans les paramètres de l'application",

View file

@ -661,6 +661,9 @@
"privacy": "गोपनीयता",
"display_settings": "प्रदर्शन सेटिंग्स",
"other_settings": "अन्य सेटिंग्स",
"require_pin_after": "इसके बाद पिन आवश्यक है",
"always": "हमेशा",
"minutes_to_pin_code": "${minute} मिनट",
"disable_exchange": "एक्सचेंज अक्षम करें",
"advanced_privacy_settings": "उन्नत गोपनीयता सेटिंग्स",
"settings_can_be_changed_later": "इन सेटिंग्स को बाद में ऐप सेटिंग में बदला जा सकता है",

View file

@ -661,6 +661,9 @@
"privacy": "Privatnost",
"display_settings": "Postavke zaslona",
"other_settings": "Ostale postavke",
"require_pin_after": "Zahtijevaj PIN nakon",
"always": "Uvijek",
"minutes_to_pin_code": "${minute} minuta",
"disable_exchange": "Onemogući exchange",
"advanced_privacy_settings": "Napredne postavke privatnosti",
"settings_can_be_changed_later": "Te se postavke mogu promijeniti kasnije u postavkama aplikacije",

View file

@ -661,6 +661,9 @@
"privacy": "Privacy",
"display_settings": "Impostazioni di visualizzazione",
"other_settings": "Altre impostazioni",
"require_pin_after": "Richiedi PIN dopo",
"always": "sempre",
"minutes_to_pin_code": "${minute} minuti",
"disable_exchange": "Disabilita scambio",
"advanced_privacy_settings": "Impostazioni avanzate sulla privacy",
"settings_can_be_changed_later": "Queste impostazioni possono essere modificate in seguito nelle impostazioni dell'app",

View file

@ -661,6 +661,9 @@
"privacy": "プライバシー",
"display_settings": "表示設定",
"other_settings": "その他の設定",
"require_pin_after": "後に PIN が必要",
"always": "いつも",
"minutes_to_pin_code": "${minute} 分",
"disable_exchange": "交換を無効にする",
"advanced_privacy_settings": "高度なプライバシー設定",
"settings_can_be_changed_later": "これらの設定は、後でアプリの設定で変更できます",

View file

@ -661,6 +661,9 @@
"privacy": "프라이버시",
"display_settings": "디스플레이 설정",
"other_settings": "기타 설정",
"require_pin_after": "다음 이후에 PIN 필요",
"always": "언제나",
"minutes_to_pin_code": "${minute}분",
"disable_exchange": "교환 비활성화",
"advanced_privacy_settings": "고급 개인 정보 설정",
"settings_can_be_changed_later": "이 설정은 나중에 앱 설정에서 변경할 수 있습니다.",

View file

@ -661,6 +661,9 @@
"privacy": "Privacy",
"display_settings": "Weergave-instellingen",
"other_settings": "Andere instellingen",
"require_pin_after": "Pincode vereist na",
"always": "altijd",
"minutes_to_pin_code": "${minute} minuten",
"disable_exchange": "Uitwisseling uitschakelen",
"advanced_privacy_settings": "Geavanceerde privacy-instellingen",
"settings_can_be_changed_later": "Deze instellingen kunnen later worden gewijzigd in de app-instellingen",

View file

@ -661,6 +661,9 @@
"privacy": "Prywatność",
"display_settings": "Ustawienia wyświetlania",
"other_settings": "Inne ustawienia",
"require_pin_after": "Wymagaj kodu PIN po",
"always": "zawsze",
"minutes_to_pin_code": "${minute} minut",
"disable_exchange": "Wyłącz wymianę",
"advanced_privacy_settings": "Zaawansowane ustawienia prywatności",
"settings_can_be_changed_later": "Te ustawienia można później zmienić w ustawieniach aplikacji",

View file

@ -660,6 +660,9 @@
"privacy": "Privacidade",
"display_settings": "Configurações de exibição",
"other_settings": "Outras configurações",
"require_pin_after": "Exigir PIN após",
"always": "sempre",
"minutes_to_pin_code": "${minute} minutos",
"disable_exchange": "Desativar troca",
"advanced_privacy_settings": "Configurações de privacidade avançadas",
"settings_can_be_changed_later": "Essas configurações podem ser alteradas posteriormente nas configurações do aplicativo",

View file

@ -661,6 +661,9 @@
"privacy": "Конфиденциальность",
"display_settings": "Настройки отображения",
"other_settings": "Другие настройки",
"require_pin_after": "Требовать ПИН после",
"always": "всегда",
"minutes_to_pin_code": "${minute} минут",
"disable_exchange": "Отключить обмен",
"advanced_privacy_settings": "Расширенные настройки конфиденциальности",
"settings_can_be_changed_later": "Эти настройки можно изменить позже в настройках приложения.",

View file

@ -660,6 +660,9 @@
"privacy": "Конфіденційність",
"display_settings": "Налаштування дисплея",
"other_settings": "Інші налаштування",
"require_pin_after": "Вимагати PIN після",
"always": "Завжди",
"minutes_to_pin_code": "${minute} хвилин",
"disable_exchange": "Вимкнути exchange",
"advanced_privacy_settings": "Розширені налаштування конфіденційності",
"settings_can_be_changed_later": "Ці параметри можна змінити пізніше в налаштуваннях програми",

View file

@ -659,6 +659,9 @@
"privacy":"隐私",
"display_settings": "显示设置",
"other_settings": "其他设置",
"require_pin_after": "之后需要 PIN",
"always": "总是",
"minutes_to_pin_code": "${minute} 分钟",
"disable_exchange": "禁用交换",
"advanced_privacy_settings": "高级隐私设置",
"settings_can_be_changed_later": "稍后可以在应用设置中更改这些设置",