[CW-225] Add pin timeout setting

This commit is contained in:
Godwin Asuquo 2022-11-22 22:52:28 +02:00
parent c67e8c5037
commit 818a8afe20
25 changed files with 250 additions and 66 deletions

View file

@ -4,6 +4,8 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/secret_store_key.dart'; import 'package:cake_wallet/entities/secret_store_key.dart';
import 'package:cake_wallet/entities/encrypt.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 { class AuthService with Store {
AuthService({required this.secureStorage, required this.sharedPreferences}); AuthService({required this.secureStorage, required this.sharedPreferences});
@ -39,4 +41,26 @@ class AuthService with Store {
return decodedPin == pin; 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 = getIt.get<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

@ -440,7 +440,8 @@ Future setup(
getIt.registerFactory(() { getIt.registerFactory(() {
final appStore = getIt.get<AppStore>(); final appStore = getIt.get<AppStore>();
final yatStore = getIt.get<YatStore>(); final yatStore = getIt.get<YatStore>();
return SettingsViewModel(appStore.settingsStore, yatStore, appStore.wallet!); final authService = getIt.get<AuthService>();
return SettingsViewModel(appStore.settingsStore, yatStore, authService, appStore.wallet!);
}); });
getIt 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

@ -23,6 +23,9 @@ class PreferencesKey {
static const shouldShowReceiveWarning = 'should_show_receive_warning'; static const shouldShowReceiveWarning = 'should_show_receive_warning';
static const shouldShowYatPopup = 'should_show_yat_popup'; static const shouldShowYatPopup = 'should_show_yat_popup';
static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1'; 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) static String moneroWalletUpdateV1Key(String name)
=> '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}'; => '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}';

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/routes.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.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/base_page.dart';
import 'package:cake_wallet/generated/i18n.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/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_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/screens/settings/widgets/settings_switcher_cell.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/view_model/settings/settings_view_model.dart'; import 'package:cake_wallet/view_model/settings/settings_view_model.dart';
@ -20,27 +22,28 @@ class SecurityBackupPage extends BasePage {
@override @override
Widget body(BuildContext context) { Widget body(BuildContext context) {
return Container( return Container(
padding: EdgeInsets.only(top: 10), padding: EdgeInsets.only(top: 10),
child: Column(mainAxisSize: MainAxisSize.min, children: [ child: Column(mainAxisSize: MainAxisSize.min, children: [
SettingsCellWithArrow( SettingsCellWithArrow(
title: S.current.show_keys, title: S.current.show_keys,
handler: (_) => Navigator.of(context).pushNamed(Routes.auth, handler: (_) => settingsViewModel.checkPinCodeRiquired() ? Navigator.of(context).pushNamed(Routes.auth,
arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) { arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) {
if (isAuthenticatedSuccessfully) { if (isAuthenticatedSuccessfully) {
auth.close(route: Routes.showKeys); auth.close(route: Routes.showKeys);
} }
}), }) : Navigator.of(context).pushNamed(Routes.showKeys),
), ),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
SettingsCellWithArrow( SettingsCellWithArrow(
title: S.current.create_backup, title: S.current.create_backup,
handler: (_) => Navigator.of(context).pushNamed(Routes.auth, handler: (_) => settingsViewModel.checkPinCodeRiquired() ? Navigator.of(context).pushNamed(Routes.auth,
arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) { arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) {
if (isAuthenticatedSuccessfully) { if (isAuthenticatedSuccessfully) {
auth.close(route: Routes.backup); auth.close(route: Routes.backup);
} }
}), }) : Navigator.of(context).pushNamed(Routes.backup),
), ),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
SettingsCellWithArrow( SettingsCellWithArrow(
@ -56,7 +59,9 @@ class SecurityBackupPage extends BasePage {
})), })),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
Observer(builder: (_) { Observer(builder: (_) {
return SettingsSwitcherCell( return Column(
children: [
SettingsSwitcherCell(
title: S.current.settings_allow_biometrical_authentication, title: S.current.settings_allow_biometrical_authentication,
value: settingsViewModel.allowBiometricalAuthentication, value: settingsViewModel.allowBiometricalAuthentication,
onValueChange: (BuildContext context, bool value) { onValueChange: (BuildContext context, bool value) {
@ -76,8 +81,19 @@ class SecurityBackupPage extends BasePage {
} else { } else {
settingsViewModel.setAllowBiometricalAuthentication(value); settingsViewModel.setAllowBiometricalAuthentication(value);
} }
});
}), }),
SettingsPickerCell<PinCodeRequiredDuration>(
title: S.current.require_pin_after,
items: PinCodeRequiredDuration.values,
selectedItem: settingsViewModel.pinCodeRequiredDuration,
onItemSelected: (PinCodeRequiredDuration code) {
settingsViewModel.setPinCodeRequiredDuration(code);
},
),
],
);
}),
]), ]),
); );
} }

