Cw 155 monero synchronization (#1014)

* Run Monero Synchronization task in background on Android

* Add monero sync task in the load function to be registered/cancelled when user changes wallets

* Revert unused file changes

* Register Sync task on all monero wallets if any

* Add Sync Modes and change task frequency based on user's choice

* Register background task after current wallet is set

* Add Sync All toggle and change task wallets to sync accordingly

* Enable background notifications in release mode temporarily

* Disable constraints and increase the frequency of tasks

* Decrease frequency of background tasks

* Delay the background task thread till the syncing thread finish (Dummy Trial-1)

* Start Sync process and wait for it to finish

* Wait for synchronization to finish before ending the background thread
Add 10 minutes timeout duration for sync process

* Connect to node before syncing wallet

* replace testing configuration with the configurations agreed on

* Fix Conflicts with main

* Update and Migrate Background tasks to null safety

* Update workmanager version in pubspec_base also

* Move Sync options to Connection and sync page
Show Sync options only for Monero and Haven
Minor Enhancements

* Remove debugging notifications
Revert aggressive mode frequency to 6 hours [skip ci]

* Add iOS configs

* Revert debugging changes
Fix conflicts with main

* Add/Extract Sync configurations to/from backup file [skip ci]
This commit is contained in:
Omar Hatem 2023-08-04 20:55:56 +03:00 committed by GitHub
parent 68a057b91b
commit aedf310c9d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 593 additions and 190 deletions

View file

@ -1,7 +1,9 @@
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
const utils = const MethodChannel('com.cake_wallet/native_utils');
void setIsAppSecureNative(bool isAppSecure) { void setIsAppSecureNative(bool isAppSecure) {
try {
final utils = const MethodChannel('com.cake_wallet/native_utils');
utils.invokeMethod<Uint8List>('setIsAppSecure', {'isAppSecure': isAppSecure}); utils.invokeMethod<Uint8List>('setIsAppSecure', {'isAppSecure': isAppSecure});
} catch (_) {}
} }

View file

@ -606,4 +606,9 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
}; };
rootObject = 97C146E61CF9000F007C117D /* Project object */; rootObject = 97C146E61CF9000F007C117D /* Project object */;
SystemCapabilities = {
com.apple.BackgroundModes = {
enabled = 1;
};
};
} }

View file

@ -1,6 +1,7 @@
import UIKit import UIKit
import Flutter import Flutter
import UnstoppableDomainsResolution import UnstoppableDomainsResolution
import workmanager
@UIApplicationMain @UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate {
@ -16,6 +17,15 @@ import UnstoppableDomainsResolution
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
} }
WorkmanagerPlugin.setPluginRegistrantCallback { registry in
// Registry in this case is the FlutterEngine that is created in Workmanager's
// performFetchWithCompletionHandler or BGAppRefreshTask.
// This will make other plugins available during a background operation.
GeneratedPluginRegistrant.register(with: registry)
}
WorkmanagerPlugin.registerTask(withIdentifier: "com.fotolockr.cakewallet.monero_sync_task")
makeSecure() makeSecure()
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController let controller : FlutterViewController = window?.rootViewController as! FlutterViewController

View file

@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.fotolockr.cakewallet.monero_sync_task</string>
</array>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
@ -113,6 +117,7 @@
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>fetch</string> <string>fetch</string>
<string>processing</string>
<string>remote-notification</string> <string>remote-notification</string>
</array> </array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>

View file

@ -243,6 +243,8 @@ class BackupService {
final sortBalanceTokensBy = data[PreferencesKey.sortBalanceBy] as int?; final sortBalanceTokensBy = data[PreferencesKey.sortBalanceBy] as int?;
final pinNativeTokenAtTop = data[PreferencesKey.pinNativeTokenAtTop] as bool?; final pinNativeTokenAtTop = data[PreferencesKey.pinNativeTokenAtTop] as bool?;
final useEtherscan = data[PreferencesKey.useEtherscan] as bool?; final useEtherscan = data[PreferencesKey.useEtherscan] as bool?;
final syncAll = data[PreferencesKey.syncAllKey] as bool?;
final syncMode = data[PreferencesKey.syncModeKey] as int?;
await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName); await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName);
@ -361,6 +363,12 @@ class BackupService {
if (useEtherscan != null) if (useEtherscan != null)
await _sharedPreferences.setBool(PreferencesKey.useEtherscan, useEtherscan); await _sharedPreferences.setBool(PreferencesKey.useEtherscan, useEtherscan);
if (syncAll != null)
await _sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll);
if (syncMode != null)
await _sharedPreferences.setInt(PreferencesKey.syncModeKey, syncMode);
await preferencesFile.delete(); await preferencesFile.delete();
} }
@ -510,6 +518,10 @@ class BackupService {
_sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop), _sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop),
PreferencesKey.useEtherscan: PreferencesKey.useEtherscan:
_sharedPreferences.getBool(PreferencesKey.useEtherscan), _sharedPreferences.getBool(PreferencesKey.useEtherscan),
PreferencesKey.syncModeKey:
_sharedPreferences.getInt(PreferencesKey.syncModeKey),
PreferencesKey.syncAllKey:
_sharedPreferences.getBool(PreferencesKey.syncAllKey),
}; };
return json.encode(preferences); return json.encode(preferences);

View file

@ -4,6 +4,7 @@ import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
import 'package:cake_wallet/buy/payfura/payfura_buy_provider.dart'; import 'package:cake_wallet/buy/payfura/payfura_buy_provider.dart';
import 'package:cake_wallet/core/yat_service.dart'; import 'package:cake_wallet/core/yat_service.dart';
import 'package:cake_wallet/entities/background_tasks.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/receive_page_option.dart'; import 'package:cake_wallet/entities/receive_page_option.dart';
@ -23,6 +24,7 @@ import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart
import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart';
import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/display_settings_page.dart';
import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart';
import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart';
import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart';
import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart';
@ -244,6 +246,8 @@ Future setup({
getIt.registerSingletonAsync<SharedPreferences>(() => SharedPreferences.getInstance()); getIt.registerSingletonAsync<SharedPreferences>(() => SharedPreferences.getInstance());
} }
getIt.registerFactory(() => BackgroundTasks());
final isBitcoinBuyEnabled = (secrets.wyreSecretKey.isNotEmpty) && final isBitcoinBuyEnabled = (secrets.wyreSecretKey.isNotEmpty) &&
(secrets.wyreApiKey.isNotEmpty) && (secrets.wyreApiKey.isNotEmpty) &&
(secrets.wyreAccountId.isNotEmpty); (secrets.wyreAccountId.isNotEmpty);
@ -681,8 +685,7 @@ Future setup({
return NodeListViewModel(_nodeSource, appStore); return NodeListViewModel(_nodeSource, appStore);
}); });
getIt.registerFactory( getIt.registerFactory(() => ConnectionSyncPage(getIt.get<DashboardViewModel>()));
() => ConnectionSyncPage(getIt.get<NodeListViewModel>(), getIt.get<DashboardViewModel>()));
getIt.registerFactory( getIt.registerFactory(
() => SecurityBackupPage(getIt.get<SecuritySettingsViewModel>(), getIt.get<AuthService>())); () => SecurityBackupPage(getIt.get<SecuritySettingsViewModel>(), getIt.get<AuthService>()));
@ -1055,5 +1058,7 @@ Future setup({
), ),
); );
getIt.registerFactory<ManageNodesPage>(() => ManageNodesPage(getIt.get<NodeListViewModel>()));
_isSetupFinished = true; _isSetupFinished = true;
} }

View file

