Merge branch 'main' of https://github.com/cake-tech/cake_wallet into CW-438-add-nano

This commit is contained in:
fosse 2023-08-02 12:34:36 -04:00
commit c8d9fcbaf4
64 changed files with 1069 additions and 602 deletions

View file

@ -45,8 +45,8 @@ android {
defaultConfig {
applicationId appProperties['id']
minSdkVersion 21
targetSdkVersion 31
minSdkVersion 24
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View file

@ -12,14 +12,14 @@ Uri createUriFromElectrumAddress(String address) => Uri.tryParse('tcp://$address
@HiveType(typeId: Node.typeId)
class Node extends HiveObject with Keyable {
Node({
this.login,
this.password,
this.useSSL,
this.trusted = false,
String? uri,
WalletType? type,
}) {
Node(
{this.login,
this.password,
this.useSSL,
this.trusted = false,
this.socksProxyAddress,
String? uri,
WalletType? type,}) {
if (uri != null) {
uriRaw = uri;
}
@ -33,7 +33,8 @@ class Node extends HiveObject with Keyable {
login = map['login'] as String?,
password = map['password'] as String?,
useSSL = map['useSSL'] as bool?,
trusted = map['trusted'] as bool? ?? false;
trusted = map['trusted'] as bool? ?? false,
socksProxyAddress = map['socksProxyPort'] as String?;
static const typeId = 1;
static const boxName = 'Nodes';
@ -56,8 +57,13 @@ class Node extends HiveObject with Keyable {
@HiveField(5, defaultValue: false)
bool trusted;
@HiveField(6)
String? socksProxyAddress;
bool get isSSL => useSSL ?? false;
bool get useSocksProxy => socksProxyAddress == null ? false : socksProxyAddress!.isNotEmpty;
Uri get uri {
switch (type) {
case WalletType.monero:
@ -85,12 +91,13 @@ class Node extends HiveObject with Keyable {
@override
bool operator ==(other) =>
other is Node &&
(other.uriRaw == uriRaw &&
other.login == login &&
other.password == password &&
other.typeRaw == typeRaw &&
other.useSSL == useSSL &&
other.trusted == trusted);
(other.uriRaw == uriRaw &&
other.login == login &&
other.password == password &&
other.typeRaw == typeRaw &&
other.useSSL == useSSL &&
other.trusted == trusted &&
other.socksProxyAddress == socksProxyAddress);
@override
int get hashCode =>
@ -99,7 +106,8 @@ class Node extends HiveObject with Keyable {
password.hashCode ^
typeRaw.hashCode ^
useSSL.hashCode ^
trusted.hashCode;
trusted.hashCode ^
socksProxyAddress.hashCode;
@override
dynamic get keyIndex {
@ -117,7 +125,7 @@ class Node extends HiveObject with Keyable {
try {
switch (type) {
case WalletType.monero:
return requestMoneroNode();
return useSocksProxy ? requestNodeWithProxy(socksProxyAddress ?? '') : requestMoneroNode();
case WalletType.bitcoin:
return requestElectrumServer();
case WalletType.litecoin:
@ -166,6 +174,22 @@ class Node extends HiveObject with Keyable {
}
}
Future<bool> requestNodeWithProxy(String proxy) async {
if (proxy.isEmpty || !proxy.contains(':')) {
return false;
}
final proxyAddress = proxy.split(':')[0];
final proxyPort = int.parse(proxy.split(':')[1]);
try {
final socket = await Socket.connect(proxyAddress, proxyPort, timeout: Duration(seconds: 5));
socket.destroy();
return true;
} catch (_) {
return false;
}
}
Future<bool> requestElectrumServer() async {
try {
await SecureSocket.connect(uri.host, uri.port,

View file

@ -39,7 +39,7 @@ typedef get_node_height = Int64 Function();
typedef is_connected = Int8 Function();
typedef setup_node = Int8 Function(
Pointer<Utf8>, Pointer<Utf8>?, Pointer<Utf8>?, Int8, Int8, Pointer<Utf8>);
Pointer<Utf8>, Pointer<Utf8>?, Pointer<Utf8>?, Int8, Int8, Pointer<Utf8>?, Pointer<Utf8>);
typedef start_refresh = Void Function();

View file

@ -39,7 +39,7 @@ typedef GetNodeHeight = int Function();
typedef IsConnected = int Function();
typedef SetupNode = int Function(
Pointer<Utf8>, Pointer<Utf8>?, Pointer<Utf8>?, int, int, Pointer<Utf8>);
Pointer<Utf8>, Pointer<Utf8>?, Pointer<Utf8>?, int, int, Pointer<Utf8>?, Pointer<Utf8>);
typedef StartRefresh = void Function();

View file

@ -154,9 +154,11 @@ bool setupNodeSync(
String? login,
String? password,
bool useSSL = false,
bool isLightWallet = false}) {
bool isLightWallet = false,
String? socksProxyAddress}) {
final addressPointer = address.toNativeUtf8();
Pointer<Utf8>? loginPointer;
Pointer<Utf8>? socksProxyAddressPointer;
Pointer<Utf8>? passwordPointer;
if (login != null) {
@ -167,6 +169,10 @@ bool setupNodeSync(
passwordPointer = password.toNativeUtf8();
}
if (socksProxyAddress != null) {
socksProxyAddressPointer = socksProxyAddress.toNativeUtf8();
}
final errorMessagePointer = ''.toNativeUtf8();
final isSetupNode = setupNodeNative(
addressPointer,
@ -174,6 +180,7 @@ bool setupNodeSync(
passwordPointer,
_boolToInt(useSSL),
_boolToInt(isLightWallet),
socksProxyAddressPointer,
errorMessagePointer) !=
0;
@ -323,13 +330,15 @@ bool _setupNodeSync(Map args) {
final password = (args['password'] ?? '') as String;
final useSSL = args['useSSL'] as bool;
final isLightWallet = args['isLightWallet'] as bool;
final socksProxyAddress = (args['socksProxyAddress'] ?? '') as String;
return setupNodeSync(
address: address,
login: login,
password: password,
useSSL: useSSL,
isLightWallet: isLightWallet);
isLightWallet: isLightWallet,
socksProxyAddress: socksProxyAddress);
}
bool _isConnected(Object _) => isConnectedSync();
@ -343,13 +352,15 @@ Future<void> setupNode(
String? login,
String? password,
bool useSSL = false,
String? socksProxyAddress,
bool isLightWallet = false}) =>
compute<Map<String, Object?>, void>(_setupNodeSync, {
'address': address,
'login': login,
'password': password,
'useSSL': useSSL,
'isLightWallet': isLightWallet
'isLightWallet': isLightWallet,
'socksProxyAddress': socksProxyAddress
});
Future<void> store() => compute<int, void>(_storeSync, 0);

View file

@ -125,7 +125,8 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance,
login: node.login,
password: node.password,
useSSL: node.useSSL ?? false,
isLightWallet: false); // FIXME: hardcoded value
isLightWallet: false, // FIXME: hardcoded value
socksProxyAddress: node.socksProxyAddress);
haven_wallet.setTrustedDaemon(node.trusted);
syncStatus = ConnectedSyncStatus();

View file

@ -3,6 +3,7 @@
#include <chrono>
#include <functional>
#include <iostream>
#include <fstream>
#include <unistd.h>
#include <mutex>
#include "thread"
@ -405,13 +406,14 @@ extern "C"
return is_connected;
}
bool setup_node(char *address, char *login, char *password, bool use_ssl, bool is_light_wallet, char *error)
bool setup_node(char *address, char *login, char *password, bool use_ssl, bool is_light_wallet, char *socksProxyAddress, char *error)
{
nice(19);
Monero::Wallet *wallet = get_current_wallet();
std::string _login = "";
std::string _password = "";
std::string _socksProxyAddress = "";
if (login != nullptr)
{
@ -423,7 +425,12 @@ extern "C"
_password = std::string(password);
}
bool inited = wallet->init(std::string(address), 0, _login, _password, use_ssl, is_light_wallet);
if (socksProxyAddress != nullptr)
{
_socksProxyAddress = std::string(socksProxyAddress);
}
bool inited = wallet->init(std::string(address), 0, _login, _password, use_ssl, is_light_wallet, _socksProxyAddress);
if (!inited)
{

View file

@ -35,7 +35,7 @@ typedef get_node_height = Int64 Function();
typedef is_connected = Int8 Function();
typedef setup_node = Int8 Function(
Pointer<Utf8>, Pointer<Utf8>?, Pointer<Utf8>?, Int8, Int8, Pointer<Utf8>);
Pointer<Utf8>, Pointer<Utf8>?, Pointer<Utf8>?, Int8, Int8, Pointer<Utf8>?, Pointer<Utf8>);
typedef start_refresh = Void Function();

View file

@ -35,7 +35,7 @@ typedef GetNodeHeight = int Function();
typedef IsConnected = int Function();
typedef SetupNode = int Function(
Pointer<Utf8>, Pointer<Utf8>?, Pointer<Utf8>?, int, int, Pointer<Utf8>);
Pointer<Utf8>, Pointer<Utf8>?, Pointer<Utf8>?, int, int, Pointer<Utf8>?, Pointer<Utf8>);
typedef StartRefresh = void Function();

View file

@ -158,9 +158,11 @@ bool setupNodeSync(
String? login,
String? password,
bool useSSL = false,
bool isLightWallet = false}) {
bool isLightWallet = false,
String? socksProxyAddress}) {
final addressPointer = address.toNativeUtf8();
Pointer<Utf8>? loginPointer;
Pointer<Utf8>? socksProxyAddressPointer;
Pointer<Utf8>? passwordPointer;
if (login != null) {
@ -171,6 +173,10 @@ bool setupNodeSync(
passwordPointer = password.toNativeUtf8();
}
if (socksProxyAddress != null) {
socksProxyAddressPointer = socksProxyAddress.toNativeUtf8();
}
final errorMessagePointer = ''.toNativeUtf8();
final isSetupNode = setupNodeNative(
addressPointer,
@ -178,6 +184,7 @@ bool setupNodeSync(
passwordPointer,
_boolToInt(useSSL),
_boolToInt(isLightWallet),
socksProxyAddressPointer,
errorMessagePointer) !=
0;
@ -328,13 +335,15 @@ bool _setupNodeSync(Map<String, Object?> args) {
final password = (args['password'] ?? '') as String;
final useSSL = args['useSSL'] as bool;
final isLightWallet = args['isLightWallet'] as bool;
final socksProxyAddress = (args['socksProxyAddress'] ?? '') as String;
return setupNodeSync(
address: address,
login: login,
password: password,
useSSL: useSSL,
isLightWallet: isLightWallet);
isLightWallet: isLightWallet,
socksProxyAddress: socksProxyAddress);
}
bool _isConnected(Object _) => isConnectedSync();
@ -348,13 +357,15 @@ Future<void> setupNode(
String? login,
String? password,
bool useSSL = false,
String? socksProxyAddress,
bool isLightWallet = false}) =>
compute<Map<String, Object?>, void>(_setupNodeSync, {
'address': address,
'login': login ,
'password': password,
'useSSL': useSSL,
'isLightWallet': isLightWallet
'isLightWallet': isLightWallet,
'socksProxyAddress': socksProxyAddress
});
Future<void> store() => compute<int, void>(_storeSync, 0);

View file

@ -138,7 +138,8 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
login: node.login,
password: node.password,
useSSL: node.isSSL,
isLightWallet: false); // FIXME: hardcoded value
isLightWallet: false, // FIXME: hardcoded value
socksProxyAddress: node.socksProxyAddress);
monero_wallet.setTrustedDaemon(node.trusted);
syncStatus = ConnectedSyncStatus();

View file

@ -20,6 +20,8 @@ class OnRamperBuyProvider {
switch (_wallet.currency) {
case CryptoCurrency.ltc:
return "LTC_LITECOIN";
case CryptoCurrency.xmr:
return "XMR_MONERO";
default:
return _wallet.currency.title;
}
@ -60,11 +62,12 @@ class OnRamperBuyProvider {
break;
}
final networkName = _wallet.currency.fullName?.toUpperCase().replaceAll(" ", "");
return Uri.https(_baseUrl, '', <String, dynamic>{
'apiKey': _apiKey,
'defaultCrypto': _normalizeCryptoCurrency,
'wallets': '${_wallet.currency.title}:${_wallet.walletAddresses.address}',
'networkWallets': '${networkName}:${_wallet.walletAddresses.address}',
'supportSell': "false",
'supportSwap': "false",
'primaryColor': primaryColor,

View file

@ -0,0 +1,10 @@
import 'package:cake_wallet/core/validator.dart';
import 'package:cake_wallet/generated/i18n.dart';
class SocksProxyNodeAddressValidator extends TextValidator {
SocksProxyNodeAddressValidator()
: super(
errorMessage: S.current.error_text_node_proxy_address,
pattern:
'^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}:[0-9]+\$');
}

View file

@ -37,7 +37,7 @@ class OpenaliasRecord {
required String ticker,
required List<RRecord> txtRecord,
}) {
String address = formattedName;
String address = '';
String name = formattedName;
String note = '';

View file

@ -1,7 +1,7 @@
import 'package:cake_wallet/entities/openalias_record.dart';
import 'package:cake_wallet/entities/yat_record.dart';
enum ParseFrom { unstoppableDomains, openAlias, yatRecord, fio, notParsed, twitter }
enum ParseFrom { unstoppableDomains, openAlias, yatRecord, fio, notParsed, twitter, contact }
class ParsedAddress {
ParsedAddress({
@ -40,13 +40,17 @@ class ParsedAddress {
);
}
factory ParsedAddress.fetchOpenAliasAddress({required OpenaliasRecord record, required String name}){
return ParsedAddress(
addresses: [record.address],
name: record.name,
description: record.description,
parseFrom: ParseFrom.openAlias,
);
factory ParsedAddress.fetchOpenAliasAddress(
{required OpenaliasRecord record, required String name}) {
if (record.address.isEmpty) {
return ParsedAddress(addresses: [name]);
}
return ParsedAddress(
addresses: [record.address],
name: record.name,
description: record.description,
parseFrom: ParseFrom.openAlias,
);
}
factory ParsedAddress.fetchFioAddress({required String address, required String name}){
@ -65,6 +69,14 @@ class ParsedAddress {
);
}
factory ParsedAddress.fetchContactAddress({required String address, required String name}){
return ParsedAddress(
addresses: [address],
name: name,
parseFrom: ParseFrom.contact,
);
}
final List<String> addresses;
final String name;
final String description;

View file

@ -4,14 +4,15 @@ part 'template.g.dart';
@HiveType(typeId: Template.typeId)
class Template extends HiveObject {
Template({
required this.nameRaw,
required this.isCurrencySelectedRaw,
required this.addressRaw,
required this.cryptoCurrencyRaw,
required this.amountRaw,
required this.fiatCurrencyRaw,
required this.amountFiatRaw});
Template(
{required this.nameRaw,
required this.isCurrencySelectedRaw,
required this.addressRaw,
required this.cryptoCurrencyRaw,
required this.amountRaw,
required this.fiatCurrencyRaw,
required this.amountFiatRaw,
this.additionalRecipientsRaw});
static const typeId = 6;
static const boxName = 'Template';
@ -37,6 +38,9 @@ class Template extends HiveObject {
@HiveField(6)
String? amountFiatRaw;
@HiveField(7)
List<Template>? additionalRecipientsRaw;
bool get isCurrencySelected => isCurrencySelectedRaw ?? false;
String get fiatCurrency => fiatCurrencyRaw ?? '';
@ -50,5 +54,6 @@ class Template extends HiveObject {
String get cryptoCurrency => cryptoCurrencyRaw ?? '';
String get amount => amountRaw ?? '';
}
List<Template>? get additionalRecipients => additionalRecipientsRaw ?? null;
}

View file

@ -7,6 +7,7 @@ import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
Timer? _timer;
@ -16,7 +17,7 @@ Future<void> startFiatRateUpdate(
return;
}
_timer = Timer.periodic(Duration(seconds: 30), (_) async {
final _updateFiat = (_) async {
try {
if (appStore.wallet == null || settingsStore.fiatApiMode == FiatApiMode.disabled) {
return;
@ -45,5 +46,20 @@ Future<void> startFiatRateUpdate(
} catch (e) {
print(e);
}
};
_timer = Timer.periodic(Duration(seconds: 30), _updateFiat);
// also run immediately:
_updateFiat(null);
// setup autorun to listen to changes in fiatApiMode
autorun((_) {
// restart the timer if fiatApiMode was re-enabled
if (settingsStore.fiatApiMode != FiatApiMode.disabled) {
_timer = Timer.periodic(Duration(seconds: 30), _updateFiat);
_updateFiat(null);
} else {
_timer?.cancel();
}
});
}

View file

@ -1,21 +1,22 @@
import 'dart:io';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/trail_button.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/utils/clipboard_util.dart';
import 'package:cake_wallet/utils/exception_handler.dart';
import 'package:cake_wallet/utils/share_util.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/backup_view_model.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/trail_button.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/backup_view_model.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:permission_handler/permission_handler.dart';
class BackupPage extends BasePage {
@ -52,7 +53,7 @@ class BackupPage extends BasePage {
child: Observer(
builder: (_) => GestureDetector(
onTap: () {
Clipboard.setData(
ClipboardUtil.setSensitiveDataToClipboard(
ClipboardData(text: backupViewModelBase.backupPassword));
showBar<void>(
context,
@ -75,15 +76,14 @@ class BackupPage extends BasePage {
]))),
Positioned(
child: Observer(
builder: (_) => LoadingPrimaryButton(
isLoading: backupViewModelBase.state is IsExecutingState,
onPressed: () => onExportBackup(context),
text: S.of(context).export_backup,
color: Theme.of(context)
.accentTextTheme!
.bodyLarge!
.color!,
textColor: Colors.white)),
builder: (_) => LoadingPrimaryButton(
isLoading: backupViewModelBase.state is IsExecutingState,
onPressed: () => onExportBackup(context),
text: S.of(context).export_backup,
color: Theme.of(context).accentTextTheme.bodyLarge!.color!,
textColor: Colors.white,
),
),
bottom: 24,
left: 24,
right: 24,

View file

@ -1,5 +1,6 @@
import 'package:cake_wallet/core/node_address_validator.dart';
import 'package:cake_wallet/core/node_port_validator.dart';
import 'package:cake_wallet/core/socks_proxy_node_address_validator.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
@ -17,7 +18,8 @@ class NodeForm extends StatelessWidget {
}) : _addressController = TextEditingController(text: editingNode?.uri.host.toString()),
_portController = TextEditingController(text: editingNode?.uri.port.toString()),
_loginController = TextEditingController(text: editingNode?.login),
_passwordController = TextEditingController(text: editingNode?.password) {
_passwordController = TextEditingController(text: editingNode?.password),
_socksAddressController = TextEditingController(text: editingNode?.socksProxyAddress){
if (editingNode != null) {
nodeViewModel
..setAddress((editingNode!.uri.host.toString()))
@ -25,7 +27,9 @@ class NodeForm extends StatelessWidget {
..setPassword((editingNode!.password ?? ''))
..setLogin((editingNode!.login ?? ''))
..setSSL((editingNode!.isSSL))
..setTrusted((editingNode!.trusted));
..setTrusted((editingNode!.trusted))
..setSocksProxy((editingNode!.useSocksProxy))
..setSocksProxyAddress((editingNode!.socksProxyAddress ?? ''));
}
if (nodeViewModel.hasAuthCredentials) {
reaction((_) => nodeViewModel.login, (String login) {
@ -45,6 +49,7 @@ class NodeForm extends StatelessWidget {
_portController.addListener(() => nodeViewModel.port = _portController.text);
_loginController.addListener(() => nodeViewModel.login = _loginController.text);
_passwordController.addListener(() => nodeViewModel.password = _passwordController.text);
_socksAddressController.addListener(() => nodeViewModel.socksProxyAddress = _socksAddressController.text);
}
final NodeCreateOrEditViewModel nodeViewModel;
@ -55,6 +60,7 @@ class NodeForm extends StatelessWidget {
final TextEditingController _portController;
final TextEditingController _loginController;
final TextEditingController _passwordController;
final TextEditingController _socksAddressController;
@override
Widget build(BuildContext context) {
@ -138,6 +144,43 @@ class NodeForm extends StatelessWidget {
],
),
),
Observer(
builder: (_) => Column(
children: [
Padding(
padding: EdgeInsets.only(top: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
StandardCheckbox(
value: nodeViewModel.useSocksProxy,
onChanged: (value) {
if (!value) {
_socksAddressController.text = '';
}
nodeViewModel.useSocksProxy = value;
},
caption: 'SOCKS Proxy',
),
],
),
),
if (nodeViewModel.useSocksProxy) ...[
SizedBox(height: 10.0),
Row(
children: <Widget>[
Expanded(
child: BaseTextFormField(
controller: _socksAddressController,
hintText: '[<ip>:]<port>',
validator: SocksProxyNodeAddressValidator(),
))
],
),
]
],
)),
]
],
),

View file

@ -1,11 +1,11 @@
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
class PreSeedPage extends BasePage {
PreSeedPage(this.type)
@ -61,7 +61,7 @@ class PreSeedPage extends BasePage {
onPressed: () =>
Navigator.of(context).popAndPushNamed(Routes.seed, arguments: true),
text: S.of(context).pre_seed_button_text,
color: Theme.of(context).accentTextTheme!.bodyLarge!.color!,
color: Theme.of(context).accentTextTheme.bodyLarge!.color!,
textColor: Colors.white)
],
),

View file

@ -1,6 +1,7 @@
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/utils/clipboard_util.dart';
import 'package:cake_wallet/utils/share_util.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/utils/show_bar.dart';
@ -92,8 +93,7 @@ class WalletSeedPage extends BasePage {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
ConstrainedBox(
constraints:
BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.3),
constraints: BoxConstraints(maxHeight: MediaQuery.of(context).size.height * 0.3),
child: AspectRatio(aspectRatio: 1, child: image),
),
Observer(builder: (_) {
@ -159,7 +159,7 @@ class WalletSeedPage extends BasePage {
child: Builder(
builder: (context) => PrimaryButton(
onPressed: () {
Clipboard.setData(
ClipboardUtil.setSensitiveDataToClipboard(
ClipboardData(text: walletSeedViewModel.seed));
showBar<void>(context, S.of(context).copied_to_clipboard);
},

View file

@ -1,4 +1,5 @@
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/template.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart';
import 'package:cake_wallet/src/screens/send/widgets/send_card.dart';
import 'package:cake_wallet/src/widgets/add_template_button.dart';
@ -241,6 +242,11 @@ class SendPage extends BasePage {
return TemplateTile(
key: UniqueKey(),
to: template.name,
hasMultipleRecipients:
template.additionalRecipients !=
null &&
template.additionalRecipients!
.length > 1,
amount: template.isCurrencySelected
? template.amount
: template.amountFiat,
@ -248,25 +254,36 @@ class SendPage extends BasePage {
? template.cryptoCurrency
: template.fiatCurrency,
onTap: () async {
final fiatFromTemplate = FiatCurrency
.all
.singleWhere((element) =>
element.title ==
template.fiatCurrency);
final output = _defineCurrentOutput();
output.address = template.address;
if (template.isCurrencySelected) {
output
.setCryptoAmount(template.amount);
if (template.additionalRecipients !=
null) {
sendViewModel.clearOutputs();
template.additionalRecipients!
.forEach((currentElement) async {
int i = template
.additionalRecipients!
.indexOf(currentElement);
Output output;
try {
output = sendViewModel.outputs[i];
} catch (e) {
sendViewModel.addOutput();
output = sendViewModel.outputs[i];
}
await _setInputsFromTemplate(
context,
output: output,
template: currentElement);
});
} else {
sendViewModel.setFiatCurrency(
fiatFromTemplate);
output.setFiatAmount(
template.amountFiat);
final output = _defineCurrentOutput();
await _setInputsFromTemplate(
context,
output: output,
template: template);
}
output.resetParsedAddress();
await output
.fetchParsedAddress(context);
},
onRemove: () {
showPopUp<void>(
@ -477,6 +494,24 @@ class SendPage extends BasePage {
_effectsInstalled = true;
}
Future<void> _setInputsFromTemplate(BuildContext context,
{required Output output, required Template template}) async {
final fiatFromTemplate = FiatCurrency.all
.singleWhere((element) => element.title == template.fiatCurrency);
output.address = template.address;
if (template.isCurrencySelected) {
output.setCryptoAmount(template.amount);
} else {
sendViewModel.setFiatCurrency(fiatFromTemplate);
output.setFiatAmount(template.amountFiat);
}
output.resetParsedAddress();
await output.fetchParsedAddress(context);
}
Output _defineCurrentOutput() {
if (controller.page == null) {
throw Exception('Controller page is null');

View file

@ -1,35 +1,21 @@
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/src/widgets/trail_button.dart';
import 'package:cake_wallet/view_model/send/template_view_model.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:keyboard_actions/keyboard_actions.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
import 'package:cake_wallet/src/widgets/address_text_field.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/screens/send/widgets/prefix_currency_icon_widget.dart';
import 'package:cake_wallet/src/screens/send/widgets/send_template_card.dart';
import 'package:smooth_page_indicator/smooth_page_indicator.dart';
class SendTemplatePage extends BasePage {
SendTemplatePage({required this.sendTemplateViewModel}) {
sendTemplateViewModel.output.reset();
}
SendTemplatePage({required this.sendTemplateViewModel});
final SendTemplateViewModel sendTemplateViewModel;
final _addressController = TextEditingController();
final _cryptoAmountController = TextEditingController();
final _fiatAmountController = TextEditingController();
final _nameController = TextEditingController();
final _formKey = GlobalKey<FormState>();
final FocusNode _cryptoAmountFocus = FocusNode();
final FocusNode _fiatAmountFocus = FocusNode();
bool _effectsInstalled = false;
final controller = PageController(initialPage: 0);
@override
String get title => S.current.exchange_new_template;
@ -44,273 +30,146 @@ class SendTemplatePage extends BasePage {
AppBarStyle get appBarStyle => AppBarStyle.transparent;
@override
Widget body(BuildContext context) {
_setEffects(context);
Widget trailing(context) => Observer(builder: (_) {
return sendTemplateViewModel.recipients.length > 1
? TrailButton(
caption: S.of(context).remove,
onPressed: () {
int pageToJump = (controller.page?.round() ?? 0) - 1;
pageToJump = pageToJump > 0 ? pageToJump : 0;
final recipient = _defineCurrentRecipient();
sendTemplateViewModel.removeRecipient(recipient);
controller.jumpToPage(pageToJump);
})
: TrailButton(
caption: S.of(context).clear,
onPressed: () {
final recipient = _defineCurrentRecipient();
_formKey.currentState?.reset();
recipient.reset();
});
});
return KeyboardActions(
config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: Theme.of(context)
.accentTextTheme!
.bodyLarge!
.backgroundColor!,
nextFocus: false,
actions: [
KeyboardActionsItem(
focusNode: _cryptoAmountFocus,
toolbarButtons: [(_) => KeyboardDoneButton()],
),
KeyboardActionsItem(
focusNode: _fiatAmountFocus,
toolbarButtons: [(_) => KeyboardDoneButton()],
)
]),
child: Container(
height: 0,
color: Theme.of(context).colorScheme.background,
child: ScrollableWithBottomSection(
@override
Widget body(BuildContext context) {
return Form(
key: _formKey,
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(bottom: 24),
content: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24),
),
gradient: LinearGradient(colors: [
Theme.of(context).primaryTextTheme!.titleMedium!.color!,
Theme.of(context)
.primaryTextTheme!
.titleMedium!
.decorationColor!,
], begin: Alignment.topLeft, end: Alignment.bottomRight),
),
child: Form(
key: _formKey,
child: Column(
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(24, 90, 24, 32),
child: Column(
children: <Widget>[
BaseTextFormField(
controller: _nameController,
hintText: S.of(context).send_name,
borderColor: Theme.of(context)
.primaryTextTheme!
.headlineSmall!
.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
placeholderTextStyle: TextStyle(
color: Theme.of(context)
.primaryTextTheme!
.headlineSmall!
.decorationColor!,
fontWeight: FontWeight.w500,
fontSize: 14),
validator: sendTemplateViewModel.templateValidator,
),
Padding(
padding: EdgeInsets.only(top: 20),
child: AddressTextField(
controller: _addressController,
onURIScanned: (uri) {
final paymentRequest = PaymentRequest.fromUri(uri);
_addressController.text = paymentRequest.address;
_cryptoAmountController.text = paymentRequest.amount;
},
options: [
AddressTextFieldOption.paste,
AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook
],
buttonColor: Theme.of(context)
.primaryTextTheme!
.headlineMedium!
.color!,
borderColor: Theme.of(context)
.primaryTextTheme!
.headlineSmall!
.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
hintStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.primaryTextTheme!
.headlineSmall!
.decorationColor!),
),
),
Padding(
padding: const EdgeInsets.only(top: 20),
child: Focus(
onFocusChange: (hasFocus) {
if (hasFocus) {
sendTemplateViewModel.selectCurrency();
}
},
child: BaseTextFormField(
focusNode: _cryptoAmountFocus,
controller: _cryptoAmountController,
keyboardType: TextInputType.numberWithOptions(
signed: false, decimal: true),
inputFormatters: [
FilteringTextInputFormatter.deny(
RegExp('[\\-|\\ ]'))
],
prefixIcon: Observer(
builder: (_) => PrefixCurrencyIcon(
title: sendTemplateViewModel
.currency.title,
isSelected:
sendTemplateViewModel
.isCurrencySelected,
)),
hintText: '0.0000',
borderColor: Theme.of(context)
.primaryTextTheme!
.headlineSmall!
.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
placeholderTextStyle: TextStyle(
color: Theme.of(context)
.primaryTextTheme!
.headlineSmall!
.decorationColor!,
fontWeight: FontWeight.w500,
fontSize: 14),
validator:
sendTemplateViewModel.amountValidator))),
Padding(
padding: const EdgeInsets.only(top: 20),
child: Focus(
onFocusChange: (hasFocus) {
if (hasFocus) {
sendTemplateViewModel.selectFiat();
}
},
child: BaseTextFormField(
focusNode: _fiatAmountFocus,
controller: _fiatAmountController,
keyboardType: TextInputType.numberWithOptions(
signed: false, decimal: true),
inputFormatters: [
FilteringTextInputFormatter.deny(
RegExp('[\\-|\\ ]'))
],
prefixIcon: Observer(
builder: (_) => PrefixCurrencyIcon(
title: sendTemplateViewModel
.fiat.title,
isSelected: sendTemplateViewModel
.isFiatSelected,
)),
hintText: '0.00',
borderColor: Theme.of(context)
.primaryTextTheme!
.headlineSmall!
.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
placeholderTextStyle: TextStyle(
color: Theme.of(context)
.primaryTextTheme!
.headlineSmall!
.decorationColor!,
fontWeight: FontWeight.w500,
fontSize: 14),
))),
],
content: FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Column(children: [
Container(
height: 460,
child: Observer(builder: (_) {
return PageView.builder(
scrollDirection: Axis.horizontal,
controller: controller,
itemCount: sendTemplateViewModel.recipients.length,
itemBuilder: (_, index) {
final template =
sendTemplateViewModel.recipients[index];
return SendTemplateCard(
template: template,
index: index,
sendTemplateViewModel: sendTemplateViewModel);
});
})),
Padding(
padding: EdgeInsets.only(
top: 10, left: 24, right: 24, bottom: 10),
child: Container(
height: 10,
child: Observer(
builder: (_) {
final count = sendTemplateViewModel.recipients.length;
return count > 1
? SmoothPageIndicator(
controller: controller,
count: count,
effect: ScrollingDotsEffect(
spacing: 6.0,
radius: 6.0,
dotWidth: 6.0,
dotHeight: 6.0,
dotColor: Theme.of(context)
.primaryTextTheme
.displaySmall!
.backgroundColor!,
activeDotColor: Theme.of(context)
.primaryTextTheme
.displayMedium!
.backgroundColor!))
: Offstage();
},
),
)
],
),
),
),
),
),
])),
bottomSectionPadding:
EdgeInsets.only(left: 24, right: 24, bottom: 24),
bottomSection: PrimaryButton(
onPressed: () {
if (_formKey.currentState != null && _formKey.currentState!.validate()) {
sendTemplateViewModel.addTemplate(
isCurrencySelected: sendTemplateViewModel.isCurrencySelected,
name: _nameController.text,
address: _addressController.text,
cryptoCurrency:sendTemplateViewModel.currency.title,
fiatCurrency: sendTemplateViewModel.fiat.title,
amount: _cryptoAmountController.text,
amountFiat: _fiatAmountController.text);
Navigator.of(context).pop();
}
},
text: S.of(context).save,
color: Colors.green,
textColor: Colors.white,
),
),
));
bottomSection: Column(children: [
// if (sendViewModel.hasMultiRecipient)
Padding(
padding: EdgeInsets.only(bottom: 12),
child: PrimaryButton(
onPressed: () {
sendTemplateViewModel.addRecipient();
Future.delayed(const Duration(milliseconds: 250), () {
controller.jumpToPage(
sendTemplateViewModel.recipients.length - 1);
});
},
text: S.of(context).add_receiver,
color: Colors.transparent,
textColor: Theme.of(context)
.accentTextTheme
.displaySmall!
.decorationColor!,
isDottedBorder: true,
borderColor: Theme.of(context)
.primaryTextTheme
.displaySmall!
.decorationColor!)),
PrimaryButton(
onPressed: () {
if (_formKey.currentState != null &&
_formKey.currentState!.validate()) {
final mainTemplate = sendTemplateViewModel.recipients[0];
print(sendTemplateViewModel.recipients.map((element) =>
element.toTemplate(
cryptoCurrency:
sendTemplateViewModel.cryptoCurrency.title,
fiatCurrency:
sendTemplateViewModel.fiatCurrency)));
sendTemplateViewModel.addTemplate(
isCurrencySelected: mainTemplate.isCurrencySelected,
name: mainTemplate.name,
address: mainTemplate.address,
amount: mainTemplate.output.cryptoAmount,
amountFiat: mainTemplate.output.fiatAmount,
additionalRecipients: sendTemplateViewModel.recipients
.map((element) => element.toTemplate(
cryptoCurrency: sendTemplateViewModel
.cryptoCurrency.title,
fiatCurrency:
sendTemplateViewModel.fiatCurrency))
.toList());
Navigator.of(context).pop();
}
},
text: S.of(context).save,
color: Colors.green,
textColor: Colors.white)
])));
}
void _setEffects(BuildContext context) {
if (_effectsInstalled) {
return;
TemplateViewModel _defineCurrentRecipient() {
if (controller.page == null) {
throw Exception('Controller page is null');
}
final output = sendTemplateViewModel.output;
reaction((_) => output.fiatAmount, (String amount) {
if (amount != _fiatAmountController.text) {
_fiatAmountController.text = amount;
}
});
reaction((_) => output.cryptoAmount, (String amount) {
if (amount != _cryptoAmountController.text) {
_cryptoAmountController.text = amount;
}
});
reaction((_) => output.address, (String address) {
if (address != _addressController.text) {
_addressController.text = address;
}
});
_cryptoAmountController.addListener(() {
final amount = _cryptoAmountController.text;
if (amount != output.cryptoAmount) {
output.setCryptoAmount(amount);
}
});
_fiatAmountController.addListener(() {
final amount = _fiatAmountController.text;
if (amount != output.fiatAmount) {
output.setFiatAmount(amount);
}
});
_addressController.addListener(() {
final address = _addressController.text;
if (output.address != address) {
output.address = address;
}
});
_effectsInstalled = true;
final itemCount = controller.page!.round();
return sendTemplateViewModel.recipients[itemCount];
}
}

View file

@ -68,6 +68,7 @@ Future<String> extractAddressFromParsed(
}
return address;
case ParseFrom.contact:
case ParseFrom.notParsed:
address = parsedAddress.addresses.first;
return address;
@ -85,4 +86,4 @@ Future<String> extractAddressFromParsed(
});
return address;
}
}

View file

@ -172,8 +172,10 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
hintStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color:
Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!),
color: Theme.of(context)
.primaryTextTheme!
.headlineSmall!
.decorationColor!),
onPushPasteButton: (context) async {
output.resetParsedAddress();
await output.fetchParsedAddress(context);
@ -182,6 +184,9 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
output.resetParsedAddress();
await output.fetchParsedAddress(context);
},
onSelectedContact: (contact) {
output.loadContact(contact);
},
validator: validator,
selectedCurrency: sendViewModel.currency,
);
@ -464,7 +469,6 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
//color: Theme.of(context).primaryTextTheme!.displaySmall!.color!,
color: Colors.white),
),
Container(

View file

@ -0,0 +1,267 @@
import 'package:cake_wallet/src/screens/send/widgets/prefix_currency_icon_widget.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/view_model/send/template_view_model.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
import 'package:cake_wallet/src/widgets/address_text_field.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:mobx/mobx.dart';
class SendTemplateCard extends StatelessWidget {
SendTemplateCard(
{super.key,
required this.template,
required this.index,
required this.sendTemplateViewModel});
final TemplateViewModel template;
final int index;
final SendTemplateViewModel sendTemplateViewModel;
final _addressController = TextEditingController();
final _cryptoAmountController = TextEditingController();
final _fiatAmountController = TextEditingController();
final _nameController = TextEditingController();
final FocusNode _cryptoAmountFocus = FocusNode();
final FocusNode _fiatAmountFocus = FocusNode();
bool _effectsInstalled = false;
@override
Widget build(BuildContext context) {
_setEffects(context);
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24),
bottomRight: Radius.circular(24)),
gradient: LinearGradient(colors: [
Theme.of(context).primaryTextTheme.titleMedium!.color!,
Theme.of(context).primaryTextTheme.titleMedium!.decorationColor!
], begin: Alignment.topLeft, end: Alignment.bottomRight)),
child: Column(children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(24, 90, 24, 32),
child: Column(children: <Widget>[
if (index == 0)
BaseTextFormField(
controller: _nameController,
hintText: sendTemplateViewModel.recipients.length > 1
? S.of(context).template_name
: S.of(context).send_name,
borderColor: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
placeholderTextStyle: TextStyle(
color: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.decorationColor!,
fontWeight: FontWeight.w500,
fontSize: 14),
validator: sendTemplateViewModel.templateValidator),
Padding(
padding: EdgeInsets.only(top: 20),
child: AddressTextField(
selectedCurrency: sendTemplateViewModel.cryptoCurrency,
controller: _addressController,
onURIScanned: (uri) {
final paymentRequest = PaymentRequest.fromUri(uri);
_addressController.text = paymentRequest.address;
_cryptoAmountController.text = paymentRequest.amount;
},
options: [
AddressTextFieldOption.paste,
AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook
],
onPushPasteButton: (context) async {
template.output.resetParsedAddress();
await template.output.fetchParsedAddress(context);
},
onPushAddressBookButton: (context) async {
template.output.resetParsedAddress();
await template.output.fetchParsedAddress(context);
},
buttonColor: Theme.of(context)
.primaryTextTheme
.headlineMedium!
.color!,
borderColor: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
hintStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.decorationColor!),
validator: sendTemplateViewModel.addressValidator)),
Padding(
padding: const EdgeInsets.only(top: 20),
child: Focus(
onFocusChange: (hasFocus) {
if (hasFocus) {
template.selectCurrency();
}
},
child: BaseTextFormField(
focusNode: _cryptoAmountFocus,
controller: _cryptoAmountController,
keyboardType: TextInputType.numberWithOptions(
signed: false, decimal: true),
inputFormatters: [
FilteringTextInputFormatter.deny(
RegExp('[\\-|\\ ]'))
],
prefixIcon: Observer(
builder: (_) => PrefixCurrencyIcon(
title: sendTemplateViewModel
.cryptoCurrency.title,
isSelected: template.isCurrencySelected)),
hintText: '0.0000',
borderColor: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
placeholderTextStyle: TextStyle(
color: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.decorationColor!,
fontWeight: FontWeight.w500,
fontSize: 14),
validator: sendTemplateViewModel.amountValidator))),
Padding(
padding: const EdgeInsets.only(top: 20),
child: Focus(
onFocusChange: (hasFocus) {
if (hasFocus) {
template.selectFiat();
}
},
child: BaseTextFormField(
focusNode: _fiatAmountFocus,
controller: _fiatAmountController,
keyboardType: TextInputType.numberWithOptions(
signed: false, decimal: true),
inputFormatters: [
FilteringTextInputFormatter.deny(
RegExp('[\\-|\\ ]'))
],
prefixIcon: Observer(
builder: (_) => PrefixCurrencyIcon(
title: sendTemplateViewModel.fiatCurrency,
isSelected: template.isFiatSelected)),
hintText: '0.00',
borderColor: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.color!,
textStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white),
placeholderTextStyle: TextStyle(
color: Theme.of(context)
.primaryTextTheme
.headlineSmall!
.decorationColor!,
fontWeight: FontWeight.w500,
fontSize: 14))))
]))
]));
}
void _setEffects(BuildContext context) {
if (_effectsInstalled) {
return;
}
final output = template.output;
if (template.address.isNotEmpty) {
_addressController.text = template.address;
}
if (template.name.isNotEmpty) {
_nameController.text = template.name;
}
if (template.output.cryptoAmount.isNotEmpty) {
_cryptoAmountController.text = template.output.cryptoAmount;
}
if (template.output.fiatAmount.isNotEmpty) {
_fiatAmountController.text = template.output.fiatAmount;
}
_addressController.addListener(() {
final address = _addressController.text;
if (template.address != address) {
template.address = address;
}
});
_cryptoAmountController.addListener(() {
final amount = _cryptoAmountController.text;
if (amount != output.cryptoAmount) {
output.setCryptoAmount(amount);
}
});
_fiatAmountController.addListener(() {
final amount = _fiatAmountController.text;
if (amount != output.fiatAmount) {
output.setFiatAmount(amount);
}
});
_nameController.addListener(() {
final name = _nameController.text;
if (name != template.name) {
template.name = name;
}
});
reaction((_) => template.address, (String address) {
if (address != _addressController.text) {
_addressController.text = address;
}
});
reaction((_) => output.cryptoAmount, (String amount) {
if (amount != _cryptoAmountController.text) {
_cryptoAmountController.text = amount;
}
});
reaction((_) => output.fiatAmount, (String amount) {
if (amount != _fiatAmountController.text) {
_fiatAmountController.text = amount;
}
});
reaction((_) => template.name, (String name) {
if (name != _nameController.text) {
_nameController.text = name;
}
});
_effectsInstalled = true;
}
}

View file

@ -1,16 +1,18 @@
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
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/receive/widgets/qr_image.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/utils/clipboard_util.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:qr_flutter/qr_flutter.dart' as qr;
import '../../../palette.dart';
import '../../widgets/primary_button.dart';
import '../../widgets/standard_list.dart';
class Setup2FAQRPage extends BasePage {
Setup2FAQRPage({required this.setup2FAViewModel});
@ -106,7 +108,8 @@ class Setup2FAQRPage extends BasePage {
height: 32,
child: InkWell(
onTap: () {
Clipboard.setData(ClipboardData(text: '${setup2FAViewModel.secretKey}'));
ClipboardUtil.setSensitiveDataToClipboard(
ClipboardData(text: '${setup2FAViewModel.secretKey}'));
showBar<void>(context, S.of(context).copied_to_clipboard);
},
child: Container(

View file

@ -1,16 +1,17 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:cake_wallet/entities/qr_view_data.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/list_row.dart';
import 'package:cake_wallet/src/widgets/section_divider.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/utils/clipboard_util.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/view_model/wallet_keys_view_model.dart';
import 'package:device_display_brightness/device_display_brightness.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/list_row.dart';
import 'package:cake_wallet/view_model/wallet_keys_view_model.dart';
import 'package:cake_wallet/routes.dart';
import 'package:qr_flutter/qr_flutter.dart';
class WalletKeysPage extends BasePage {
@ -56,7 +57,7 @@ class WalletKeysPage extends BasePage {
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12.0),
color: Theme.of(context).accentTextTheme!.bodySmall!.color!,
color: Theme.of(context).accentTextTheme.bodySmall!.color!,
),
child: Center(
child: Padding(
@ -84,7 +85,7 @@ class WalletKeysPage extends BasePage {
separatorBuilder: (context, index) => Container(
height: 1,
padding: EdgeInsets.only(left: 24),
color: Theme.of(context).accentTextTheme!.titleLarge!.backgroundColor!,
color: Theme.of(context).accentTextTheme.titleLarge!.backgroundColor!,
child: const SectionDivider(),
),
itemCount: walletKeysViewModel.items.length,
@ -93,7 +94,7 @@ class WalletKeysPage extends BasePage {
return GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: item.value));
ClipboardUtil.setSensitiveDataToClipboard(ClipboardData(text: item.value));
showBar<void>(context, S.of(context).copied_key_to_clipboard(item.title));
},
child: ListRow(

View file

@ -1,4 +1,3 @@
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:flutter/services.dart';
@ -16,10 +15,7 @@ class AddressTextField extends StatelessWidget {
{required this.controller,
this.isActive = true,
this.placeholder,
this.options = const [
AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook
],
this.options = const [AddressTextFieldOption.qrCode, AddressTextFieldOption.addressBook],
this.onURIScanned,
this.focusNode,
this.isBorderExist = true,
@ -31,6 +27,7 @@ class AddressTextField extends StatelessWidget {
this.validator,
this.onPushPasteButton,
this.onPushAddressBookButton,
this.onSelectedContact,
this.selectedCurrency});
static const prefixIconWidth = 34.0;
@ -52,6 +49,7 @@ class AddressTextField extends StatelessWidget {
final FocusNode? focusNode;
final Function(BuildContext context)? onPushPasteButton;
final Function(BuildContext context)? onPushAddressBookButton;
final Function(ContactBase contact)? onSelectedContact;
final CryptoCurrency? selectedCurrency;
@override
@ -66,34 +64,27 @@ class AddressTextField extends StatelessWidget {
controller: controller,
focusNode: focusNode,
style: textStyle ??
TextStyle(
fontSize: 16,
color: Theme.of(context).primaryTextTheme!.titleLarge!.color!),
TextStyle(fontSize: 16, color: Theme.of(context).primaryTextTheme.titleLarge!.color!),
decoration: InputDecoration(
suffixIcon: SizedBox(
width: prefixIconWidth * options.length +
(spaceBetweenPrefixIcons * options.length),
width: prefixIconWidth * options.length + (spaceBetweenPrefixIcons * options.length),
),
hintStyle: hintStyle ??
TextStyle(fontSize: 16, color: Theme.of(context).hintColor),
hintStyle: hintStyle ?? TextStyle(fontSize: 16, color: Theme.of(context).hintColor),
hintText: placeholder ?? S.current.widgets_address,
focusedBorder: isBorderExist
? UnderlineInputBorder(
borderSide: BorderSide(
color: borderColor ?? Theme.of(context).dividerColor,
width: 1.0))
color: borderColor ?? Theme.of(context).dividerColor, width: 1.0))
: InputBorder.none,
disabledBorder: isBorderExist
? UnderlineInputBorder(
borderSide: BorderSide(
color: borderColor ?? Theme.of(context).dividerColor,
width: 1.0))
color: borderColor ?? Theme.of(context).dividerColor, width: 1.0))
: InputBorder.none,
enabledBorder: isBorderExist
? UnderlineInputBorder(
borderSide: BorderSide(
color: borderColor ?? Theme.of(context).dividerColor,
width: 1.0))
color: borderColor ?? Theme.of(context).dividerColor, width: 1.0))
: InputBorder.none,
),
validator: validator,
@ -102,11 +93,11 @@ class AddressTextField extends StatelessWidget {
top: 2,
right: 0,
child: SizedBox(
width: prefixIconWidth * options.length +
(spaceBetweenPrefixIcons * options.length),
width: prefixIconWidth * options.length + (spaceBetweenPrefixIcons * options.length),
child: Row(
mainAxisAlignment: ResponsiveLayoutUtil.instance.isMobile
? MainAxisAlignment.spaceBetween : MainAxisAlignment.end,
mainAxisAlignment: ResponsiveLayoutUtil.instance.isMobile
? MainAxisAlignment.spaceBetween
: MainAxisAlignment.end,
children: [
SizedBox(width: 5),
if (this.options.contains(AddressTextFieldOption.paste)) ...[
@ -122,20 +113,14 @@ class AddressTextField extends StatelessWidget {
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: buttonColor ??
Theme.of(context)
.accentTextTheme
!
.titleLarge!
.color!,
borderRadius:
BorderRadius.all(Radius.circular(6))),
Theme.of(context).accentTextTheme.titleLarge!.color!,
borderRadius: BorderRadius.all(Radius.circular(6))),
child: Image.asset(
'assets/images/paste_ios.png',
color: iconColor ??
Theme.of(context)
.primaryTextTheme
!
.headlineMedium!
.headlineMedium!
.decorationColor!,
)),
),
@ -155,28 +140,21 @@ class AddressTextField extends StatelessWidget {
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: buttonColor ??
Theme.of(context)
.accentTextTheme
.titleLarge!
.color!,
borderRadius:
BorderRadius.all(Radius.circular(6))),
Theme.of(context).accentTextTheme.titleLarge!.color!,
borderRadius: BorderRadius.all(Radius.circular(6))),
child: Image.asset(
'assets/images/qr_code_icon.png',
color: iconColor ??
Theme.of(context)
.primaryTextTheme
!.headlineMedium!
.headlineMedium!
.decorationColor!,
)),
),
))
] else
SizedBox(width: 5),
if (this
.options
.contains(AddressTextFieldOption.addressBook)) ...[
if (this.options.contains(AddressTextFieldOption.addressBook)) ...[
Container(
width: prefixIconWidth,
height: prefixIconHeight,
@ -184,26 +162,19 @@ class AddressTextField extends StatelessWidget {
child: Semantics(
label: S.of(context).address_book,
child: InkWell(
onTap: () async =>
_presetAddressBookPicker(context),
onTap: () async => _presetAddressBookPicker(context),
child: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: buttonColor ??
Theme.of(context)
.accentTextTheme
!
.titleLarge!
.color!,
borderRadius:
BorderRadius.all(Radius.circular(6))),
Theme.of(context).accentTextTheme.titleLarge!.color!,
borderRadius: BorderRadius.all(Radius.circular(6))),
child: Image.asset(
'assets/images/open_book.png',
color: iconColor ??
Theme.of(context)
.primaryTextTheme
!
.headlineMedium!
.headlineMedium!
.decorationColor!,
)),
),
@ -221,30 +192,31 @@ class AddressTextField extends StatelessWidget {
if (code.isEmpty) {
return;
}
try {
final uri = Uri.parse(code);
controller?.text = uri.path;
onURIScanned?.call(uri);
} catch(_){
} catch (_) {
controller?.text = code;
}
}
Future<void> _presetAddressBookPicker(BuildContext context) async {
final contact = await Navigator.of(context)
.pushNamed(Routes.pickerAddressBook,arguments: selectedCurrency);
.pushNamed(Routes.pickerAddressBook, arguments: selectedCurrency);
if (contact is ContactBase && contact.address != null) {
if (contact is ContactBase) {
controller?.text = contact.address;
onPushAddressBookButton?.call(context);
onSelectedContact?.call(contact);
}
}
Future<void> _pasteAddress(BuildContext context) async {
final clipboard = await Clipboard.getData('text/plain');
final address = clipboard?.text ?? '';
if (address.isNotEmpty) {
controller?.text = address;
}

View file

@ -66,6 +66,7 @@ class NavBar extends StatelessWidget implements ObstructingPreferredSizeWidget {
leading: leading,
automaticallyImplyLeading: false,
automaticallyImplyMiddle: false,
transitionBetweenRoutes: false,
middle: middle,
trailing: trailing,
backgroundColor: backgroundColor,

View file

@ -8,7 +8,8 @@ class TemplateTile extends StatefulWidget {
required this.amount,
required this.from,
required this.onTap,
required this.onRemove
required this.onRemove,
this.hasMultipleRecipients,
}) : super(key: key);
final String to;
@ -16,6 +17,7 @@ class TemplateTile extends StatefulWidget {
final String from;
final VoidCallback onTap;
final VoidCallback onRemove;
final bool? hasMultipleRecipients;
@override
TemplateTileState createState() => TemplateTileState(
@ -51,45 +53,47 @@ class TemplateTileState extends State<TemplateTile> {
final toIcon = Image.asset('assets/images/to_icon.png', color: color);
final content = Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
amount,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: color
),
),
Padding(
padding: EdgeInsets.only(left: 5),
child: Text(
from,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: color
),
),
),
Padding(
padding: EdgeInsets.only(left: 5),
child: toIcon,
),
Padding(
padding: EdgeInsets.only(left: 5),
child: Text(
to,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: color
),
),
),
],
);
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: widget.hasMultipleRecipients ?? false
? [
Text(
to,
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w600, color: color),
),
]
: [
Text(
amount,
style: TextStyle(
fontSize: 16, fontWeight: FontWeight.w600, color: color),
),
Padding(
padding: EdgeInsets.only(left: 5),
child: Text(
from,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: color),
),
),
Padding(
padding: EdgeInsets.only(left: 5),
child: toIcon,
),
Padding(
padding: EdgeInsets.only(left: 5),
child: Text(
to,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: color),
),
),
]);
final tile = Container(
padding: EdgeInsets.only(right: 10),

View file

@ -23,25 +23,27 @@ abstract class SendTemplateBase with Store {
templates.replaceRange(0, templates.length, templateSource.values.toList());
@action
Future<void> addTemplate({
required String name,
required bool isCurrencySelected,
required String address,
required String cryptoCurrency,
required String fiatCurrency,
required String amount,
required String amountFiat}) async {
Future<void> addTemplate(
{required String name,
required bool isCurrencySelected,
required String address,
required String cryptoCurrency,
required String fiatCurrency,
required String amount,
required String amountFiat,
required List<Template> additionalRecipients}) async {
final template = Template(
nameRaw: name,
isCurrencySelectedRaw: isCurrencySelected,
addressRaw: address,
cryptoCurrencyRaw: cryptoCurrency,
fiatCurrencyRaw: fiatCurrency,
amountRaw: amount,
amountFiatRaw: amountFiat);
nameRaw: name,
isCurrencySelectedRaw: isCurrencySelected,
addressRaw: address,
cryptoCurrencyRaw: cryptoCurrency,
fiatCurrencyRaw: fiatCurrency,
amountRaw: amount,
amountFiatRaw: amountFiat,
additionalRecipientsRaw: additionalRecipients);
await templateSource.add(template);
}
@action
Future<void> remove({required Template template}) async => await template.delete();
}
}

View file

@ -0,0 +1,15 @@
import 'package:flutter/services.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:sensitive_clipboard/sensitive_clipboard.dart';
class ClipboardUtil {
static Future<void> setSensitiveDataToClipboard(ClipboardData data) async {
if (DeviceInfo.instance.isMobile) {
await SensitiveClipboard.copy(data.text);
return;
}
return Clipboard.setData(data);
}
}

View file

@ -67,7 +67,8 @@ abstract class BalanceViewModelBase with Store {
final price = fiatConvertationStore.prices[appStore.wallet!.currency];
if (price == null) {
throw Exception('No price for ${appStore.wallet!.currency} (current wallet)');
// price should update on next fetch:
return 0;
}
return price;

View file

@ -19,7 +19,9 @@ abstract class NodeCreateOrEditViewModelBase with Store {
port = '',
login = '',
password = '',
trusted = false;
trusted = false,
useSocksProxy = false,
socksProxyAddress = '';
@observable
ExecutionState state;
@ -45,6 +47,12 @@ abstract class NodeCreateOrEditViewModelBase with Store {
@observable
bool trusted;
@observable
bool useSocksProxy;
@observable
String socksProxyAddress;
@computed
bool get isReady => address.isNotEmpty && port.isNotEmpty;
@ -73,6 +81,8 @@ abstract class NodeCreateOrEditViewModelBase with Store {
password = '';
useSSL = false;
trusted = false;
useSocksProxy = false;
socksProxyAddress = '';
}
@action
@ -93,6 +103,12 @@ abstract class NodeCreateOrEditViewModelBase with Store {
@action
void setTrusted(bool val) => trusted = val;
@action
void setSocksProxy(bool val) => useSocksProxy = val;
@action
void setSocksProxyAddress(String val) => socksProxyAddress = val;
@action
Future<void> save({Node? editingNode, bool saveAsCurrent = false}) async {
final node = Node(
@ -101,7 +117,8 @@ abstract class NodeCreateOrEditViewModelBase with Store {
login: login,
password: password,
useSSL: useSSL,
trusted: trusted);
trusted: trusted,
socksProxyAddress: socksProxyAddress);
try {
state = IsExecutingState();
if (editingNode != null) {
@ -130,7 +147,8 @@ abstract class NodeCreateOrEditViewModelBase with Store {
login: login,
password: password,
useSSL: useSSL,
trusted: trusted);
trusted: trusted,
socksProxyAddress: socksProxyAddress);
try {
connectionState = IsExecutingState();
final isAlive = await node.requestNode();

View file

@ -18,6 +18,8 @@ import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/entities/contact_base.dart';
part 'output.g.dart';
const String cryptoNumberPattern = '0.0';
@ -244,4 +246,11 @@ abstract class OutputBase with Store {
extractedAddress = await extractAddressFromParsed(context, parsedAddress);
note = parsedAddress.description;
}
void loadContact(ContactBase contact) {
address = contact.name;
parsedAddress = ParsedAddress.fetchContactAddress(address: contact.address, name: contact.name);
extractedAddress = parsedAddress.addresses.first;
note = parsedAddress.description;
}
}

View file

@ -1,4 +1,5 @@
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cake_wallet/view_model/send/template_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/entities/template.dart';
@ -6,10 +7,7 @@ import 'package:cake_wallet/store/templates/send_template_store.dart';
import 'package:cake_wallet/core/template_validator.dart';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/core/amount_validator.dart';
import 'package:cake_wallet/core/validator.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
@ -19,72 +17,74 @@ class SendTemplateViewModel = SendTemplateViewModelBase
with _$SendTemplateViewModel;
abstract class SendTemplateViewModelBase with Store {
SendTemplateViewModelBase(this._wallet, this._settingsStore,
this._sendTemplateStore, this._fiatConversationStore)
: output = Output(_wallet, _settingsStore, _fiatConversationStore, () => _wallet.currency) {
output = Output(_wallet, _settingsStore, _fiatConversationStore, () => currency);
}
Output output;
Validator get amountValidator =>
AmountValidator(currency: walletTypeToCryptoCurrency(_wallet.type));
Validator get addressValidator => AddressValidator(type: _wallet.currency);
Validator get templateValidator => TemplateValidator();
CryptoCurrency get currency => _wallet.currency;
FiatCurrency get fiat => _settingsStore.fiatCurrency;
@observable
bool isCurrencySelected = true;
@observable
bool isFiatSelected = false;
@action
void selectCurrency () {
isCurrencySelected = true;
isFiatSelected = false;
}
@action
void selectFiat () {
isFiatSelected = true;
isCurrencySelected = false;
}
@computed
ObservableList<Template> get templates => _sendTemplateStore.templates;
final WalletBase _wallet;
final SettingsStore _settingsStore;
final SendTemplateStore _sendTemplateStore;
final FiatConversionStore _fiatConversationStore;
SendTemplateViewModelBase(this._wallet, this._settingsStore,
this._sendTemplateStore, this._fiatConversationStore)
: recipients = ObservableList<TemplateViewModel>() {
addRecipient();
}
ObservableList<TemplateViewModel> recipients;
@action
void addRecipient() {
recipients.add(TemplateViewModel(
cryptoCurrency: cryptoCurrency,
wallet: _wallet,
settingsStore: _settingsStore,
fiatConversationStore: _fiatConversationStore));
}
@action
void removeRecipient(TemplateViewModel recipient) {
recipients.remove(recipient);
}
AmountValidator get amountValidator =>
AmountValidator(currency: walletTypeToCryptoCurrency(_wallet.type));
AddressValidator get addressValidator =>
AddressValidator(type: _wallet.currency);
TemplateValidator get templateValidator => TemplateValidator();
@computed
CryptoCurrency get cryptoCurrency => _wallet.currency;
@computed
String get fiatCurrency => _settingsStore.fiatCurrency.title;
@computed
ObservableList<Template> get templates => _sendTemplateStore.templates;
@action
void updateTemplate() => _sendTemplateStore.update();
@action
void addTemplate(
{required String name,
required bool isCurrencySelected,
required String address,
required String cryptoCurrency,
required String fiatCurrency,
required String amount,
required String amountFiat}) {
required bool isCurrencySelected,
required String address,
required String amount,
required String amountFiat,
required List<Template> additionalRecipients}) {
_sendTemplateStore.addTemplate(
name: name,
isCurrencySelected: isCurrencySelected,
address: address,
cryptoCurrency: cryptoCurrency,
cryptoCurrency: cryptoCurrency.title,
fiatCurrency: fiatCurrency,
amount: amount,
amountFiat: amountFiat);
amountFiat: amountFiat,
additionalRecipients: additionalRecipients);
updateTemplate();
}
@action
void removeTemplate({required Template template}) {
_sendTemplateStore.remove(template: template);
updateTemplate();

View file

@ -0,0 +1,80 @@
import 'package:cake_wallet/entities/template.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
part 'template_view_model.g.dart';
class TemplateViewModel = TemplateViewModelBase with _$TemplateViewModel;
abstract class TemplateViewModelBase with Store {
final CryptoCurrency cryptoCurrency;
final WalletBase _wallet;
final SettingsStore _settingsStore;
final FiatConversionStore _fiatConversationStore;
TemplateViewModelBase(
{required this.cryptoCurrency,
required WalletBase wallet,
required SettingsStore settingsStore,
required FiatConversionStore fiatConversationStore})
: _wallet = wallet,
_settingsStore = settingsStore,
_fiatConversationStore = fiatConversationStore,
output = Output(wallet, settingsStore, fiatConversationStore,
() => wallet.currency) {
output = Output(
_wallet, _settingsStore, _fiatConversationStore, () => cryptoCurrency);
}
@observable
Output output;
@observable
String name = '';
@observable
String address = '';
@observable
bool isCurrencySelected = true;
@observable
bool isFiatSelected = false;
@action
void selectCurrency() {
isCurrencySelected = true;
isFiatSelected = false;
}
@action
void selectFiat() {
isFiatSelected = true;
isCurrencySelected = false;
}
@action
void reset() {
name = '';
address = '';
isCurrencySelected = true;
isFiatSelected = false;
output.reset();
}
Template toTemplate(
{required String cryptoCurrency, required String fiatCurrency}) {
return Template(
isCurrencySelectedRaw: isCurrencySelected,
nameRaw: name,
addressRaw: address,
cryptoCurrencyRaw: cryptoCurrency,
fiatCurrencyRaw: fiatCurrency,
amountRaw: output.cryptoAmount,
amountFiatRaw: output.fiatAmount);
}
}

View file

@ -3,7 +3,6 @@ dependencies:
sdk: flutter
flutter_localizations:
sdk: flutter
flutter_cupertino_localizations: ^1.0.1
intl: ^0.17.0
url_launcher: ^6.1.4
qr_flutter:
@ -82,6 +81,7 @@ dependencies:
path_provider_android: 2.0.24
shared_preferences_android: 2.0.17
url_launcher_android: 6.0.24
sensitive_clipboard: ^1.0.0
bitcoin_flutter:
git:
url: https://github.com/cake-tech/bitcoin_flutter.git
@ -130,4 +130,4 @@ flutter:
- asset: assets/fonts/Lato-Regular.ttf
- asset: assets/fonts/Lato-Medium.ttf
- asset: assets/fonts/Lato-Semibold.ttf
- asset: assets/fonts/Lato-Bold.ttf
- asset: assets/fonts/Lato-Bold.ttf

View file

@ -250,8 +250,8 @@
"transaction_details_recipient_address": "عناوين المستلم",
"wallet_list_title": "محفظة Monero",
"wallet_list_create_new_wallet": "إنشاء محفظة جديدة",
"wallet_list_edit_wallet": "تحرير المحفظة",
"wallet_list_wallet_name": "اسم المحفظة",
"wallet_list_edit_wallet" : "تحرير المحفظة",
"wallet_list_wallet_name" : "اسم المحفظة",
"wallet_list_restore_wallet": "استعادة المحفظة",
"wallet_list_load_wallet": "تحميل المحفظة",
"wallet_list_loading_wallet": "جار تحميل محفظة ${wallet_name}",
@ -269,6 +269,7 @@
"error_text_address": "يجب أن يتوافق عنوان المحفظة مع نوع\nالعملة المشفرة",
"error_text_node_address": "الرجاء إدخال عنوان IPv4",
"error_text_node_port": "منفذ العقدة يمكن أن يحتوي فقط على أرقام بين 0 و 65535",
"error_text_node_proxy_address": "الرجاء إدخال <عنوان IPv4>: <port> ، على سبيل المثال 127.0.0.1:9050",
"error_text_payment_id": "يمكن أن يحتوي معرّف الدفع فقط من 16 إلى 64 حرفًا hex",
"error_text_xmr": "لا يمكن أن تتجاوز قيمة XMR الرصيد المتاح.\nيجب أن يكون عدد الكسور أقل من أو يساوي 12",
"error_text_fiat": "لا يمكن أن تتجاوز قيمة المبلغ الرصيد المتاح.\nيجب أن يكون عدد الكسور أقل أو يساوي 2",
@ -652,5 +653,6 @@
"generate_name": "توليد الاسم",
"balance_page": "صفحة التوازن",
"share": "يشارك",
"slidable": "قابل للانزلاق"
"slidable": "قابل للانزلاق",
"template_name": "اسم القالب"
}

View file

@ -269,6 +269,7 @@
"error_text_address": "Адресът на портфейла трябва да отговаря \n на вида криптовалута",
"error_text_node_address": "Моля, въведете iPv4 адрес",
"error_text_node_port": "Node port-ът е цяло число между 0 и 65535",
"error_text_node_proxy_address": "Моля, въведете <IPv4 адрес>:<порт>, например 127.0.0.1:9050",
"error_text_payment_id": "Payment ID-то може да съдържа само между 16 и 64 шестнайсетични символа",
"error_text_xmr": "XMR сумата не може да надхвърля наличния баланс.\nБроят на цифрите след десетичната запетая може да бъде най-много 12",
"error_text_fiat": "Сумата не може да надвишава наличния баланс.\nThe number of fraction digits must be less or equal to 2",
@ -648,5 +649,6 @@
"generate_name": "Генериране на име",
"balance_page": "Страница за баланс",
"share": "Дял",
"slidable": "Плъзгащ се"
"slidable": "Плъзгащ се",
"template_name": "Име на шаблон"
}

View file

@ -269,6 +269,7 @@
"error_text_address": "Adresa peněženky musí odpovídat typu\nkryptoměny",
"error_text_node_address": "prosím zadejte IPv4 adresu",
"error_text_node_port": "Port uzlu musí být číslo mezi 0 a 65535",
"error_text_node_proxy_address": "Zadejte prosím <adresa IPv4>:<port>, například 127.0.0.1:9050",
"error_text_payment_id": "ID platby se musí skládat z 16 až 64 hexadecimálních znaků",
"error_text_xmr": "Hodnota XMR nemůže překročit dostupný zůstatek.\nPočet desetinných míst musí být menší, nebo roven 12",
"error_text_fiat": "Částka nemůže překročit dostupný zůstatek.\nPočet desetinných míst musí být menší, nebo roven 2",
@ -648,5 +649,6 @@
"generate_name": "Generovat jméno",
"balance_page": "Stránka zůstatku",
"share": "Podíl",
"slidable": "Posuvné"
"slidable": "Posuvné",
"template_name": "Název šablony"
}

View file

@ -269,6 +269,7 @@
"error_text_address": "Die Walletadresse muss dem Typ der Kryptowährung\nentsprechen",
"error_text_node_address": "Bitte geben Sie eine iPv4-Adresse ein",
"error_text_node_port": "Der Knotenport darf nur Nummern zwischen 0 und 65535 enthalten",
"error_text_node_proxy_address": "Bitte geben Sie <IPv4-Adresse>:<Port> ein, zum Beispiel 127.0.0.1:9050",
"error_text_payment_id": "Die Zahlungs-ID darf nur 16 bis 64 hexadezimale Zeichen enthalten",
"error_text_xmr": "Der XMR-Wert darf das verfügbare Guthaben nicht überschreiten.\nDie Anzahl der Nachkommastellen muss kleiner oder gleich 12 sein",
"error_text_fiat": "Der Wert des Betrags darf den verfügbaren Kontostand nicht überschreiten.\nDie Anzahl der Nachkommastellen muss kleiner oder gleich 2 sein",
@ -654,5 +655,6 @@
"generate_name": "Namen generieren",
"balance_page": "Balance-Seite",
"share": "Aktie",
"slidable": "Verschiebbar"
"slidable": "Verschiebbar",
"template_name": "Vorlagenname"
}

View file

@ -269,6 +269,7 @@
"error_text_address": "Wallet address must correspond to the type\nof cryptocurrency",
"error_text_node_address": "Please enter a iPv4 address",
"error_text_node_port": "Node port can only contain numbers between 0 and 65535",
"error_text_node_proxy_address": "Please enter <IPv4 address>:<port>, for example 127.0.0.1:9050",
"error_text_payment_id": "Payment ID can only contain from 16 to 64 chars in hex",
"error_text_xmr": "XMR value can't exceed available balance.\nThe number of fraction digits must be less or equal to 12",
"error_text_fiat": "Value of amount can't exceed available balance.\nThe number of fraction digits must be less or equal to 2",
@ -654,5 +655,6 @@
"generate_name": "Generate Name",
"balance_page": "Balance Page",
"share": "Share",
"slidable": "Slidable"
"slidable": "Slidable",
"template_name": "Template Name"
}

View file

@ -269,6 +269,7 @@
"error_text_address": "La dirección de la billetera debe corresponder al tipo \nde criptomoneda",
"error_text_node_address": "Por favor, introduzca una dirección iPv4",
"error_text_node_port": "El puerto de nodo solo puede contener números entre 0 y 65535",
"error_text_node_proxy_address": "Ingrese <dirección IPv4>:<puerto>, por ejemplo 127.0.0.1:9050",
"error_text_payment_id": "La ID de pago solo puede contener de 16 a 64 caracteres en hexadecimal",
"error_text_xmr": "El valor XMR no puede exceder el saldo disponible.\nTEl número de dígitos de fracción debe ser menor o igual a 12",
"error_text_fiat": "El valor de la cantidad no puede exceder el saldo disponible.\nEl número de dígitos de fracción debe ser menor o igual a 2",
@ -654,5 +655,6 @@
"generate_name": "Generar nombre",
"balance_page": "Página de saldo",
"share": "Compartir",
"slidable": "deslizable"
"slidable": "deslizable",
"template_name": "Nombre de la plantilla"
}

View file

@ -269,6 +269,7 @@
"error_text_address": "L'adresse du portefeuille (wallet) doit correspondre au type de\ncryptomonnaie",
"error_text_node_address": "Merci d'entrer une adresse IPv4",
"error_text_node_port": "Le port d'un nœud doit être un nombre compris entre 0 et 65535",
"error_text_node_proxy_address": "Veuillez saisir <adresse IPv4> :<port>, par exemple 127.0.0.1:9050",
"error_text_payment_id": "Un ID de paiement ne peut être constitué que de 16 à 64 caractères hexadécimaux",
"error_text_xmr": "La valeur de XMR dépasse le solde disponible.\nLa partie décimale doit comporter au plus 12 chiffres",
"error_text_fiat": "La valeur du montant ne peut dépasser le solde disponible.\nLa partie décimale doit comporter au plus 2 chiffres",
@ -654,5 +655,6 @@
"generate_name": "Générer un nom",
"balance_page": "Page Solde",
"share": "Partager",
"slidable": "Glissable"
"slidable": "Glissable",
"template_name": "Nom du modèle"
}

View file

@ -269,6 +269,7 @@
"error_text_address": "Adireshin hujja ya kamata ya dace da irin\nna cryptocurrency",
"error_text_node_address": "Da fatan a shigar da iPv4 adireshin",
"error_text_node_port": "Node tashar jiragen ruwa zai iya ƙunsar lambobi tsakanin 0 zuwa 65535 kawai",
"error_text_node_proxy_address": "Da fatan za a shigar da <IPv4 address>:<port>, misali 127.0.0.1:9050",
"error_text_payment_id": "ID na biyan kudi kawai zai iya ɗaukar daga 16 zuwa 64 haruffa a cikin hex",
"error_text_xmr": "XMR adadin ba zai iya wuce available balance.\nAdadin haruffan gaba zai kamata ya zama ko ƙasa daga na 12",
"error_text_fiat": "Adadin kudin ba zai iya wuce available balance.\nAdadin haruffan gaba zai kamata ya zama ko ƙasa daga na 2",
@ -634,5 +635,6 @@
"generate_name": "Ƙirƙirar Suna",
"balance_page": "Ma'auni Page",
"share": "Raba",
"slidable": "Mai iya zamewa"
"slidable": "Mai iya zamewa",
"template_name": "Sunan Samfura"
}

View file

@ -269,6 +269,7 @@
"error_text_address": "वॉलेट पता प्रकार के अनुरूप होना चाहिए\nक्रिप्टोकरेंसी का",
"error_text_node_address": "कृपया एक IPv4 पता दर्ज करें",
"error_text_node_port": "नोड पोर्ट में केवल 0 और 65535 के बीच संख्याएँ हो सकती हैं",
"error_text_node_proxy_address": "कृपया <IPv4 पता> दर्ज करें: <पोर्ट>, उदाहरण के लिए 127.0.0.1:9050",
"error_text_payment_id": "पेमेंट आईडी केवल हेक्स में 16 से 64 चार्ट तक हो सकती है",
"error_text_xmr": "एक्सएमआर मूल्य उपलब्ध शेष राशि से अधिक नहीं हो सकता.\nअंश अंकों की संख्या 12 से कम या इसके बराबर होनी चाहिए",
"error_text_fiat": "राशि का मूल्य उपलब्ध शेष राशि से अधिक नहीं हो सकता.\nअंश अंकों की संख्या कम या 2 के बराबर होनी चाहिए",
@ -654,5 +655,6 @@
"generate_name": "नाम जनरेट करें",
"balance_page": "बैलेंस पेज",
"share": "शेयर करना",
"slidable": "फिसलने लायक"
"slidable": "फिसलने लायक",
"template_name": "टेम्पलेट नाम"
}

View file

@ -269,6 +269,7 @@
"error_text_address": "Adresa novčanika mora odgovarati\nvrsti kriptovalute",
"error_text_node_address": "Molimo unesite iPv4 adresu",
"error_text_node_port": "Node port smije sadržavati samo brojeve između 0 i 65535",
"error_text_node_proxy_address": "Unesite <IPv4 adresa>:<port>, na primjer 127.0.0.1:9050",
"error_text_payment_id": "ID plaćanja smije sadržavati samo od 16 do 64 znakova hex vrijednosti",
"error_text_xmr": "XMR vrijednost ne smije biti veća od raspoloživog iznosa.\nBroj decimala smije biti 12 ili manji.",
"error_text_fiat": "Vrijednost iznosa ne smije biti veća od raspoloživog iznosa.\nBroj decimala smije biti 2 ili manji.",
@ -654,5 +655,6 @@
"generate_name": "Generiraj ime",
"balance_page": "Stranica sa stanjem",
"share": "Udio",
"slidable": "Klizna"
"slidable": "Klizna",
"template_name": "Naziv predloška"
}

View file

@ -269,6 +269,7 @@
"error_text_address": "Alamat dompet harus sesuai dengan tipe\nmata uang kripto",
"error_text_node_address": "Silakan masukkan alamat iPv4",
"error_text_node_port": "Port node hanya dapat berisi angka antara 0 dan 65535",
"error_text_node_proxy_address": "Masukkan <alamat IPv4>:<port>, misalnya 127.0.0.1:9050",
"error_text_payment_id": "ID pembayaran hanya dapat berisi dari 16 hingga 64 karakter dalam hex",
"error_text_xmr": "Nilai XMR tidak boleh melebihi saldo yang tersedia.\nJumlah digit pecahan harus kurang atau sama dengan 12",
"error_text_fiat": "Nilai jumlah tidak boleh melebihi saldo yang tersedia.\nJumlah digit pecahan harus kurang atau sama dengan 2",
@ -644,5 +645,6 @@
"generate_name": "Hasilkan Nama",
"balance_page": "Halaman Saldo",
"share": "Membagikan",
"slidable": "Dapat digeser"
"slidable": "Dapat digeser",
"template_name": "Nama Templat"
}

View file

@ -269,6 +269,7 @@
"error_text_address": "L'indirizzo del Portafoglio deve corrispondere alla tipologia\ndi criptovaluta",
"error_text_node_address": "Gentilmente inserisci un indirizzo iPv4",
"error_text_node_port": "La porta del nodo può contenere solo numeri compresi tra 0 e 65535",
"error_text_node_proxy_address": "Inserisci <indirizzo IPv4>:<porta>, ad esempio 127.0.0.1:9050",
"error_text_payment_id": "L'ID del pagamento può contenere solo da 16 a 64 caratteri in esadecimale",
"error_text_xmr": "Il valore in XMR non può eccedere il saldo disponibile.\nIl numero delle cifre decimali deve essere inferiore o uguale a 12",
"error_text_fiat": "L'ammontare non può eccedere il saldo dispoinibile.\nIl numero di cifre decimali deve essere inferiore o uguale a 2",
@ -654,5 +655,6 @@
"generate_name": "Genera nome",
"balance_page": "Pagina di equilibrio",
"share": "Condividere",
"slidable": "Scorrevole"
"slidable": "Scorrevole",
"template_name": "Nome modello"
}

View file

@ -269,6 +269,7 @@
"error_text_address": "ウォレットアドレスは、\n暗号通貨",
"error_text_node_address": "iPv4アドレスを入力してください",
"error_text_node_port": "ードポートには、0〜65535の数字のみを含めることができます",
"error_text_node_proxy_address": "<IPv4 アドレス>:<ポート> を入力してください (例: 127.0.0.1:9050)",
"error_text_payment_id": "支払いIDには、16進数で16〜64文字しか含めることができません",
"error_text_xmr": "XMR値は利用可能な残高を超えることはできません.\n小数桁数は12以下でなければなりません",
"error_text_fiat": "金額は利用可能な残高を超えることはできません.\n小数桁の数は2以下でなければなりません",
@ -654,5 +655,6 @@
"generate_name": "名前の生成",
"balance_page": "残高ページ",
"share": "共有",
"slidable": "スライド可能"
"slidable": "スライド可能",
"template_name": "テンプレート名"
}

View file

@ -269,6 +269,7 @@
"error_text_address": "지갑 주소는 유형과 일치해야합니다\n암호 화폐",
"error_text_node_address": "iPv4 주소를 입력하십시오",
"error_text_node_port": "노드 포트는 0에서 65535 사이의 숫자 만 포함 할 수 있습니다",
"error_text_node_proxy_address": "<IPv4 주소>:<포트>를 입력하십시오(예: 127.0.0.1:9050).",
"error_text_payment_id": "지불 ID는 16 ~ 64 자의 16 진 문자 만 포함 할 수 있습니다",
"error_text_xmr": "XMR 값은 사용 가능한 잔액을 초과 할 수 없습니다.\n소수 자릿수는 12 이하 여야합니다",
"error_text_fiat": "금액은 사용 가능한 잔액을 초과 할 수 없습니다.\n소수 자릿수는 2보다 작거나 같아야합니다",
@ -654,5 +655,6 @@
"generate_name": "이름 생성",
"balance_page": "잔액 페이지",
"share": "공유하다",
"slidable": "슬라이딩 가능"
"slidable": "슬라이딩 가능",
"template_name": "템플릿 이름"
}

View file

@ -269,6 +269,7 @@
"error_text_address": "Wallet လိပ်စာသည် အမျိုးအစား\no cryptocurrency နှင့် ကိုက်ညီရပါမည်။",
"error_text_node_address": "ကျေးဇူးပြု၍ iPv4 လိပ်စာကို ထည့်ပါ။",
"error_text_node_port": "နော်ဒီဆိပ်ကမ်း တွင် 0 နှင့် 65535 အကြား နံပါတ်များသာ ပါဝင်နိုင်သည်။",
"error_text_node_proxy_address": "ကျေးဇူးပြု၍ <IPv4 လိပ်စာ>:<port>၊ ဥပမာ 127.0.0.1:9050 ထည့်ပါ",
"error_text_payment_id": "ငွေပေးချေမှု ID တွင် hex တွင် စာလုံး 16 လုံးမှ 64 လုံးသာ ပါဝင်နိုင်သည်။",
"error_text_xmr": "XMR တန်ဖိုးသည် ရနိုင်သောလက်ကျန်ကို ကျော်လွန်၍မရပါ။\nအပိုင်းကိန်းဂဏန်းများ သည် 12 နှင့် လျော့နည်းရမည်",
"error_text_fiat": "ပမာဏ၏တန်ဖိုးသည် ရနိုင်သောလက်ကျန်ကို မကျော်လွန်နိုင်ပါ။\nအပိုင်းကိန်းဂဏန်းအရေအတွက်သည် 2 နှင့် လျော့နည်းရမည်",
@ -654,5 +655,6 @@
"generate_name": "အမည်ဖန်တီးပါ။",
"balance_page": "လက်ကျန်စာမျက်နှာ",
"share": "မျှဝေပါ။",
"slidable": "လျှောချနိုင်သည်။"
"slidable": "လျှောချနိုင်သည်။",
"template_name": "နမူနာပုံစံ"
}

View file

@ -269,6 +269,7 @@
"error_text_address": "Portemonnee-adres moet overeenkomen met het type\nvan cryptocurrency",
"error_text_node_address": "Voer een iPv4-adres in",
"error_text_node_port": "Knooppuntpoort kan alleen nummers tussen 0 en 65535 bevatten",
"error_text_node_proxy_address": "Voer <IPv4-adres>:<poort> in, bijvoorbeeld 127.0.0.1:9050",
"error_text_payment_id": "Betalings-ID kan alleen 16 tot 64 tekens bevatten in hexadecimale volgorde",
"error_text_xmr": "XMR-waarde kan het beschikbare saldo niet overschrijden.\nHet aantal breukcijfers moet kleiner zijn dan of gelijk zijn aan 12",
"error_text_fiat": "Waarde van bedrag kan het beschikbare saldo niet overschrijden.\nHet aantal breukcijfers moet kleiner zijn dan of gelijk zijn aan 2",
@ -654,5 +655,6 @@
"generate_name": "Naam genereren",
"balance_page": "Saldo pagina",
"share": "Deel",
"slidable": "Verschuifbaar"
"slidable": "Verschuifbaar",
"template_name": "Sjabloonnaam"
}

View file

@ -269,6 +269,7 @@
"error_text_address": "Adres musi odpowiadać typowi kryptowaluty",
"error_text_node_address": "Wpisz adres iPv4",
"error_text_node_port": "Port węzła może zawierać tylko liczby od 0 do 65535",
"error_text_node_proxy_address": "Wprowadź <adres IPv4>:<port>, na przykład 127.0.0.1:9050",
"error_text_payment_id": "ID może zawierać od 16 do 64 znaków w formacie szesnastkowym",
"error_text_xmr": "Wartość XMR nie może przekraczać dostępnego salda.\nLiczba cyfr ułamkowych musi być mniejsza lub równa 12",
"error_text_fiat": "Wartość kwoty nie może przekroczyć dostępnego salda.\nLiczba cyfr ułamkowych musi być mniejsza lub równa 2",
@ -654,5 +655,6 @@
"generate_name": "Wygeneruj nazwę",
"balance_page": "Strona salda",
"share": "Udział",
"slidable": "Przesuwne"
"slidable": "Przesuwne",
"template_name": "Nazwa szablonu"
}

View file

@ -269,6 +269,7 @@
"error_text_address": "O endereço da carteira deve corresponder à\ncriptomoeda selecionada",
"error_text_node_address": "Digite um endereço iPv4",
"error_text_node_port": "A porta do nó deve conter apenas números entre 0 e 65535",
"error_text_node_proxy_address": "Insira <endereço IPv4>:<porta>, por exemplo 127.0.0.1:9050",
"error_text_payment_id": "O ID de pagamento pode conter apenas de 16 a 64 caracteres em hexadecimal",
"error_text_xmr": "A quantia em XMR não pode exceder o saldo disponível.\nTO número de dígitos decimais deve ser menor ou igual a 12",
"error_text_fiat": "O valor do valor não pode exceder o saldo disponível.\nO número de dígitos decimais deve ser menor ou igual a 2",
@ -653,5 +654,6 @@
"generate_name": "Gerar nome",
"balance_page": "Página de saldo",
"share": "Compartilhar",
"slidable": "Deslizável"
"slidable": "Deslizável",
"template_name": "Nome do modelo"
}

View file

@ -269,6 +269,8 @@
"error_text_address": "Адрес кошелька должен соответствовать типу\nкриптовалюты",
"error_text_node_address": "Пожалуйста, введите iPv4 адрес",
"error_text_node_port": "Порт ноды может содержать только цифры от 0 до 65535",
"error_text_node_proxy_address": "Введите <IPv4-адрес>:<порт>, например 127.0.0.1:9050.",
"error_text_payment_id": "Идентификатор платежа может содержать от 16 до 64 символов в hex",
"error_text_xmr": "Значение XMR не может превышать доступный баланс.\nКоличество цифр после запятой должно быть меньше или равно 12",
"error_text_fiat": "Значение суммы не может превышать доступный баланс.\nКоличество цифр после запятой должно быть меньше или равно 2",
@ -654,5 +656,6 @@
"generate_name": "Создать имя",
"balance_page": "Страница баланса",
"share": "Делиться",
"slidable": "Скользящий"
"slidable": "Скользящий",
"template_name": "Имя Шаблона"
}

