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';
const utils = const MethodChannel('com.cake_wallet/native_utils');
void setIsAppSecureNative(bool isAppSecure) {
utils.invokeMethod<Uint8List>('setIsAppSecure', {'isAppSecure': isAppSecure});
try {
final utils = const MethodChannel('com.cake_wallet/native_utils');
utils.invokeMethod<Uint8List>('setIsAppSecure', {'isAppSecure': isAppSecure});
} catch (_) {}
}

View file

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

View file

@ -1,6 +1,7 @@
import UIKit
import Flutter
import UnstoppableDomainsResolution
import workmanager
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
@ -16,6 +17,15 @@ import UnstoppableDomainsResolution
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()
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">
<plist version="1.0">
<dict>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>com.fotolockr.cakewallet.monero_sync_task</string>
</array>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@ -113,6 +117,7 @@
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>processing</string>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>

View file

@ -243,6 +243,8 @@ class BackupService {
final sortBalanceTokensBy = data[PreferencesKey.sortBalanceBy] as int?;
final pinNativeTokenAtTop = data[PreferencesKey.pinNativeTokenAtTop] 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);
@ -361,6 +363,12 @@ class BackupService {
if (useEtherscan != null)
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();
}
@ -510,6 +518,10 @@ class BackupService {
_sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop),
PreferencesKey.useEtherscan:
_sharedPreferences.getBool(PreferencesKey.useEtherscan),
PreferencesKey.syncModeKey:
_sharedPreferences.getInt(PreferencesKey.syncModeKey),
PreferencesKey.syncAllKey:
_sharedPreferences.getBool(PreferencesKey.syncAllKey),
};
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/payfura/payfura_buy_provider.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/parse_address_from_domain.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_receive_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/privacy_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.registerFactory(() => BackgroundTasks());
final isBitcoinBuyEnabled = (secrets.wyreSecretKey.isNotEmpty) &&
(secrets.wyreApiKey.isNotEmpty) &&
(secrets.wyreAccountId.isNotEmpty);
@ -681,8 +685,7 @@ Future setup({
return NodeListViewModel(_nodeSource, appStore);
});
getIt.registerFactory(
() => ConnectionSyncPage(getIt.get<NodeListViewModel>(), getIt.get<DashboardViewModel>()));
getIt.registerFactory(() => ConnectionSyncPage(getIt.get<DashboardViewModel>()));
getIt.registerFactory(
() => SecurityBackupPage(getIt.get<SecuritySettingsViewModel>(), getIt.get<AuthService>()));
@ -1055,5 +1058,7 @@ Future setup({
),
);
getIt.registerFactory<ManageNodesPage>(() => ManageNodesPage(getIt.get<NodeListViewModel>()));
_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:shared_preferences/shared_preferences.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/core/key_service.dart';
import 'package:cw_core/wallet_service.dart';
import 'package:cake_wallet/entities/background_tasks.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/core/wallet_loading_service.dart';
@ -24,4 +23,6 @@ Future<void> loadCurrentWallet() async {
final walletLoadingService = getIt.get<WalletLoadingService>();
final wallet = await walletLoadingService.load(type, name);
appStore.changeCurrentWallet(wallet);
getIt.get<BackgroundTasks>().registerSyncTask();
}

View file