@ -0,0 +1,164 @@
import 'dart:io';
import 'package:cake_wallet/core/wallet_loading_service.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/view_model/settings/sync_mode.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart';
import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:workmanager/workmanager.dart';
import 'package:cake_wallet/main.dart';
import 'package:cake_wallet/di.dart';
const moneroSyncTaskKey = "com.fotolockr.cakewallet.monero_sync_task";
@pragma('vm:entry-point')
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) async {
try {
switch (task) {
case moneroSyncTaskKey:
/// The work manager runs on a separate isolate from the main flutter isolate.
/// thus we initialize app configs first; hive, getIt, etc...
await initializeAppConfigs();
final walletLoadingService = getIt.get<WalletLoadingService>();
final node = getIt.get<SettingsStore>().getCurrentNode(WalletType.monero);
final typeRaw = getIt.get<SharedPreferences>().getInt(PreferencesKey.currentWalletType);
WalletBase? wallet;
if (inputData!['sync_all'] as bool) {
/// get all Monero wallets of the user and sync them
final List<WalletListItem> moneroWallets = getIt
.get<WalletListViewModel>()
.wallets
.where((element) => element.type == WalletType.monero)
.toList();
for (int i = 0; i < moneroWallets.length; i++) {
wallet = await walletLoadingService.load(WalletType.monero, moneroWallets[i].name);
await wallet.connectToNode(node: node);
await wallet.startSync();
}
} else {
/// if the user chose to sync only active wallet
/// if the current wallet is monero; sync it only
if (typeRaw == WalletType.monero.index) {
final name =
getIt.get<SharedPreferences>().getString(PreferencesKey.currentWalletName);
wallet = await walletLoadingService.load(WalletType.monero, name!);
await wallet.connectToNode(node: node);
await wallet.startSync();
}
}
if (wallet?.syncStatus.progress() == null) {
return Future.error("No Monero wallet found");
}
for (int i = 0;; i++) {
await Future<void>.delayed(const Duration(seconds: 1));
if (wallet?.syncStatus.progress() == 1.0) {
break;
}
if (i > 600) {
return Future.error("Synchronization Timed out");
}
}
break;
}
return Future.value(true);
} catch (error, stackTrace) {
print(error);
print(stackTrace);
return Future.error(error);
}
});
}
class BackgroundTasks {
void registerSyncTask({bool changeExisting = false}) async {
try {
bool hasMonero = getIt
.get<WalletListViewModel>()
.wallets
.any((element) => element.type == WalletType.monero);
/// if its not android nor ios, or the user has no monero wallets; exit
if (!DeviceInfo.instance.isMobile || !hasMonero) {
return;
}
final settingsStore = getIt.get<SettingsStore>();
final SyncMode syncMode = settingsStore.currentSyncMode;
final bool syncAll = settingsStore.currentSyncAll;
if (syncMode.type == SyncType.disabled) {
cancelSyncTask();
return;
}
await Workmanager().initialize(
callbackDispatcher,
isInDebugMode: kDebugMode,
);
final inputData = <String, dynamic>{"sync_all": syncAll};
final constraints = Constraints(
networkType:
syncMode.type == SyncType.unobtrusive ? NetworkType.unmetered : NetworkType.connected,
requiresBatteryNotLow: syncMode.type == SyncType.unobtrusive,
requiresCharging: syncMode.type == SyncType.unobtrusive,
requiresDeviceIdle: syncMode.type == SyncType.unobtrusive,
);
if (Platform.isIOS) {
await Workmanager().registerOneOffTask(
moneroSyncTaskKey,
moneroSyncTaskKey,
initialDelay: syncMode.frequency,
existingWorkPolicy: ExistingWorkPolicy.replace,
inputData: inputData,
constraints: constraints,
);
return;
}
await Workmanager().registerPeriodicTask(
moneroSyncTaskKey,
moneroSyncTaskKey,
initialDelay: syncMode.frequency,
frequency: syncMode.frequency,
existingWorkPolicy: changeExisting ? ExistingWorkPolicy.replace : ExistingWorkPolicy.keep,
inputData: inputData,
constraints: constraints,
);
} catch (error, stackTrace) {
print(error);
print(stackTrace);
}
}
void cancelSyncTask() {
try {
Workmanager().cancelByUniqueName(moneroSyncTaskKey);
} catch (error, stackTrace) {
print(error);
print(stackTrace);
}
}
}

View file

@ -1,8 +1,7 @@
import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/di.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/entities/background_tasks.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart';
@ -24,4 +23,6 @@ Future<void> loadCurrentWallet() async {
final walletLoadingService = getIt.get<WalletLoadingService>(); final walletLoadingService = getIt.get<WalletLoadingService>();
final wallet = await walletLoadingService.load(type, name); final wallet = await walletLoadingService.load(type, name);
appStore.changeCurrentWallet(wallet); appStore.changeCurrentWallet(wallet);
getIt.get<BackgroundTasks>().registerSyncTask();
} }

View file

@ -36,6 +36,8 @@ class PreferencesKey {
static const shouldShowReceiveWarning = 'should_show_receive_warning'; static const shouldShowReceiveWarning = 'should_show_receive_warning';
static const shouldShowYatPopup = 'should_show_yat_popup'; static const shouldShowYatPopup = 'should_show_yat_popup';
static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1'; static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1';
static const syncModeKey = 'sync_mode';
static const syncAllKey = 'sync_all';
static const pinTimeOutDuration = 'pin_timeout_duration'; static const pinTimeOutDuration = 'pin_timeout_duration';
static const lastAuthTimeMilliseconds = 'last_auth_time_milliseconds'; static const lastAuthTimeMilliseconds = 'last_auth_time_milliseconds';
static const lastPopupDate = 'last_popup_date'; static const lastPopupDate = 'last_popup_date';

View file

@ -57,8 +57,18 @@ Future<void> main() async {
return true; return true;
}; };
final appDir = await getApplicationDocumentsDirectory();
await Hive.close(); await Hive.close();
await initializeAppConfigs();
runApp(App());
}, (error, stackTrace) async {
ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stackTrace));
});
}
Future<void> initializeAppConfigs() async {
final appDir = await getApplicationDocumentsDirectory();
Hive.init(appDir.path); Hive.init(appDir.path);
if (!Hive.isAdapterRegistered(Contact.typeId)) { if (!Hive.isAdapterRegistered(Contact.typeId)) {
@ -142,10 +152,6 @@ Future<void> main() async {
secureStorage: secureStorage, secureStorage: secureStorage,
anonpayInvoiceInfo: anonpayInvoiceInfo, anonpayInvoiceInfo: anonpayInvoiceInfo,
initialMigrationVersion: 21); initialMigrationVersion: 21);
runApp(App());
}, (error, stackTrace) async {
ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stackTrace));
});
} }
Future<void> initialSetup( Future<void> initialSetup(

View file

@ -19,6 +19,7 @@ import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_dashbo
import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart';
import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settings_page.dart';
import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/display_settings_page.dart';
import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart';
import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart';
import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart';
import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart';
@ -605,6 +606,9 @@ Route<dynamic> createRoute(RouteSettings settings) {
), ),
); );
case Routes.manageNodes:
return MaterialPageRoute<void>(builder: (_) => getIt.get<ManageNodesPage>());
default: default:
return MaterialPageRoute<void>( return MaterialPageRoute<void>(
builder: (_) => Scaffold( builder: (_) => Scaffold(

View file

@ -90,4 +90,5 @@ class Routes {
static const modify2FAPage = '/modify_2fa_page'; static const modify2FAPage = '/modify_2fa_page';
static const homeSettings = '/home_settings'; static const homeSettings = '/home_settings';
static const editToken = '/edit_token'; static const editToken = '/edit_token';
static const manageNodes = '/manage_nodes';
} }

View file

@ -1,25 +1,25 @@
import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/node.dart'; import 'package:cake_wallet/view_model/settings/sync_mode.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/nodes/widgets/node_list_row.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
class ConnectionSyncPage extends BasePage { class ConnectionSyncPage extends BasePage {
ConnectionSyncPage(this.nodeListViewModel, this.dashboardViewModel); ConnectionSyncPage(this.dashboardViewModel);
@override @override
String get title => S.current.connection_sync; String get title => S.current.connection_sync;
final NodeListViewModel nodeListViewModel;
final DashboardViewModel dashboardViewModel; final DashboardViewModel dashboardViewModel;
@override @override
@ -33,72 +33,39 @@ class ConnectionSyncPage extends BasePage {
title: S.current.reconnect, title: S.current.reconnect,
handler: (context) => _presentReconnectAlert(context), handler: (context) => _presentReconnectAlert(context),
), ),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), const StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
if (dashboardViewModel.hasRescan) if (dashboardViewModel.hasRescan) ...[
SettingsCellWithArrow( SettingsCellWithArrow(
title: S.current.rescan, title: S.current.rescan,
handler: (context) => Navigator.of(context).pushNamed(Routes.rescan), handler: (context) => Navigator.of(context).pushNamed(Routes.rescan),
), ),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), const StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
Semantics( if (DeviceInfo.instance.isMobile) ...[
button: true, Observer(builder: (context) {
child: NodeHeaderListRow( return SettingsPickerCell<SyncMode>(
title: S.of(context).add_new_node, title: S.current.background_sync_mode,
onTap: (_) async => items: SyncMode.all,
await Navigator.of(context).pushNamed(Routes.newNode), displayItem: (SyncMode syncMode) => syncMode.name,
), selectedItem: dashboardViewModel.syncMode,
), onItemSelected: dashboardViewModel.setSyncMode,
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
SizedBox(height: 100),
Observer(
builder: (BuildContext context) {
return Flexible(
child: SectionStandardList(
sectionCount: 1,
context: context,
dividerPadding: EdgeInsets.symmetric(horizontal: 24),
itemCounter: (int sectionIndex) {
return nodeListViewModel.nodes.length;
},
itemBuilder: (_, sectionIndex, index) {
final node = nodeListViewModel.nodes[index];
final isSelected = node.keyIndex == nodeListViewModel.currentNode.keyIndex;
final nodeListRow = NodeListRow(
title: node.uriRaw,
node: node,
isSelected: isSelected,
onTap: (_) async {
if (isSelected) {
return;
}
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle:
S.of(context).change_current_node_title,
alertContent: nodeListViewModel
.getAlertContent(node.uriRaw),
leftButtonText: S.of(context).cancel,
rightButtonText: S.of(context).change,
actionLeftButton: () =>
Navigator.of(context).pop(),
actionRightButton: () async {
await nodeListViewModel.setAsCurrent(node);
Navigator.of(context).pop();
},
); );
}); }),
}, const StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
Observer(builder: (context) {
return SettingsSwitcherCell(
title: S.current.sync_all_wallets,
value: dashboardViewModel.syncAll,
onValueChange: (_, bool value) => dashboardViewModel.setSyncAll(value),
); );
}),
return nodeListRow; const StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
}, ],
), ],
); SettingsCellWithArrow(
}, title: S.current.manage_nodes,
handler: (context) => Navigator.of(context).pushNamed(Routes.manageNodes),
), ),
const StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
], ],
), ),
); );