View file

@ -269,6 +269,7 @@
"error_text_address": "ที่อยู่กระเป๋าจะต้องสอดคล้องกับประเภท\nของเหรียญคริปโตเนียม",
"error_text_node_address": "โปรดป้อนที่อยู่ iPv4",
"error_text_node_port": "พอร์ตโหนดสามารถมีตัวเลขเท่านั้นระหว่าง 0 ถึง 65535",
"error_text_node_proxy_address": "โปรดป้อน <ที่อยู่ IPv4>:<พอร์ต> เช่น 127.0.0.1:9050",
"error_text_payment_id": "Payment ID สามารถมีขนาดระหว่าง 16 ถึง 64 ตัวอักษรตามแบบ hex",
"error_text_xmr": "มูลค่า XMR ไม่สามารถเกินยอดคงเหลือได้\nจำนวนสตริงทศนิยมต้องน้อยกว่าหรือเท่ากับ 12",
"error_text_fiat": "มูลค่าของจำนวนเงินไม่สามารถเกินยอดคงเหลือได้\nจำนวนสตริงทศนิยมต้องน้อยกว่าหรือเท่ากับ 2",
@ -654,5 +655,6 @@
"generate_name": "สร้างชื่อ",
"balance_page": "หน้ายอดคงเหลือ",
"share": "แบ่งปัน",
"slidable": "เลื่อนได้"
"slidable": "เลื่อนได้",
"template_name": "ชื่อแม่แบบ"
}