@ -36,6 +36,8 @@ class PreferencesKey {
static const shouldShowReceiveWarning = 'should_show_receive_warning';
static const shouldShowYatPopup = 'should_show_yat_popup';
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 lastAuthTimeMilliseconds = 'last_auth_time_milliseconds';
static const lastPopupDate = 'last_popup_date';

View file

@ -57,97 +57,103 @@ Future<void> main() async {
return true;
};
final appDir = await getApplicationDocumentsDirectory();
await Hive.close();
Hive.init(appDir.path);
if (!Hive.isAdapterRegistered(Contact.typeId)) {
Hive.registerAdapter(ContactAdapter());
}
await initializeAppConfigs();
if (!Hive.isAdapterRegistered(Node.typeId)) {
Hive.registerAdapter(NodeAdapter());
}
if (!Hive.isAdapterRegistered(TransactionDescription.typeId)) {
Hive.registerAdapter(TransactionDescriptionAdapter());
}
if (!Hive.isAdapterRegistered(Trade.typeId)) {
Hive.registerAdapter(TradeAdapter());
}
if (!Hive.isAdapterRegistered(WalletInfo.typeId)) {
Hive.registerAdapter(WalletInfoAdapter());
}
if (!Hive.isAdapterRegistered(walletTypeTypeId)) {
Hive.registerAdapter(WalletTypeAdapter());
}
if (!Hive.isAdapterRegistered(Template.typeId)) {
Hive.registerAdapter(TemplateAdapter());
}
if (!Hive.isAdapterRegistered(ExchangeTemplate.typeId)) {
Hive.registerAdapter(ExchangeTemplateAdapter());
}
if (!Hive.isAdapterRegistered(Order.typeId)) {
Hive.registerAdapter(OrderAdapter());
}
if (!isMoneroOnly && !Hive.isAdapterRegistered(UnspentCoinsInfo.typeId)) {
Hive.registerAdapter(UnspentCoinsInfoAdapter());
}
if (!Hive.isAdapterRegistered(AnonpayInvoiceInfo.typeId)) {
Hive.registerAdapter(AnonpayInvoiceInfoAdapter());
}
final secureStorage = FlutterSecureStorage();
final transactionDescriptionsBoxKey =
await getEncryptionKey(secureStorage: secureStorage, forKey: TransactionDescription.boxKey);
final tradesBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: Trade.boxKey);
final ordersBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: Order.boxKey);
final contacts = await Hive.openBox<Contact>(Contact.boxName);
final nodes = await Hive.openBox<Node>(Node.boxName);
final transactionDescriptions = await Hive.openBox<TransactionDescription>(
TransactionDescription.boxName,
encryptionKey: transactionDescriptionsBoxKey);
final trades = await Hive.openBox<Trade>(Trade.boxName, encryptionKey: tradesBoxKey);
final orders = await Hive.openBox<Order>(Order.boxName, encryptionKey: ordersBoxKey);
final walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName);
final templates = await Hive.openBox<Template>(Template.boxName);
final exchangeTemplates = await Hive.openBox<ExchangeTemplate>(ExchangeTemplate.boxName);
final anonpayInvoiceInfo = await Hive.openBox<AnonpayInvoiceInfo>(AnonpayInvoiceInfo.boxName);
Box<UnspentCoinsInfo>? unspentCoinsInfoSource;
if (!isMoneroOnly) {
unspentCoinsInfoSource = await Hive.openBox<UnspentCoinsInfo>(UnspentCoinsInfo.boxName);
}
await initialSetup(
sharedPreferences: await SharedPreferences.getInstance(),
nodes: nodes,
walletInfoSource: walletInfoSource,
contactSource: contacts,
tradesSource: trades,
ordersSource: orders,
unspentCoinsInfoSource: unspentCoinsInfoSource,
// fiatConvertationService: fiatConvertationService,
templates: templates,
exchangeTemplates: exchangeTemplates,
transactionDescriptions: transactionDescriptions,
secureStorage: secureStorage,
anonpayInvoiceInfo: anonpayInvoiceInfo,
initialMigrationVersion: 21);
runApp(App());
}, (error, stackTrace) async {
ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stackTrace));
});
}
Future<void> initializeAppConfigs() async {
final appDir = await getApplicationDocumentsDirectory();
Hive.init(appDir.path);
if (!Hive.isAdapterRegistered(Contact.typeId)) {
Hive.registerAdapter(ContactAdapter());
}
if (!Hive.isAdapterRegistered(Node.typeId)) {
Hive.registerAdapter(NodeAdapter());
}
if (!Hive.isAdapterRegistered(TransactionDescription.typeId)) {
Hive.registerAdapter(TransactionDescriptionAdapter());
}
if (!Hive.isAdapterRegistered(Trade.typeId)) {
Hive.registerAdapter(TradeAdapter());
}
if (!Hive.isAdapterRegistered(WalletInfo.typeId)) {
Hive.registerAdapter(WalletInfoAdapter());
}
if (!Hive.isAdapterRegistered(walletTypeTypeId)) {
Hive.registerAdapter(WalletTypeAdapter());
}
if (!Hive.isAdapterRegistered(Template.typeId)) {
Hive.registerAdapter(TemplateAdapter());
}
if (!Hive.isAdapterRegistered(ExchangeTemplate.typeId)) {
Hive.registerAdapter(ExchangeTemplateAdapter());
}
if (!Hive.isAdapterRegistered(Order.typeId)) {
Hive.registerAdapter(OrderAdapter());
}
if (!isMoneroOnly && !Hive.isAdapterRegistered(UnspentCoinsInfo.typeId)) {
Hive.registerAdapter(UnspentCoinsInfoAdapter());
}
if (!Hive.isAdapterRegistered(AnonpayInvoiceInfo.typeId)) {
Hive.registerAdapter(AnonpayInvoiceInfoAdapter());
}
final secureStorage = FlutterSecureStorage();
final transactionDescriptionsBoxKey =
await getEncryptionKey(secureStorage: secureStorage, forKey: TransactionDescription.boxKey);
final tradesBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: Trade.boxKey);
final ordersBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: Order.boxKey);
final contacts = await Hive.openBox<Contact>(Contact.boxName);
final nodes = await Hive.openBox<Node>(Node.boxName);
final transactionDescriptions = await Hive.openBox<TransactionDescription>(
TransactionDescription.boxName,
encryptionKey: transactionDescriptionsBoxKey);
final trades = await Hive.openBox<Trade>(Trade.boxName, encryptionKey: tradesBoxKey);
final orders = await Hive.openBox<Order>(Order.boxName, encryptionKey: ordersBoxKey);
final walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName);
final templates = await Hive.openBox<Template>(Template.boxName);
final exchangeTemplates = await Hive.openBox<ExchangeTemplate>(ExchangeTemplate.boxName);
final anonpayInvoiceInfo = await Hive.openBox<AnonpayInvoiceInfo>(AnonpayInvoiceInfo.boxName);
Box<UnspentCoinsInfo>? unspentCoinsInfoSource;
if (!isMoneroOnly) {
unspentCoinsInfoSource = await Hive.openBox<UnspentCoinsInfo>(UnspentCoinsInfo.boxName);
}
await initialSetup(
sharedPreferences: await SharedPreferences.getInstance(),
nodes: nodes,
walletInfoSource: walletInfoSource,
contactSource: contacts,
tradesSource: trades,
ordersSource: orders,
unspentCoinsInfoSource: unspentCoinsInfoSource,
// fiatConvertationService: fiatConvertationService,
templates: templates,
exchangeTemplates: exchangeTemplates,
transactionDescriptions: transactionDescriptions,
secureStorage: secureStorage,
anonpayInvoiceInfo: anonpayInvoiceInfo,
initialMigrationVersion: 21);
}
Future<void> initialSetup(
{required SharedPreferences sharedPreferences,
required Box<Node> nodes,

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/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/manage_nodes_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/security_backup_page.dart';
@ -605,6 +606,9 @@ Route<dynamic> createRoute(RouteSettings settings) {
),
);
case Routes.manageNodes:
return MaterialPageRoute<void>(builder: (_) => getIt.get<ManageNodesPage>());
default:
return MaterialPageRoute<void>(
builder: (_) => Scaffold(

View file

@ -90,4 +90,5 @@ class Routes {
static const modify2FAPage = '/modify_2fa_page';
static const homeSettings = '/home_settings';
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_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/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/cupertino.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/generated/i18n.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/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 {
ConnectionSyncPage(this.nodeListViewModel, this.dashboardViewModel);
ConnectionSyncPage(this.dashboardViewModel);
@override
String get title => S.current.connection_sync;
final NodeListViewModel nodeListViewModel;
final DashboardViewModel dashboardViewModel;
@override
@ -33,72 +33,39 @@ class ConnectionSyncPage extends BasePage {
title: S.current.reconnect,
handler: (context) => _presentReconnectAlert(context),
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
if (dashboardViewModel.hasRescan)
const StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
if (dashboardViewModel.hasRescan) ...[
SettingsCellWithArrow(
title: S.current.rescan,
handler: (context) => Navigator.of(context).pushNamed(Routes.rescan),
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
Semantics(
button: true,
child: NodeHeaderListRow(
title: S.of(context).add_new_node,
onTap: (_) async =>
await Navigator.of(context).pushNamed(Routes.newNode),
),
),
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();
},
);
});
},
);
return nodeListRow;
},
),
);
},
const StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
if (DeviceInfo.instance.isMobile) ...[
Observer(builder: (context) {
return SettingsPickerCell<SyncMode>(
title: S.current.background_sync_mode,
items: SyncMode.all,
displayItem: (SyncMode syncMode) => syncMode.name,
selectedItem: dashboardViewModel.syncMode,
onItemSelected: dashboardViewModel.setSyncMode,
);
}),
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),
);
}),
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,26 +20,32 @@ class OtherSettingsPage extends BasePage {
@override
Widget body(BuildContext context) {
return Observer(builder: (_) {
return Container(
padding: EdgeInsets.only(top: 10),
child: Column(children: [
SettingsPickerCell(
title: S.current.settings_fee_priority,
items: priorityForWalletType(_otherSettingsViewModel.walletType),
displayItem: _otherSettingsViewModel.getDisplayPriority,
selectedItem: _otherSettingsViewModel.transactionPriority,
onItemSelected: _otherSettingsViewModel.onDisplayPrioritySelected,
return Observer(
builder: (_) {
return Container(
padding: EdgeInsets.only(top: 10),
child: Column(
children: [
SettingsPickerCell(
title: S.current.settings_fee_priority,
items: priorityForWalletType(_otherSettingsViewModel.walletType),
displayItem: _otherSettingsViewModel.getDisplayPriority,
selectedItem: _otherSettingsViewModel.transactionPriority,
onItemSelected: _otherSettingsViewModel.onDisplayPrioritySelected,
),
SettingsCellWithArrow(
title: S.current.settings_terms_and_conditions,
handler: (BuildContext context) =>
Navigator.of(context).pushNamed(Routes.readDisclaimer),
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
Spacer(),
SettingsVersionCell(
title: S.of(context).version(_otherSettingsViewModel.currentVersion))
],
),
SettingsCellWithArrow(
title: S.current.settings_terms_and_conditions,
handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.readDisclaimer),
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
Spacer(),
SettingsVersionCell(title: S.of(context).version(_otherSettingsViewModel.currentVersion))
]),
);
});
);
},
);
}
}

View file

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

View file

@ -2,10 +2,12 @@ import 'dart:io';
import 'package:cake_wallet/bitcoin/bitcoin.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/pin_code_required_duration.dart';
import 'package:cake_wallet/entities/preferences_key.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/ethereum/ethereum.dart';
import 'package:cw_core/transaction_priority.dart';
@ -34,7 +36,8 @@ class SettingsStore = SettingsStoreBase with _$SettingsStore;
abstract class SettingsStoreBase with Store {
SettingsStoreBase(
{required SharedPreferences sharedPreferences,
{required BackgroundTasks backgroundTasks,
required SharedPreferences sharedPreferences,
required bool initialShouldShowMarketPlaceInDashboard,
required FiatCurrency initialFiatCurrency,
required BalanceDisplayMode initialBalanceDisplayMode,
@ -51,6 +54,8 @@ abstract class SettingsStoreBase with Store {
required ThemeBase initialTheme,
required int initialPinLength,
required String initialLanguageCode,
required SyncMode initialSyncMode,
required bool initialSyncAll,
// required String initialCurrentLocale,
required this.appVersion,
required this.deviceName,
@ -78,6 +83,7 @@ abstract class SettingsStoreBase with Store {
TransactionPriority? initialEthereumTransactionPriority})
: nodes = ObservableMap<WalletType, Node>.of(nodes),
_sharedPreferences = sharedPreferences,
_backgroundTasks = backgroundTasks,
fiatCurrency = initialFiatCurrency,
balanceDisplayMode = initialBalanceDisplayMode,
shouldSaveRecipientAddress = initialSaveRecipientAddress,
@ -107,6 +113,8 @@ abstract class SettingsStoreBase with Store {
initialShouldRequireTOTP2FAForCreatingNewWallets,
shouldRequireTOTP2FAForAllSecurityAndBackupSettings =
initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings,
currentSyncMode = initialSyncMode,
currentSyncAll = initialSyncAll,
priority = ObservableMap<WalletType, TransactionPriority>() {
//this.nodes = ObservableMap<WalletType, Node>.of(nodes);
@ -287,6 +295,18 @@ abstract class SettingsStoreBase with Store {
(BalanceDisplayMode mode) => sharedPreferences.setInt(
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(
(_) => exchangeStatus,
(ExchangeApiMode mode) =>
@ -422,11 +442,18 @@ abstract class SettingsStoreBase with Store {
@observable
bool useEtherscan;
@observable
SyncMode currentSyncMode;
@observable
bool currentSyncAll;
String appVersion;
String deviceName;
SharedPreferences _sharedPreferences;
final SharedPreferences _sharedPreferences;
final BackgroundTasks _backgroundTasks;
ObservableMap<WalletType, Node> nodes;
@ -455,6 +482,7 @@ abstract class SettingsStoreBase with Store {
BalanceDisplayMode initialBalanceDisplayMode = BalanceDisplayMode.availableBalance,
ThemeBase? initialTheme}) async {
final sharedPreferences = await getIt.getAsync<SharedPreferences>();
final backgroundTasks = getIt.get<BackgroundTasks>();
final currentFiatCurrency = FiatCurrency.deserialize(
raw: sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey)!);
@ -597,6 +625,11 @@ abstract class SettingsStoreBase with Store {
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(
sharedPreferences: sharedPreferences,
initialShouldShowMarketPlaceInDashboard: shouldShowMarketPlaceInDashboard,
@ -641,6 +674,9 @@ abstract class SettingsStoreBase with Store {
initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings:
shouldRequireTOTP2FAForAllSecurityAndBackupSettings,
initialEthereumTransactionPriority: ethereumTransactionPriority,
backgroundTasks: backgroundTasks,
initialSyncMode: savedSyncMode,
initialSyncAll: savedSyncAll,
shouldShowYatPopup: shouldShowYatPopup);
}

View file

@ -1,6 +1,7 @@
import 'package:cake_wallet/entities/exchange_api_mode.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/settings/sync_mode.dart';
import 'package:cake_wallet/wallet_type_utils.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/balance.dart';
@ -403,4 +404,16 @@ abstract class DashboardViewModelBase with Store {
hasBuyAction = !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:flutter/material.dart';
class ChoicesListItem<ItemType> extends SettingsListItem {
ChoicesListItem(
{required String title,
required this.selectedItem,
required this.items,
this.displayItem,
String Function(ItemType item)? displayItem,
void Function(ItemType item)? onItemSelected})
: _onItemSelected = onItemSelected,
_displayItem = displayItem,
super(title);
final ItemType selectedItem;
final List<ItemType> items;
final String Function(ItemType item)? displayItem;
final String Function(ItemType item)? _displayItem;
final void Function(ItemType item)? _onItemSelected;
void onItemSelected(dynamic item) {
@ -22,4 +21,11 @@ class ChoicesListItem<ItemType> extends SettingsListItem {
_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/di.dart';
import 'package:cake_wallet/entities/background_tasks.dart';
import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
@ -71,6 +73,7 @@ abstract class WalletCreationVMBase with Store {
walletInfo.address = wallet.walletAddresses.address;
await _walletInfoSource.add(walletInfo);
_appStore.changeCurrentWallet(wallet);
getIt.get<BackgroundTasks>().registerSyncTask();
_appStore.authenticationStore.allowed();
state = ExecutedSuccessfullyState();
} catch (e) {

View file

@ -1,6 +1,5 @@
import 'package:cake_wallet/core/auth_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:mobx/mobx.dart';
import 'package:cake_wallet/store/app_store.dart';
@ -58,8 +57,8 @@ abstract class WalletListViewModelBase with Store {
name: info.name,
type: info.type,
key: info.key,
isCurrent: info.name == _appStore.wallet!.name &&
info.type == _appStore.wallet!.type,
isCurrent: info.name == _appStore.wallet?.name &&
info.type == _appStore.wallet?.type,
isEnabled: availableWalletTypes.contains(info.type),
),
),

View file

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

View file

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

View file

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

View file

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

View file

@ -538,6 +538,8 @@
"open_gift_card": "Geschenkkarte öffnen",
"contact_support": "Support kontaktieren",
"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!",
"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",
@ -668,6 +670,7 @@
"balance_page": "Balance-Seite",
"share": "Aktie",
"slidable": "Verschiebbar",
"manage_nodes": "Knoten verwalten",
"etherscan_history": "Etherscan-Geschichte",
"template_name": "Vorlagenname"
}

View file

@ -538,6 +538,8 @@
"open_gift_card": "Open Gift Card",
"contact_support": "Contact Support",
"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!",
"cake_pay_learn_more": "Instantly purchase and redeem gift cards in the app!\nSwipe left to right to learn more.",
"automatic": "Automatic",
@ -668,6 +670,7 @@
"balance_page": "Balance Page",
"share": "Share",
"slidable": "Slidable",
"manage_nodes": "Manage nodes",
"etherscan_history": "Etherscan history",
"template_name": "Template Name"
}

View file

@ -538,6 +538,8 @@
"open_gift_card": "Abrir tarjeta de regalo",
"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",
"background_sync_mode": "Modo de sincronización en segundo plano",
"sync_all_wallets": "Sincronizar todas las billeteras",
"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.",
"automatic": "Automático",
@ -668,6 +670,7 @@
"balance_page": "Página de saldo",
"share": "Compartir",
"slidable": "deslizable",
"manage_nodes": "Administrar nodos",
"etherscan_history": "historia de etherscan",
"template_name": "Nombre de la plantilla"
}

View file

@ -536,8 +536,10 @@
"gift_card_is_generated": "La carte-cadeau est générée",
"open_gift_card": "Ouvrir la carte-cadeau",
"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",
"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 !",
"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",
@ -668,6 +670,7 @@
"balance_page": "Page Solde",
"share": "Partager",
"slidable": "Glissable",
"manage_nodes": "Gérer les nœuds",
"etherscan_history": "Historique d'Etherscan",
"template_name": "Nom du modèle"
}

View file

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

View file

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

View file

@ -538,6 +538,8 @@
"open_gift_card": "Otvori darovnu karticu",
"contact_support": "Kontaktirajte podršku",
"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!",
"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",
@ -668,6 +670,7 @@
"balance_page": "Stranica sa stanjem",
"share": "Udio",
"slidable": "Klizna",
"manage_nodes": "Upravljanje čvorovima",
"etherscan_history": "Etherscan povijest",
"template_name": "Naziv predloška"
}

View file

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

View file

@ -538,6 +538,8 @@
"open_gift_card": "Apri carta regalo",
"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",
"background_sync_mode": "Modalità di sincronizzazione in background",
"sync_all_wallets": "Sincronizza tutti i portafogli",
"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ù.",
"automatic": "Automatico",
@ -668,6 +670,7 @@
"balance_page": "Pagina di equilibrio",
"share": "Condividere",
"slidable": "Scorrevole",
"manage_nodes": "Gestisci i nodi",
"etherscan_history": "Storia Etherscan",
"template_name": "Nome modello"
}

View file

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

View file

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

View file

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

View file

@ -538,6 +538,8 @@
"open_gift_card": "Geschenkkaart openen",
"contact_support": "Contact opnemen met ondersteuning",
"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!",
"cake_pay_learn_more": "Koop en wissel cadeaubonnen direct in de app in!\nSwipe van links naar rechts voor meer informatie.",
"automatic": "automatisch",
@ -668,6 +670,7 @@
"balance_page": "Saldo pagina",
"share": "Deel",
"slidable": "Verschuifbaar",
"manage_nodes": "Beheer knooppunten",
"etherscan_history": "Etherscan-geschiedenis",
"template_name": "Sjabloonnaam"
}

View file

@ -538,6 +538,8 @@
"open_gift_card": "Otwórz kartę podarunkową",
"contact_support": "Skontaktuj się z pomocą techniczną",
"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!",
"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",
@ -668,6 +670,7 @@
"balance_page": "Strona salda",
"share": "Udział",
"slidable": "Przesuwne",
"manage_nodes": "Zarządzaj węzłami",
"etherscan_history": "Historia Etherscanu",
"template_name": "Nazwa szablonu"
}

View file

@ -537,6 +537,8 @@
"open_gift_card": "Abrir vale-presente",
"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",
"background_sync_mode": "Modo de sincronização em segundo plano",
"sync_all_wallets": "Sincronize todas as carteiras",
"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.",
"automatic": "Automático",
@ -667,6 +669,7 @@
"balance_page": "Página de saldo",
"share": "Compartilhar",
"slidable": "Deslizável",
"manage_nodes": "Gerenciar nós",
"etherscan_history": "história Etherscan",
"template_name": "Nome do modelo"
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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