View file

@ -0,0 +1,85 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/nodes/widgets/node_list_row.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class ManageNodesPage extends BasePage {
ManageNodesPage(this.nodeListViewModel);
final NodeListViewModel nodeListViewModel;
@override
String get title => S.current.manage_nodes;
@override
Widget body(BuildContext context) {
return Padding(
padding: EdgeInsets.only(top: 10),
child: Column(
children: [
Semantics(
button: true,
child: NodeHeaderListRow(
title: S.of(context).add_new_node,
onTap: (_) async => await Navigator.of(context).pushNamed(Routes.newNode),
),
),
const StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
SizedBox(height: 20),
Observer(
builder: (BuildContext context) {
return Flexible(
child: SectionStandardList(
sectionCount: 1,
context: context,
dividerPadding: EdgeInsets.symmetric(horizontal: 24),
itemCounter: (int sectionIndex) {
return nodeListViewModel.nodes.length;
},
itemBuilder: (_, sectionIndex, index) {
final node = nodeListViewModel.nodes[index];
final isSelected = node.keyIndex == nodeListViewModel.currentNode.keyIndex;
final nodeListRow = NodeListRow(
title: node.uriRaw,
node: node,
isSelected: isSelected,
onTap: (_) async {
if (isSelected) {
return;
}
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: S.of(context).change_current_node_title,
alertContent: nodeListViewModel.getAlertContent(node.uriRaw),
leftButtonText: S.of(context).cancel,
rightButtonText: S.of(context).change,
actionLeftButton: () => Navigator.of(context).pop(),
actionRightButton: () async {
await nodeListViewModel.setAsCurrent(node);
Navigator.of(context).pop();
},
);
});
},
);
return nodeListRow;
},
),
);
},
),
],
),
);
}
}

View file

@ -20,10 +20,12 @@ class OtherSettingsPage extends BasePage {
@override @override
Widget body(BuildContext context) { Widget body(BuildContext context) {
return Observer(builder: (_) { return Observer(
builder: (_) {
return Container( return Container(
padding: EdgeInsets.only(top: 10), padding: EdgeInsets.only(top: 10),
child: Column(children: [ child: Column(
children: [
SettingsPickerCell( SettingsPickerCell(
title: S.current.settings_fee_priority, title: S.current.settings_fee_priority,
items: priorityForWalletType(_otherSettingsViewModel.walletType), items: priorityForWalletType(_otherSettingsViewModel.walletType),
@ -33,13 +35,17 @@ class OtherSettingsPage extends BasePage {
), ),
SettingsCellWithArrow( SettingsCellWithArrow(
title: S.current.settings_terms_and_conditions, title: S.current.settings_terms_and_conditions,
handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.readDisclaimer), handler: (BuildContext context) =>
Navigator.of(context).pushNamed(Routes.readDisclaimer),
), ),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
Spacer(), Spacer(),
SettingsVersionCell(title: S.of(context).version(_otherSettingsViewModel.currentVersion)) SettingsVersionCell(
]), title: S.of(context).version(_otherSettingsViewModel.currentVersion))
],
),
);
},
); );
});
} }
} }

View file