View file

@ -269,6 +269,7 @@
"error_text_address": "Cüzdan adresi kripto para biriminin\ntürüne karşılık gelmelidir",
"error_text_node_address": "Lütfen iPv4 adresi girin",
"error_text_node_port": "Düğüm port'u yalnızca 0 ve 65535 arasında rakam içerebilir",
"error_text_node_proxy_address": "Lütfen <IPv4 adresi>:<bağlantı noktası> girin, örneğin 127.0.0.1:9050",
"error_text_payment_id": "Ödeme ID'si yalnızca onaltılık (hex) olarak 16 veya 64 karakter içerebilir",
"error_text_xmr": "XMR miktarı kullanılabilir bakiyeyi aşamaz.\nKesir basamaklarının sayısı 12'ye eşit veya daha az olmalıdır",
"error_text_fiat": "Tutarın değeri, mevcut bakiyeyi aşamaz.\nKesir basamaklarının sayısı 2'ye eşit veya daha az olmalıdır",
@ -654,5 +655,6 @@
"generate_name": "İsim Oluştur",
"balance_page": "Bakiye Sayfası",
"share": "Paylaşmak",
"slidable": "kaydırılabilir"
"slidable": "kaydırılabilir",
"template_name": "şablon adı"
}

View file

@ -269,6 +269,7 @@
"error_text_address": "Адреса гаманця повинна відповідати типу\nкриптовалюти",
"error_text_node_address": "Будь ласка, введіть iPv4 адресу",
"error_text_node_port": "Порт вузла може містити тільки цифри від 0 до 65535",
"error_text_node_proxy_address": "Будь ласка, введіть <IPv4-адреса>:<порт>, наприклад 127.0.0.1:9050",
"error_text_payment_id": "Ідентифікатор платежу може містити від 16 до 64 символів в hex",
"error_text_xmr": "Значення XMR не може перевищувати доступний баланс.\nКількість цифр після коми повинно бути меншим або дорівнювати 12",
"error_text_fiat": "Значення суми не може перевищувати доступний баланс.\nКількість цифр після коми повинно бути меншим або дорівнювати 2",
@ -654,5 +655,6 @@
"generate_name": "Згенерувати назву",
"balance_page": "Сторінка балансу",
"share": "Поділіться",
"slidable": "Розсувний"
"slidable": "Розсувний",
"template_name": "Назва шаблону"
}

