[CW-225] Add pin timeout setting

Godwin Asuquo committed Nov 22, 2022 at 22:52 UTC 818a8afe208418136cfa49cdf801be928d0a1d74
25 files changed +249 -65
lib/core/auth_service.dart
+24
@@ -4,6 +4,8 @@ import 'package:shared_preferences/shared_preferences.dart';
4 import 'package:cake_wallet/entities/preferences_key.dart';
5 import 'package:cake_wallet/entities/secret_store_key.dart';
6 import 'package:cake_wallet/entities/encrypt.dart';
7 +import 'package:cake_wallet/di.dart';
8 +import 'package:cake_wallet/store/settings_store.dart';
9
10 class AuthService with Store {
11 AuthService({required this.secureStorage, required this.sharedPreferences});
@@ -39,4 +41,26 @@ class AuthService with Store {
41
42 return decodedPin == pin;
43 }
44 +
45 + void saveLastAuthTime(){
46 + int timestamp = DateTime.now().millisecondsSinceEpoch;
47 + sharedPreferences.setInt(PreferencesKey.lastAuthTimeMilliseconds, timestamp);
48 + }
49 +
50 + bool requireAuth(){
51 + final timestamp = sharedPreferences.getInt(PreferencesKey.lastAuthTimeMilliseconds);
52 + final duration = _durationToRequireAuth(timestamp ?? 0);
53 + final requiredPinInterval = getIt.get<SettingsStore>().pinTimeOutDuration;
54 +
55 + return duration >= requiredPinInterval.value;
56 + }
57 +
58 + int _durationToRequireAuth(int timestamp){
59 +
60 + DateTime before = DateTime.fromMillisecondsSinceEpoch(timestamp);
61 + DateTime now = DateTime.now();
62 + Duration timeDifference = now.difference(before);
63 +
64 + return timeDifference.inMinutes;
65 + }
66 }
lib/di.dart
+2 -1
@@ -440,7 +440,8 @@ Future setup(
440 getIt.registerFactory(() {
441 final appStore = getIt.get<AppStore>();
442 final yatStore = getIt.get<YatStore>();
443 - return SettingsViewModel(appStore.settingsStore, yatStore, appStore.wallet!);
443 + final authService = getIt.get<AuthService>();
444 + return SettingsViewModel(appStore.settingsStore, yatStore, authService, appStore.wallet!);
445 });
446
447 getIt
lib/entities/pin_code_required_duration.dart new
+32
@@ -0,0 +1,32 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 +
3 +enum PinCodeRequiredDuration {
4 + always(0),
5 + tenminutes(10),
6 + onehour(60);
7 +
8 + const PinCodeRequiredDuration(this.value);
9 + final int value;
10 +
11 + static PinCodeRequiredDuration deserialize({required int raw}) =>
12 + PinCodeRequiredDuration.values.firstWhere((e) => e.value == raw);
13 +
14 + @override
15 + String toString(){
16 + String label = '';
17 + switch (this) {
18 + case PinCodeRequiredDuration.always:
19 + label = S.current.always;
20 + break;
21 + case PinCodeRequiredDuration.tenminutes:
22 + label = S.current.minutes_to_pin_code('10');
23 + break;
24 + case PinCodeRequiredDuration.onehour:
25 + label = S.current.minutes_to_pin_code('60');
26 + break;
27 + }
28 + return label;
29 +
30 + }
31 +
32 +}
\ No newline at end of file
lib/entities/preferences_key.dart
+3
@@ -23,6 +23,9 @@ class PreferencesKey {
23 static const shouldShowReceiveWarning = 'should_show_receive_warning';
24 static const shouldShowYatPopup = 'should_show_yat_popup';
25 static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1';
26 + static const pinTimeOutDuration = 'pin_timeout_duration';
27 + static const lastAuthTimeMilliseconds = 'last_auth_time_milliseconds';
28 +
29
30 static String moneroWalletUpdateV1Key(String name)
31 => '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}';
lib/src/screens/settings/security_backup_page.dart
+39 -23
@@ -1,9 +1,11 @@
1 +import 'package:cake_wallet/entities/pin_code_required_duration.dart';
2 import 'package:cake_wallet/routes.dart';
3 import 'package:cake_wallet/src/screens/auth/auth_page.dart';
4 import 'package:cake_wallet/src/screens/base_page.dart';
5 import 'package:cake_wallet/generated/i18n.dart';
6 import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
7 import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart';
8 +import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart';
9 import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
10 import 'package:cake_wallet/src/widgets/standard_list.dart';
11 import 'package:cake_wallet/view_model/settings/settings_view_model.dart';
@@ -20,27 +22,28 @@ class SecurityBackupPage extends BasePage {
22
23 @override
24 Widget body(BuildContext context) {
25 +
26 return Container(
27 padding: EdgeInsets.only(top: 10),
28 child: Column(mainAxisSize: MainAxisSize.min, children: [
29 SettingsCellWithArrow(
30 title: S.current.show_keys,
28 - handler: (_) => Navigator.of(context).pushNamed(Routes.auth,
31 + handler: (_) => settingsViewModel.checkPinCodeRiquired() ? Navigator.of(context).pushNamed(Routes.auth,
32 arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) {
33 if (isAuthenticatedSuccessfully) {
34 auth.close(route: Routes.showKeys);
35 }
33 - }),
36 + }) : Navigator.of(context).pushNamed(Routes.showKeys),
37 ),
38 StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
39 SettingsCellWithArrow(
40 title: S.current.create_backup,
38 - handler: (_) => Navigator.of(context).pushNamed(Routes.auth,
41 + handler: (_) => settingsViewModel.checkPinCodeRiquired() ? Navigator.of(context).pushNamed(Routes.auth,
42 arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) {
43 if (isAuthenticatedSuccessfully) {
44 auth.close(route: Routes.backup);
45 }
43 - }),
46 + }) : Navigator.of(context).pushNamed(Routes.backup),
47 ),
48 StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
49 SettingsCellWithArrow(
@@ -56,28 +59,41 @@ class SecurityBackupPage extends BasePage {
59 })),
60 StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
61 Observer(builder: (_) {
59 - return SettingsSwitcherCell(
60 - title: S.current.settings_allow_biometrical_authentication,
61 - value: settingsViewModel.allowBiometricalAuthentication,
62 - onValueChange: (BuildContext context, bool value) {
63 - if (value) {
64 - Navigator.of(context).pushNamed(Routes.auth,
65 - arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) async {
66 - if (isAuthenticatedSuccessfully) {
67 - if (await settingsViewModel.biometricAuthenticated()) {
68 - settingsViewModel.setAllowBiometricalAuthentication(isAuthenticatedSuccessfully);
69 - }
62 + return Column(
63 + children: [
64 + SettingsSwitcherCell(
65 + title: S.current.settings_allow_biometrical_authentication,
66 + value: settingsViewModel.allowBiometricalAuthentication,
67 + onValueChange: (BuildContext context, bool value) {
68 + if (value) {
69 + Navigator.of(context).pushNamed(Routes.auth,
70 + arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) async {
71 + if (isAuthenticatedSuccessfully) {
72 + if (await settingsViewModel.biometricAuthenticated()) {
73 + settingsViewModel.setAllowBiometricalAuthentication(isAuthenticatedSuccessfully);
74 + }
75 + } else {
76 + settingsViewModel.setAllowBiometricalAuthentication(isAuthenticatedSuccessfully);
77 + }
78 +
79 + auth.close();
80 + });
81 } else {
71 - settingsViewModel.setAllowBiometricalAuthentication(isAuthenticatedSuccessfully);
82 + settingsViewModel.setAllowBiometricalAuthentication(value);
83 }
73 -
74 - auth.close();
75 - });
76 - } else {
77 - settingsViewModel.setAllowBiometricalAuthentication(value);
78 - }
79 - });
84 + }),
85 + SettingsPickerCell<PinCodeRequiredDuration>(
86 + title: S.current.require_pin_after,
87 + items: PinCodeRequiredDuration.values,
88 + selectedItem: settingsViewModel.pinCodeRequiredDuration,
89 + onItemSelected: (PinCodeRequiredDuration code) {
90 + settingsViewModel.setPinCodeRequiredDuration(code);
91 + },
92 + ),
93 + ],
94 + );
95 }),
96 +
97 ]),
98 );
99 }
lib/src/screens/wallet_list/wallet_list_page.dart
+36 -11
@@ -220,7 +220,8 @@ class WalletListBodyState extends State<WalletListBody> {
220 }
221
222 Future<void> _loadWallet(WalletListItem wallet) async {
223 - await Navigator.of(context).pushNamed(Routes.auth, arguments:
223 + if(await widget.walletListViewModel.checkIfAuthRequired()){
224 + await Navigator.of(context).pushNamed(Routes.auth, arguments:
225 (bool isAuthenticatedSuccessfully, AuthPageState auth) async {
226 if (!isAuthenticatedSuccessfully) {
227 return;
@@ -241,17 +242,36 @@ class WalletListBodyState extends State<WalletListBody> {
242 .wallet_list_failed_to_load(wallet.name, e.toString()));
243 }
244 });
245 + }else{
246 + try {
247 + changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name));
248 + await widget.walletListViewModel.loadWallet(wallet);
249 + hideProgressText();
250 + Navigator.of(context).pop();
251 + } catch (e) {
252 + changeProcessText(S
253 + .of(context)
254 + .wallet_list_failed_to_load(wallet.name, e.toString()));
255 + }
256 + }
257 }
258
259 Future<void> _removeWallet(WalletListItem wallet) async {
247 - await Navigator.of(context).pushNamed(Routes.auth, arguments:
260 + if(widget.walletListViewModel.checkIfAuthRequired()){
261 + await Navigator.of(context).pushNamed(Routes.auth, arguments:
262 (bool isAuthenticatedSuccessfully, AuthPageState auth) async {
263 if (!isAuthenticatedSuccessfully) {
264 return;
265 }
266 + _onSuccessfulAuth(wallet, auth);
267 + });
268 + }else{
269 + _onSuccessfulAuth(wallet, null);
270 + }
271 + }
272
253 - bool confirmed = false;
254 -
273 + _onSuccessfulAuth(WalletListItem wallet, AuthPageState? auth)async{
274 + bool confirmed = false;
275 await showPopUp<void>(
276 context: context,
277 builder: (BuildContext context) {
@@ -270,18 +290,23 @@ class WalletListBodyState extends State<WalletListBody> {
290
291 if (confirmed) {
292 try {
273 - auth.changeProcessText(
274 - S.of(context).wallet_list_removing_wallet(wallet.name));
293 + auth != null ?
294 + auth.changeProcessText(
295 + S.of(context).wallet_list_removing_wallet(wallet.name))
296 + : changeProcessText( S.of(context).wallet_list_removing_wallet(wallet.name));
297 await widget.walletListViewModel.remove(wallet);
298 } catch (e) {
277 - auth.changeProcessText(S
278 - .of(context)
279 - .wallet_list_failed_to_remove(wallet.name, e.toString()));
299 + auth != null ?
300 + auth.changeProcessText(
301 + S.of(context).wallet_list_failed_to_remove(wallet.name, e.toString()),
302 + )
303 + : changeProcessText(
304 + S.of(context).wallet_list_failed_to_remove(wallet.name, e.toString()),
305 + );
306 }
307 }
308
283 - auth.close();
284 - });
309 + auth?.close();
310 }
311
312 void changeProcessText(String text) {
lib/store/settings_store.dart
+16 -4
@@ -1,9 +1,9 @@
1 import 'package:cake_wallet/bitcoin/bitcoin.dart';
2 +import 'package:cake_wallet/entities/pin_code_required_duration.dart';
3 import 'package:cake_wallet/entities/preferences_key.dart';
4 import 'package:cw_core/transaction_priority.dart';
5 import 'package:cake_wallet/themes/theme_base.dart';
6 import 'package:cake_wallet/themes/theme_list.dart';
6 -import 'package:flutter/foundation.dart';
7 import 'package:flutter/material.dart';
8 import 'package:hive/hive.dart';
9 import 'package:mobx/mobx.dart';
@@ -17,7 +17,6 @@ import 'package:cake_wallet/entities/fiat_currency.dart';
17 import 'package:cw_core/node.dart';
18 import 'package:cake_wallet/monero/monero.dart';
19 import 'package:cake_wallet/entities/action_list_display_mode.dart';
20 -import 'package:cake_wallet/.secrets.g.dart' as secrets;
20
21 part 'settings_store.g.dart';
22
@@ -39,6 +38,7 @@ abstract class SettingsStoreBase with Store {
38 required this.shouldShowYatPopup,
39 required this.isBitcoinBuyEnabled,
40 required this.actionlistDisplayMode,
41 + required this.pinTimeOutDuration,
42 TransactionPriority? initialBitcoinTransactionPriority,
43 TransactionPriority? initialMoneroTransactionPriority})
44 : nodes = ObservableMap<WalletType, Node>.of(nodes),
@@ -108,6 +108,11 @@ abstract class SettingsStoreBase with Store {
108 (String languageCode) => sharedPreferences.setString(
109 PreferencesKey.currentLanguageCode, languageCode));
110
111 + reaction(
112 + (_) => pinTimeOutDuration,
113 + (PinCodeRequiredDuration pinCodeInterval) => sharedPreferences.setInt(
114 + PreferencesKey.pinTimeOutDuration, pinCodeInterval.value));
115 +
116 reaction(
117 (_) => balanceDisplayMode,
118 (BalanceDisplayMode mode) => sharedPreferences.setInt(
@@ -124,6 +129,7 @@ abstract class SettingsStoreBase with Store {
129
130 static const defaultPinLength = 4;
131 static const defaultActionsMode = 11;
132 + static const defaultPinCodeTimeOutDuration = 10;
133
134 @observable
135 FiatCurrency fiatCurrency;
@@ -149,6 +155,9 @@ abstract class SettingsStoreBase with Store {
155 @observable
156 int pinCodeLength;
157
158 + @observable
159 + PinCodeRequiredDuration pinTimeOutDuration;
160 +
161 @computed
162 ThemeData get theme => currentTheme.themeData;
163
@@ -227,13 +236,15 @@ abstract class SettingsStoreBase with Store {
236 : ThemeType.bright.index;
237 final savedTheme = ThemeList.deserialize(
238 raw: sharedPreferences.getInt(PreferencesKey.currentTheme) ??
230 - legacyTheme ??
231 - 0);
239 + legacyTheme);
240 final actionListDisplayMode = ObservableList<ActionListDisplayMode>();
241 actionListDisplayMode.addAll(deserializeActionlistDisplayModes(
242 sharedPreferences.getInt(PreferencesKey.displayActionListModeKey) ??
243 defaultActionsMode));
244 var pinLength = sharedPreferences.getInt(PreferencesKey.currentPinLength);
245 + final pinCodeTimeOutDuration = PinCodeRequiredDuration.deserialize(raw: sharedPreferences.getInt(PreferencesKey.pinTimeOutDuration)
246 + ?? defaultPinCodeTimeOutDuration);
247 +
248 // If no value
249 if (pinLength == null || pinLength == 0) {
250 pinLength = defaultPinLength;
@@ -287,6 +298,7 @@ abstract class SettingsStoreBase with Store {
298 initialTheme: savedTheme,
299 actionlistDisplayMode: actionListDisplayMode,
300 initialPinLength: pinLength,
301 + pinTimeOutDuration: pinCodeTimeOutDuration,
302 initialLanguageCode: savedLanguageCode,
303 initialMoneroTransactionPriority: moneroTransactionPriority,
304 initialBitcoinTransactionPriority: bitcoinTransactionPriority,
lib/view_model/auth_view_model.dart
+10 -2
@@ -17,7 +17,9 @@ abstract class AuthViewModelBase with Store {
17 AuthViewModelBase(this._authService, this._sharedPreferences,
18 this._settingsStore, this._biometricAuth)
19 : _failureCounter = 0,
20 - state = InitialExecutionState();
20 + state = InitialExecutionState(){
21 + reaction((_) => state, _saveLastAuthTime);
22 + }
23
24 static const maxFailedLogins = 3;
25 static const banTimeout = 180; // 3 minutes
@@ -57,7 +59,7 @@ abstract class AuthViewModelBase with Store {
59
60 if (isSuccessfulAuthenticated) {
61 WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
60 - state = ExecutedSuccessfullyState();
62 + state = ExecutedSuccessfullyState();
63 _failureCounter = 0;
64 });
65 } else {
@@ -118,4 +120,10 @@ abstract class AuthViewModelBase with Store {
120 state = FailureState(e.toString());
121 }
122 }
123 +
124 + void _saveLastAuthTime(ExecutionState state){
125 + if(state is ExecutedSuccessfullyState){
126 + _authService.saveLastAuthTime();
127 + }
128 + }
129 }
lib/view_model/settings/settings_view_model.dart
+22 -8
@@ -1,3 +1,5 @@
1 +import 'package:cake_wallet/core/auth_service.dart';
2 +import 'package:cake_wallet/entities/pin_code_required_duration.dart';
3 import 'package:cake_wallet/store/yat/yat_store.dart';
4 import 'package:mobx/mobx.dart';
5 import 'package:package_info/package_info.dart';
@@ -41,6 +43,7 @@ abstract class SettingsViewModelBase with Store {
43 SettingsViewModelBase(
44 this._settingsStore,
45 this._yatStore,
46 + this._authService,
47 WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
48 TransactionInfo>
49 wallet)
@@ -94,6 +97,10 @@ abstract class SettingsViewModelBase with Store {
97 @computed
98 FiatCurrency get fiatCurrency => _settingsStore.fiatCurrency;
99
100 + @computed
101 + PinCodeRequiredDuration get pinCodeRequiredDuration =>
102 + _settingsStore.pinTimeOutDuration;
103 +
104 @computed
105 String get languageCode => _settingsStore.languageCode;
106
@@ -135,6 +142,7 @@ abstract class SettingsViewModelBase with Store {
142 final Map<String, String> itemHeaders;
143 final SettingsStore _settingsStore;
144 final YatStore _yatStore;
145 + final AuthService _authService;
146 final WalletType walletType;
147 final BiometricAuth _biometricAuth;
148 final WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
@@ -207,19 +215,25 @@ abstract class SettingsViewModelBase with Store {
215 }
216 }
217
218 + @action
219 + setPinCodeRequiredDuration(PinCodeRequiredDuration duration) =>
220 + _settingsStore.pinTimeOutDuration = duration;
221 +
222 String getDisplayPriority(dynamic priority) {
211 - final _priority = priority as TransactionPriority;
223 + final _priority = priority as TransactionPriority;
224
213 - if (_wallet.type == WalletType.bitcoin
214 - || _wallet.type == WalletType.litecoin) {
215 - final rate = bitcoin!.getFeeRate(_wallet, _priority);
216 - return bitcoin!.bitcoinTransactionPriorityWithLabel(_priority, rate);
217 - }
225 + if (_wallet.type == WalletType.bitcoin
226 + || _wallet.type == WalletType.litecoin) {
227 + final rate = bitcoin!.getFeeRate(_wallet, _priority);
228 + return bitcoin!.bitcoinTransactionPriorityWithLabel(_priority, rate);
229 + }
230
219 - return priority.toString();
231 + return priority.toString();
232 }
233
234 void onDisplayPrioritySelected(TransactionPriority priority) =>
223 - _settingsStore.priority[_wallet.type] = priority;
235 + _settingsStore.priority[_wallet.type] = priority;
236 +
237 + bool checkPinCodeRiquired() => _authService.requireAuth();
238
239 }
lib/view_model/wallet_list/wallet_list_view_model.dart
+5 -1
@@ -1,5 +1,5 @@
1 +import 'package:cake_wallet/core/auth_service.dart';
2 import 'package:cake_wallet/core/wallet_loading_service.dart';
2 -import 'package:cake_wallet/view_model/wallet_new_vm.dart';
3 import 'package:hive/hive.dart';
4 import 'package:mobx/mobx.dart';
5 import 'package:cake_wallet/di.dart';
@@ -55,4 +55,8 @@ abstract class WalletListViewModelBase with Store {
55 info.type == _appStore.wallet!.type,
56 isEnabled: availableWalletTypes.contains(info.type))));
57 }
58 +
59 + bool checkIfAuthRequired(){
60 + return getIt.get<AuthService>().requireAuth();
61 + }
62 }
res/values/strings_de.arb
+4 -1
@@ -655,5 +655,8 @@
655 "privacy_settings": "Datenschutzeinstellungen",
656 "privacy": "Datenschutz",
657 "display_settings": "Anzeigeeinstellungen",
658 - "other_settings": "Andere Einstellungen"
658 + "other_settings": "Andere Einstellungen",
659 + "require_pin_after": "PIN anfordern nach",
660 + "always": "immer",
661 + "minutes_to_pin_code": "${minute} Minuten"
662 }
res/values/strings_en.arb
+4 -1
@@ -658,5 +658,8 @@
658 "privacy_settings": "Privacy settings",
659 "privacy": "Privacy",
660 "display_settings": "Display settings",
661 - "other_settings": "Other settings"
661 + "other_settings": "Other settings",
662 + "require_pin_after": "Require PIN after",
663 + "always": "Always",
664 + "minutes_to_pin_code": "${minute} minutes"
665 }
res/values/strings_es.arb
+4 -1
@@ -655,5 +655,8 @@
655 "privacy_settings": "Configuración de privacidad",
656 "privacy": "Privacidad",
657 "display_settings": "Configuración de pantalla",
658 - "other_settings": "Otras configuraciones"
658 + "other_settings": "Otras configuraciones",
659 + "require_pin_after": "Requerir PIN después de",
660 + "always": "siempre",
661 + "minutes_to_pin_code": "${minute} minutos"
662 }
res/values/strings_fr.arb
+4 -1
@@ -653,5 +653,8 @@
653 "privacy_settings": "Paramètres de confidentialité",
654 "privacy": "Confidentialité",
655 "display_settings": "Paramètres d'affichage",
656 - "other_settings": "Autres paramètres"
656 + "other_settings": "Autres paramètres",
657 + "require_pin_after": "NIP requis après",
658 + "always": "toujours",
659 + "minutes_to_pin_code": "${minute} minutes"
660 }
res/values/strings_hi.arb
+4 -1
@@ -655,5 +655,8 @@
655 "privacy_settings": "गोपनीयता सेटिंग्स",
656 "privacy": "गोपनीयता",
657 "display_settings": "प्रदर्शन सेटिंग्स",
658 - "other_settings": "अन्य सेटिंग्स"
658 + "other_settings": "अन्य सेटिंग्स",
659 + "require_pin_after": "इसके बाद पिन आवश्यक है",
660 + "always": "हमेशा",
661 + "minutes_to_pin_code": "${minute} मिनट"
662 }
res/values/strings_hr.arb
+4 -1
@@ -655,5 +655,8 @@
655 "privacy_settings": "Postavke privatnosti",
656 "privacy": "Privatnost",
657 "display_settings": "Postavke zaslona",
658 - "other_settings": "Ostale postavke"
658 + "other_settings": "Ostale postavke",
659 + "require_pin_after": "Zahtijevaj PIN nakon",
660 + "always": "Uvijek",
661 + "minutes_to_pin_code": "${minute} minuta"
662 }
res/values/strings_it.arb
+4 -1
@@ -655,5 +655,8 @@
655 "privacy_settings": "Impostazioni privacy",
656 "privacy": "Privacy",
657 "display_settings": "Impostazioni di visualizzazione",
658 - "other_settings": "Altre impostazioni"
658 + "other_settings": "Altre impostazioni",
659 + "require_pin_after": "Richiedi PIN dopo",
660 + "always": "sempre",
661 + "minutes_to_pin_code": "${minute} minuti"
662 }
res/values/strings_ja.arb
+4 -1
@@ -655,5 +655,8 @@
655 "privacy_settings": "プライバシー設定",
656 "privacy": "プライバシー",
657 "display_settings": "表示設定",
658 - "other_settings": "その他の設定"
658 + "other_settings": "その他の設定",
659 + "require_pin_after": "後に PIN が必要",
660 + "always": "いつも",
661 + "minutes_to_pin_code": "${minute} 分"
662 }
res/values/strings_ko.arb
+4 -1
@@ -655,5 +655,8 @@
655 "privacy_settings": "개인정보 설정",
656 "privacy": "프라이버시",
657 "display_settings": "디스플레이 설정",
658 - "other_settings": "기타 설정"
658 + "other_settings": "기타 설정",
659 + "require_pin_after": "다음 이후에 PIN 필요",
660 + "always": "언제나",
661 + "minutes_to_pin_code": "${minute}분"
662 }
res/values/strings_nl.arb
+4 -1
@@ -655,5 +655,8 @@
655 "privacy_settings": "Privacy-instellingen",
656 "privacy": "Privacy",
657 "display_settings": "Weergave-instellingen",
658 - "other_settings": "Andere instellingen"
658 + "other_settings": "Andere instellingen",
659 + "require_pin_after": "Pincode vereist na",
660 + "always": "altijd",
661 + "minutes_to_pin_code": "${minute} minuten"
662 }
res/values/strings_pl.arb
+4 -1
@@ -655,5 +655,8 @@
655 "privacy_settings": "Ustawienia prywatności",
656 "privacy": "Prywatność",
657 "display_settings": "Ustawienia wyświetlania",
658 - "other_settings": "Inne ustawienia"
658 + "other_settings": "Inne ustawienia",
659 + "require_pin_after": "Wymagaj kodu PIN po",
660 + "always": "zawsze",
661 + "minutes_to_pin_code": "${minute} minut"
662 }
res/values/strings_pt.arb
+4 -1
@@ -655,5 +655,8 @@
655 "privacy_settings": "Configurações de privacidade",
656 "privacy": "Privacidade",
657 "display_settings": "Configurações de exibição",
658 - "other_settings": "Outras configurações"
658 + "other_settings": "Outras configurações",
659 + "require_pin_after": "Exigir PIN após",
660 + "always": "sempre",
661 + "minutes_to_pin_code": "${minute} minutos"
662 }
res/values/strings_ru.arb
+4 -1
@@ -655,5 +655,8 @@
655 "privacy_settings": "Настройки конфиденциальности",
656 "privacy": "Конфиденциальность",
657 "display_settings": "Настройки отображения",
658 - "other_settings": "Другие настройки"
658 + "other_settings": "Другие настройки",
659 + "require_pin_after": "Требовать ПИН после",
660 + "always": "всегда",
661 + "minutes_to_pin_code": "${minute} минут"
662 }
res/values/strings_uk.arb
+4 -1
@@ -654,6 +654,9 @@
654 "privacy_settings": "Налаштування конфіденційності",
655 "privacy": "Конфіденційність",
656 "display_settings": "Налаштування дисплея",
657 - "other_settings": "Інші налаштування"
657 + "other_settings": "Інші налаштування",
658 + "require_pin_after": "Вимагати PIN після",
659 + "always": "Завжди",
660 + "minutes_to_pin_code": "${minute} хвилин"
661
662 }
res/values/strings_zh.arb
+4 -1
@@ -653,5 +653,8 @@
653 "privacy_settings": "隐私设置",
654 "privacy":"隐私",
655 "display_settings": "显示设置",
656 - "other_settings": "其他设置"
656 + "other_settings": "其他设置",
657 + "require_pin_after": "之后需要 PIN",
658 + "always": "总是",
659 + "minutes_to_pin_code": "${minute} 分钟"
660 }