@ -78,7 +78,7 @@ class SectionHeaderListRow extends StatelessWidget {
class StandardListSeparator extends StatelessWidget { class StandardListSeparator extends StatelessWidget {
StandardListSeparator({this.padding, this.height = 1}); const StandardListSeparator({this.padding, this.height = 1});
final EdgeInsets? padding; final EdgeInsets? padding;
final double height; final double height;

View file

@ -2,10 +2,12 @@ import 'dart:io';
import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/entities/cake_2fa_preset_options.dart'; import 'package:cake_wallet/entities/cake_2fa_preset_options.dart';
import 'package:cake_wallet/entities/background_tasks.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/pin_code_required_duration.dart'; import 'package:cake_wallet/entities/pin_code_required_duration.dart';
import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/sort_balance_types.dart'; import 'package:cake_wallet/entities/sort_balance_types.dart';
import 'package:cake_wallet/view_model/settings/sync_mode.dart';
import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/transaction_priority.dart';
@ -34,7 +36,8 @@ class SettingsStore = SettingsStoreBase with _$SettingsStore;
abstract class SettingsStoreBase with Store { abstract class SettingsStoreBase with Store {
SettingsStoreBase( SettingsStoreBase(
{required SharedPreferences sharedPreferences, {required BackgroundTasks backgroundTasks,
required SharedPreferences sharedPreferences,
required bool initialShouldShowMarketPlaceInDashboard, required bool initialShouldShowMarketPlaceInDashboard,
required FiatCurrency initialFiatCurrency, required FiatCurrency initialFiatCurrency,
required BalanceDisplayMode initialBalanceDisplayMode, required BalanceDisplayMode initialBalanceDisplayMode,
@ -51,6 +54,8 @@ abstract class SettingsStoreBase with Store {
required ThemeBase initialTheme, required ThemeBase initialTheme,
required int initialPinLength, required int initialPinLength,
required String initialLanguageCode, required String initialLanguageCode,
required SyncMode initialSyncMode,
required bool initialSyncAll,
// required String initialCurrentLocale, // required String initialCurrentLocale,
required this.appVersion, required this.appVersion,
required this.deviceName, required this.deviceName,
@ -78,6 +83,7 @@ abstract class SettingsStoreBase with Store {
TransactionPriority? initialEthereumTransactionPriority}) TransactionPriority? initialEthereumTransactionPriority})
: nodes = ObservableMap<WalletType, Node>.of(nodes), : nodes = ObservableMap<WalletType, Node>.of(nodes),
_sharedPreferences = sharedPreferences, _sharedPreferences = sharedPreferences,
_backgroundTasks = backgroundTasks,
fiatCurrency = initialFiatCurrency, fiatCurrency = initialFiatCurrency,
balanceDisplayMode = initialBalanceDisplayMode, balanceDisplayMode = initialBalanceDisplayMode,
shouldSaveRecipientAddress = initialSaveRecipientAddress, shouldSaveRecipientAddress = initialSaveRecipientAddress,
@ -107,6 +113,8 @@ abstract class SettingsStoreBase with Store {
initialShouldRequireTOTP2FAForCreatingNewWallets, initialShouldRequireTOTP2FAForCreatingNewWallets,
shouldRequireTOTP2FAForAllSecurityAndBackupSettings = shouldRequireTOTP2FAForAllSecurityAndBackupSettings =
initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings, initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings,
currentSyncMode = initialSyncMode,
currentSyncAll = initialSyncAll,
priority = ObservableMap<WalletType, TransactionPriority>() { priority = ObservableMap<WalletType, TransactionPriority>() {
//this.nodes = ObservableMap<WalletType, Node>.of(nodes); //this.nodes = ObservableMap<WalletType, Node>.of(nodes);
@ -287,6 +295,18 @@ abstract class SettingsStoreBase with Store {
(BalanceDisplayMode mode) => sharedPreferences.setInt( (BalanceDisplayMode mode) => sharedPreferences.setInt(
PreferencesKey.currentBalanceDisplayModeKey, mode.serialize())); PreferencesKey.currentBalanceDisplayModeKey, mode.serialize()));
reaction((_) => currentSyncMode, (SyncMode syncMode) {
sharedPreferences.setInt(PreferencesKey.syncModeKey, syncMode.type.index);
_backgroundTasks.registerSyncTask(changeExisting: true);
});
reaction((_) => currentSyncAll, (bool syncAll) {
sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll);
_backgroundTasks.registerSyncTask(changeExisting: true);
});
reaction( reaction(
(_) => exchangeStatus, (_) => exchangeStatus,
(ExchangeApiMode mode) => (ExchangeApiMode mode) =>
@ -422,11 +442,18 @@ abstract class SettingsStoreBase with Store {
@observable @observable
bool useEtherscan; bool useEtherscan;
@observable
SyncMode currentSyncMode;
@observable
bool currentSyncAll;
String appVersion; String appVersion;
String deviceName; String deviceName;
SharedPreferences _sharedPreferences; final SharedPreferences _sharedPreferences;
final BackgroundTasks _backgroundTasks;
ObservableMap<WalletType, Node> nodes; ObservableMap<WalletType, Node> nodes;
@ -455,6 +482,7 @@ abstract class SettingsStoreBase with Store {
BalanceDisplayMode initialBalanceDisplayMode = BalanceDisplayMode.availableBalance, BalanceDisplayMode initialBalanceDisplayMode = BalanceDisplayMode.availableBalance,
ThemeBase? initialTheme}) async { ThemeBase? initialTheme}) async {
final sharedPreferences = await getIt.getAsync<SharedPreferences>(); final sharedPreferences = await getIt.getAsync<SharedPreferences>();
final backgroundTasks = getIt.get<BackgroundTasks>();
final currentFiatCurrency = FiatCurrency.deserialize( final currentFiatCurrency = FiatCurrency.deserialize(
raw: sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey)!); raw: sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey)!);
@ -597,6 +625,11 @@ abstract class SettingsStoreBase with Store {
nodes[WalletType.ethereum] = ethereumNode; nodes[WalletType.ethereum] = ethereumNode;
} }
final savedSyncMode = SyncMode.all.firstWhere((element) {
return element.type.index == (sharedPreferences.getInt(PreferencesKey.syncModeKey) ?? 1);
});
final savedSyncAll = sharedPreferences.getBool(PreferencesKey.syncAllKey) ?? true;
return SettingsStore( return SettingsStore(
sharedPreferences: sharedPreferences, sharedPreferences: sharedPreferences,
initialShouldShowMarketPlaceInDashboard: shouldShowMarketPlaceInDashboard, initialShouldShowMarketPlaceInDashboard: shouldShowMarketPlaceInDashboard,
@ -641,6 +674,9 @@ abstract class SettingsStoreBase with Store {
initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings: initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings:
shouldRequireTOTP2FAForAllSecurityAndBackupSettings, shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
initialEthereumTransactionPriority: ethereumTransactionPriority, initialEthereumTransactionPriority: ethereumTransactionPriority,
backgroundTasks: backgroundTasks,
initialSyncMode: savedSyncMode,
initialSyncAll: savedSyncAll,
shouldShowYatPopup: shouldShowYatPopup); shouldShowYatPopup: shouldShowYatPopup);
} }

View file

@ -1,6 +1,7 @@
import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart';
import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
import 'package:cake_wallet/view_model/settings/sync_mode.dart';
import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cake_wallet/wallet_type_utils.dart';
import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/balance.dart'; import 'package:cw_core/balance.dart';
@ -403,4 +404,16 @@ abstract class DashboardViewModelBase with Store {
hasBuyAction = !isHaven; hasBuyAction = !isHaven;
hasSellAction = !isHaven; hasSellAction = !isHaven;
} }
@computed
SyncMode get syncMode => settingsStore.currentSyncMode;
@action
void setSyncMode(SyncMode syncMode) => settingsStore.currentSyncMode = syncMode;
@computed
bool get syncAll => settingsStore.currentSyncAll;
@action
void setSyncAll(bool value) => settingsStore.currentSyncAll = value;
} }

View file

@ -1,20 +1,19 @@
import 'package:flutter/foundation.dart';
import 'package:cake_wallet/view_model/settings/settings_list_item.dart'; import 'package:cake_wallet/view_model/settings/settings_list_item.dart';
import 'package:flutter/material.dart';
class ChoicesListItem<ItemType> extends SettingsListItem { class ChoicesListItem<ItemType> extends SettingsListItem {
ChoicesListItem( ChoicesListItem(
{required String title, {required String title,
required this.selectedItem, required this.selectedItem,
required this.items, required this.items,
this.displayItem, String Function(ItemType item)? displayItem,
void Function(ItemType item)? onItemSelected}) void Function(ItemType item)? onItemSelected})
: _onItemSelected = onItemSelected, : _onItemSelected = onItemSelected,
_displayItem = displayItem,
super(title); super(title);
final ItemType selectedItem; final ItemType selectedItem;
final List<ItemType> items; final List<ItemType> items;
final String Function(ItemType item)? displayItem; final String Function(ItemType item)? _displayItem;
final void Function(ItemType item)? _onItemSelected; final void Function(ItemType item)? _onItemSelected;
void onItemSelected(dynamic item) { void onItemSelected(dynamic item) {
@ -22,4 +21,11 @@ class ChoicesListItem<ItemType> extends SettingsListItem {
_onItemSelected?.call(item); _onItemSelected?.call(item);
} }
} }
String displayItem(dynamic item) {
if (item is ItemType && _displayItem != null) {
return _displayItem!.call(item);
}
return item.toString();
}
} }

View file

@ -0,0 +1,15 @@
enum SyncType { disabled, unobtrusive, aggressive }
class SyncMode {
SyncMode(this.name, this.type, this.frequency);
final String name;
final SyncType type;
final Duration frequency;
static final all = [
SyncMode("Disabled", SyncType.disabled, Duration.zero),
SyncMode("Unobtrusive", SyncType.unobtrusive, Duration(days: 1)),
SyncMode("Aggressive", SyncType.aggressive, Duration(hours: 6)),
];
}

View file

@ -1,4 +1,6 @@
import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/background_tasks.dart';
import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
@ -71,6 +73,7 @@ abstract class WalletCreationVMBase with Store {
walletInfo.address = wallet.walletAddresses.address; walletInfo.address = wallet.walletAddresses.address;
await _walletInfoSource.add(walletInfo); await _walletInfoSource.add(walletInfo);
_appStore.changeCurrentWallet(wallet); _appStore.changeCurrentWallet(wallet);
getIt.get<BackgroundTasks>().registerSyncTask();
_appStore.authenticationStore.allowed(); _appStore.authenticationStore.allowed();
state = ExecutedSuccessfullyState(); state = ExecutedSuccessfullyState();
} catch (e) { } catch (e) {

View file

@ -1,6 +1,5 @@
import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/app_store.dart';
@ -58,8 +57,8 @@ abstract class WalletListViewModelBase with Store {
name: info.name, name: info.name,
type: info.type, type: info.type,
key: info.key, key: info.key,
isCurrent: info.name == _appStore.wallet!.name && isCurrent: info.name == _appStore.wallet?.name &&
info.type == _appStore.wallet!.type, info.type == _appStore.wallet?.type,
isEnabled: availableWalletTypes.contains(info.type), isEnabled: availableWalletTypes.contains(info.type),
), ),
), ),