View file

@ -269,6 +269,7 @@
"error_text_address": "والیٹ کا پتہ cryptocurrency کی قسم\\nکے مطابق ہونا چاہیے۔",
"error_text_node_address": "براہ کرم ایک iPv4 پتہ درج کریں۔",
"error_text_node_port": "نوڈ پورٹ میں صرف 0 اور 65535 کے درمیان نمبر ہوسکتے ہیں۔",
"error_text_node_proxy_address": "براہ کرم <IPv4 ایڈریس>:<port> درج کریں، مثال کے طور پر 127.0.0.1:9050",
"error_text_payment_id": "ادائیگی کی ID ہیکس میں صرف 16 سے 64 حروف پر مشتمل ہو سکتی ہے۔",
"error_text_xmr": "XMR قدر دستیاب بیلنس سے زیادہ نہیں ہو سکتی۔\\nفرکشن ہندسوں کی تعداد 12 سے کم یا اس کے برابر ہونی چاہیے۔",
"error_text_fiat": "رقم کی قدر دستیاب بیلنس سے زیادہ نہیں ہو سکتی۔\\nفرکشن ہندسوں کی تعداد 2 کے برابر یا کم ہونی چاہیے۔",
@ -648,5 +649,6 @@
"generate_name": "نام پیدا کریں۔",
"balance_page": "بیلنس صفحہ",
"share": "بانٹیں",
"slidable": "سلائیڈ ایبل"
"slidable": "سلائیڈ ایبل",
"template_name": "ٹیمپلیٹ کا نام"
}

