CW-1000 Background sync improvements (#2142)

* feat: background sync improvements - dev options on ci build cherrypick - add permissions for background sync to AndroidManifestBase - enable desugaring + update java compatibility to 17 - update walletconnect_flutter_v2 - update ens_dart - update nostr_tools - add notification for new transactions found in background - expose more settings from flutter_daemon in UI - remove battery optimization setting when it's already disabled - fix notification permission handling - fix background sync last trigger saving - prevent notifications from being duplicated * potential fix for multiple notifications firing for the same tx * improve logging in background sync * ui improvements to ignore battery optimization popup * feat: logs for bg sync disable decred bgsync * fix: call store() directly to be sure that it is writing the data * chore: rename logs to background sync logs * Update lib/view_model/dashboard/dashboard_view_model.dart Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * chore: remove unused key --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

cyan committed Apr 24, 2025 at 19:06 UTC 02e74b5997b94b8fe86c4826f326a78075d09901
48 files changed +1373 -68
android/app/build.gradle
+9
@@ -42,6 +42,14 @@ android {
42 disable 'InvalidPackage'
43 }
44
45 + compileOptions {
46 + coreLibraryDesugaringEnabled true
47 +
48 + sourceCompatibility JavaVersion.VERSION_17
49 + targetCompatibility JavaVersion.VERSION_17
50 + }
51 +
52 +
53 namespace "com.cakewallet.cake_wallet"
54
55 defaultConfig {
@@ -91,6 +99,7 @@ dependencies {
99 testImplementation 'junit:junit:4.12'
100 androidTestImplementation 'androidx.test:runner:1.3.0'
101 androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
102 + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
103 }
104 configurations {
105 implementation.exclude module:'proto-google-common-protos'
android/app/src/main/AndroidManifestBase.xml
+8
@@ -24,6 +24,10 @@
24 <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
25 <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
26
27 + <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
28 + <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
29 + <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
30 + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
31
32 <application
33 android:name=".Application"
@@ -35,6 +39,10 @@
39 android:versionName="__versionName__"
40 android:requestLegacyExternalStorage="true"
41 android:extractNativeLibs="true">
42 + <service
43 + android:name="androidx.work.impl.foreground.SystemForegroundService"
44 + android:foregroundServiceType="dataSync"
45 + android:exported="false" />
46 <activity
47 android:name=".MainActivity"
48 android:launchMode="singleInstance"
cw_monero/lib/monero_wallet.dart
+4 -3
@@ -256,8 +256,9 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
256 Future<void> stopSync() async {
257 if (isBackgroundSyncRunning) {
258 printV("Stopping background sync");
259 - await save();
259 + monero.Wallet_store(wptr!);
260 monero.Wallet_stopBackgroundSync(wptr!, '');
261 + monero_wallet.store();
262 isBackgroundSyncRunning = false;
263 }
264 await save();
@@ -268,9 +269,9 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
269 Future<void> stopBackgroundSync(String password) async {
270 if (isBackgroundSyncRunning) {
271 printV("Stopping background sync");
271 - await save();
272 + monero.Wallet_store(wptr!);
273 monero.Wallet_stopBackgroundSync(wptr!, password);
273 - await save();
274 + monero.Wallet_store(wptr!);
275 isBackgroundSyncRunning = false;
276 }
277 }
cw_shared_external/android/build.gradle
+1 -1
@@ -9,7 +9,7 @@ buildscript {
9 }
10
11 dependencies {
12 - classpath 'com.android.tools.build:gradle:7.3.0'
12 + classpath 'com.android.tools.build:gradle:8.7.1'
13 classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14 }
15 }
lib/core/background_sync.dart
+104 -8
@@ -1,26 +1,101 @@
1 import 'dart:async';
2 -import 'dart:math';
2 +import 'dart:io';
3
4 import 'package:cake_wallet/core/key_service.dart';
5 import 'package:cake_wallet/core/wallet_loading_service.dart';
6 import 'package:cake_wallet/di.dart';
7 +import 'package:cake_wallet/entities/preferences_key.dart';
8 import 'package:cake_wallet/store/settings_store.dart';
9 +import 'package:cake_wallet/utils/feature_flag.dart';
10 import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart';
11 import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart';
12 import 'package:cw_core/sync_status.dart';
13 +import 'package:cw_core/transaction_direction.dart';
14 import 'package:cw_core/utils/print_verbose.dart';
15 import 'package:cw_core/wallet_type.dart';
13 -import 'package:flutter/foundation.dart';
14 -import 'package:http/http.dart' as http;
16 +import 'package:flutter_local_notifications/flutter_local_notifications.dart';
17 +import 'package:shared_preferences/shared_preferences.dart';
18
19 class BackgroundSync {
20 + final FlutterLocalNotificationsPlugin _notificationsPlugin = FlutterLocalNotificationsPlugin();
21 + bool _isInitialized = false;
22 +
23 + Future<void> _initializeNotifications() async {
24 + if (_isInitialized) return;
25 +
26 + const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
27 +
28 + const iosSettings = DarwinInitializationSettings(
29 + requestAlertPermission: true,
30 + requestBadgePermission: true,
31 + requestSoundPermission: true,
32 + );
33 +
34 + const initializationSettings = InitializationSettings(
35 + android: androidSettings,
36 + iOS: iosSettings,
37 + );
38 +
39 + await _notificationsPlugin.initialize(initializationSettings);
40 + _isInitialized = true;
41 + }
42 +
43 + Future<bool> requestPermissions() async {
44 + if (Platform.isIOS || Platform.isMacOS) {
45 + return await _notificationsPlugin
46 + .resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()
47 + ?.requestPermissions(
48 + alert: true,
49 + badge: true,
50 + sound: true,
51 + ) ?? false;
52 + } else if (Platform.isAndroid) {
53 + return await _notificationsPlugin
54 + .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
55 + ?.areNotificationsEnabled() ?? false;
56 + }
57 + return false;
58 + }
59 +
60 + Future<void> showNotification(String title, String content) async {
61 + await _initializeNotifications();
62 + final hasPermission = await requestPermissions();
63 +
64 + if (!hasPermission) {
65 + printV('Notification permissions not granted');
66 + return;
67 + }
68 +
69 + const androidDetails = AndroidNotificationDetails(
70 + 'transactions',
71 + 'Transactions',
72 + channelDescription: 'Channel for notifications about transactions',
73 + importance: Importance.defaultImportance,
74 + priority: Priority.defaultPriority,
75 + );
76 +
77 + const iosDetails = DarwinNotificationDetails();
78 +
79 + const notificationDetails = NotificationDetails(
80 + android: androidDetails,
81 + iOS: iosDetails,
82 + );
83 +
84 + await _notificationsPlugin.show(
85 + DateTime.now().millisecondsSinceEpoch.hashCode,
86 + title,
87 + content,
88 + notificationDetails,
89 + );
90 + }
91 +
92 Future<void> sync() async {
93 printV("Background sync started");
19 - await _syncMonero();
94 + await _syncWallets();
95 printV("Background sync completed");
96 }
97
23 - Future<void> _syncMonero() async {
98 + Future<void> _syncWallets() async {
99 final walletLoadingService = getIt.get<WalletLoadingService>();
100 final walletListViewModel = getIt.get<WalletListViewModel>();
101 final settingsStore = getIt.get<SettingsStore>();
@@ -28,10 +103,10 @@ class BackgroundSync {
103
104 final List<WalletListItem> moneroWallets = walletListViewModel.wallets
105 .where((element) => !element.isHardware)
31 - .where((element) => [WalletType.monero].contains(element.type))
106 + .where((element) => ![WalletType.haven, WalletType.decred].contains(element.type))
107 .toList();
108 for (int i = 0; i < moneroWallets.length; i++) {
34 - final wallet = await walletLoadingService.load(moneroWallets[i].type, moneroWallets[i].name);
109 + final wallet = await walletLoadingService.load(moneroWallets[i].type, moneroWallets[i].name, isBackground: true);
110 int syncedTicks = 0;
111 final keyService = getIt.get<KeyService>();
112
@@ -75,7 +150,7 @@ class BackgroundSync {
150 } else {
151 syncedTicks = 0;
152 }
78 - if (kDebugMode) {
153 + if (FeatureFlag.hasDevOptions) {
154 if (syncStatus is SyncingSyncStatus) {
155 final blocksLeft = syncStatus.blocksLeft;
156 printV("$blocksLeft Blocks Left");
@@ -100,6 +175,27 @@ class BackgroundSync {
175 }
176 }
177 }
178 + final txs = wallet.transactionHistory;
179 + final sortedTxs = txs.transactions.values.toList()..sort((a, b) => a.date.compareTo(b.date));
180 + final sharedPreferences = await SharedPreferences.getInstance();
181 + for (final tx in sortedTxs) {
182 + final lastTriggerString = sharedPreferences.getString(PreferencesKey.backgroundSyncLastTrigger(wallet.name));
183 + final lastTriggerDate = lastTriggerString != null
184 + ? DateTime.parse(lastTriggerString)
185 + : DateTime.now();
186 + final keys = sharedPreferences.getKeys();
187 + if (tx.date.isBefore(lastTriggerDate)) {
188 + printV("w: ${wallet.name}, tx: ${tx.date} is before $lastTriggerDate (lastTriggerString: $lastTriggerString) (k: ${keys.length})");
189 + continue;
190 + }
191 + await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name), tx.date.add(Duration(minutes: 1)).toIso8601String());
192 + final action = tx.direction == TransactionDirection.incoming ? "Received" : "Sent";
193 + if (sharedPreferences.getBool(PreferencesKey.backgroundSyncNotificationsEnabled) ?? false) {
194 + await showNotification("$action ${wallet.currency.fullName} in ${wallet.name}", "${tx.amountFormatted()}");
195 + }
196 + printV("${wallet.currency.fullName} in ${wallet.name}: TX: ${tx.date} ${tx.amount} ${tx.direction}");
197 + }
198 + wallet.id;
199 await wallet.stopBackgroundSync(await keyService.getWalletPassword(walletName: wallet.name));
200 await wallet.close(shouldCleanup: true);
201 }
lib/core/wallet_loading_service.dart
+4 -1
@@ -52,8 +52,11 @@ class WalletLoadingService {
52 }
53 }
54
55 - Future<WalletBase> load(WalletType type, String name, {String? password}) async {
55 + Future<WalletBase> load(WalletType type, String name, {String? password, bool isBackground = false}) async {
56 try {
57 + if (!isBackground) {
58 + await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(name), DateTime.now().toIso8601String());
59 + }
60 final walletService = walletServiceFactory.call(type);
61 final walletPassword = password ?? (await keyService.getWalletPassword(walletName: name));
62 final wallet = await walletService.openWallet(name, walletPassword);
lib/di.dart
+23 -12
@@ -33,11 +33,13 @@ import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart';
33 import 'package:cake_wallet/haven/cw_haven.dart';
34 import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
35 import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
36 +import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart';
37 import 'package:cake_wallet/src/screens/settings/background_sync_page.dart';
38 import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
39 import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
40 import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
41 import 'package:cake_wallet/view_model/dev/monero_background_sync.dart';
42 +import 'package:cake_wallet/view_model/dev/shared_preferences.dart';
43 import 'package:cake_wallet/view_model/link_view_model.dart';
44 import 'package:cake_wallet/tron/tron.dart';
45 import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
@@ -266,6 +268,8 @@ import 'buy/kryptonim/kryptonim.dart';
268 import 'buy/meld/meld_buy_provider.dart';
269 import 'src/screens/buy/buy_sell_page.dart';
270 import 'cake_pay/cake_pay_payment_credantials.dart';
271 +import 'package:cake_wallet/view_model/dev/background_sync_logs_view_model.dart';
272 +import 'package:cake_wallet/src/screens/dev/background_sync_logs_page.dart';
273
274 final getIt = GetIt.instance;
275
@@ -883,9 +887,8 @@ Future<void> setup({
887 nanoAccountCreationViewModel:
888 getIt.get<NanoAccountEditOrCreateViewModel>(param1: account)));
889
886 - getIt.registerFactory(() {
887 - return DisplaySettingsViewModel(getIt.get<SettingsStore>());
888 - });
890 + getIt.registerFactory(() =>
891 + DisplaySettingsViewModel(getIt.get<SettingsStore>()));
892
893 getIt.registerFactory(() =>
894 SilentPaymentsSettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!));
@@ -893,22 +896,20 @@ Future<void> setup({
896 getIt.registerFactory(
897 () => MwebSettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!));
898
896 - getIt.registerFactory(() {
897 - return PrivacySettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!);
898 - });
899 + getIt.registerFactory(() =>
900 + PrivacySettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!));
901
902 getIt.registerFactory(() => TrocadorExchangeProvider());
903
904 getIt.registerFactory(() => TrocadorProvidersViewModel(
905 getIt.get<SettingsStore>(), getIt.get<TrocadorExchangeProvider>()));
906
905 - getIt.registerFactory(() {
906 - return OtherSettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!,
907 - getIt.get<SendViewModel>());});
907 + getIt.registerFactory(() =>
908 + OtherSettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!,
909 + getIt.get<SendViewModel>()));
910
909 - getIt.registerFactory(() {
910 - return SecuritySettingsViewModel(getIt.get<SettingsStore>());
911 - });
911 + getIt.registerFactory(() =>
912 + SecuritySettingsViewModel(getIt.get<SettingsStore>()));
913
914 getIt.registerFactory(() => WalletSeedViewModel(getIt.get<AppStore>().wallet!));
915
@@ -916,6 +917,8 @@ Future<void> setup({
917
918 getIt.registerFactory(() => DevMoneroBackgroundSync(getIt.get<AppStore>().wallet!));
919
920 + getIt.registerFactory(() => DevSharedPreferences());
921 +
922 getIt.registerFactoryParam<WalletSeedPage, bool, void>((bool isWalletCreated, _) =>
923 WalletSeedPage(getIt.get<WalletSeedViewModel>(), isNewWalletCreated: isWalletCreated));
924
@@ -1456,6 +1459,14 @@ Future<void> setup({
1459 getIt.registerFactory(() => SeedVerificationPage(getIt.get<WalletSeedViewModel>()));
1460
1461 getIt.registerFactory(() => DevMoneroBackgroundSyncPage(getIt.get<DevMoneroBackgroundSync>()));
1462 +
1463 getIt.registerFactory(() => DevMoneroCallProfilerPage());
1464 +
1465 + getIt.registerFactory(() => DevSharedPreferencesPage(getIt.get<DevSharedPreferences>()));
1466 +
1467 + getIt.registerFactory(() => BackgroundSyncLogsViewModel());
1468 +
1469 + getIt.registerFactory(() => DevBackgroundSyncLogsPage(getIt.get<BackgroundSyncLogsViewModel>()));
1470 +
1471 _isSetupFinished = true;
1472 }
lib/entities/preferences_key.dart
+2
@@ -105,4 +105,6 @@ class PreferencesKey {
105 static const walletConnectPairingTopicsList = 'wallet_connect_pairing_topics_list';
106 static String walletConnectPairingTopicsListForWallet(String publicKey) =>
107 '${PreferencesKey.walletConnectPairingTopicsList}_${publicKey}';
108 + static String backgroundSyncLastTrigger(String walletId) => 'background_sync_last_trigger_${walletId}';
109 + static const backgroundSyncNotificationsEnabled = 'background_sync_notifications_enabled';
110 }
lib/router.dart
+11
@@ -36,6 +36,8 @@ import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart';
36 import 'package:cake_wallet/src/screens/dashboard/sign_page.dart';
37 import 'package:cake_wallet/src/screens/dev/monero_background_sync.dart';
38 import 'package:cake_wallet/src/screens/dev/moneroc_call_profiler.dart';
39 +import 'package:cake_wallet/src/screens/dev/shared_preferences_page.dart';
40 +import 'package:cake_wallet/src/screens/dev/background_sync_logs_page.dart';
41 import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart';
42 import 'package:cake_wallet/src/screens/exchange/exchange_page.dart';
43 import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart';
@@ -836,6 +838,15 @@ Route<dynamic> createRoute(RouteSettings settings) {
838 return MaterialPageRoute<void>(
839 builder: (_) => getIt.get<DevMoneroBackgroundSyncPage>(),
840 );
841 + case Routes.devSharedPreferences:
842 + return MaterialPageRoute<void>(
843 + builder: (_) => getIt.get<DevSharedPreferencesPage>(),
844 + );
845 +
846 + case Routes.devBackgroundSyncLogs:
847 + return MaterialPageRoute<void>(
848 + builder: (_) => getIt.get<DevBackgroundSyncLogsPage>(),
849 + );
850
851 case Routes.devMoneroCallProfiler:
852 return MaterialPageRoute<void>(
lib/routes.dart
+4
@@ -111,8 +111,12 @@ class Routes {
111 static const importNFTPage = '/import_nft_page';
112 static const torPage = '/tor_page';
113 static const backgroundSync = '/background_sync';
114 +
115 static const devMoneroBackgroundSync = '/dev/monero_background_sync';
116 static const devMoneroCallProfiler = '/dev/monero_call_profiler';
117 + static const devSharedPreferences = '/dev/shared_preferences';
118 + static const devBackgroundSyncLogs = '/dev/background_sync_logs';
119 +
120 static const signPage = '/sign_page';
121 static const connectDevices = '/device/connect';
122 static const urqrAnimatedPage = '/urqr/animated_page';
lib/src/screens/dev/background_sync_logs_page.dart new
+314
@@ -0,0 +1,314 @@
1 +import 'package:cake_wallet/src/screens/base_page.dart';
2 +import 'package:cake_wallet/view_model/dev/background_sync_logs_view_model.dart';
3 +import 'package:flutter/material.dart';
4 +import 'package:flutter/services.dart';
5 +import 'package:flutter_mobx/flutter_mobx.dart';
6 +import 'package:intl/intl.dart';
7 +
8 +class DevBackgroundSyncLogsPage extends BasePage {
9 + final BackgroundSyncLogsViewModel viewModel;
10 +
11 + DevBackgroundSyncLogsPage(this.viewModel) {
12 + viewModel.loadLogs();
13 + }
14 +
15 + @override
16 + String? get title => "[dev] background sync logs";
17 +
18 + @override
19 + Widget? trailing(BuildContext context) {
20 + return IconButton(
21 + icon: Icon(Icons.refresh),
22 + onPressed: () => viewModel.loadLogs(),
23 + );
24 + }
25 +
26 + @override
27 + Widget body(BuildContext context) {
28 + return Observer(
29 + builder: (_) {
30 + if (viewModel.isLoading) {
31 + return Center(child: CircularProgressIndicator());
32 + }
33 +
34 + if (viewModel.error != null) {
35 + return Center(child: Text("Error: ${viewModel.error}"));
36 + }
37 +
38 + if (viewModel.logData == null) {
39 + return Center(
40 + child: Column(
41 + mainAxisAlignment: MainAxisAlignment.center,
42 + children: [
43 + Text("No logs loaded"),
44 + SizedBox(height: 16),
45 + ElevatedButton(
46 + onPressed: () => viewModel.loadLogs(),
47 + child: Text("Load Logs"),
48 + ),
49 + ],
50 + ),
51 + );
52 + }
53 +
54 + return DefaultTabController(
55 + length: 2,
56 + child: Column(
57 + children: [
58 + TabBar(
59 + tabs: [
60 + Tab(text: "Logs (${viewModel.logs.length})"),
61 + Tab(text: "Sessions (${viewModel.sessions.length})"),
62 + ],
63 + ),
64 + Expanded(
65 + child: TabBarView(
66 + children: [
67 + _buildLogsTab(context),
68 + _buildSessionsTab(context),
69 + ],
70 + ),
71 + ),
72 + _buildActionButtons(context),
73 + ],
74 + ),
75 + );
76 + },
77 + );
78 + }
79 +
80 + Widget _buildLogsTab(BuildContext context) {
81 + final logs = viewModel.logs;
82 + if (logs.isEmpty) {
83 + return Center(child: Text("No logs available"));
84 + }
85 +
86 + final dateFormat = DateFormat('yyyy-MM-dd HH:mm:ss.SSS');
87 +
88 + return ListView.builder(
89 + itemCount: logs.length,
90 + itemBuilder: (context, index) {
91 + final log = logs[index];
92 + return ListTile(
93 + title: Text(
94 + log.message,
95 + style: TextStyle(
96 + fontSize: 14,
97 + fontFamily: 'Monospace',
98 + ),
99 + ),
100 + subtitle: Text(
101 + '${dateFormat.format(log.timestamp)} | ${log.level}' +
102 + (log.sessionId != null ? ' | Session: ${log.sessionId}' : ''),
103 + style: TextStyle(
104 + fontSize: 12,
105 + color: _getLevelColor(log.level),
106 + ),
107 + ),
108 + dense: true,
109 + onTap: () {
110 + Clipboard.setData(ClipboardData(text: log.message));
111 + ScaffoldMessenger.of(context).showSnackBar(
112 + SnackBar(content: Text('Log message copied to clipboard')),
113 + );
114 + },
115 + onLongPress: () {
116 + Clipboard.setData(ClipboardData(
117 + text: '${dateFormat.format(log.timestamp)} [${log.level}] ${log.message}'));
118 + ScaffoldMessenger.of(context).showSnackBar(
119 + SnackBar(content: Text('Full log entry copied to clipboard')),
120 + );
121 + },
122 + tileColor: index % 2 == 0 ? Colors.transparent : Colors.black.withOpacity(0.03),
123 + );
124 + },
125 + );
126 + }
127 +
128 + Widget _buildSessionsTab(BuildContext context) {
129 + final sessions = viewModel.sessions;
130 + if (sessions.isEmpty) {
131 + return Center(child: Text("No sessions available"));
132 + }
133 +
134 + final dateFormat = DateFormat('yyyy-MM-dd HH:mm:ss');
135 +
136 + return ListView.builder(
137 + itemCount: sessions.length,
138 + itemBuilder: (context, index) {
139 + final session = sessions[index];
140 + final isActive = session.endTime == null;
141 +
142 + return ExpansionTile(
143 + title: Text(
144 + session.name,
145 + style: TextStyle(
146 + fontWeight: FontWeight.bold,
147 + color: isActive ? Colors.green : null,
148 + ),
149 + ),
150 + subtitle: Text(
151 + 'ID: ${session.id} | Started: ${dateFormat.format(session.startTime)}',
152 + style: TextStyle(fontSize: 12),
153 + ),
154 + children: [
155 + Padding(
156 + padding: const EdgeInsets.all(16.0),
157 + child: Column(
158 + crossAxisAlignment: CrossAxisAlignment.start,
159 + children: [
160 + Text('Start: ${session.startTime.toString()}'),
161 + if (session.endTime != null)
162 + Text('End: ${session.endTime.toString()}'),
163 + if (session.duration != null)
164 + Text('Duration: ${_formatDuration(session.duration!)}'),
165 + SizedBox(height: 8),
166 + _buildSessionLogs(context, session.id),
167 + ],
168 + ),
169 + ),
170 + ],
171 + );
172 + },
173 + );
174 + }
175 +
176 + Widget _buildSessionLogs(BuildContext context, int sessionId) {
177 + final sessionLogs = viewModel.logs
178 + .where((log) => log.sessionId == sessionId)
179 + .toList();
180 +
181 + if (sessionLogs.isEmpty) {
182 + return Text('No logs for this session');
183 + }
184 +
185 + return Column(
186 + crossAxisAlignment: CrossAxisAlignment.start,
187 + children: [
188 + Text('Session Logs (${sessionLogs.length}):',
189 + style: TextStyle(fontWeight: FontWeight.bold)),
190 + SizedBox(height: 8),
191 + Container(
192 + height: 200,
193 + decoration: BoxDecoration(
194 + color: Colors.black.withOpacity(0.05),
195 + borderRadius: BorderRadius.circular(4),
196 + ),
197 + child: ListView.builder(
198 + itemCount: sessionLogs.length,
199 + itemBuilder: (context, index) {
200 + final log = sessionLogs[index];
201 + return Padding(
202 + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
203 + child: Text(
204 + '[${log.level}] ${log.message}',
205 + style: TextStyle(
206 + fontSize: 12,
207 + fontFamily: 'Monospace',
208 + color: _getLevelColor(log.level),
209 + ),
210 + ),
211 + );
212 + },
213 + ),
214 + ),
215 + ],
216 + );
217 + }
218 +
219 + Widget _buildActionButtons(BuildContext context) {
220 + return Padding(
221 + padding: const EdgeInsets.all(16.0),
222 + child: Row(
223 + mainAxisAlignment: MainAxisAlignment.spaceEvenly,
224 + children: [
225 + ElevatedButton.icon(
226 + icon: Icon(Icons.refresh),
227 + label: Text('Refresh'),
228 + onPressed: () => viewModel.loadLogs(),
229 + ),
230 + ElevatedButton.icon(
231 + icon: Icon(Icons.copy),
232 + label: Text('Copy All'),
233 + onPressed: () => _copyAllLogs(context),
234 + ),
235 + ElevatedButton.icon(
236 + icon: Icon(Icons.delete),
237 + label: Text('Clear'),
238 + onPressed: () => _confirmClearLogs(context),
239 + style: ElevatedButton.styleFrom(
240 + backgroundColor: Colors.red,
241 + foregroundColor: Colors.white,
242 + ),
243 + ),
244 + ],
245 + ),
246 + );
247 + }
248 +
249 + void _copyAllLogs(BuildContext context) {
250 + if (viewModel.logData == null) return;
251 +
252 + final buffer = StringBuffer();
253 + final dateFormat = DateFormat('yyyy-MM-dd HH:mm:ss.SSS');
254 +
255 + for (final log in viewModel.logs) {
256 + buffer.writeln('${dateFormat.format(log.timestamp)} [${log.level}] ${log.message}');
257 + }
258 +
259 + Clipboard.setData(ClipboardData(text: buffer.toString()));
260 + ScaffoldMessenger.of(context).showSnackBar(
261 + SnackBar(content: Text('All logs copied to clipboard')),
262 + );
263 + }
264 +
265 + void _confirmClearLogs(BuildContext context) {
266 + showDialog(
267 + context: context,
268 + builder: (BuildContext context) {
269 + return AlertDialog(
270 + title: Text('Clear Logs'),
271 + content: Text('Are you sure you want to clear the logs display?'),
272 + actions: <Widget>[
273 + TextButton(
274 + child: Text('Cancel'),
275 + onPressed: () => Navigator.of(context).pop(),
276 + ),
277 + TextButton(
278 + child: Text('Clear'),
279 + style: TextButton.styleFrom(foregroundColor: Colors.red),
280 + onPressed: () {
281 + viewModel.clearLogs();
282 + Navigator.of(context).pop();
283 + },
284 + ),
285 + ],
286 + );
287 + },
288 + );
289 + }
290 +
291 + Color _getLevelColor(String level) {
292 + switch (level.toLowerCase()) {
293 + case 'error':
294 + return Colors.red;
295 + case 'warning':
296 + return Colors.orange;
297 + case 'info':
298 + return Colors.blue;
299 + case 'debug':
300 + return Colors.green;
301 + case 'trace':
302 + return Colors.purple;
303 + default:
304 + return Colors.grey;
305 + }
306 + }
307 +
308 + String _formatDuration(Duration duration) {
309 + String twoDigits(int n) => n.toString().padLeft(2, '0');
310 + String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
311 + String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
312 + return '${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds';
313 + }
314 +}
\ No newline at end of file
lib/src/screens/dev/shared_preferences_page.dart new
+404
@@ -0,0 +1,404 @@
1 +import 'package:cake_wallet/src/screens/base_page.dart';
2 +import 'package:cake_wallet/view_model/dev/shared_preferences.dart';
3 +import 'package:flutter/material.dart';
4 +import 'package:flutter/services.dart';
5 +import 'package:flutter_mobx/flutter_mobx.dart';
6 +
7 +class DevSharedPreferencesPage extends BasePage {
8 + final DevSharedPreferences viewModel;
9 +
10 + DevSharedPreferencesPage(this.viewModel);
11 +
12 + @override
13 + String? get title => "[dev] shared preferences";
14 +
15 + @override
16 + Widget? trailing(BuildContext context) {
17 + return IconButton(
18 + icon: Icon(Icons.add),
19 + onPressed: () => _showCreateDialog(context),
20 + );
21 + }
22 +
23 + @override
24 + Widget body(BuildContext context) {
25 + return Observer(
26 + builder: (_) {
27 + if (viewModel.sharedPreferences == null) {
28 + return Center(child: Text("No shared preferences found"));
29 + }
30 + final keys = viewModel.keys;
31 + Map<String, dynamic> values = {};
32 + for (final key in keys) {
33 + values[key] = viewModel.get(key);
34 + }
35 + Map<String, PreferenceType> types = {};
36 + for (final key in keys) {
37 + types[key] = viewModel.getPreferenceType(key);
38 + }
39 + return ListView.builder(
40 + itemCount: keys.length,
41 + itemBuilder: (context, index) {
42 + final key = keys[index];
43 + final type = types[key]!;
44 + return ListTile(
45 + onTap: () {
46 + Clipboard.setData(ClipboardData(text: key + ": " + values[key].toString()));
47 + },
48 + onLongPress: () {
49 + _showEditDialog(context, key, type, values[key]);
50 + },
51 + title: switch (type) {
52 + PreferenceType.bool => Text(key, style: TextStyle(color: Colors.blue)),
53 + PreferenceType.int => Text(key, style: TextStyle(color: Colors.green)),
54 + PreferenceType.double => Text(key, style: TextStyle(color: Colors.yellow)),
55 + PreferenceType.listString => Text(key, style: TextStyle(color: Colors.purple)),
56 + PreferenceType.string => Text(key),
57 + PreferenceType.unknown => Text(key),
58 + },
59 + subtitle: switch (type) {
60 + PreferenceType.bool => Text("bool: ${values[key]}"),
61 + PreferenceType.int => Text("int: ${values[key]}"),
62 + PreferenceType.double => Text("double: ${values[key]}"),
63 + PreferenceType.listString => values[key].isEmpty as bool ? Text("listString: []") : Text("listString:\n- ${values[key].join("\n- ")}"),
64 + PreferenceType.string => Text("string: ${values[key]}"),
65 + PreferenceType.unknown => Text("UNKNOWN(${values[key].runtimeType}): ${values[key]}"),
66 + },
67 + );
68 + },
69 + );
70 + },
71 + );
72 + }
73 +
74 + void _showEditDialog(BuildContext context, String key, PreferenceType type, dynamic currentValue) {
75 + dynamic newValue = currentValue;
76 + bool isListString = type == PreferenceType.listString;
77 + List<String> listItems = isListString ? List<String>.from(currentValue as Iterable<dynamic>) : [];
78 + TextEditingController textController = TextEditingController(
79 + text: isListString ? '' : currentValue?.toString() ?? '');
80 +
81 + showDialog(
82 + context: context,
83 + builder: (BuildContext context) {
84 + return StatefulBuilder(
85 + builder: (context, setState) {
86 + return AlertDialog(
87 + title: Text('Edit $key'),
88 + content: SizedBox(
89 + width: double.maxFinite,
90 + height: double.maxFinite,
91 + child: SingleChildScrollView(
92 + child: _buildDialogContent(
93 + type,
94 + newValue,
95 + listItems,
96 + textController,
97 + (value) => setState(() => newValue = value),
98 + (items) => setState(() => listItems = items),
99 + ),
100 + ),
101 + ),
102 + actions: <Widget>[
103 + TextButton(
104 + child: Text('Delete'),
105 + style: TextButton.styleFrom(foregroundColor: Colors.red),
106 + onPressed: () {
107 + _showDeleteConfirmation(context, key);
108 + },
109 + ),
110 + TextButton(
111 + child: Text('Cancel'),
112 + onPressed: () => Navigator.of(context).pop(),
113 + ),
114 + TextButton(
115 + child: Text('Save'),
116 + onPressed: () async {
117 + if (_validateAndUpdateValue(
118 + context,
119 + type,
120 + textController,
121 + listItems,
122 + (value) => newValue = value
123 + )) {
124 + await viewModel.set(key, type, newValue);
125 + Navigator.of(context).pop();
126 + }
127 + },
128 + ),
129 + ],
130 + );
131 + },
132 + );
133 + },
134 + );
135 + }
136 +
137 + void _showDeleteConfirmation(BuildContext context, String key) {
138 + showDialog(
139 + context: context,
140 + builder: (BuildContext context) {
141 + return AlertDialog(
142 + title: Text('Delete Preference'),
143 + content: Text('Are you sure you want to delete "$key"?'),
144 + actions: <Widget>[
145 + TextButton(
146 + child: Text('Cancel'),
147 + onPressed: () => Navigator.of(context).pop(),
148 + ),
149 + TextButton(
150 + child: Text('Delete'),
151 + style: TextButton.styleFrom(foregroundColor: Colors.red),
152 + onPressed: () {
153 + viewModel.delete(key);
154 + Navigator.of(context).pop();
155 + Navigator.of(context).pop();
156 + },
157 + ),
158 + ],
159 + );
160 + },
161 + );
162 + }
163 +
164 + Widget _buildDialogContent(
165 + PreferenceType type,
166 + dynamic value,
167 + List<String> listItems,
168 + TextEditingController textController,
169 + Function(dynamic) onValueChanged,
170 + Function(List<String>) onListChanged,
171 + ) {
172 + return switch (type) {
173 + PreferenceType.bool => _buildBoolEditor(value as bool, onValueChanged),
174 + PreferenceType.int => _buildNumberEditor(textController, 'Integer value', true),
175 + PreferenceType.double => _buildNumberEditor(textController, 'Double value', false),
176 + PreferenceType.string => _buildTextEditor(textController),
177 + PreferenceType.listString => _buildListEditor(listItems, textController, onListChanged),
178 + PreferenceType.unknown => Text('Cannot edit unknown type'),
179 + };
180 + }
181 +
182 + Widget _buildBoolEditor(bool value, Function(bool) onChanged) {
183 + return CheckboxListTile(
184 + title: Text('Value'),
185 + value: value,
186 + onChanged: (newValue) {
187 + if (newValue != null) onChanged(newValue);
188 + },
189 + );
190 + }
191 +
192 + Widget _buildTextEditor(TextEditingController controller) {
193 + return Column(
194 + mainAxisSize: MainAxisSize.min,
195 + children: [
196 + TextField(
197 + controller: controller,
198 + decoration: InputDecoration(labelText: 'String value'),
199 + maxLines: null,
200 + ),
201 + ],
202 + );
203 + }
204 +
205 + Widget _buildNumberEditor(TextEditingController controller, String label, bool isInteger) {
206 + return Column(
207 + mainAxisSize: MainAxisSize.min,
208 + children: [
209 + TextField(
210 + controller: controller,
211 + decoration: InputDecoration(labelText: label),
212 + keyboardType: isInteger
213 + ? TextInputType.number
214 + : TextInputType.numberWithOptions(decimal: true),
215 + inputFormatters: isInteger
216 + ? [FilteringTextInputFormatter.digitsOnly]
217 + : [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*$'))],
218 + ),
219 + ],
220 + );
221 + }
222 +
223 + Widget _buildListEditor(
224 + List<String> items,
225 + TextEditingController controller,
226 + Function(List<String>) onListChanged,
227 + ) {
228 + return Column(
229 + mainAxisSize: MainAxisSize.min,
230 + children: [
231 + SizedBox(
232 + height: 200,
233 + child: ReorderableListView(
234 + shrinkWrap: true,
235 + children: [
236 + for (int i = 0; i < items.length; i++)
237 + ListTile(
238 + key: Key('$i'),
239 + title: Text(items[i]),
240 + trailing: IconButton(
241 + icon: Icon(Icons.delete),
242 + onPressed: () {
243 + final newList = List<String>.from(items);
244 + newList.removeAt(i);
245 + onListChanged(newList);
246 + },
247 + ),
248 + )
249 + ],
250 + onReorder: (int oldIndex, int newIndex) {
251 + final newList = List<String>.from(items);
252 + if (oldIndex < newIndex) {
253 + newIndex -= 1;
254 + }
255 + final item = newList.removeAt(oldIndex);
256 + newList.insert(newIndex, item);
257 + onListChanged(newList);
258 + },
259 + ),
260 + ),
261 + Row(
262 + children: [
263 + Expanded(
264 + child: TextField(
265 + controller: controller,
266 + decoration: InputDecoration(labelText: 'New item'),
267 + ),
268 + ),
269 + IconButton(
270 + icon: Icon(Icons.add),
271 + onPressed: () {
272 + if (controller.text.isNotEmpty) {
273 + final newList = List<String>.from(items);
274 + newList.add(controller.text);
275 + onListChanged(newList);
276 + controller.clear();
277 + }
278 + },
279 + ),
280 + ],
281 + ),
282 + ],
283 + );
284 + }
285 +
286 + bool _validateAndUpdateValue(
287 + BuildContext context,
288 + PreferenceType type,
289 + TextEditingController controller,
290 + List<String> listItems,
291 + Function(dynamic) setNewValue,
292 + ) {
293 + switch (type) {
294 + case PreferenceType.int:
295 + if (controller.text.isNotEmpty) {
296 + try {
297 + setNewValue(int.parse(controller.text));
298 + } catch (e) {
299 + _showErrorMessage(context, 'Invalid integer value');
300 + return false;
301 + }
302 + }
303 + break;
304 + case PreferenceType.double:
305 + if (controller.text.isNotEmpty) {
306 + try {
307 + setNewValue(double.parse(controller.text));
308 + } catch (e) {
309 + _showErrorMessage(context, 'Invalid double value');
310 + return false;
311 + }
312 + }
313 + break;
314 + case PreferenceType.string:
315 + setNewValue(controller.text);
316 + break;
317 + case PreferenceType.listString:
318 + setNewValue(listItems);
319 + break;
320 + default:
321 + break;
322 + }
323 + return true;
324 + }
325 +
326 + void _showErrorMessage(BuildContext context, String message) {
327 + ScaffoldMessenger.of(context).showSnackBar(
328 + SnackBar(content: Text(message)),
329 + );
330 + }
331 +
332 + void _showCreateDialog(BuildContext context) {
333 + PreferenceType selectedType = PreferenceType.string;
334 + TextEditingController keyController = TextEditingController();
335 +
336 + showDialog(
337 + context: context,
338 + builder: (BuildContext context) {
339 + return StatefulBuilder(
340 + builder: (context, setState) {
341 + return AlertDialog(
342 + title: Text('Create Preference'),
343 + content: SingleChildScrollView(
344 + child: Column(
345 + mainAxisSize: MainAxisSize.min,
346 + children: [
347 + TextField(
348 + controller: keyController,
349 + decoration: InputDecoration(labelText: 'Preference Key'),
350 + ),
351 + SizedBox(height: 16),
352 + DropdownButtonFormField<PreferenceType>(
353 + value: selectedType,
354 + decoration: InputDecoration(labelText: 'Type'),
355 + items: [
356 + DropdownMenuItem(value: PreferenceType.string, child: Text('String')),
357 + DropdownMenuItem(value: PreferenceType.bool, child: Text('Boolean')),
358 + DropdownMenuItem(value: PreferenceType.int, child: Text('Integer')),
359 + DropdownMenuItem(value: PreferenceType.double, child: Text('Double')),
360 + DropdownMenuItem(value: PreferenceType.listString, child: Text('List of Strings')),
361 + ],
362 + onChanged: (value) {
363 + if (value != null) {
364 + setState(() {
365 + selectedType = value;
366 + });
367 + }
368 + },
369 + ),
370 + ],
371 + ),
372 + ),
373 + actions: <Widget>[
374 + TextButton(
375 + child: Text('Cancel'),
376 + onPressed: () => Navigator.of(context).pop(),
377 + ),
378 + TextButton(
379 + child: Text('Create'),
380 + onPressed: () {
381 + if (keyController.text.isEmpty) {
382 + _showErrorMessage(context, 'Key cannot be empty');
383 + return;
384 + }
385 +
386 + viewModel.set(keyController.text, selectedType, switch (selectedType) {
387 + PreferenceType.bool => false,
388 + PreferenceType.int => 0,
389 + PreferenceType.double => 0.0,
390 + PreferenceType.string => '',
391 + PreferenceType.listString => [],
392 + PreferenceType.unknown => null,
393 + });
394 + Navigator.of(context).pop();
395 + },
396 + ),
397 + ],
398 + );
399 + },
400 + );
401 + },
402 + );
403 + }
404 +}
lib/src/screens/settings/background_sync_page.dart
+72 -35
@@ -12,6 +12,7 @@ import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
12 import 'package:cake_wallet/view_model/settings/sync_mode.dart';
13 import 'package:flutter/material.dart';
14 import 'package:flutter_mobx/flutter_mobx.dart';
15 +import 'package:permission_handler/permission_handler.dart';
16
17 class BackgroundSyncPage extends BasePage {
18 BackgroundSyncPage(this.dashboardViewModel);
@@ -28,30 +29,30 @@ class BackgroundSyncPage extends BasePage {
29 child: Column(
30 mainAxisSize: MainAxisSize.min,
31 children: [
31 - if (dashboardViewModel.hasBatteryOptimization)
32 - Observer(builder: (context) {
33 - return SettingsSwitcherCell(
34 - title: S.current.unrestricted_background_service,
35 - value: !dashboardViewModel.batteryOptimizationEnabled,
36 - onValueChange: (_, bool value) {
37 - dashboardViewModel.disableBatteryOptimization();
38 - },
39 - );
40 - }),
32 Observer(builder: (context) {
33 return SettingsSwitcherCell(
34 title: S.current.background_sync,
35 value: dashboardViewModel.backgroundSyncEnabled,
45 - onValueChange: (dashboardViewModel.batteryOptimizationEnabled && dashboardViewModel.hasBatteryOptimization) ? (_, bool value) {
46 - unawaited(showPopUp(context: context, builder: (context) => AlertWithOneAction(
47 - alertTitle: S.current.background_sync,
48 - alertContent: S.current.unrestricted_background_service_notice,
49 - buttonText: S.current.ok,
50 - buttonAction: () => Navigator.of(context).pop(),
51 - )));
52 - } : (_, bool value) {
36 + onValueChange: (_, bool value) async {
37 if (value) {
54 - dashboardViewModel.enableBackgroundSync();
38 + if (dashboardViewModel.batteryOptimizationEnabled) {
39 + await showPopUp(context: context, builder: (context) => AlertWithOneAction(
40 + alertTitle: S.current.background_sync,
41 + alertContent: S.current.unrestricted_background_service_notice,
42 + buttonText: S.current.ok,
43 + buttonAction: () => Navigator.of(context).pop(),
44 + ));
45 + await dashboardViewModel.disableBatteryOptimization();
46 + for (var i = 0; i < 4 * 60; i++) {
47 + await Future.delayed(Duration(milliseconds: 250));
48 + if (!dashboardViewModel.batteryOptimizationEnabled) {
49 + await dashboardViewModel.enableBackgroundSync();
50 + return;
51 + }
52 + }
53 + } else {
54 + dashboardViewModel.enableBackgroundSync();
55 + }
56 } else {
57 dashboardViewModel.disableBackgroundSync();
58 }
@@ -68,22 +69,58 @@ class BackgroundSyncPage extends BasePage {
69 dashboardViewModel.setSyncMode(syncMode);
70 });
71 }),
71 -
72 - // Observer(builder: (context) {
73 - // return SettingsSwitcherCell(
74 - // title: S.current.background_sync_on_battery,
75 - // value: dashboardViewModel.backgroundSyncOnBattery,
76 - // onValueChange: (_, bool value) =>
77 - // dashboardViewModel.setBackgroundSyncOnBattery(value),
78 - // );
79 - // }),
80 - // Observer(builder: (context) {
81 - // return SettingsSwitcherCell(
82 - // title: S.current.background_sync_on_data,
83 - // value: dashboardViewModel.backgroundSyncOnData,
84 - // onValueChange: (_, bool value) => dashboardViewModel.setBackgroundSyncOnData(value),
85 - // );
86 - // }),
72 + if (dashboardViewModel.hasBgsyncNetworkConstraints)
73 + Observer(builder: (context) {
74 + return SettingsSwitcherCell(
75 + title: S.current.background_sync_on_unmetered_network,
76 + value: dashboardViewModel.backgroundSyncNetworkUnmetered,
77 + onValueChange: (_, bool value) => dashboardViewModel.setBackgroundSyncNetworkUnmetered(value),
78 + );
79 + }),
80 + if (dashboardViewModel.hasBgsyncBatteryNotLowConstraints)
81 + Observer(builder: (context) {
82 + return SettingsSwitcherCell(
83 + title: S.current.background_sync_on_battery_low,
84 + value: !dashboardViewModel.backgroundSyncBatteryNotLow,
85 + onValueChange: (_, bool value) => dashboardViewModel.setBackgroundSyncBatteryNotLow(!value),
86 + );
87 + }),
88 + if (dashboardViewModel.hasBgsyncChargingConstraints)
89 + Observer(builder: (context) {
90 + return SettingsSwitcherCell(
91 + title: S.current.background_sync_on_charging,
92 + value: dashboardViewModel.backgroundSyncCharging,
93 + onValueChange: (_, bool value) => dashboardViewModel.setBackgroundSyncCharging(value),
94 + );
95 + }),
96 + if (dashboardViewModel.hasBgsyncDeviceIdleConstraints)
97 + Observer(builder: (context) {
98 + return SettingsSwitcherCell(
99 + title: S.current.background_sync_on_device_idle,
100 + value: dashboardViewModel.backgroundSyncDeviceIdle,
101 + onValueChange: (_, bool value) => dashboardViewModel.setBackgroundSyncDeviceIdle(value),
102 + );
103 + }),
104 + Observer(builder: (context) {
105 + return SettingsSwitcherCell(
106 + title: S.current.new_transactions_notifications,
107 + value: dashboardViewModel.backgroundSyncNotificationsEnabled,
108 + onValueChange: (_, bool value) {
109 + try {
110 + dashboardViewModel.setBackgroundSyncNotificationsEnabled(value);
111 + } catch (e) {
112 + showPopUp(context: context, builder: (context) => AlertWithOneAction(
113 + alertTitle: S.current.error,
114 + alertContent: S.current.notification_permission_denied,
115 + buttonText: S.current.ok,
116 + buttonAction: () {
117 + Navigator.of(context).pop();
118 + },
119 + ));
120 + }
121 + },
122 + );
123 + }),
124 ],
125 ),
126 );
lib/src/screens/settings/connection_sync_page.dart
+1 -1
@@ -49,7 +49,7 @@ class ConnectionSyncPage extends BasePage {
49 title: S.current.manage_nodes,
50 handler: (context) => Navigator.of(context).pushNamed(Routes.manageNodes),
51 ),
52 - if (dashboardViewModel.hasBackgroundSync && Platform.isAndroid && FeatureFlag.isBackgroundSyncEnabled) ...[
52 + if (Platform.isAndroid && FeatureFlag.isBackgroundSyncEnabled) ...[
53 SettingsCellWithArrow(
54 title: S.current.background_sync,
55 handler: (context) => Navigator.of(context).pushNamed(Routes.backgroundSync),
lib/src/screens/settings/other_settings_page.dart
+12 -2
@@ -6,12 +6,10 @@ import 'package:cake_wallet/src/screens/base_page.dart';
6 import 'package:cake_wallet/src/screens/settings/widgets/setting_priority_picker_cell.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';
9 import 'package:cake_wallet/src/screens/settings/widgets/settings_version_cell.dart';
10 import 'package:cake_wallet/utils/feature_flag.dart';
11 import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart';
12 import 'package:cw_core/wallet_type.dart';
14 -import 'package:flutter/foundation.dart';
13 import 'package:flutter/material.dart';
14 import 'package:flutter_mobx/flutter_mobx.dart';
15
@@ -77,6 +75,18 @@ class OtherSettingsPage extends BasePage {
75 handler: (BuildContext context) =>
76 Navigator.of(context).pushNamed(Routes.devMoneroCallProfiler),
77 ),
78 + if (FeatureFlag.hasDevOptions)
79 + SettingsCellWithArrow(
80 + title: '[dev] shared preferences',
81 + handler: (BuildContext context) =>
82 + Navigator.of(context).pushNamed(Routes.devSharedPreferences),
83 + ),
84 + if (FeatureFlag.hasDevOptions)
85 + SettingsCellWithArrow(
86 + title: '[dev] background sync logs',
87 + handler: (BuildContext context) =>
88 + Navigator.of(context).pushNamed(Routes.devBackgroundSyncLogs),
89 + ),
90 Spacer(),
91 SettingsVersionCell(
92 title: S.of(context).version(_otherSettingsViewModel.currentVersion)),
lib/view_model/dashboard/dashboard_view_model.dart
+84 -1
@@ -51,6 +51,7 @@ import 'package:flutter/services.dart';
51 import 'package:flutter_daemon/flutter_daemon.dart';
52 import 'package:http/http.dart' as http;
53 import 'package:mobx/mobx.dart';
54 +import 'package:permission_handler/permission_handler.dart';
55 import 'package:shared_preferences/shared_preferences.dart';
56
57 import '../../themes/theme_base.dart';
@@ -180,7 +181,7 @@ abstract class DashboardViewModelBase with Store {
181 isShowThirdYatIntroduction = false;
182 unawaited(isBackgroundSyncEnabled());
183 unawaited(isBatteryOptimizationEnabled());
183 -
184 + unawaited(_loadConstraints());
185 final _wallet = wallet;
186
187 if (_wallet.type == WalletType.monero) {
@@ -536,6 +537,88 @@ abstract class DashboardViewModelBase with Store {
537 return resp;
538 }
539
540 + @observable
541 + late bool backgroundSyncNotificationsEnabled = sharedPreferences.getBool(PreferencesKey.backgroundSyncNotificationsEnabled) ?? false;
542 +
543 + @action
544 + Future<void> setBackgroundSyncNotificationsEnabled(bool value) async {
545 + if (!value) {
546 + backgroundSyncNotificationsEnabled = false;
547 + sharedPreferences.setBool(PreferencesKey.backgroundSyncNotificationsEnabled, false);
548 + return;
549 + }
550 + PermissionStatus permissionStatus = await Permission.notification.status;
551 + if (permissionStatus != PermissionStatus.granted) {
552 + final resp = await Permission.notification.request();
553 + if (resp == PermissionStatus.denied) {
554 + throw Exception("Notification permission denied");
555 + }
556 + }
557 + backgroundSyncNotificationsEnabled = value;
558 + await sharedPreferences.setBool(PreferencesKey.backgroundSyncNotificationsEnabled, value);
559 + }
560 +
561 +
562 + bool get hasBgsyncNetworkConstraints => Platform.isAndroid;
563 + bool get hasBgsyncBatteryNotLowConstraints => Platform.isAndroid;
564 + bool get hasBgsyncChargingConstraints => Platform.isAndroid;
565 + bool get hasBgsyncDeviceIdleConstraints => Platform.isAndroid;
566 +
567 + @observable
568 + bool backgroundSyncNetworkUnmetered = false;
569 +
570 + @observable
571 + bool backgroundSyncBatteryNotLow = false;
572 +
573 + @observable
574 + bool backgroundSyncCharging = false;
575 +
576 + @observable
577 + bool backgroundSyncDeviceIdle = false;
578 +
579 + Future<void> _loadConstraints() async {
580 + backgroundSyncNetworkUnmetered = await FlutterDaemon().getNetworkType();
581 + backgroundSyncBatteryNotLow = await FlutterDaemon().getBatteryNotLow();
582 + backgroundSyncCharging = await FlutterDaemon().getRequiresCharging();
583 + backgroundSyncDeviceIdle = await FlutterDaemon().getDeviceIdle();
584 + }
585 +
586 + @action
587 + Future<void> setBackgroundSyncNetworkUnmetered(bool value) async {
588 + backgroundSyncNetworkUnmetered = value;
589 + await FlutterDaemon().setNetworkType(value);
590 + if (await isBackgroundSyncEnabled()) {
591 + await enableBackgroundSync();
592 + }
593 + }
594 +
595 + @action
596 + Future<void> setBackgroundSyncBatteryNotLow(bool value) async {
597 + backgroundSyncBatteryNotLow = value;
598 + await FlutterDaemon().setBatteryNotLow(value);
599 + if (await isBackgroundSyncEnabled()) {
600 + await enableBackgroundSync();
601 + }
602 + }
603 +
604 + @action
605 + Future<void> setBackgroundSyncCharging(bool value) async {
606 + backgroundSyncCharging = value;
607 + await FlutterDaemon().setRequiresCharging(value);
608 + if (await isBackgroundSyncEnabled()) {
609 + await enableBackgroundSync();
610 + }
611 + }
612 +
613 + @action
614 + Future<void> setBackgroundSyncDeviceIdle(bool value) async {
615 + backgroundSyncDeviceIdle = value;
616 + await FlutterDaemon().setDeviceIdle(value);
617 + if (await isBackgroundSyncEnabled()) {
618 + await enableBackgroundSync();
619 + }
620 + }
621 +
622 bool get hasBatteryOptimization => Platform.isAndroid;
623
624 @observable
lib/view_model/dev/background_sync_logs_view_model.dart new
+44
@@ -0,0 +1,44 @@
1 +import 'package:flutter_daemon/flutter_daemon.dart';
2 +import 'package:mobx/mobx.dart';
3 +
4 +part 'background_sync_logs_view_model.g.dart';
5 +class BackgroundSyncLogsViewModel = BackgroundSyncLogsViewModelBase with _$BackgroundSyncLogsViewModel;
6 +
7 +abstract class BackgroundSyncLogsViewModelBase with Store {
8 + final FlutterDaemon _daemon = FlutterDaemon();
9 +
10 + @observable
11 + LogData? logData;
12 +
13 + @observable
14 + bool isLoading = false;
15 +
16 + @observable
17 + String? error;
18 +
19 + @computed
20 + List<LogEntry> get logs => logData?.logs ?? [];
21 +
22 + @computed
23 + List<LogSession> get sessions => logData?.sessions ?? [];
24 +
25 + @action
26 + Future<void> loadLogs() async {
27 + isLoading = true;
28 + error = null;
29 +
30 + try {
31 + logData = await _daemon.getLogs();
32 + } catch (e) {
33 + error = e.toString();
34 + } finally {
35 + isLoading = false;
36 + }
37 + }
38 +
39 + @action
40 + Future<void> clearLogs() async {
41 + await _daemon.clearLogs();
42 + await loadLogs();
43 + }
44 +}
\ No newline at end of file
lib/view_model/dev/shared_preferences.dart new
+92
@@ -0,0 +1,92 @@
1 +import 'package:mobx/mobx.dart';
2 +import 'package:shared_preferences/shared_preferences.dart';
3 +
4 +part 'shared_preferences.g.dart';
5 +
6 +class DevSharedPreferences = DevSharedPreferencesBase with _$DevSharedPreferences;
7 +
8 +enum PreferenceType {
9 + unknown,
10 + string,
11 + int,
12 + double,
13 + bool,
14 + listString
15 +}
16 +
17 +abstract class DevSharedPreferencesBase with Store {
18 + DevSharedPreferencesBase() {
19 + SharedPreferences.getInstance().then((value) {
20 + sharedPreferences = value;
21 + });
22 + }
23 +
24 + @observable
25 + SharedPreferences? sharedPreferences;
26 +
27 + @computed
28 + List<String> get keys => (sharedPreferences?.getKeys().toList()?..sort()) ?? [];
29 +
30 + @action
31 + Future<void> delete(String key) async {
32 + if (sharedPreferences == null) {
33 + return;
34 + }
35 + await sharedPreferences!.remove(key);
36 + }
37 +
38 + dynamic get(String key) {
39 + if (sharedPreferences == null) {
40 + return null;
41 + }
42 + return sharedPreferences!.get(key);
43 + }
44 +
45 + Future<void> set(String key, PreferenceType type, dynamic value) async {
46 + if (sharedPreferences == null) {
47 + return;
48 + }
49 + switch (type) {
50 + case PreferenceType.string:
51 + await sharedPreferences!.setString(key, value as String);
52 + break;
53 + case PreferenceType.bool:
54 + await sharedPreferences!.setBool(key, value as bool);
55 + break;
56 + case PreferenceType.int:
57 + await sharedPreferences!.setInt(key, value as int);
58 + break;
59 + case PreferenceType.double:
60 + await sharedPreferences!.setDouble(key, value as double);
61 + break;
62 + case PreferenceType.listString:
63 + await sharedPreferences!.setStringList(key, List<String>.from(value as Iterable<dynamic>));
64 + break;
65 + default:
66 + throw Exception("Unknown preference type: $type");
67 + }
68 + }
69 +
70 + PreferenceType getPreferenceType(String key) {
71 + if (sharedPreferences == null) {
72 + return PreferenceType.unknown;
73 + }
74 + final value = sharedPreferences!.get(key);
75 + if (value is String) {
76 + return PreferenceType.string;
77 + }
78 + if (value is bool) {
79 + return PreferenceType.bool;
80 + }
81 + if (value is int) {
82 + return PreferenceType.int;
83 + }
84 + if (value is double) {
85 + return PreferenceType.double;
86 + }
87 + if (value is List<String>) {
88 + return PreferenceType.listString;
89 + }
90 + return PreferenceType.unknown;
91 + }
92 +}
lib/view_model/send/send_view_model.dart
+4
@@ -14,6 +14,7 @@ import 'package:cake_wallet/entities/evm_transaction_error_fees_handler.dart';
14 import 'package:cake_wallet/entities/fiat_currency.dart';
15 import 'package:cake_wallet/entities/parsed_address.dart';
16 import 'package:cake_wallet/entities/template.dart';
17 +import 'package:cake_wallet/entities/preferences_key.dart';
18 import 'package:cake_wallet/entities/transaction_description.dart';
19 import 'package:cake_wallet/entities/wallet_contact.dart';
20 import 'package:cake_wallet/ethereum/ethereum.dart';
@@ -52,6 +53,7 @@ import 'package:cw_core/wallet_type.dart';
53 import 'package:flutter/material.dart';
54 import 'package:hive/hive.dart';
55 import 'package:mobx/mobx.dart';
56 +import 'package:shared_preferences/shared_preferences.dart';
57
58 part 'send_view_model.g.dart';
59
@@ -587,6 +589,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
589 transactionNote: note,
590 ));
591 }
592 + final sharedPreferences = await SharedPreferences.getInstance();
593 + await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name), DateTime.now().add(Duration(minutes: 1)).toIso8601String());
594
595 state = TransactionCommitted();
596 } catch (e) {
pubspec_base.yaml
+8 -4
@@ -93,8 +93,8 @@ dependencies:
93 eth_sig_util: ^0.0.9
94 ens_dart:
95 git:
96 - url: https://github.com/cake-tech/ens_dart.git
97 - ref: main
96 + url: https://github.com/MrCyjaneK/ens_dart.git
97 + ref: 9fa09b9db69b8645d5d50a844652aa570451d101
98 fluttertoast: 8.2.12
99 # tor:
100 # git:
@@ -103,7 +103,10 @@ dependencies:
103 socks5_proxy: ^1.0.4
104 flutter_svg: ^2.0.9
105 polyseed: ^0.0.7
106 - nostr_tools: ^1.0.9
106 + nostr_tools:
107 + git:
108 + url: https://github.com/MrCyjaneK/nostr_tools.git
109 + ref: 089d5a2dd751429a040ba10fb24fcbae564053e5
110 ledger_flutter_plus:
111 git:
112 url: https://github.com/vespr-wallet/ledger-flutter-plus
@@ -121,7 +124,8 @@ dependencies:
124 flutter_daemon:
125 git:
126 url: https://github.com/MrCyjaneK/flutter_daemon
124 - ref: 5c369e0e69e6f459357b9802bc694a221397298a
127 + ref: 6d5270d64b5dd588fce12fd0a0c7314c37e6cff1
128 + flutter_local_notifications: ^19.0.0
129
130 dev_dependencies:
131 flutter_test:
res/values/strings_ar.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "في انتظار تأكيد الدفع",
74 "background_sync": "مزامنة الخلفية",
75 "background_sync_mode": "وضع مزامنة الخلفية",
76 + "background_sync_on_battery_low": "تزامن على البطارية المنخفضة",
77 + "background_sync_on_charging": "تزامن فقط عند الشحن",
78 + "background_sync_on_device_idle": "تزامن فقط عند عدم استخدام الجهاز",
79 + "background_sync_on_unmetered_network": "تتطلب شبكة غير مستوفاة",
80 "backup": "نسخ الاحتياطي",
81 "backup_file": "ملف النسخ الاحتياطي",
82 "backup_password": "كلمة مرور النسخ الاحتياطي",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "تسمية",
488 "new_subaddress_title": "عنوان جديد",
489 "new_template": "قالب جديد",
490 + "new_transactions_notifications": "إرسال إشعارات حول المعاملات الجديدة",
491 "new_wallet": "إنشاء محفظة جديدة",
492 "newConnection": "ﺪﻳﺪﺟ ﻝﺎﺼﺗﺍ",
493 "no_cards_found": "لم يتم العثور على بطاقات",
@@ -507,6 +512,7 @@
512 "normal": "طبيعي",
513 "note_optional": "ملاحظة (اختياري)",
514 "note_tap_to_change": "ملاحظة (انقر للتغيير)",
515 + "notification_permission_denied": "تم رفض إذن الإخطار بشكل جيد ، يرجى تمكينه يدويًا في الإعدادات",
516 "nullURIError": "ﻍﺭﺎﻓ (URI) ﻢﻈﺘﻨﻤﻟﺍ ﺩﺭﺍﻮﻤﻟﺍ ﻑﺮﻌﻣ",
517 "offer_expires_in": "ينتهي العرض في:",
518 "offline": "غير متصل على الانترنت",
res/values/strings_bg.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "Чака се потвърждение на плащането",
74 "background_sync": "Фон Синхх",
75 "background_sync_mode": "Режим на синхронизиране на фона",
76 + "background_sync_on_battery_low": "Синхронизирайте на ниска батерия",
77 + "background_sync_on_charging": "Синхронизирайте само при зареждане",
78 + "background_sync_on_device_idle": "Синхронизирайте само когато устройството не се използва",
79 + "background_sync_on_unmetered_network": "Изисквайте незадоволена мрежа",
80 "backup": "Резервно копие",
81 "backup_file": "Резервно копие",
82 "backup_password": "Парола за възстановяване",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "Име на Label",
488 "new_subaddress_title": "Нов адрес",
489 "new_template": "Нов шаблон",
490 + "new_transactions_notifications": "Изпратете известия за нови транзакции",
491 "new_wallet": "Нов портфейл",
492 "newConnection": "Нова връзка",
493 "no_cards_found": "Не са намерени карти",
@@ -507,6 +512,7 @@
512 "normal": "нормално",
513 "note_optional": "Бележка (не е задължително)",
514 "note_tap_to_change": "Бележка (натиснете за промяна)",
515 + "notification_permission_denied": "Разрешението за уведомяване е отказано, моля, моля, активирайте го в настройки",
516 "nullURIError": "URI е нула",
517 "offer_expires_in": "Предложението изтича след: ",
518 "offline": "Офлайн",
res/values/strings_cs.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "Čeká se na potvrzení platby",
74 "background_sync": "Synchronizace pozadí",
75 "background_sync_mode": "Režim synchronizace pozadí",
76 + "background_sync_on_battery_low": "Synchronizace na nízké baterii",
77 + "background_sync_on_charging": "Synchronizovat pouze při nabíjení",
78 + "background_sync_on_device_idle": "Synchronizujte pouze tehdy, když se zařízení nepoužívá",
79 + "background_sync_on_unmetered_network": "Vyžadovat nemetrovou síť",
80 "backup": "Záloha",
81 "backup_file": "Soubor se zálohou",
82 "backup_password": "Heslo pro zálohy",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "Popisek",
488 "new_subaddress_title": "Nová adresa",
489 "new_template": "Nová šablona",
490 + "new_transactions_notifications": "Zašlete oznámení o nových transakcích",
491 "new_wallet": "Nová peněženka",
492 "newConnection": "Nové připojení",
493 "no_cards_found": "Žádné karty nenalezeny",
@@ -507,6 +512,7 @@
512 "normal": "Normální",
513 "note_optional": "Poznámka (nepovinné)",
514 "note_tap_to_change": "Poznámka (poklepáním upravit)",
515 + "notification_permission_denied": "Oznámení o oznámení bylo oprávněně zamítnuto, prosím ručně jej povolte v nastavení",
516 "nullURIError": "URI je nulové",
517 "offer_expires_in": "Nabídka vyprší: ",
518 "offline": "Offline",
res/values/strings_de.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "Warten auf Zahlungsbestätigung",
74 "background_sync": "Hintergrundsynchronisation",
75 "background_sync_mode": "Hintergrundsynchronisierungsmodus",
76 + "background_sync_on_battery_low": "Synchronisieren Sie einen niedrigen Akku",
77 + "background_sync_on_charging": "Nur beim Laden synchronisieren",
78 + "background_sync_on_device_idle": "Nur dann synchronisieren, wenn das Gerät nicht verwendet wird",
79 + "background_sync_on_unmetered_network": "Erfordern ein nicht modisches Netzwerk",
80 "backup": "Sicherung",
81 "backup_file": "Sicherungsdatei",
82 "backup_password": "Passwort sichern",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "Bezeichnung",
488 "new_subaddress_title": "Neue Adresse",
489 "new_template": "neue Vorlage",
490 + "new_transactions_notifications": "Senden Sie Benachrichtigungen über neue Transaktionen",
491 "new_wallet": "Neue Wallet",
492 "newConnection": "Neue Verbindung",
493 "no_cards_found": "Keine Karten gefunden",
@@ -507,6 +512,7 @@
512 "normal": "Normal",
513 "note_optional": "Bemerkung (optional)",
514 "note_tap_to_change": "Bemerkung (zum Ändern tippen)",
515 + "notification_permission_denied": "Die Benachrichtigungsgenehmigung wurde verweigert verweigert. Bitte ermöglichen Sie dies manuell in Einstellungen",
516 "nullURIError": "URI ist null",
517 "offer_expires_in": "Angebot läuft ab in: ",
518 "offline": "offline",
res/values/strings_en.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "Awaiting Payment Confirmation",
74 "background_sync": "Background sync",
75 "background_sync_mode": "Background sync mode",
76 + "background_sync_on_battery_low": "Synchronize on low battery",
77 + "background_sync_on_charging": "Synchronize only when charging",
78 + "background_sync_on_device_idle": "Synchronize only when device is not being used",
79 + "background_sync_on_unmetered_network": "Require unmetred network",
80 "backup": "Backup",
81 "backup_file": "Backup file",
82 "backup_password": "Backup password",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "Label name",
488 "new_subaddress_title": "New address",
489 "new_template": "New Template",
490 + "new_transactions_notifications": "Send notifications about new transactions",
491 "new_wallet": "New Wallet",
492 "newConnection": "New Connection",
493 "no_cards_found": "No cards found",
@@ -507,6 +512,7 @@
512 "normal": "Normal",
513 "note_optional": "Note (optional)",
514 "note_tap_to_change": "Note (tap to change)",
515 + "notification_permission_denied": "Notification permission got permamently denied, please manually enable it in settings",
516 "nullURIError": "URI is null",
517 "offer_expires_in": "Offer expires in: ",
518 "offline": "Offline",
res/values/strings_es.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "Esperando confirmación de pago",
74 "background_sync": "Sincronización en segundo plano",
75 "background_sync_mode": "Modo de sincronización en segundo plano",
76 + "background_sync_on_battery_low": "Sincronizar con batería baja",
77 + "background_sync_on_charging": "Sincronizar solo al cargar",
78 + "background_sync_on_device_idle": "Sincronizar solo cuando el dispositivo no se usa",
79 + "background_sync_on_unmetered_network": "Requerir una red no metida",
80 "backup": "Apoyo",
81 "backup_file": "Archivo de respaldo",
82 "backup_password": "Contraseña de respaldo",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "Nombre de etiqueta",
488 "new_subaddress_title": "Nueva direccion",
489 "new_template": "Nueva plantilla",
490 + "new_transactions_notifications": "Enviar notificaciones sobre nuevas transacciones",
491 "new_wallet": "Nueva billetera",
492 "newConnection": "Nueva conexión",
493 "no_cards_found": "No se encuentran cartas",
@@ -507,6 +512,7 @@
512 "normal": "Normal",
513 "note_optional": "Nota (opcional)",
514 "note_tap_to_change": "Nota (toque para cambiar)",
515 + "notification_permission_denied": "El permiso de notificación se negó de manera permanente, por favor, habilite manualmente en la configuración",
516 "nullURIError": "URI es nula",
517 "offer_expires_in": "Oferta expira en: ",
518 "offline": "fuera de línea",
res/values/strings_fr.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "En attente de confirmation de paiement",
74 "background_sync": "Synchronisation de fond",
75 "background_sync_mode": "Mode de synchronisation en arrière-plan",
76 + "background_sync_on_battery_low": "Synchroniser sur une batterie basse",
77 + "background_sync_on_charging": "Synchroniser uniquement lors de la charge",
78 + "background_sync_on_device_idle": "Synchroniser uniquement lorsque l'appareil n'est pas utilisé",
79 + "background_sync_on_unmetered_network": "Exiger un réseau non métallique",
80 "backup": "Sauvegarde",
81 "backup_file": "Fichier de sauvegarde",
82 "backup_password": "Mot de passe de sauvegarde",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "Nom",
488 "new_subaddress_title": "Nouvelle adresse",
489 "new_template": "Nouveau Modèle",
490 + "new_transactions_notifications": "Envoyer des notifications sur les nouvelles transactions",
491 "new_wallet": "Nouveau Portefeuille (Wallet)",
492 "newConnection": "Nouvelle connexion",
493 "no_cards_found": "Pas de cartes trouvées",
@@ -507,6 +512,7 @@
512 "normal": "Normal",
513 "note_optional": "Note (optionnelle)",
514 "note_tap_to_change": "Note (appuyez pour changer)",
515 + "notification_permission_denied": "L'autorisation de notification a été refusée permanente, veuillez l'activer manuellement dans les paramètres",
516 "nullURIError": "L'URI est nul",
517 "offer_expires_in": "L'Offre expire dans: ",
518 "offline": "Hors ligne",
res/values/strings_ha.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "Ana jiran Tabbacin Biyan Kuɗi",
74 "background_sync": "Tunawa da Setc",
75 "background_sync_mode": "Yanayin Sync",
76 + "background_sync_on_battery_low": "Aiki tare a kan baturin",
77 + "background_sync_on_charging": "Aiki tare kawai lokacin caji",
78 + "background_sync_on_device_idle": "Aiki tare kawai lokacin da ba a amfani da na'urar",
79 + "background_sync_on_unmetered_network": "Bukatar cibiyar sadarwar da ba ta dace ba",
80 "backup": "Ajiyayyen",
81 "backup_file": "Ajiyayyen fayil",
82 "backup_password": "Ajiyayyen kalmar sirri",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "Lakabin suna",
488 "new_subaddress_title": "Adireshin sabuwa",
489 "new_template": "Sabon Samfura",
490 + "new_transactions_notifications": "Aika sanarwa game da sababbin ma'amaloli",
491 "new_wallet": "Sabuwar Wallet",
492 "newConnection": "Sabuwar Haɗi",
493 "no_cards_found": "Babu katunan da aka samo",
@@ -507,6 +512,7 @@
512 "normal": "Na al'ada",
513 "note_optional": "Bayani (optional)",
514 "note_tap_to_change": "Bayani (tap don canja)",
515 + "notification_permission_denied": "Izinin sanarwar da aka samu an ƙaryata game da shi, don Allah a kunna shi a cikin saiti",
516 "nullURIError": "URI banza ne",
517 "offer_expires_in": "tayin zai ƙare a:",
518 "offline": "Offline",
res/values/strings_hi.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "भुगतान की पुष्टि की प्रतीक्षा में",
74 "background_sync": "पृष्ठभूमि सिंक",
75 "background_sync_mode": "बैकग्राउंड सिंक मोड",
76 + "background_sync_on_battery_low": "कम बैटरी पर सिंक्रनाइज़ करें",
77 + "background_sync_on_charging": "चार्ज करते समय केवल सिंक्रनाइज़ करें",
78 + "background_sync_on_device_idle": "केवल तब सिंक्रनाइज़ करें जब डिवाइस का उपयोग नहीं किया जा रहा है",
79 + "background_sync_on_unmetered_network": "अनमेट्रेड नेटवर्क की आवश्यकता है",
80 "backup": "बैकअप",
81 "backup_file": "बैकअपफ़ाइल",
82 "backup_password": "बैकअप पासवर्ड",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "लेबल का नाम",
488 "new_subaddress_title": "नया पता",
489 "new_template": "नया टेम्पलेट",
490 + "new_transactions_notifications": "नए लेनदेन के बारे में सूचनाएं भेजें",
491 "new_wallet": "नया बटुआ",
492 "newConnection": "नया कनेक्शन",
493 "no_cards_found": "कोई कार्ड नहीं मिला",
@@ -507,6 +512,7 @@
512 "normal": "सामान्य",
513 "note_optional": "नोट (वैकल्पिक)",
514 "note_tap_to_change": "नोट (टैप टू चेंज)",
515 + "notification_permission_denied": "अधिसूचना की अनुमति को पारगम्य रूप से अस्वीकार कर दिया गया, कृपया इसे मैन्युअल रूप से सेटिंग्स में सक्षम करें",
516 "nullURIError": "यूआरआई शून्य है",
517 "offer_expires_in": "में ऑफर समाप्त हो रहा है: ",
518 "offline": "ऑफ़लाइन",
res/values/strings_hr.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "Čeka se potvrda plaćanja",
74 "background_sync": "Sinkronizacija pozadine",
75 "background_sync_mode": "Sinkronizacija u pozadini",
76 + "background_sync_on_battery_low": "Sinkronizirati na niskoj bateriji",
77 + "background_sync_on_charging": "Sinkronizirati samo prilikom punjenja",
78 + "background_sync_on_device_idle": "Sinkronizirati samo kada se uređaj ne koristi",
79 + "background_sync_on_unmetered_network": "Zahtijevaju nezadovoljnu mrežu",
80 "backup": "Sigurnosna kopija",
81 "backup_file": "Sigurnosna kopija datoteke",
82 "backup_password": "Lozinka za sigurnosnu kopiju",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "Oznaka",
488 "new_subaddress_title": "Nova adresa",
489 "new_template": "novi predložak",
490 + "new_transactions_notifications": "Pošaljite obavijesti o novim transakcijama",
491 "new_wallet": "Novi novčanik",
492 "newConnection": "Nova veza",
493 "no_cards_found": "Nisu pronađene kartice",
@@ -507,6 +512,7 @@
512 "normal": "Normalno",
513 "note_optional": "Poruka (nije obvezno)",
514 "note_tap_to_change": "Poruka (dodirnite za promjenu)",
515 + "notification_permission_denied": "Dozvola za obavijest ostalo je odbijeno, molimo vas da ga ručno omogućite u postavkama",
516 "nullURIError": "URI je nula",
517 "offer_expires_in": "Ponuda istječe za: ",
518 "offline": "izvan mreže",
res/values/strings_hy.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "Վճարման հաստատման սպասում",
74 "background_sync": "Ֆոնային համաժամեցում",
75 "background_sync_mode": "Հետին պլանի համաժամացման ռեժիմ",
76 + "background_sync_on_battery_low": "Համաժամեցրեք ցածր մարտկոցի վրա",
77 + "background_sync_on_charging": "Համաժամացրեք միայն լիցքավորելու ժամանակ",
78 + "background_sync_on_device_idle": "Համաժամացրեք միայն այն ժամանակ, երբ սարքը չի օգտագործվում",
79 + "background_sync_on_unmetered_network": "Պահանջում են չմշակված ցանց",
80 "backup": "Կրկնօրինակ",
81 "backup_file": "Կրկնօրինակի ֆայլ",
82 "backup_password": "Կրկնօրինակի գաղտնաբառ",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "Պիտակի անուն",
488 "new_subaddress_title": "Նոր հասցե",
489 "new_template": "Նոր նմուշ",
490 + "new_transactions_notifications": "Ուղարկեք ծանուցումներ նոր գործարքների վերաբերյալ",
491 "new_wallet": "Նոր դրամապանակ",
492 "newConnection": "Նոր կապ",
493 "no_cards_found": "Ոչ մի քարտ չի գտնվել",
@@ -506,6 +511,7 @@
511 "normal": "Նորմալ",
512 "note_optional": "Նշում (ոչ պարտադիր)",
513 "note_tap_to_change": "Նշում (սեղմեք փոխելու համար)",
514 + "notification_permission_denied": "Տեղեկացման թույլտվությունը թափանցում է, խնդրում ենք ձեռքով միացնել այն պարամետրերում",
515 "nullURIError": "URI-ն դատարկ է",
516 "offer_expires_in": "Առաջարկը վաղեմության է հասնում ",
517 "offline": "Անցանց",
res/values/strings_id.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "Menunggu Konfirmasi Pembayaran",
74 "background_sync": "Sinkronisasi Latar Belakang",
75 "background_sync_mode": "Mode Sinkronisasi Latar Belakang",
76 + "background_sync_on_battery_low": "Sinkronisasi pada baterai rendah",
77 + "background_sync_on_charging": "Menyinkronkan hanya saat pengisian",
78 + "background_sync_on_device_idle": "Menyinkronkan hanya jika perangkat tidak digunakan",
79 + "background_sync_on_unmetered_network": "Membutuhkan jaringan yang belum diproduksi",
80 "backup": "Cadangan",
81 "backup_file": "File cadangan",
82 "backup_password": "Kata sandi cadangan",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "Nama label",
488 "new_subaddress_title": "Alamat baru",
489 "new_template": "Template Baru",
490 + "new_transactions_notifications": "Kirim pemberitahuan tentang transaksi baru",
491 "new_wallet": "Dompet Baru",
492 "newConnection": "Koneksi Baru",
493 "no_cards_found": "Tidak ada kartu yang ditemukan",
@@ -507,6 +512,7 @@
512 "normal": "Normal",
513 "note_optional": "Catatan (opsional)",
514 "note_tap_to_change": "Catatan (tap untuk mengubah)",
515 + "notification_permission_denied": "Izin pemberitahuan ditolak secara permanen, mohon aktifkan secara manual dalam pengaturan",
516 "nullURIError": "URI adalah nol",
517 "offer_expires_in": "Penawaran kedaluwarsa dalam: ",
518 "offline": "Offline",
res/values/strings_it.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "In attesa di conferma del pagamento",
74 "background_sync": "Sincronizzazione in background",
75 "background_sync_mode": "Modalità di sincronizzazione in background",
76 + "background_sync_on_battery_low": "Sincronizza sulla batteria bassa",
77 + "background_sync_on_charging": "Sincronizzare solo quando si carica",
78 + "background_sync_on_device_idle": "Sincronizzare solo quando il dispositivo non viene utilizzato",
79 + "background_sync_on_unmetered_network": "Richiedono una rete non riservata",
80 "backup": "Backup",
81 "backup_file": "Backup file",
82 "backup_password": "Backup password",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "Nome etichetta",
488 "new_subaddress_title": "Nuovo indirizzo",
489 "new_template": "Nuovo modello",
490 + "new_transactions_notifications": "Invia notifiche su nuove transazioni",
491 "new_wallet": "Nuovo portafoglio",
492 "newConnection": "Nuova connessione",
493 "no_cards_found": "Nessuna carta trovata",
@@ -507,6 +512,7 @@
512 "normal": "Normale",
513 "note_optional": "Nota (opzionale)",
514 "note_tap_to_change": "Nota (clicca per cambiare)",
515 + "notification_permission_denied": "L'autorizzazione alla notifica è stata negato per via per via per via per via per via per via per via per via, per favore, abilitalo manualmente",
516 "nullURIError": "L'URI è nullo",
517 "offer_expires_in": "L'offerta termina tra: ",
518 "offline": "Offline",
res/values/strings_ja.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "支払い確認を待っています",
74 "background_sync": "背景同期",
75 "background_sync_mode": "バックグラウンド同期モード",
76 + "background_sync_on_battery_low": "低いバッテリーで同期します",
77 + "background_sync_on_charging": "充電の場合にのみ同期します",
78 + "background_sync_on_device_idle": "デバイスが使用されていない場合にのみ同期します",
79 + "background_sync_on_unmetered_network": "未成年のネットワークが必要です",
80 "backup": "バックアップ",
81 "backup_file": "バックアップファイル",
82 "backup_password": "バックアップパスワード",
@@ -484,6 +488,7 @@
488 "new_subaddress_label_name": "ラベル名",
489 "new_subaddress_title": "新しいアドレス",
490 "new_template": "新しいテンプレート",
491 + "new_transactions_notifications": "新しいトランザクションに関する通知を送信します",
492 "new_wallet": "新しいウォレット",
493 "newConnection": "新しい接続",
494 "no_cards_found": "カードは見つかりません",
@@ -508,6 +513,7 @@
513 "normal": "普通",
514 "note_optional": "注(オプション)",
515 "note_tap_to_change": "注(タップして変更)",
516 + "notification_permission_denied": "通知の許可はまったく拒否されました。設定で手動で有効にしてください",
517 "nullURIError": "URIがnullです",
518 "offer_expires_in": "で有効期限が切れます: ",
519 "offline": "オフライン",
res/values/strings_ko.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "결제 확인 대기 중",
74 "background_sync": "배경 동기화",
75 "background_sync_mode": "백그라운드 동기화 모드",
76 + "background_sync_on_battery_low": "낮은 배터리에서 동기화하십시오",
77 + "background_sync_on_charging": "충전 할 때만 동기화하십시오",
78 + "background_sync_on_device_idle": "장치를 사용하지 않을 때만 동기화하십시오",
79 + "background_sync_on_unmetered_network": "충족되지 않은 네트워크가 필요합니다",
80 "backup": "지원",
81 "backup_file": "백업 파일",
82 "backup_password": "백업 비밀번호",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "라벨 이름",
488 "new_subaddress_title": "새 주소",
489 "new_template": "새 템플릿",
490 + "new_transactions_notifications": "새로운 거래에 대한 알림을 보냅니다",
491 "new_wallet": "새 월렛",
492 "newConnection": "새로운 연결",
493 "no_cards_found": "카드를 찾지 못했습니다",
@@ -507,6 +512,7 @@
512 "normal": "정상",
513 "note_optional": "참고 (선택 사항)",
514 "note_tap_to_change": "메모 (변경하려면 탭하세요)",
515 + "notification_permission_denied": "알림 허가가 부패하게 거부되었습니다. 설정에서 수동으로 활성화하십시오.",
516 "nullURIError": "URI가 null입니다.",
517 "offer_expires_in": "쿠폰 만료일: ",
518 "offline": "오프라인",
res/values/strings_my.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "ငွေပေးချေမှု အတည်ပြုချက်ကို စောင့်မျှော်နေပါသည်။",
74 "background_sync": "နောက်ခံထပ်တူပြုခြင်း",
75 "background_sync_mode": "နောက်ခံထပ်တူပြုခြင်း mode ကို",
76 + "background_sync_on_battery_low": "အနိမ့်ဘက်ထရီအပေါ်တစ်ပြိုင်တည်းချိန်ကိုက်",
77 + "background_sync_on_charging": "အားသွင်းသည့်အခါသာထပ်တူပြုခြင်း",
78 + "background_sync_on_device_idle": "စက်ကိုအသုံးမပြုသည့်စက်ကိုသာတစ်ပြိုင်တည်းချိန်ကိုက်ပါ",
79 + "background_sync_on_unmetered_network": "unmetred ကွန်ယက်လိုအပ်သည်",
80 "backup": "မိတ္တူ",
81 "backup_file": "အရန်ဖိုင်",
82 "backup_password": "စကားဝှက်ကို အရန်သိမ်းဆည်းပါ။",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "အညွှန်းအမည်",
488 "new_subaddress_title": "လိပ်စာအသစ်",
489 "new_template": "ပုံစံအသစ်",
490 + "new_transactions_notifications": "အသစ်သောအရောင်းအဝယ်အကြောင်းသတိပေးချက်များပေးပို့ပါ",
491 "new_wallet": "ပိုက်ဆံအိတ်အသစ်",
492 "newConnection": "ချိတ်ဆက်မှုအသစ်",
493 "no_cards_found": "ကဒ်များမရှိပါ",
@@ -507,6 +512,7 @@
512 "normal": "ပုံမှန်",
513 "note_optional": "မှတ်ချက် (ချန်လှပ်ထားနိုင်သည်)",
514 "note_tap_to_change": "မှတ်ချက် (ပြောင်းလဲရန် တို့ပါ)",
515 + "notification_permission_denied": "အသိပေးချက်ခွင့်ပြုချက်ကိုအစက်အပြောက်ကိုငြင်းဆိုခဲ့သည်, ကျေးဇူးပြု. ၎င်းကိုချိန်ညှိချက်များတွင်လက်ဖြင့်ပြုလုပ်ပါ",
516 "nullURIError": "URI သည် null ဖြစ်သည်။",
517 "offer_expires_in": "ကမ်းလှမ်းချက် သက်တမ်းကုန်သည်:",
518 "offline": "အော့ဖ်လိုင်း",
res/values/strings_nl.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "In afwachting van betalingsbevestiging",
74 "background_sync": "Achtergrondsynchronisatie",
75 "background_sync_mode": "Achtergrondsynchronisatiemodus",
76 + "background_sync_on_battery_low": "Synchroniseren op lage batterij",
77 + "background_sync_on_charging": "Synchroniseer alleen bij het opladen",
78 + "background_sync_on_device_idle": "Synchroniseer alleen wanneer het apparaat niet wordt gebruikt",
79 + "background_sync_on_unmetered_network": "Vereist een onvermekte netwerk",
80 "backup": "Back-up",
81 "backup_file": "Backup bestand",
82 "backup_password": "Reservewachtwoord",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "Label naam",
488 "new_subaddress_title": "Nieuw adres",
489 "new_template": "Nieuwe sjabloon",
490 + "new_transactions_notifications": "Stuur meldingen over nieuwe transacties",
491 "new_wallet": "Nieuwe portemonnee",
492 "newConnection": "Nieuwe verbinding",
493 "no_cards_found": "Geen kaarten gevonden",
@@ -507,6 +512,7 @@
512 "normal": "Normaal",
513 "note_optional": "Opmerking (optioneel)",
514 "note_tap_to_change": "Opmerking (tik om te wijzigen)",
515 + "notification_permission_denied": "Meldingstoestemming is permanent geweigerd, schakel het handmatig in instellingen in",
516 "nullURIError": "URI is nul",
517 "offer_expires_in": "Aanbieding verloopt over: ",
518 "offline": "Offline",
res/values/strings_pl.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "Oczekiwanie na potwierdzenie płatności",
74 "background_sync": "Synchronizacja w tle",
75 "background_sync_mode": "Tryb synchronizacji w tle",
76 + "background_sync_on_battery_low": "Synchronizować na niskiej baterii",
77 + "background_sync_on_charging": "Synchronizować tylko podczas ładowania",
78 + "background_sync_on_device_idle": "Synchronizować tylko wtedy, gdy urządzenie nie jest używane",
79 + "background_sync_on_unmetered_network": "Wymagaj niezametrowanej sieci",
80 "backup": "Kopia zapasowa",
81 "backup_file": "Plik kopii zapasowej",
82 "backup_password": "Hasło kpoii zapasowej",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "Etykieta nazwy adresu",
488 "new_subaddress_title": "Nowy adres",
489 "new_template": "Nowy szablon",
490 + "new_transactions_notifications": "Wyślij powiadomienia o nowych transakcjach",
491 "new_wallet": "Nowy portfel",
492 "newConnection": "Nowe połączenie",
493 "no_cards_found": "Nie znaleziono żadnych kart",
@@ -507,6 +512,7 @@
512 "normal": "Normalna",
513 "note_optional": "Notatka (opcjonalnie)",
514 "note_tap_to_change": "Notatka (dotknij, aby zmienić)",
515 + "notification_permission_denied": "Zezwolenie na powiadomienie zostało odrzucone, prosimy ręcznie włączyć go w ustawieniach",
516 "nullURIError": "URI ma wartość zerową",
517 "offer_expires_in": "Oferta wygasa za ",
518 "offline": "Offline",
res/values/strings_pt.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "Aguardando confirmação de pagamento",
74 "background_sync": "Sincronização de fundo",
75 "background_sync_mode": "Modo de sincronização em segundo plano",
76 + "background_sync_on_battery_low": "Sincronizar com bateria baixa",
77 + "background_sync_on_charging": "Sincronize apenas ao carregar",
78 + "background_sync_on_device_idle": "Sincronize apenas quando o dispositivo não está sendo usado",
79 + "background_sync_on_unmetered_network": "Requer rede não meta",
80 "backup": "Cópia de segurança",
81 "backup_file": "Arquivo de backup",
82 "backup_password": "Senha de backup",
@@ -484,6 +488,7 @@
488 "new_subaddress_label_name": "Nome",
489 "new_subaddress_title": "Novo endereço",
490 "new_template": "Novo modelo",
491 + "new_transactions_notifications": "Envie notificações sobre novas transações",
492 "new_wallet": "Nova carteira",
493 "newConnection": "Nova conexão",
494 "no_cards_found": "Nenhum cartão encontrado",
@@ -508,6 +513,7 @@
513 "normal": "Normal",
514 "note_optional": "Nota (opcional)",
515 "note_tap_to_change": "Nota (toque para alterar)",
516 + "notification_permission_denied": "A permissão de notificação foi negada com permamer, por favor, ativá -la manualmente em configurações",
517 "nullURIError": "URI é nulo",
518 "offer_expires_in": "A oferta expira em: ",
519 "offline": "offline",
res/values/strings_ru.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "Ожидается подтверждения платежа",
74 "background_sync": "Фоновая синхронизация",
75 "background_sync_mode": "Режим фоновой синхронизации",
76 + "background_sync_on_battery_low": "Синхронизировать на низкой батареи",
77 + "background_sync_on_charging": "Синхронизировать только при зарядке",
78 + "background_sync_on_device_idle": "Синхронизировать только тогда, когда устройство не используется",
79 + "background_sync_on_unmetered_network": "Требуется незамеченная сеть",
80 "backup": "Резервная копия",
81 "backup_file": "Файл резервной копии",
82 "backup_password": "Пароль резервной копии",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "Имя",
488 "new_subaddress_title": "Новый адрес",
489 "new_template": "Новый шаблон",
490 + "new_transactions_notifications": "Отправить уведомления о новых транзакциях",
491 "new_wallet": "Новый кошелёк",
492 "newConnection": "Новое соединение",
493 "no_cards_found": "Карт не найдено",
@@ -507,6 +512,7 @@
512 "normal": "Нормальный",
513 "note_optional": "Примечание (необязательно)",
514 "note_tap_to_change": "Примечание (нажмите для изменения)",
515 + "notification_permission_denied": "Разрешение уведомления было отклонено, пожалуйста, вручную включить его в настройках",
516 "nullURIError": "URI имеет значение null",
517 "offer_expires_in": "Предложение истекает через: ",
518 "offline": "Не в сети",
res/values/strings_th.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "รอการยืนยันการชำระเงิน",
74 "background_sync": "การซิงค์พื้นหลัง",
75 "background_sync_mode": "โหมดซิงค์พื้นหลัง",
76 + "background_sync_on_battery_low": "ซิงโครไนซ์กับแบตเตอรี่ต่ำ",
77 + "background_sync_on_charging": "ซิงโครไนซ์เฉพาะเมื่อชาร์จ",
78 + "background_sync_on_device_idle": "ซิงโครไนซ์เฉพาะเมื่อไม่ใช้อุปกรณ์",
79 + "background_sync_on_unmetered_network": "ต้องการเครือข่ายที่ไม่ได้รับการแก้ไข",
80 "backup": "สำรองข้อมูล",
81 "backup_file": "ไฟล์สำรองข้อมูล",
82 "backup_password": "รหัสผ่านสำรองข้อมูล",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "ชื่อป้ายกำกับ",
488 "new_subaddress_title": "ที่อยู่ใหม่",
489 "new_template": "แม่แบบใหม่",
490 + "new_transactions_notifications": "ส่งการแจ้งเตือนเกี่ยวกับธุรกรรมใหม่",
491 "new_wallet": "กระเป๋าใหม่",
492 "newConnection": "การเชื่อมต่อใหม่",
493 "no_cards_found": "ไม่พบการ์ด",
@@ -507,6 +512,7 @@
512 "normal": "ปกติ",
513 "note_optional": "บันทึก (ไม่จำเป็น)",
514 "note_tap_to_change": "หมายเหตุ (กดเพื่อเปลี่ยน)",
515 + "notification_permission_denied": "การอนุญาตการแจ้งเตือนได้รับการปฏิเสธอย่างอนุญาตโปรดเปิดใช้งานด้วยตนเองในการตั้งค่า",
516 "nullURIError": "URI เป็นโมฆะ",
517 "offer_expires_in": "ข้อเสนอจะหมดอายุใน: ",
518 "offline": "ออฟไลน์",
res/values/strings_tl.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "Nanghihintay ng Kumpirmasyon sa Pagbabayad",
74 "background_sync": "Pag -sync ng background",
75 "background_sync_mode": "Background sync mode",
76 + "background_sync_on_battery_low": "Mag -synchronize sa mababang baterya",
77 + "background_sync_on_charging": "Mag -synchronize lamang kapag singilin",
78 + "background_sync_on_device_idle": "Mag -synchronize lamang kapag hindi ginagamit ang aparato",
79 + "background_sync_on_unmetered_network": "Nangangailangan ng hindi natukoy na network",
80 "backup": "Backup",
81 "backup_file": "Backup na file",
82 "backup_password": "Backup na password",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "Pangalan ng label",
488 "new_subaddress_title": "Bagong address",
489 "new_template": "Bagong Template",
490 + "new_transactions_notifications": "Magpadala ng mga abiso tungkol sa mga bagong transaksyon",
491 "new_wallet": "Bagong Wallet",
492 "newConnection": "Bagong Koneksyon",
493 "no_cards_found": "Walang nahanap na mga card",
@@ -507,6 +512,7 @@
512 "normal": "Normal",
513 "note_optional": "Tala (opsyonal)",
514 "note_tap_to_change": "Tala (i-tap para baguhin)",
515 + "notification_permission_denied": "Ang pahintulot ng abiso ay pinahihintulutan na tumanggi, mangyaring manu -manong paganahin ito sa mga setting",
516 "nullURIError": "Ang URI ay null",
517 "offer_expires_in": "Mag-expire ang alok sa: ",
518 "offline": "Offline",
res/values/strings_tr.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "Ödemenin onaylanması bekleniyor",
74 "background_sync": "Arka plan senkronizasyonu",
75 "background_sync_mode": "Arka Plan Senkronizasyon Modu",
76 + "background_sync_on_battery_low": "Düşük pille senkronize edin",
77 + "background_sync_on_charging": "Yalnızca şarj ederken senkronize edin",
78 + "background_sync_on_device_idle": "Yalnızca cihaz kullanılmadığında senkronize edin",
79 + "background_sync_on_unmetered_network": "Karşıdamlanmamış ağ gerektirir",
80 "backup": "Yedek",
81 "backup_file": "Yedek dosyası",
82 "backup_password": "Yedek parolası",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "Etiket ismi",
488 "new_subaddress_title": "Yeni adres",
489 "new_template": "Yeni Şablon",
490 + "new_transactions_notifications": "Yeni işlemler hakkında bildirimler gönderin",
491 "new_wallet": "Yeni Cüzdan",
492 "newConnection": "Yeni bağlantı",
493 "no_cards_found": "Kart bulunamadı",
@@ -507,6 +512,7 @@
512 "normal": "Normal",
513 "note_optional": "Not (isteğe bağlı)",
514 "note_tap_to_change": "Not (değiştirmek için dokunun)",
515 + "notification_permission_denied": "Bildirim izni perdence reddedildi, lütfen ayarlarda manuel olarak etkinleştirin",
516 "nullURIError": "URI boş",
517 "offer_expires_in": "Teklifin bitmesine kalan: ",
518 "offline": "Çevrimdışı",
res/values/strings_uk.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "Очікується підтвердження платежу",
74 "background_sync": "Фонове синхронізація",
75 "background_sync_mode": "Фоновий режим синхронізації",
76 + "background_sync_on_battery_low": "Синхронізувати на низькому батареї",
77 + "background_sync_on_charging": "Синхронізуватися лише при зарядці",
78 + "background_sync_on_device_idle": "Синхронізувати лише тоді, коли пристрій не використовується",
79 + "background_sync_on_unmetered_network": "Вимагати незадоволеної мережі",
80 "backup": "Резервна копія",
81 "backup_file": "Файл резервної копії",
82 "backup_password": "Пароль резервної копії",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "Ім'я",
488 "new_subaddress_title": "Нова адреса",
489 "new_template": "Новий шаблон",
490 + "new_transactions_notifications": "Надішліть сповіщення про нові транзакції",
491 "new_wallet": "Новий гаманець",
492 "newConnection": "Нове підключення",
493 "no_cards_found": "Карт не знайдено",
@@ -507,6 +512,7 @@
512 "normal": "нормальний",
513 "note_optional": "Примітка (необов’язково)",
514 "note_tap_to_change": "Примітка (натисніть для зміни)",
515 + "notification_permission_denied": "Повідомлення дозволу отримали безперервно, будь ласка, вручну ввімкніть його в налаштуваннях",
516 "nullURIError": "URI нульовий",
517 "offer_expires_in": "Пропозиція закінчиться через: ",
518 "offline": "Офлайн",
res/values/strings_ur.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "ادائیگی کی تصدیق کے منتظر",
74 "background_sync": "پس منظر کی ہم آہنگی",
75 "background_sync_mode": "پس منظر کی مطابقت پذیری کا موڈ",
76 + "background_sync_on_battery_low": "کم بیٹری پر ہم وقت سازی کریں",
77 + "background_sync_on_charging": "چارج کرتے وقت صرف ہم وقت سازی کریں",
78 + "background_sync_on_device_idle": "صرف اس وقت مطابقت پذیر کریں جب آلہ استعمال نہ ہو",
79 + "background_sync_on_unmetered_network": "بے ساختہ نیٹ ورک کی ضرورت ہے",
80 "backup": "بیک اپ",
81 "backup_file": "بیک اپ فائل",
82 "backup_password": "بیک اپ پاس ورڈ",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "لیبل کا نام",
488 "new_subaddress_title": "نیا پتہ",
489 "new_template": "نیا سانچہ",
490 + "new_transactions_notifications": "نئے لین دین کے بارے میں اطلاعات بھیجیں",
491 "new_wallet": "نیا پرس",
492 "newConnection": "ﻦﺸﮑﻨﮐ ﺎﯿﻧ",
493 "no_cards_found": "کوئی کارڈ نہیں ملا",
@@ -507,6 +512,7 @@
512 "normal": "نارمل",
513 "note_optional": "نوٹ (اختیاری)",
514 "note_tap_to_change": "نوٹ (تبدیل کرنے کے لیے تھپتھپائیں)",
515 + "notification_permission_denied": "نوٹیفکیشن کی اجازت کو یقینی طور پر انکار کردیا گیا ، براہ کرم اسے دستی طور پر ترتیبات میں اہل بنائیں",
516 "nullURIError": "URI ۔ﮯﮨ ﻡﺪﻌﻟﺎﮐ",
517 "offer_expires_in": "پیشکش کی میعاد اس وقت ختم ہو جاتی ہے:",
518 "offline": "آف لائن",
res/values/strings_vi.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "Đang chờ xác nhận thanh toán",
74 "background_sync": "Đồng bộ nền",
75 "background_sync_mode": "Chế độ đồng bộ nền",
76 + "background_sync_on_battery_low": "Đồng bộ hóa trên pin thấp",
77 + "background_sync_on_charging": "Chỉ đồng bộ hóa khi sạc",
78 + "background_sync_on_device_idle": "Chỉ đồng bộ hóa khi thiết bị không được sử dụng",
79 + "background_sync_on_unmetered_network": "Yêu cầu mạng không được thiết kế",
80 "backup": "Sao lưu",
81 "backup_file": "Tập tin sao lưu",
82 "backup_password": "Mật khẩu sao lưu",
@@ -482,6 +486,7 @@
486 "new_subaddress_label_name": "Tên nhãn",
487 "new_subaddress_title": "Địa chỉ mới",
488 "new_template": "Mẫu mới",
489 + "new_transactions_notifications": "Gửi thông báo về các giao dịch mới",
490 "new_wallet": "Ví mới",
491 "newConnection": "Kết nối mới",
492 "no_cards_found": "Không tìm thấy thẻ",
@@ -505,6 +510,7 @@
510 "normal": "Bình thường",
511 "note_optional": "Ghi chú (tùy chọn)",
512 "note_tap_to_change": "Ghi chú (nhấn để thay đổi)",
513 + "notification_permission_denied": "Giấy phép thông báo bị từ chối, vui lòng bật thủ công nó trong cài đặt",
514 "nullURIError": "URI là null",
515 "offer_expires_in": "Ưu đãi hết hạn trong: ",
516 "offline": "Ngoại tuyến",
res/values/strings_yo.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "À ń dúró de ìjẹ́rìísí àránṣẹ́",
74 "background_sync": "Imuṣiṣẹ Labẹ",
75 "background_sync_mode": "Ipo amuṣiṣẹpọ abẹlẹ",
76 + "background_sync_on_battery_low": "Muuṣiṣẹ lori batiri kekere",
77 + "background_sync_on_charging": "Muṣiṣẹpọ nikan nigbati gbigba agbara",
78 + "background_sync_on_device_idle": "Muṣiṣẹpọ nikan nigbati ẹrọ ko lo",
79 + "background_sync_on_unmetered_network": "Nilo nẹtiwọki ti ko ni aabo",
80 "backup": "Ṣẹ̀dà",
81 "backup_file": "Ṣẹ̀dà akọsílẹ̀",
82 "backup_password": "Ṣẹ̀dà ọ̀rọ̀ aṣínà",
@@ -484,6 +488,7 @@
488 "new_subaddress_label_name": "Orúkọ",
489 "new_subaddress_title": "Àdírẹ́sì títun",
490 "new_template": "Àwòṣe títun",
491 + "new_transactions_notifications": "Firanṣẹ awọn iwifunni nipa awọn iṣowo tuntun",
492 "new_wallet": "Àpamọ́wọ́ títun",
493 "newConnection": "Tuntun Asopọ",
494 "no_cards_found": "Ko si awọn kaadi ti a rii",
@@ -508,6 +513,7 @@
513 "normal": "Deede",
514 "note_optional": "Àkọsílẹ̀ (ìyàn nìyí)",
515 "note_tap_to_change": "Àkọsílẹ̀ (ẹ tẹ̀ láti pààrọ̀)",
516 + "notification_permission_denied": "Igbanilaaye iwifunni ni sẹsẹ sẹsẹ, jọwọ jẹ ki o mu ṣiṣẹ ni awọn eto",
517 "nullURIError": "URI jẹ asan",
518 "offer_expires_in": "Ìrònúdábàá máa gbẹ́mìí mì ní: ",
519 "offline": "kò wà lórí ayélujára",
res/values/strings_zh.arb
+6
@@ -73,6 +73,10 @@
73 "awaiting_payment_confirmation": "等待付款确认",
74 "background_sync": "背景同步",
75 "background_sync_mode": "后台同步模式",
76 + "background_sync_on_battery_low": "在低电池上同步",
77 + "background_sync_on_charging": "仅在充电时同步",
78 + "background_sync_on_device_idle": "仅在不使用设备时同步",
79 + "background_sync_on_unmetered_network": "需要未经许可的网络",
80 "backup": "备份",
81 "backup_file": "备份文件",
82 "backup_password": "备份密码",
@@ -483,6 +487,7 @@
487 "new_subaddress_label_name": "标签名称",
488 "new_subaddress_title": "新地址",
489 "new_template": "新模板",
490 + "new_transactions_notifications": "发送有关新交易的通知",
491 "new_wallet": "新钱包",
492 "newConnection": "新连接",
493 "no_cards_found": "找不到卡",
@@ -507,6 +512,7 @@
512 "normal": "普通的",
513 "note_optional": "注释(可选)",
514 "note_tap_to_change": "注释(轻按即可更改)",
515 + "notification_permission_denied": "通知许可被e opply拒绝,请在设置中手动启用它",
516 "nullURIError": "URI 为空",
517 "offer_expires_in": "优惠有效期至 ",
518 "offline": "离线",