View file

@ -66,6 +66,7 @@ dependencies:
# check unorm_dart for usage and for replace # check unorm_dart for usage and for replace
permission_handler: ^10.0.0 permission_handler: ^10.0.0
device_display_brightness: ^0.0.6 device_display_brightness: ^0.0.6
workmanager: ^0.5.1
platform_device_id: ^1.0.1 platform_device_id: ^1.0.1
wakelock: ^0.6.2 wakelock: ^0.6.2
flutter_mailer: ^2.0.2 flutter_mailer: ^2.0.2

View file

@ -667,5 +667,6 @@
"share": "يشارك", "share": "يشارك",
"slidable": "قابل للانزلاق", "slidable": "قابل للانزلاق",
"etherscan_history": "Etherscan تاريخ", "etherscan_history": "Etherscan تاريخ",
"manage_nodes": "ﺪﻘﻌﻟﺍ ﺓﺭﺍﺩﺇ",
"template_name": "اسم القالب" "template_name": "اسم القالب"
} }

View file

@ -663,5 +663,6 @@
"share": "Дял", "share": "Дял",
"slidable": "Плъзгащ се", "slidable": "Плъзгащ се",
"etherscan_history": "История на Etherscan", "etherscan_history": "История на Etherscan",
"manage_nodes": "Управление на възли",
"template_name": "Име на шаблон" "template_name": "Име на шаблон"
} }

View file

@ -662,6 +662,7 @@
"balance_page": "Stránka zůstatku", "balance_page": "Stránka zůstatku",
"share": "Podíl", "share": "Podíl",
"slidable": "Posuvné", "slidable": "Posuvné",
"manage_nodes": "Spravovat uzly",
"etherscan_history": "Historie Etherscanu", "etherscan_history": "Historie Etherscanu",
"template_name": "Název šablony" "template_name": "Název šablony"
} }

View file

@ -538,6 +538,8 @@
"open_gift_card": "Geschenkkarte öffnen", "open_gift_card": "Geschenkkarte öffnen",
"contact_support": "Support kontaktieren", "contact_support": "Support kontaktieren",
"gift_cards_unavailable": "Geschenkkarten können derzeit nur über Monero, Bitcoin und Litecoin erworben werden", "gift_cards_unavailable": "Geschenkkarten können derzeit nur über Monero, Bitcoin und Litecoin erworben werden",
"background_sync_mode": "Hintergrundsynchronisierungsmodus",
"sync_all_wallets": "Alle Wallets synchronisieren",
"introducing_cake_pay": "Einführung von Cake Pay!", "introducing_cake_pay": "Einführung von Cake Pay!",
"cake_pay_learn_more": "Kaufen und lösen Sie Geschenkkarten sofort in der App ein!\nWischen Sie von links nach rechts, um mehr zu erfahren.", "cake_pay_learn_more": "Kaufen und lösen Sie Geschenkkarten sofort in der App ein!\nWischen Sie von links nach rechts, um mehr zu erfahren.",
"automatic": "Automatisch", "automatic": "Automatisch",
@ -668,6 +670,7 @@
"balance_page": "Balance-Seite", "balance_page": "Balance-Seite",
"share": "Aktie", "share": "Aktie",
"slidable": "Verschiebbar", "slidable": "Verschiebbar",
"manage_nodes": "Knoten verwalten",
"etherscan_history": "Etherscan-Geschichte", "etherscan_history": "Etherscan-Geschichte",
"template_name": "Vorlagenname" "template_name": "Vorlagenname"
} }

View file

@ -538,6 +538,8 @@
"open_gift_card": "Open Gift Card", "open_gift_card": "Open Gift Card",
"contact_support": "Contact Support", "contact_support": "Contact Support",
"gift_cards_unavailable": "Gift cards are available for purchase only with Monero, Bitcoin, and Litecoin at this time", "gift_cards_unavailable": "Gift cards are available for purchase only with Monero, Bitcoin, and Litecoin at this time",
"background_sync_mode": "Background sync mode",
"sync_all_wallets": "Sync all wallets",
"introducing_cake_pay": "Introducing Cake Pay!", "introducing_cake_pay": "Introducing Cake Pay!",
"cake_pay_learn_more": "Instantly purchase and redeem gift cards in the app!\nSwipe left to right to learn more.", "cake_pay_learn_more": "Instantly purchase and redeem gift cards in the app!\nSwipe left to right to learn more.",
"automatic": "Automatic", "automatic": "Automatic",
@ -668,6 +670,7 @@
"balance_page": "Balance Page", "balance_page": "Balance Page",
"share": "Share", "share": "Share",
"slidable": "Slidable", "slidable": "Slidable",
"manage_nodes": "Manage nodes",
"etherscan_history": "Etherscan history", "etherscan_history": "Etherscan history",
"template_name": "Template Name" "template_name": "Template Name"
} }

View file

@ -538,6 +538,8 @@
"open_gift_card": "Abrir tarjeta de regalo", "open_gift_card": "Abrir tarjeta de regalo",
"contact_support": "Contactar con Soporte", "contact_support": "Contactar con Soporte",
"gift_cards_unavailable": "Las tarjetas de regalo están disponibles para comprar solo a través de Monero, Bitcoin y Litecoin en este momento", "gift_cards_unavailable": "Las tarjetas de regalo están disponibles para comprar solo a través de Monero, Bitcoin y Litecoin en este momento",
"background_sync_mode": "Modo de sincronización en segundo plano",
"sync_all_wallets": "Sincronizar todas las billeteras",
"introducing_cake_pay": "¡Presentamos Cake Pay!", "introducing_cake_pay": "¡Presentamos Cake Pay!",
"cake_pay_learn_more": "¡Compre y canjee tarjetas de regalo al instante en la aplicación!\nDeslice el dedo de izquierda a derecha para obtener más información.", "cake_pay_learn_more": "¡Compre y canjee tarjetas de regalo al instante en la aplicación!\nDeslice el dedo de izquierda a derecha para obtener más información.",
"automatic": "Automático", "automatic": "Automático",
@ -668,6 +670,7 @@
"balance_page": "Página de saldo", "balance_page": "Página de saldo",
"share": "Compartir", "share": "Compartir",
"slidable": "deslizable", "slidable": "deslizable",
"manage_nodes": "Administrar nodos",
"etherscan_history": "historia de etherscan", "etherscan_history": "historia de etherscan",
"template_name": "Nombre de la plantilla" "template_name": "Nombre de la plantilla"
} }