View file

@ -220,6 +220,7 @@ class WalletListBodyState extends State<WalletListBody> {
} }
Future<void> _loadWallet(WalletListItem wallet) async { Future<void> _loadWallet(WalletListItem wallet) async {
if(await widget.walletListViewModel.checkIfAuthRequired()){
await Navigator.of(context).pushNamed(Routes.auth, arguments: await Navigator.of(context).pushNamed(Routes.auth, arguments:
(bool isAuthenticatedSuccessfully, AuthPageState auth) async { (bool isAuthenticatedSuccessfully, AuthPageState auth) async {
if (!isAuthenticatedSuccessfully) { if (!isAuthenticatedSuccessfully) {
@ -241,17 +242,36 @@ class WalletListBodyState extends State<WalletListBody> {
.wallet_list_failed_to_load(wallet.name, e.toString())); .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 { Future<void> _removeWallet(WalletListItem wallet) async {
if(widget.walletListViewModel.checkIfAuthRequired()){
await Navigator.of(context).pushNamed(Routes.auth, arguments: await Navigator.of(context).pushNamed(Routes.auth, arguments:
(bool isAuthenticatedSuccessfully, AuthPageState auth) async { (bool isAuthenticatedSuccessfully, AuthPageState auth) async {
if (!isAuthenticatedSuccessfully) { if (!isAuthenticatedSuccessfully) {
return; return;
} }
_onSuccessfulAuth(wallet, auth);
});
}else{
_onSuccessfulAuth(wallet, null);
}
}
_onSuccessfulAuth(WalletListItem wallet, AuthPageState? auth)async{
bool confirmed = false; bool confirmed = false;
await showPopUp<void>( await showPopUp<void>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
@ -270,18 +290,23 @@ class WalletListBodyState extends State<WalletListBody> {
if (confirmed) { if (confirmed) {
try { try {
auth != null ?
auth.changeProcessText( auth.changeProcessText(
S.of(context).wallet_list_removing_wallet(wallet.name)); S.of(context).wallet_list_removing_wallet(wallet.name))
: changeProcessText( S.of(context).wallet_list_removing_wallet(wallet.name));
await widget.walletListViewModel.remove(wallet); await widget.walletListViewModel.remove(wallet);
} catch (e) { } catch (e) {
auth.changeProcessText(S auth != null ?
.of(context) auth.changeProcessText(
.wallet_list_failed_to_remove(wallet.name, e.toString())); 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) { void changeProcessText(String text) {

View file

@ -1,9 +1,9 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart'; 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:cake_wallet/entities/preferences_key.dart';
import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/transaction_priority.dart';
import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/themes/theme_list.dart'; import 'package:cake_wallet/themes/theme_list.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
@ -17,7 +17,6 @@ import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cw_core/node.dart'; import 'package:cw_core/node.dart';
import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/entities/action_list_display_mode.dart'; import 'package:cake_wallet/entities/action_list_display_mode.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
part 'settings_store.g.dart'; part 'settings_store.g.dart';
@ -39,6 +38,7 @@ abstract class SettingsStoreBase with Store {
required this.shouldShowYatPopup, required this.shouldShowYatPopup,
required this.isBitcoinBuyEnabled, required this.isBitcoinBuyEnabled,
required this.actionlistDisplayMode, required this.actionlistDisplayMode,
required this.pinTimeOutDuration,
TransactionPriority? initialBitcoinTransactionPriority, TransactionPriority? initialBitcoinTransactionPriority,
TransactionPriority? initialMoneroTransactionPriority}) TransactionPriority? initialMoneroTransactionPriority})
: nodes = ObservableMap<WalletType, Node>.of(nodes), : nodes = ObservableMap<WalletType, Node>.of(nodes),
@ -108,6 +108,11 @@ abstract class SettingsStoreBase with Store {
(String languageCode) => sharedPreferences.setString( (String languageCode) => sharedPreferences.setString(
PreferencesKey.currentLanguageCode, languageCode)); PreferencesKey.currentLanguageCode, languageCode));
reaction(
(_) => pinTimeOutDuration,
(PinCodeRequiredDuration pinCodeInterval) => sharedPreferences.setInt(
PreferencesKey.pinTimeOutDuration, pinCodeInterval.value));
reaction( reaction(
(_) => balanceDisplayMode, (_) => balanceDisplayMode,
(BalanceDisplayMode mode) => sharedPreferences.setInt( (BalanceDisplayMode mode) => sharedPreferences.setInt(
@ -124,6 +129,7 @@ abstract class SettingsStoreBase with Store {
static const defaultPinLength = 4; static const defaultPinLength = 4;
static const defaultActionsMode = 11; static const defaultActionsMode = 11;
static const defaultPinCodeTimeOutDuration = 10;
@observable @observable
FiatCurrency fiatCurrency; FiatCurrency fiatCurrency;
@ -149,6 +155,9 @@ abstract class SettingsStoreBase with Store {
@observable @observable
int pinCodeLength; int pinCodeLength;
@observable
PinCodeRequiredDuration pinTimeOutDuration;
@computed @computed
ThemeData get theme => currentTheme.themeData; ThemeData get theme => currentTheme.themeData;
@ -227,13 +236,15 @@ abstract class SettingsStoreBase with Store {
: ThemeType.bright.index; : ThemeType.bright.index;
final savedTheme = ThemeList.deserialize( final savedTheme = ThemeList.deserialize(
raw: sharedPreferences.getInt(PreferencesKey.currentTheme) ?? raw: sharedPreferences.getInt(PreferencesKey.currentTheme) ??
legacyTheme ?? legacyTheme);
0);
final actionListDisplayMode = ObservableList<ActionListDisplayMode>(); final actionListDisplayMode = ObservableList<ActionListDisplayMode>();
actionListDisplayMode.addAll(deserializeActionlistDisplayModes( actionListDisplayMode.addAll(deserializeActionlistDisplayModes(
sharedPreferences.getInt(PreferencesKey.displayActionListModeKey) ?? sharedPreferences.getInt(PreferencesKey.displayActionListModeKey) ??
defaultActionsMode)); defaultActionsMode));
var pinLength = sharedPreferences.getInt(PreferencesKey.currentPinLength); var pinLength = sharedPreferences.getInt(PreferencesKey.currentPinLength);
final pinCodeTimeOutDuration = PinCodeRequiredDuration.deserialize(raw: sharedPreferences.getInt(PreferencesKey.pinTimeOutDuration)
?? defaultPinCodeTimeOutDuration);
// If no value // If no value
if (pinLength == null || pinLength == 0) { if (pinLength == null || pinLength == 0) {
pinLength = defaultPinLength; pinLength = defaultPinLength;
@ -287,6 +298,7 @@ abstract class SettingsStoreBase with Store {
initialTheme: savedTheme, initialTheme: savedTheme,
actionlistDisplayMode: actionListDisplayMode, actionlistDisplayMode: actionListDisplayMode,
initialPinLength: pinLength, initialPinLength: pinLength,
pinTimeOutDuration: pinCodeTimeOutDuration,
initialLanguageCode: savedLanguageCode, initialLanguageCode: savedLanguageCode,
initialMoneroTransactionPriority: moneroTransactionPriority, initialMoneroTransactionPriority: moneroTransactionPriority,
initialBitcoinTransactionPriority: bitcoinTransactionPriority, initialBitcoinTransactionPriority: bitcoinTransactionPriority,

View file

@ -17,7 +17,9 @@ abstract class AuthViewModelBase with Store {
AuthViewModelBase(this._authService, this._sharedPreferences, AuthViewModelBase(this._authService, this._sharedPreferences,
this._settingsStore, this._biometricAuth) this._settingsStore, this._biometricAuth)
: _failureCounter = 0, : _failureCounter = 0,
state = InitialExecutionState(); state = InitialExecutionState(){
reaction((_) => state, _saveLastAuthTime);
}
static const maxFailedLogins = 3; static const maxFailedLogins = 3;
static const banTimeout = 180; // 3 minutes static const banTimeout = 180; // 3 minutes
@ -118,4 +120,10 @@ abstract class AuthViewModelBase with Store {
state = FailureState(e.toString()); state = FailureState(e.toString());
} }
} }
void _saveLastAuthTime(ExecutionState state){
if(state is ExecutedSuccessfullyState){
_authService.saveLastAuthTime();
}
}
} }

View file

@ -1,3 +1,5 @@
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/entities/pin_code_required_duration.dart';
import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:package_info/package_info.dart'; import 'package:package_info/package_info.dart';
@ -41,6 +43,7 @@ abstract class SettingsViewModelBase with Store {
SettingsViewModelBase( SettingsViewModelBase(
this._settingsStore, this._settingsStore,
this._yatStore, this._yatStore,
this._authService,
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
TransactionInfo> TransactionInfo>
wallet) wallet)
@ -94,6 +97,10 @@ abstract class SettingsViewModelBase with Store {
@computed @computed
FiatCurrency get fiatCurrency => _settingsStore.fiatCurrency; FiatCurrency get fiatCurrency => _settingsStore.fiatCurrency;
@computed
PinCodeRequiredDuration get pinCodeRequiredDuration =>
_settingsStore.pinTimeOutDuration;
@computed @computed
String get languageCode => _settingsStore.languageCode; String get languageCode => _settingsStore.languageCode;
@ -135,6 +142,7 @@ abstract class SettingsViewModelBase with Store {
final Map<String, String> itemHeaders; final Map<String, String> itemHeaders;
final SettingsStore _settingsStore; final SettingsStore _settingsStore;
final YatStore _yatStore; final YatStore _yatStore;
final AuthService _authService;
final WalletType walletType; final WalletType walletType;
final BiometricAuth _biometricAuth; final BiometricAuth _biometricAuth;
final WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, final WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
@ -207,6 +215,10 @@ abstract class SettingsViewModelBase with Store {
} }
} }
@action
setPinCodeRequiredDuration(PinCodeRequiredDuration duration) =>
_settingsStore.pinTimeOutDuration = duration;
String getDisplayPriority(dynamic priority) { String getDisplayPriority(dynamic priority) {
final _priority = priority as TransactionPriority; final _priority = priority as TransactionPriority;
@ -222,4 +234,6 @@ abstract class SettingsViewModelBase with Store {
void onDisplayPrioritySelected(TransactionPriority priority) => void onDisplayPrioritySelected(TransactionPriority priority) =>
_settingsStore.priority[_wallet.type] = priority; _settingsStore.priority[_wallet.type] = priority;
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/core/wallet_loading_service.dart';
import 'package:cake_wallet/view_model/wallet_new_vm.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/di.dart';
@ -55,4 +55,8 @@ abstract class WalletListViewModelBase with Store {
info.type == _appStore.wallet!.type, info.type == _appStore.wallet!.type,
isEnabled: availableWalletTypes.contains(info.type)))); isEnabled: availableWalletTypes.contains(info.type))));
} }
bool checkIfAuthRequired(){
return getIt.get<AuthService>().requireAuth();
}
} }

View file

@ -655,5 +655,8 @@
"privacy_settings": "Datenschutzeinstellungen", "privacy_settings": "Datenschutzeinstellungen",
"privacy": "Datenschutz", "privacy": "Datenschutz",
"display_settings": "Anzeigeeinstellungen", "display_settings": "Anzeigeeinstellungen",
"other_settings": "Andere Einstellungen" "other_settings": "Andere Einstellungen",
"require_pin_after": "PIN anfordern nach",
"always": "immer",
"minutes_to_pin_code": "${minute} Minuten"
} }

View file

@ -658,5 +658,8 @@
"privacy_settings": "Privacy settings", "privacy_settings": "Privacy settings",
"privacy": "Privacy", "privacy": "Privacy",
"display_settings": "Display settings", "display_settings": "Display settings",
"other_settings": "Other settings" "other_settings": "Other settings",
"require_pin_after": "Require PIN after",
"always": "Always",
"minutes_to_pin_code": "${minute} minutes"
} }

View file

@ -655,5 +655,8 @@
"privacy_settings": "Configuración de privacidad", "privacy_settings": "Configuración de privacidad",
"privacy": "Privacidad", "privacy": "Privacidad",
"display_settings": "Configuración de pantalla", "display_settings": "Configuración de pantalla",
"other_settings": "Otras configuraciones" "other_settings": "Otras configuraciones",
"require_pin_after": "Requerir PIN después de",
"always": "siempre",
"minutes_to_pin_code": "${minute} minutos"
} }

View file

@ -653,5 +653,8 @@
"privacy_settings": "Paramètres de confidentialité", "privacy_settings": "Paramètres de confidentialité",
"privacy": "Confidentialité", "privacy": "Confidentialité",
"display_settings": "Paramètres d'affichage", "display_settings": "Paramètres d'affichage",
"other_settings": "Autres paramètres" "other_settings": "Autres paramètres",
"require_pin_after": "NIP requis après",
"always": "toujours",
"minutes_to_pin_code": "${minute} minutes"
} }

View file

@ -655,5 +655,8 @@
"privacy_settings": "गोपनीयता सेटिंग्स", "privacy_settings": "गोपनीयता सेटिंग्स",
"privacy": "गोपनीयता", "privacy": "गोपनीयता",
"display_settings": "प्रदर्शन सेटिंग्स", "display_settings": "प्रदर्शन सेटिंग्स",
"other_settings": "अन्य सेटिंग्स" "other_settings": "अन्य सेटिंग्स",
"require_pin_after": "इसके बाद पिन आवश्यक है",
"always": "हमेशा",
"minutes_to_pin_code": "${minute} मिनट"
} }

View file

@ -655,5 +655,8 @@
"privacy_settings": "Postavke privatnosti", "privacy_settings": "Postavke privatnosti",
"privacy": "Privatnost", "privacy": "Privatnost",
"display_settings": "Postavke zaslona", "display_settings": "Postavke zaslona",
"other_settings": "Ostale postavke" "other_settings": "Ostale postavke",
"require_pin_after": "Zahtijevaj PIN nakon",
"always": "Uvijek",
"minutes_to_pin_code": "${minute} minuta"
} }

View file

@ -655,5 +655,8 @@
"privacy_settings": "Impostazioni privacy", "privacy_settings": "Impostazioni privacy",
"privacy": "Privacy", "privacy": "Privacy",
"display_settings": "Impostazioni di visualizzazione", "display_settings": "Impostazioni di visualizzazione",
"other_settings": "Altre impostazioni" "other_settings": "Altre impostazioni",
"require_pin_after": "Richiedi PIN dopo",
"always": "sempre",
"minutes_to_pin_code": "${minute} minuti"
} }

View file

@ -655,5 +655,8 @@
"privacy_settings": "プライバシー設定", "privacy_settings": "プライバシー設定",
"privacy": "プライバシー", "privacy": "プライバシー",
"display_settings": "表示設定", "display_settings": "表示設定",
"other_settings": "その他の設定" "other_settings": "その他の設定",
"require_pin_after": "後に PIN が必要",
"always": "いつも",
"minutes_to_pin_code": "${minute} 分"
} }

View file

@ -655,5 +655,8 @@
"privacy_settings": "개인정보 설정", "privacy_settings": "개인정보 설정",
"privacy": "프라이버시", "privacy": "프라이버시",
"display_settings": "디스플레이 설정", "display_settings": "디스플레이 설정",
"other_settings": "기타 설정" "other_settings": "기타 설정",
"require_pin_after": "다음 이후에 PIN 필요",
"always": "언제나",
"minutes_to_pin_code": "${minute}분"
} }

