[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
}