View file

@ -536,8 +536,10 @@
"gift_card_is_generated": "La carte-cadeau est générée", "gift_card_is_generated": "La carte-cadeau est générée",
"open_gift_card": "Ouvrir la carte-cadeau", "open_gift_card": "Ouvrir la carte-cadeau",
"contact_support": "Contacter l'assistance", "contact_support": "Contacter l'assistance",
"gift_cards": "Cartes-Cadeaux",
"gift_cards_unavailable": "Les cartes-cadeaux ne sont disponibles à l'achat que via Monero, Bitcoin et Litecoin pour le moment", "gift_cards_unavailable": "Les cartes-cadeaux ne sont disponibles à l'achat que via Monero, Bitcoin et Litecoin pour le moment",
"background_sync_mode": "Mode de synchronisation en arrière-plan",
"sync_all_wallets": "Synchroniser tous les portefeuilles",
"gift_cards": "Cartes-Cadeaux",
"introducing_cake_pay": "Présentation de Cake Pay !", "introducing_cake_pay": "Présentation de Cake Pay !",
"cake_pay_learn_more": "Achetez et utilisez instantanément des cartes-cadeaux dans l'application !\nBalayer de gauche à droite pour en savoir plus.", "cake_pay_learn_more": "Achetez et utilisez instantanément des cartes-cadeaux dans l'application !\nBalayer de gauche à droite pour en savoir plus.",
"automatic": "Automatique", "automatic": "Automatique",
@ -668,6 +670,7 @@
"balance_page": "Page Solde", "balance_page": "Page Solde",
"share": "Partager", "share": "Partager",
"slidable": "Glissable", "slidable": "Glissable",
"manage_nodes": "Gérer les nœuds",
"etherscan_history": "Historique d'Etherscan", "etherscan_history": "Historique d'Etherscan",
"template_name": "Nom du modèle" "template_name": "Nom du modèle"
} }

View file

@ -649,5 +649,6 @@
"share": "Raba", "share": "Raba",
"slidable": "Mai iya zamewa", "slidable": "Mai iya zamewa",
"etherscan_history": "Etherscan tarihin kowane zamani", "etherscan_history": "Etherscan tarihin kowane zamani",
"manage_nodes": "Sarrafa nodes",
"template_name": "Sunan Samfura" "template_name": "Sunan Samfura"
} }

View file

@ -538,6 +538,8 @@
"open_gift_card": "गिफ्ट कार्ड खोलें", "open_gift_card": "गिफ्ट कार्ड खोलें",
"contact_support": "सहायता से संपर्क करें", "contact_support": "सहायता से संपर्क करें",
"gift_cards_unavailable": "उपहार कार्ड इस समय केवल मोनेरो, बिटकॉइन और लिटकोइन के माध्यम से खरीदने के लिए उपलब्ध हैं", "gift_cards_unavailable": "उपहार कार्ड इस समय केवल मोनेरो, बिटकॉइन और लिटकोइन के माध्यम से खरीदने के लिए उपलब्ध हैं",
"background_sync_mode": "बैकग्राउंड सिंक मोड",
"sync_all_wallets": "सभी वॉलेट सिंक करें",
"introducing_cake_pay": "परिचय Cake Pay!", "introducing_cake_pay": "परिचय Cake Pay!",
"cake_pay_learn_more": "ऐप में उपहार कार्ड तुरंत खरीदें और रिडीम करें!\nअधिक जानने के लिए बाएं से दाएं स्वाइप करें।", "cake_pay_learn_more": "ऐप में उपहार कार्ड तुरंत खरीदें और रिडीम करें!\nअधिक जानने के लिए बाएं से दाएं स्वाइप करें।",
"automatic": "स्वचालित", "automatic": "स्वचालित",
@ -668,6 +670,7 @@
"balance_page": "बैलेंस पेज", "balance_page": "बैलेंस पेज",
"share": "शेयर करना", "share": "शेयर करना",
"slidable": "फिसलने लायक", "slidable": "फिसलने लायक",
"manage_nodes": "नोड्स प्रबंधित करें",
"etherscan_history": "इथरस्कैन इतिहास", "etherscan_history": "इथरस्कैन इतिहास",
"template_name": "टेम्पलेट नाम" "template_name": "टेम्पलेट नाम"
} }

View file

@ -538,6 +538,8 @@
"open_gift_card": "Otvori darovnu karticu", "open_gift_card": "Otvori darovnu karticu",
"contact_support": "Kontaktirajte podršku", "contact_support": "Kontaktirajte podršku",
"gift_cards_unavailable": "Poklon kartice trenutno su dostupne za kupnju samo putem Monera, Bitcoina i Litecoina", "gift_cards_unavailable": "Poklon kartice trenutno su dostupne za kupnju samo putem Monera, Bitcoina i Litecoina",
"background_sync_mode": "Sinkronizacija u pozadini",
"sync_all_wallets": "Sinkronizirajte sve novčanike",
"introducing_cake_pay": "Predstavljamo Cake Pay!", "introducing_cake_pay": "Predstavljamo Cake Pay!",
"cake_pay_learn_more": "Azonnal vásárolhat és válthat be ajándékutalványokat az alkalmazásban!\nTovábbi információért csúsztassa balról jobbra az ujját.", "cake_pay_learn_more": "Azonnal vásárolhat és válthat be ajándékutalványokat az alkalmazásban!\nTovábbi információért csúsztassa balról jobbra az ujját.",
"automatic": "Automatski", "automatic": "Automatski",
@ -668,6 +670,7 @@
"balance_page": "Stranica sa stanjem", "balance_page": "Stranica sa stanjem",
"share": "Udio", "share": "Udio",
"slidable": "Klizna", "slidable": "Klizna",
"manage_nodes": "Upravljanje čvorovima",
"etherscan_history": "Etherscan povijest", "etherscan_history": "Etherscan povijest",
"template_name": "Naziv predloška" "template_name": "Naziv predloška"
} }

View file

@ -658,6 +658,7 @@
"balance_page": "Halaman Saldo", "balance_page": "Halaman Saldo",
"share": "Membagikan", "share": "Membagikan",
"slidable": "Dapat digeser", "slidable": "Dapat digeser",
"manage_nodes": "Kelola node",
"etherscan_history": "Sejarah Etherscan", "etherscan_history": "Sejarah Etherscan",
"template_name": "Nama Templat" "template_name": "Nama Templat"
} }

View file

@ -538,6 +538,8 @@
"open_gift_card": "Apri carta regalo", "open_gift_card": "Apri carta regalo",
"contact_support": "Contatta l'assistenza", "contact_support": "Contatta l'assistenza",
"gift_cards_unavailable": "Le carte regalo sono disponibili per l'acquisto solo tramite Monero, Bitcoin e Litecoin in questo momento", "gift_cards_unavailable": "Le carte regalo sono disponibili per l'acquisto solo tramite Monero, Bitcoin e Litecoin in questo momento",
"background_sync_mode": "Modalità di sincronizzazione in background",
"sync_all_wallets": "Sincronizza tutti i portafogli",
"introducing_cake_pay": "Presentazione di Cake Pay!", "introducing_cake_pay": "Presentazione di Cake Pay!",
"cake_pay_learn_more": "Acquista e riscatta istantaneamente carte regalo nell'app!\nScorri da sinistra a destra per saperne di più.", "cake_pay_learn_more": "Acquista e riscatta istantaneamente carte regalo nell'app!\nScorri da sinistra a destra per saperne di più.",
"automatic": "Automatico", "automatic": "Automatico",
@ -668,6 +670,7 @@
"balance_page": "Pagina di equilibrio", "balance_page": "Pagina di equilibrio",
"share": "Condividere", "share": "Condividere",
"slidable": "Scorrevole", "slidable": "Scorrevole",
"manage_nodes": "Gestisci i nodi",
"etherscan_history": "Storia Etherscan", "etherscan_history": "Storia Etherscan",
"template_name": "Nome modello" "template_name": "Nome modello"
} }