View file

@ -655,5 +655,8 @@
"privacy_settings": "Privacy-instellingen", "privacy_settings": "Privacy-instellingen",
"privacy": "Privacy", "privacy": "Privacy",
"display_settings": "Weergave-instellingen", "display_settings": "Weergave-instellingen",
"other_settings": "Andere instellingen" "other_settings": "Andere instellingen",
"require_pin_after": "Pincode vereist na",
"always": "altijd",
"minutes_to_pin_code": "${minute} minuten"
} }

View file

@ -655,5 +655,8 @@
"privacy_settings": "Ustawienia prywatności", "privacy_settings": "Ustawienia prywatności",
"privacy": "Prywatność", "privacy": "Prywatność",
"display_settings": "Ustawienia wyświetlania", "display_settings": "Ustawienia wyświetlania",
"other_settings": "Inne ustawienia" "other_settings": "Inne ustawienia",
"require_pin_after": "Wymagaj kodu PIN po",
"always": "zawsze",
"minutes_to_pin_code": "${minute} minut"
} }

View file

@ -655,5 +655,8 @@
"privacy_settings": "Configurações de privacidade", "privacy_settings": "Configurações de privacidade",
"privacy": "Privacidade", "privacy": "Privacidade",
"display_settings": "Configurações de exibição", "display_settings": "Configurações de exibição",
"other_settings": "Outras configurações" "other_settings": "Outras configurações",
"require_pin_after": "Exigir PIN após",
"always": "sempre",
"minutes_to_pin_code": "${minute} minutos"
} }

View file

@ -655,5 +655,8 @@
"privacy_settings": "Настройки конфиденциальности", "privacy_settings": "Настройки конфиденциальности",
"privacy": "Конфиденциальность", "privacy": "Конфиденциальность",
"display_settings": "Настройки отображения", "display_settings": "Настройки отображения",
"other_settings": "Другие настройки" "other_settings": "Другие настройки",
"require_pin_after": "Требовать ПИН после",
"always": "всегда",
"minutes_to_pin_code": "${minute} минут"
} }

View file

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

View file

@ -653,5 +653,8 @@
"privacy_settings": "隐私设置", "privacy_settings": "隐私设置",
"privacy":"隐私", "privacy":"隐私",
"display_settings": "显示设置", "display_settings": "显示设置",
"other_settings": "其他设置" "other_settings": "其他设置",
"require_pin_after": "之后需要 PIN",
"always": "总是",
"minutes_to_pin_code": "${minute} 分钟"
} }