View file

@ -250,8 +250,8 @@
"transaction_details_recipient_address": "Àwọn àdírẹ́sì olùgbà",
"wallet_list_title": "Àpamọ́wọ́ Monero",
"wallet_list_create_new_wallet": "Ṣe àpamọ́wọ́ títun",
"wallet_list_edit_wallet": "Ṣatunkọ apamọwọ",
"wallet_list_wallet_name": "Orukọ apamọwọ",
"wallet_list_edit_wallet" : "Ṣatunkọ apamọwọ",
"wallet_list_wallet_name" : "Orukọ apamọwọ",
"wallet_list_restore_wallet": "Restore àpamọ́wọ́",
"wallet_list_load_wallet": "Load àpamọ́wọ́",
"wallet_list_loading_wallet": "Ń ṣí àpamọ́wọ́ ${wallet_name}",
@ -269,6 +269,7 @@
"error_text_address": "Àdírẹ́sì àpamọ́wọ́ gbọ́dọ̀ báramu irú owó",
"error_text_node_address": "Ẹ jọ̀wọ́ tẹ̀ àdírẹ́sì iPv4",
"error_text_node_port": "Ojú ìkànpọ̀ apẹka lè ni nìkan nínú òǹkà l'áàárín òdo àti márùn-úndínlógojí lé ní ẹ̀ẹ́dẹgbẹ̀ta lé ní ọ̀kẹ́ mẹ́ta lé ní ẹ̀ẹ́dẹ́gbàta",
"error_text_node_proxy_address": "Jọwọ tẹ <IPv4 adirẹsi>:<port>, fun apẹẹrẹ 127.0.0.1:9050",
"error_text_payment_id": "Iye ẹyọ ọ̀rọ̀ nínú àmì ìdánimọ̀ àránṣẹ́ gbọ́dọ̀ wà l'áàárín aárùndínlógún dé ẹẹ́rinlélọ́gọ́ta.",
"error_text_xmr": "Iye XMR kò lè tóbi ju ìyókù.\nIye díjíìtì léyìn ẹsẹ kò gbọ́dọ̀ tóbi ju eéjìlá.",
"error_text_fiat": "Iye àránṣẹ́ kò tóbi ju ìyókù owó.\nIye díjíìtì léyìn ẹsẹ kò gbọ́dọ̀ tóbi ju eéjì.",
@ -650,5 +651,6 @@
"generate_name": "Ṣẹda Orukọ",
"balance_page": "Oju-iwe iwọntunwọnsi",
"share": "Pinpin",
"slidable": "Slidable"
"slidable": "Slidable",
"template_name": "Orukọ Awoṣe"
}

View file

@ -269,6 +269,7 @@
"error_text_address": "钱包地址必须与类型对应\n加密货币",
"error_text_node_address": "请输入一个IPv4地址",
"error_text_node_port": "节点端口只能包含0到65535之间的数字",
"error_text_node_proxy_address": "请输入<IPv4地址>:<端口>例如127.0.0.1:9050",
"error_text_payment_id": "付款ID只能包含16到64个字符十六进制",
"error_text_xmr": "XMR值不能超过可用余额.\n小数位数必须小于或等于12",
"error_text_fiat": "金额不能超过可用余额.\n小数位数必须小于或等于2",
@ -653,5 +654,6 @@
"generate_name": "生成名称",
"balance_page": "余额页",
"share": "分享",
"slidable": "可滑动"
"slidable": "可滑动",
"template_name": "模板名称"
}