View file

@ -538,6 +538,8 @@
"open_gift_card": "オープンギフトカード", "open_gift_card": "オープンギフトカード",
"contact_support": "サポートに連絡する", "contact_support": "サポートに連絡する",
"gift_cards_unavailable": "現時点では、ギフトカードはMonero、Bitcoin、Litecoinからのみ購入できます。", "gift_cards_unavailable": "現時点では、ギフトカードはMonero、Bitcoin、Litecoinからのみ購入できます。",
"background_sync_mode": "バックグラウンド同期モード",
"sync_all_wallets": "すべてのウォレットを同期",
"introducing_cake_pay": "序章Cake Pay", "introducing_cake_pay": "序章Cake Pay",
"cake_pay_learn_more": "アプリですぐにギフトカードを購入して引き換えましょう!\n左から右にスワイプして詳細をご覧ください。", "cake_pay_learn_more": "アプリですぐにギフトカードを購入して引き換えましょう!\n左から右にスワイプして詳細をご覧ください。",
"automatic": "自動", "automatic": "自動",
@ -668,6 +670,7 @@
"balance_page": "残高ページ", "balance_page": "残高ページ",
"share": "共有", "share": "共有",
"slidable": "スライド可能", "slidable": "スライド可能",
"manage_nodes": "ノードの管理",
"etherscan_history": "イーサスキャンの歴史", "etherscan_history": "イーサスキャンの歴史",
"template_name": "テンプレート名" "template_name": "テンプレート名"
} }

View file

@ -538,6 +538,8 @@
"open_gift_card": "기프트 카드 열기", "open_gift_card": "기프트 카드 열기",
"contact_support": "지원팀에 문의", "contact_support": "지원팀에 문의",
"gift_cards_unavailable": "기프트 카드는 현재 Monero, Bitcoin 및 Litecoin을 통해서만 구매할 수 있습니다.", "gift_cards_unavailable": "기프트 카드는 현재 Monero, Bitcoin 및 Litecoin을 통해서만 구매할 수 있습니다.",
"background_sync_mode": "백그라운드 동기화 모드",
"sync_all_wallets": "모든 지갑 동기화",
"introducing_cake_pay": "소개 Cake Pay!", "introducing_cake_pay": "소개 Cake Pay!",
"cake_pay_learn_more": "앱에서 즉시 기프트 카드를 구매하고 사용하세요!\n자세히 알아보려면 왼쪽에서 오른쪽으로 스와이프하세요.", "cake_pay_learn_more": "앱에서 즉시 기프트 카드를 구매하고 사용하세요!\n자세히 알아보려면 왼쪽에서 오른쪽으로 스와이프하세요.",
"automatic": "자동적 인", "automatic": "자동적 인",
@ -668,6 +670,7 @@
"balance_page": "잔액 페이지", "balance_page": "잔액 페이지",
"share": "공유하다", "share": "공유하다",
"slidable": "슬라이딩 가능", "slidable": "슬라이딩 가능",
"manage_nodes": "노드 관리",
"etherscan_history": "이더스캔 역사", "etherscan_history": "이더스캔 역사",
"template_name": "템플릿 이름" "template_name": "템플릿 이름"
} }

View file

@ -668,6 +668,7 @@
"balance_page": "လက်ကျန်စာမျက်နှာ", "balance_page": "လက်ကျန်စာမျက်နှာ",
"share": "မျှဝေပါ။", "share": "မျှဝေပါ။",
"slidable": "လျှောချနိုင်သည်။", "slidable": "လျှောချနိုင်သည်။",
"manage_nodes": "ဆုံမှတ်များကို စီမံပါ။",
"etherscan_history": "Etherscan သမိုင်း", "etherscan_history": "Etherscan သမိုင်း",
"template_name": "နမူနာပုံစံ" "template_name": "နမူနာပုံစံ"
} }

View file

@ -538,6 +538,8 @@
"open_gift_card": "Geschenkkaart openen", "open_gift_card": "Geschenkkaart openen",
"contact_support": "Contact opnemen met ondersteuning", "contact_support": "Contact opnemen met ondersteuning",
"gift_cards_unavailable": "Cadeaubonnen kunnen momenteel alleen worden gekocht via Monero, Bitcoin en Litecoin", "gift_cards_unavailable": "Cadeaubonnen kunnen momenteel alleen worden gekocht via Monero, Bitcoin en Litecoin",
"background_sync_mode": "Achtergrondsynchronisatiemodus",
"sync_all_wallets": "Alle portemonnees synchroniseren",
"introducing_cake_pay": "Introductie van Cake Pay!", "introducing_cake_pay": "Introductie van Cake Pay!",
"cake_pay_learn_more": "Koop en wissel cadeaubonnen direct in de app in!\nSwipe van links naar rechts voor meer informatie.", "cake_pay_learn_more": "Koop en wissel cadeaubonnen direct in de app in!\nSwipe van links naar rechts voor meer informatie.",
"automatic": "automatisch", "automatic": "automatisch",
@ -668,6 +670,7 @@
"balance_page": "Saldo pagina", "balance_page": "Saldo pagina",
"share": "Deel", "share": "Deel",
"slidable": "Verschuifbaar", "slidable": "Verschuifbaar",
"manage_nodes": "Beheer knooppunten",
"etherscan_history": "Etherscan-geschiedenis", "etherscan_history": "Etherscan-geschiedenis",
"template_name": "Sjabloonnaam" "template_name": "Sjabloonnaam"
} }

View file

@ -538,6 +538,8 @@
"open_gift_card": "Otwórz kartę podarunkową", "open_gift_card": "Otwórz kartę podarunkową",
"contact_support": "Skontaktuj się z pomocą techniczną", "contact_support": "Skontaktuj się z pomocą techniczną",
"gift_cards_unavailable": "Karty podarunkowe można obecnie kupić tylko za pośrednictwem Monero, Bitcoin i Litecoin", "gift_cards_unavailable": "Karty podarunkowe można obecnie kupić tylko za pośrednictwem Monero, Bitcoin i Litecoin",
"background_sync_mode": "Tryb synchronizacji w tle",
"sync_all_wallets": "Synchronizuj wszystkie portfele",
"introducing_cake_pay": "Przedstawiamy Cake Pay!", "introducing_cake_pay": "Przedstawiamy Cake Pay!",
"cake_pay_learn_more": "Kupuj i wykorzystuj karty podarunkowe od razu w aplikacji!\nPrzesuń od lewej do prawej, aby dowiedzieć się więcej.", "cake_pay_learn_more": "Kupuj i wykorzystuj karty podarunkowe od razu w aplikacji!\nPrzesuń od lewej do prawej, aby dowiedzieć się więcej.",
"automatic": "Automatyczny", "automatic": "Automatyczny",
@ -668,6 +670,7 @@
"balance_page": "Strona salda", "balance_page": "Strona salda",
"share": "Udział", "share": "Udział",
"slidable": "Przesuwne", "slidable": "Przesuwne",
"manage_nodes": "Zarządzaj węzłami",
"etherscan_history": "Historia Etherscanu", "etherscan_history": "Historia Etherscanu",
"template_name": "Nazwa szablonu" "template_name": "Nazwa szablonu"
} }

View file

@ -537,6 +537,8 @@
"open_gift_card": "Abrir vale-presente", "open_gift_card": "Abrir vale-presente",
"contact_support": "Contatar Suporte", "contact_support": "Contatar Suporte",
"gift_cards_unavailable": "Os cartões-presente estão disponíveis para compra apenas através do Monero, Bitcoin e Litecoin no momento", "gift_cards_unavailable": "Os cartões-presente estão disponíveis para compra apenas através do Monero, Bitcoin e Litecoin no momento",
"background_sync_mode": "Modo de sincronização em segundo plano",
"sync_all_wallets": "Sincronize todas as carteiras",
"introducing_cake_pay": "Apresentando o Cake Pay!", "introducing_cake_pay": "Apresentando o Cake Pay!",
"cake_pay_learn_more": "Compre e resgate vales-presente instantaneamente no app!\nDeslize da esquerda para a direita para saber mais.", "cake_pay_learn_more": "Compre e resgate vales-presente instantaneamente no app!\nDeslize da esquerda para a direita para saber mais.",
"automatic": "Automático", "automatic": "Automático",
@ -667,6 +669,7 @@
"balance_page": "Página de saldo", "balance_page": "Página de saldo",
"share": "Compartilhar", "share": "Compartilhar",
"slidable": "Deslizável", "slidable": "Deslizável",
"manage_nodes": "Gerenciar nós",
"etherscan_history": "história Etherscan", "etherscan_history": "história Etherscan",
"template_name": "Nome do modelo" "template_name": "Nome do modelo"
} }

View file

@ -539,6 +539,8 @@
"open_gift_card": "Открыть подарочную карту", "open_gift_card": "Открыть подарочную карту",
"contact_support": "Связаться со службой поддержки", "contact_support": "Связаться со службой поддержки",
"gift_cards_unavailable": "В настоящее время подарочные карты можно приобрести только через Monero, Bitcoin и Litecoin.", "gift_cards_unavailable": "В настоящее время подарочные карты можно приобрести только через Monero, Bitcoin и Litecoin.",
"background_sync_mode": "Режим фоновой синхронизации",
"sync_all_wallets": "Синхронизировать все кошельки",
"introducing_cake_pay": "Представляем Cake Pay!", "introducing_cake_pay": "Представляем Cake Pay!",
"cake_pay_learn_more": "Мгновенно покупайте и используйте подарочные карты в приложении!\nПроведите по экрану слева направо, чтобы узнать больше.", "cake_pay_learn_more": "Мгновенно покупайте и используйте подарочные карты в приложении!\nПроведите по экрану слева направо, чтобы узнать больше.",
"automatic": "автоматический", "automatic": "автоматический",
@ -669,6 +671,7 @@
"balance_page": "Страница баланса", "balance_page": "Страница баланса",
"share": "Делиться", "share": "Делиться",
"slidable": "Скользящий", "slidable": "Скользящий",
"manage_nodes": "Управление узлами",
"etherscan_history": "История Эфириума", "etherscan_history": "История Эфириума",
"template_name": "Имя Шаблона" "template_name": "Имя Шаблона"
} }

View file

@ -668,6 +668,7 @@
"balance_page": "หน้ายอดคงเหลือ", "balance_page": "หน้ายอดคงเหลือ",
"share": "แบ่งปัน", "share": "แบ่งปัน",
"slidable": "เลื่อนได้", "slidable": "เลื่อนได้",
"manage_nodes": "จัดการโหนด",
"etherscan_history": "ประวัติอีเธอร์สแกน", "etherscan_history": "ประวัติอีเธอร์สแกน",
"template_name": "ชื่อแม่แบบ" "template_name": "ชื่อแม่แบบ"
} }

View file

@ -669,6 +669,7 @@
"balance_page": "Bakiye Sayfası", "balance_page": "Bakiye Sayfası",
"share": "Paylaşmak", "share": "Paylaşmak",
"slidable": "kaydırılabilir", "slidable": "kaydırılabilir",
"manage_nodes": "Düğümleri yönet",
"etherscan_history": "Etherscan geçmişi", "etherscan_history": "Etherscan geçmişi",
"template_name": "şablon adı" "template_name": "şablon adı"
} }

View file

@ -538,6 +538,8 @@
"open_gift_card": "Відкрити подарункову картку", "open_gift_card": "Відкрити подарункову картку",
"contact_support": "Звернутися до служби підтримки", "contact_support": "Звернутися до служби підтримки",
"gift_cards_unavailable": "Наразі подарункові картки можна придбати лише через Monero, Bitcoin і Litecoin", "gift_cards_unavailable": "Наразі подарункові картки можна придбати лише через Monero, Bitcoin і Litecoin",
"background_sync_mode": "Фоновий режим синхронізації",
"sync_all_wallets": "Синхронізувати всі гаманці",
"introducing_cake_pay": "Представляємо Cake Pay!", "introducing_cake_pay": "Представляємо Cake Pay!",
"cake_pay_learn_more": "Миттєво купуйте та активуйте подарункові картки в додатку!\nПроведіть пальцем зліва направо, щоб дізнатися більше.", "cake_pay_learn_more": "Миттєво купуйте та активуйте подарункові картки в додатку!\nПроведіть пальцем зліва направо, щоб дізнатися більше.",
"automatic": "Автоматичний", "automatic": "Автоматичний",
@ -668,6 +670,7 @@
"balance_page": "Сторінка балансу", "balance_page": "Сторінка балансу",
"share": "Поділіться", "share": "Поділіться",
"slidable": "Розсувний", "slidable": "Розсувний",
"manage_nodes": "Керуйте вузлами",
"etherscan_history": "Історія Etherscan", "etherscan_history": "Історія Etherscan",
"template_name": "Назва шаблону" "template_name": "Назва шаблону"
} }

View file

@ -662,6 +662,7 @@
"balance_page": "بیلنس صفحہ", "balance_page": "بیلنس صفحہ",
"share": "بانٹیں", "share": "بانٹیں",
"slidable": "سلائیڈ ایبل", "slidable": "سلائیڈ ایبل",
"manage_nodes": "۔ﮟﯾﺮﮐ ﻢﻈﻧ ﺎﮐ ﺱﮈﻮﻧ",
"etherscan_history": "ﺦﯾﺭﺎﺗ ﯽﮐ ﻦﯿﮑﺳﺍ ﺮﮭﺘﯾﺍ", "etherscan_history": "ﺦﯾﺭﺎﺗ ﯽﮐ ﻦﯿﮑﺳﺍ ﺮﮭﺘﯾﺍ",
"template_name": "ٹیمپلیٹ کا نام" "template_name": "ٹیمپلیٹ کا نام"
} }

View file

@ -664,6 +664,7 @@
"balance_page": "Oju-iwe iwọntunwọnsi", "balance_page": "Oju-iwe iwọntunwọnsi",
"share": "Pinpin", "share": "Pinpin",
"slidable": "Slidable", "slidable": "Slidable",
"manage_nodes": "Ṣakoso awọn apa",
"etherscan_history": "Etherscan itan", "etherscan_history": "Etherscan itan",
"template_name": "Orukọ Awoṣe" "template_name": "Orukọ Awoṣe"
} }

View file

@ -537,6 +537,8 @@
"open_gift_card": "打开礼品卡", "open_gift_card": "打开礼品卡",
"contact_support": "联系支持", "contact_support": "联系支持",
"gift_cards_unavailable": "目前只能通过门罗币、比特币和莱特币购买礼品卡", "gift_cards_unavailable": "目前只能通过门罗币、比特币和莱特币购买礼品卡",
"background_sync_mode": "后台同步模式",
"sync_all_wallets": "同步所有钱包",
"introducing_cake_pay": "介绍 Cake Pay!", "introducing_cake_pay": "介绍 Cake Pay!",
"cake_pay_learn_more": "立即在应用中购买和兑换礼品卡!\n从左向右滑动以了解详情。", "cake_pay_learn_more": "立即在应用中购买和兑换礼品卡!\n从左向右滑动以了解详情。",
"automatic": "自动的", "automatic": "自动的",
@ -667,6 +669,7 @@
"balance_page": "余额页", "balance_page": "余额页",
"share": "分享", "share": "分享",
"slidable": "可滑动", "slidable": "可滑动",
"manage_nodes": "管理节点",
"etherscan_history": "以太扫描历史", "etherscan_history": "以太扫描历史",
"template_name": "模板名称" "template_name": "模板名称"
} }