CW 68 exchange automatic rate selector (#472)

* Add 'Exchange provider picker'
Save user selections

* Save user's exchange providers selection

* Add text for selected providers availability

* Fix selected providers not updating

* Load limits based on highest maximum in the selected providers

* Change received and deposit amount to be the best value from the selected providers

* Add provider name next to Trade ID
Set selected provider based on amount calculated

* Grey out providers who doesn't support selected currency pair

* Fix disabled providers

* Add Provider logo in Confirm Screen

* Only choose a provider if it satisfies its limits

* Fix amount validation

* Fix typo in error message

* Add a queue of possible exchange providers sorted by the best rate to try next if one failed

* Fix string locale typo

* Add Localization for other languages

* Add Placeholder text when there are no providers selected

* Check Exchange provider availability before creating a trade

* Fix "Fixed Rate" changing unconditionally

* Enable "convert to" field regardless of the provider

* Remove "Choose one" from providers picker

* Merge Master

* Fix Conflicts with master

* Add missing isEnabled field in simple swap provider
This commit is contained in:
Omar Hatem 2022-09-01 16:12:38 +02:00 committed by GitHub
parent 92458e2f4b
commit c50eeee58b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 627 additions and 251 deletions

View file

@ -485,7 +485,9 @@ Future setup(
_tradesSource,
getIt.get<ExchangeTemplateStore>(),
getIt.get<TradesStore>(),
getIt.get<AppStore>().settingsStore));
getIt.get<AppStore>().settingsStore,
getIt.get<SharedPreferences>(),
));
getIt.registerFactory(() => ExchangeTradeViewModel(
wallet: getIt.get<AppStore>().wallet,

View file

@ -26,4 +26,6 @@ class PreferencesKey {
static String moneroWalletUpdateV1Key(String name)
=> '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}';
static const exchangeProvidersSelection = 'exchange-providers-selection';
}

View file

@ -39,6 +39,9 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
@override
bool get isAvailable => true;
@override
bool get isEnabled => true;
@override
ExchangeProviderDescription get description =>
ExchangeProviderDescription.changeNow;

View file

@ -13,6 +13,7 @@ abstract class ExchangeProvider {
List<ExchangePair> pairList;
ExchangeProviderDescription description;
bool get isAvailable;
bool get isEnabled;
@override
String toString() => title;

View file

@ -2,20 +2,23 @@ import 'package:cw_core/enumerable_item.dart';
class ExchangeProviderDescription extends EnumerableItem<int>
with Serializable<int> {
const ExchangeProviderDescription({String title, int raw})
const ExchangeProviderDescription({String title, int raw, this.horizontalLogo = false, this.image})
: super(title: title, raw: raw);
static const xmrto = ExchangeProviderDescription(title: 'XMR.TO', raw: 0);
final bool horizontalLogo;
final String image;
static const xmrto = ExchangeProviderDescription(title: 'XMR.TO', raw: 0, image: 'assets/images/xmrto.png');
static const changeNow =
ExchangeProviderDescription(title: 'ChangeNOW', raw: 1);
ExchangeProviderDescription(title: 'ChangeNOW', raw: 1, image: 'assets/images/changenow.png');
static const morphToken =
ExchangeProviderDescription(title: 'MorphToken', raw: 2);
ExchangeProviderDescription(title: 'MorphToken', raw: 2, image: 'assets/images/morph.png');
static const sideShift =
ExchangeProviderDescription(title: 'SideShift', raw: 3);
static const simpleSwap =
ExchangeProviderDescription(title: 'SimpleSwap', raw: 4);
ExchangeProviderDescription(title: 'SideShift', raw: 3, image: 'assets/images/sideshift.png');
static const simpleSwap =
ExchangeProviderDescription(title: 'SimpleSwap', raw: 4, image: 'assets/images/simpleSwap.png');
static ExchangeProviderDescription deserialize({int raw}) {
switch (raw) {

View file

@ -63,6 +63,9 @@ class MorphTokenExchangeProvider extends ExchangeProvider {
@override
bool get isAvailable => true;
@override
bool get isEnabled => true;
@override
ExchangeProviderDescription get description =>
ExchangeProviderDescription.morphToken;

View file

@ -244,6 +244,9 @@ class SideShiftExchangeProvider extends ExchangeProvider {
@override
bool get isAvailable => true;
@override
bool get isEnabled => true;
@override
String get title => 'SideShift';

View file

@ -197,6 +197,9 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
@override
bool get isAvailable => true;
@override
bool get isEnabled => true;
@override
String get title => 'SimpleSwap';

View file

@ -44,6 +44,9 @@ class XMRTOExchangeProvider extends ExchangeProvider {
@override
bool get isAvailable => _isAvailable;
@override
bool get isEnabled => true;
@override
ExchangeProviderDescription get description =>
ExchangeProviderDescription.xmrto;

View file

@ -354,8 +354,12 @@ class ExchangePage extends BasePage {
padding: EdgeInsets.only(bottom: 15),
child: Observer(builder: (_) {
final description = exchangeViewModel.isFixedRateMode
? exchangeViewModel.isAvailableInSelected
? S.of(context).amount_is_guaranteed
: S.of(context).amount_is_estimate;
: S.of(context).fixed_pair_not_supported
: exchangeViewModel.isAvailableInSelected
? S.of(context).amount_is_estimate
: S.of(context).variable_pair_not_supported;
return Center(
child: Text(
description,
@ -399,8 +403,8 @@ class ExchangePage extends BasePage {
},
color: Theme.of(context).accentTextTheme.body2.color,
textColor: Colors.white,
isLoading:
exchangeViewModel.tradeState is TradeIsCreating)),
isDisabled: exchangeViewModel.selectedProviders.isEmpty,
isLoading: exchangeViewModel.tradeState is TradeIsCreating)),
]),
)),
));
@ -518,13 +522,6 @@ class ExchangePage extends BasePage {
exchangeViewModel.changeReceiveCurrency(
currency: CryptoCurrency.fromString(template.receiveCurrency));
switch (template.provider) {
case 'ChangeNOW':
exchangeViewModel.changeProvider(
provider: exchangeViewModel.providerList[0]);
break;
}
exchangeViewModel.changeDepositAmount(amount: template.amount);
exchangeViewModel.depositAddress = template.depositAddress;
exchangeViewModel.receiveAddress = template.receiveAddress;
@ -744,11 +741,10 @@ class ExchangePage extends BasePage {
});
_receiveAmountFocus.addListener(() {
if(receiveAmountController.text.isNotEmpty){
exchangeViewModel.isFixedRateMode = true;
}
exchangeViewModel.changeReceiveAmount(
amount: receiveAmountController.text);
if (_receiveAmountFocus.hasFocus) {
exchangeViewModel.isFixedRateMode = true;
}
exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text);
});
_depositAmountFocus.addListener(() {

View file

@ -1,11 +1,9 @@
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/check_box_picker.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/exchange/exchange_provider.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/picker.dart';
import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart';
class PresentProviderPicker extends StatelessWidget {
@ -38,11 +36,16 @@ class PresentProviderPicker extends StatelessWidget {
fontWeight: FontWeight.w600,
color: Colors.white)),
Observer(
builder: (_) => Text('${exchangeViewModel.provider.title}',
style: TextStyle(
fontSize: 10.0,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.headline.color)))
builder: (_) => Text(
exchangeViewModel.selectedProviders.isEmpty
? S.of(context).choose_one
: exchangeViewModel.selectedProviders.length > 1
? S.of(context).automatic
: exchangeViewModel.selectedProviders.first.title,
style: TextStyle(
fontSize: 10.0,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.headline.color)))
],
),
SizedBox(width: 5),
@ -54,41 +57,19 @@ class PresentProviderPicker extends StatelessWidget {
));
}
void _presentProviderPicker(BuildContext context) {
final items = exchangeViewModel.providersForCurrentPair();
final selectedItem = items.indexOf(exchangeViewModel.provider);
final images = <Image>[];
String description;
for (var provider in items) {
switch (provider.description) {
case ExchangeProviderDescription.xmrto:
images.add(Image.asset('assets/images/xmr_btc.png'));
break;
case ExchangeProviderDescription.changeNow:
images.add(Image.asset('assets/images/change_now.png'));
break;
case ExchangeProviderDescription.morphToken:
images.add(Image.asset('assets/images/morph_icon.png'));
break;
case ExchangeProviderDescription.sideShift:
images.add(Image.asset('assets/images/sideshift.png', width: 20));
break;
case ExchangeProviderDescription.simpleSwap:
images.add(Image.asset('assets/images/simpleSwap.png', width: 20));
break;
}
}
showPopUp<void>(
builder: (BuildContext popUpContext) => Picker(
items: items,
images: images,
selectedAtIndex: selectedItem,
void _presentProviderPicker(BuildContext context) async {
await showPopUp<void>(
builder: (BuildContext popUpContext) => CheckBoxPicker(
items: exchangeViewModel.providerList
.map((e) => CheckBoxItem(
e.title,
exchangeViewModel.selectedProviders.contains(e),
isDisabled: !exchangeViewModel.providersForCurrentPair().contains(e),
))
.toList(),
title: S.of(context).change_exchange_provider,
description: description,
onItemSelected: (ExchangeProvider provider) {
if (!provider.isAvailable) {
onChanged: (int index, bool value) {
if (!exchangeViewModel.providerList[index].isAvailable) {
showPopUp<void>(
builder: (BuildContext popUpContext) => AlertWithOneAction(
alertTitle: 'Error',
@ -98,8 +79,14 @@ class PresentProviderPicker extends StatelessWidget {
context: context);
return;
}
exchangeViewModel.changeProvider(provider: provider);
if (value) {
exchangeViewModel.addExchangeProvider(exchangeViewModel.providerList[index]);
} else {
exchangeViewModel.removeExchangeProvider(exchangeViewModel.providerList[index]);
}
}),
context: context);
exchangeViewModel.saveSelectedProviders();
}
}

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/store/dashboard/trades_store.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:flutter/material.dart';
@ -56,7 +57,7 @@ class ExchangeConfirmPage extends BasePage {
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
S.of(context).trade_id,
"${trade.provider.title} ${S.of(context).trade_id}",
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.w500,
@ -101,7 +102,23 @@ class ExchangeConfirmPage extends BasePage {
],
),
),
Flexible(child: Offstage()),
Flexible(
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
(trade.provider.image?.isNotEmpty ?? false)
? Image.asset(trade.provider.image, height: 50)
: const SizedBox(),
if (!trade.provider.horizontalLogo)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(trade.provider.title),
),
],
),
),
),
],
)),
PrimaryButton(

View file

@ -0,0 +1,166 @@
import 'dart:ui';
import 'package:cake_wallet/palette.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/src/widgets/alert_background.dart';
import 'package:cake_wallet/src/widgets/alert_close_button.dart';
import 'package:cake_wallet/generated/i18n.dart';
class CheckBoxPicker extends StatefulWidget {
CheckBoxPicker({
@required this.items,
@required this.onChanged,
this.title,
this.displayItem,
this.isSeparated = true,
});
final List<CheckBoxItem> items;
final String title;
final Widget Function(CheckBoxItem) displayItem;
final bool isSeparated;
final Function(int, bool) onChanged;
@override
CheckBoxPickerState createState() => CheckBoxPickerState(items);
}
class CheckBoxPickerState extends State<CheckBoxPicker> {
CheckBoxPickerState(this.items);
final List<CheckBoxItem> items;
ScrollController controller = ScrollController();
@override
Widget build(BuildContext context) {
return AlertBackground(
child: Stack(
alignment: Alignment.center,
children: <Widget>[
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (widget.title?.isNotEmpty ?? false)
Container(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Text(
widget.title,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontFamily: 'Lato',
fontWeight: FontWeight.bold,
decoration: TextDecoration.none,
color: Colors.white,
),
),
),
Padding(
padding: EdgeInsets.only(left: 24, right: 24, top: 24),
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(30)),
child: Container(
color: Theme.of(context).accentTextTheme.title.color,
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.65,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Stack(
alignment: Alignment.center,
children: <Widget>[
(items?.length ?? 0) > 3
? Scrollbar(
controller: controller,
child: itemsList(),
)
: itemsList(),
],
),
),
],
),
),
),
),
)
],
),
AlertCloseButton(),
],
),
);
}
Widget itemsList() {
return Container(
color: Theme.of(context).accentTextTheme.headline6.backgroundColor,
child: ListView.separated(
padding: EdgeInsets.zero,
controller: controller,
shrinkWrap: true,
separatorBuilder: (context, index) => widget.isSeparated
? Divider(
color: Theme.of(context).accentTextTheme.title.backgroundColor,
height: 1,
)
: const SizedBox(),
itemCount: items == null || items.isEmpty ? 0 : items.length,
itemBuilder: (context, index) => buildItem(index),
),
);
}
Widget buildItem(int index) {
final item = items[index];
return GestureDetector(
onTap: () {
Navigator.of(context).pop();
},
child: Container(
height: 55,
color: Theme.of(context).accentTextTheme.headline6.color,
padding: EdgeInsets.only(left: 24, right: 24),
child: CheckboxListTile(
value: item.value,
activeColor: item.value
? Palette.blueCraiola
: Theme.of(context).accentTextTheme.subhead.decorationColor,
checkColor: Colors.white,
title: widget.displayItem?.call(item) ??
Text(
item.title,
style: TextStyle(
fontSize: 14,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: item.isDisabled
? Colors.grey.withOpacity(0.5)
: Theme.of(context).primaryTextTheme.title.color,
decoration: TextDecoration.none,
),
),
onChanged: (bool value) {
item.value = value;
widget.onChanged(index, value);
setState(() {});
},
controlAffinity: ListTileControlAffinity.leading,
),
),
);
}
}
class CheckBoxItem {
CheckBoxItem(this.title, this.value, {this.isDisabled = false});
final String title;
final bool isDisabled;
bool value;
}

View file

@ -116,7 +116,7 @@ abstract class ExchangeTradeViewModelBase with Store {
items?.clear();
items.add(ExchangeTradeItem(
title: S.current.id, data: '${trade.id}', isCopied: true));
title: "${trade.provider.title} ${S.current.id}", data: '${trade.id}', isCopied: true));
if (trade.extraId != null) {
final title = trade.from == CryptoCurrency.xrp

View file

@ -1,3 +1,7 @@
import 'dart:collection';
import 'dart:convert';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart';
import 'package:cake_wallet/exchange/sideshift/sideshift_request.dart';
import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart';
@ -27,6 +31,7 @@ import 'package:cake_wallet/exchange/morphtoken/morphtoken_exchange_provider.dar
import 'package:cake_wallet/exchange/morphtoken/morphtoken_request.dart';
import 'package:cake_wallet/store/templates/exchange_template_store.dart';
import 'package:cake_wallet/exchange/exchange_template.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'exchange_view_model.g.dart';
@ -34,10 +39,24 @@ class ExchangeViewModel = ExchangeViewModelBase with _$ExchangeViewModel;
abstract class ExchangeViewModelBase with Store {
ExchangeViewModelBase(this.wallet, this.trades, this._exchangeTemplateStore,
this.tradesStore, this._settingsStore) {
this.tradesStore, this._settingsStore, this.sharedPreferences) {
const excludeDepositCurrencies = [CryptoCurrency.xhv];
const excludeReceiveCurrencies = [CryptoCurrency.xlm, CryptoCurrency.xrp, CryptoCurrency.bnb, CryptoCurrency.xhv];
providerList = [ChangeNowExchangeProvider(), SideShiftExchangeProvider(), SimpleSwapExchangeProvider()];
currentTradeAvailableProviders = SplayTreeMap<double, ExchangeProvider>();
final Map<String, dynamic> exchangeProvidersSelection = json
.decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map<String, dynamic>;
/// if the provider is not in the user settings (user's first time or newly added provider)
/// then use its default value decided by us
selectedProviders = ObservableList.of(providerList.where(
(element) => exchangeProvidersSelection[element.title] == null
? element.isEnabled
: (exchangeProvidersSelection[element.title] as bool))
.toList());
_initialPairBasedOnWallet();
isDepositAddressEnabled = !(depositCurrency == wallet.currency);
isReceiveAddressEnabled = !(receiveCurrency == wallet.currency);
@ -48,7 +67,7 @@ abstract class ExchangeViewModelBase with Store {
? wallet.walletAddresses.address : '';
limitsState = LimitsInitialState();
tradeState = ExchangeTradeStateInitial();
_cryptoNumberFormat = NumberFormat()..maximumFractionDigits = 12;
_cryptoNumberFormat = NumberFormat()..maximumFractionDigits = wallet.type == WalletType.bitcoin ? 8 : 12;
provider = providersForCurrentPair().first;
final initialProvider = provider;
provider.checkIsAvailable().then((bool isAvailable) {
@ -79,10 +98,20 @@ abstract class ExchangeViewModelBase with Store {
final Box<Trade> trades;
final ExchangeTemplateStore _exchangeTemplateStore;
final TradesStore tradesStore;
final SharedPreferences sharedPreferences;
@observable
ExchangeProvider provider;
/// Maps in dart are not sorted by default
/// SplayTreeMap is a map sorted by keys
/// will use it to sort available providers
/// depending on the amount they yield for the current trade
SplayTreeMap<double, ExchangeProvider> currentTradeAvailableProviders;
@observable
ObservableList<ExchangeProvider> selectedProviders;
@observable
List<ExchangeProvider> providerList;
@ -147,17 +176,7 @@ abstract class ExchangeViewModelBase with Store {
NumberFormat _cryptoNumberFormat;
SettingsStore _settingsStore;
@action
void changeProvider({ExchangeProvider provider}) {
this.provider = provider;
depositAmount = '';
receiveAmount = '';
isFixedRateMode = false;
_defineIsReceiveAmountEditable();
loadLimits();
}
final SettingsStore _settingsStore;
@action
void changeDepositCurrency({CryptoCurrency currency}) {
@ -188,20 +207,46 @@ abstract class ExchangeViewModelBase with Store {
return;
}
final _amount = double.parse(amount.replaceAll(',', '.')) ?? 0;
final _enteredAmount = double.parse(amount.replaceAll(',', '.')) ?? 0;
provider
.calculateAmount(
from: receiveCurrency,
to: depositCurrency,
amount: _amount,
isFixedRateMode: isFixedRateMode,
isReceiveAmount: true)
.then((amount) => _cryptoNumberFormat
currentTradeAvailableProviders.clear();
for (var provider in selectedProviders) {
provider
.calculateAmount(
from: receiveCurrency,
to: depositCurrency,
amount: _enteredAmount,
isFixedRateMode: isFixedRateMode,
isReceiveAmount: true)
.then((amount) {
final from = isFixedRateMode
? receiveCurrency
: depositCurrency;
final to = isFixedRateMode
? depositCurrency
: receiveCurrency;
provider.fetchLimits(
from: from,
to: to,
isFixedRateMode: isFixedRateMode,
).then((limits) {
/// if the entered amount doesn't exceed the limits of this provider
if ((limits.max ?? double.maxFinite) >= _enteredAmount
&& (limits.min ?? 0) <= _enteredAmount) {
/// add this provider as its valid for this trade
/// will be sorted ascending already since
/// we seek the least deposit amount
currentTradeAvailableProviders[amount] = provider;
}
return amount;
}).then((amount) => depositAmount = _cryptoNumberFormat
.format(amount)
.toString()
.replaceAll(RegExp('\\,'), ''))
.then((amount) => depositAmount = amount);
.replaceAll(RegExp('\\,'), ''));
});
}
}
@action
@ -215,23 +260,56 @@ abstract class ExchangeViewModelBase with Store {
return;
}
final _amount = double.parse(amount.replaceAll(',', '.')) ?? 0;
provider
.calculateAmount(
from: depositCurrency,
to: receiveCurrency,
amount: _amount,
isFixedRateMode: isFixedRateMode,
isReceiveAmount: false)
.then((amount) => _cryptoNumberFormat
final _enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0;
currentTradeAvailableProviders.clear();
for (var provider in selectedProviders) {
provider
.calculateAmount(
from: depositCurrency,
to: receiveCurrency,
amount: _enteredAmount,
isFixedRateMode: isFixedRateMode,
isReceiveAmount: false)
.then((amount) {
final from = isFixedRateMode
? receiveCurrency
: depositCurrency;
final to = isFixedRateMode
? depositCurrency
: receiveCurrency;
provider.fetchLimits(
from: from,
to: to,
isFixedRateMode: isFixedRateMode,
).then((limits) {
/// if the entered amount doesn't exceed the limits of this provider
if ((limits.max ?? double.maxFinite) >= _enteredAmount
&& (limits.min ?? 0) <= _enteredAmount) {
/// add this provider as its valid for this trade
/// subtract from maxFinite so the provider
/// with the largest amount would be sorted ascending
currentTradeAvailableProviders[double.maxFinite - amount] = provider;
}
return amount;
}).then((amount) => receiveAmount =
receiveAmount = _cryptoNumberFormat
.format(amount)
.toString()
.replaceAll(RegExp('\\,'), ''))
.then((amount) => receiveAmount = amount);
.replaceAll(RegExp('\\,'), ''));
});
}
}
@action
Future loadLimits() async {
if (selectedProviders.isEmpty) {
return;
}
limitsState = LimitsIsLoading();
try {
@ -241,10 +319,29 @@ abstract class ExchangeViewModelBase with Store {
final to = isFixedRateMode
? depositCurrency
: receiveCurrency;
limits = await provider.fetchLimits(
limits = await selectedProviders.first.fetchLimits(
from: from,
to: to,
isFixedRateMode: isFixedRateMode);
/// if the first provider limits is bounded then check with other providers
/// for the highest maximum limit
if (limits.max != null) {
for (int i = 1;i < selectedProviders.length;i++) {
final Limits tempLimits = await selectedProviders[i].fetchLimits(
from: from,
to: to,
isFixedRateMode: isFixedRateMode);
/// set the limits with the maximum provider limit
/// if there is a provider with null max then it's the maximum limit
if ((tempLimits.max ?? double.maxFinite) > limits.max) {
limits = tempLimits;
}
}
}
limitsState = LimitsLoadedSuccessfully(limits: limits);
} catch (e) {
limitsState = LimitsLoadedFailure(error: e.toString());
@ -255,102 +352,97 @@ abstract class ExchangeViewModelBase with Store {
Future createTrade() async {
TradeRequest request;
String amount;
CryptoCurrency currency;
if (provider is SideShiftExchangeProvider) {
request = SideShiftRequest(
for (var provider in currentTradeAvailableProviders.values) {
if (!(await provider.checkIsAvailable())) {
continue;
}
if (provider is SideShiftExchangeProvider) {
request = SideShiftRequest(
depositMethod: depositCurrency,
settleMethod: receiveCurrency,
depositAmount: depositAmount?.replaceAll(',', '.'),
settleAddress: receiveAddress,
refundAddress: depositAddress,
);
amount = depositAmount;
currency = depositCurrency;
}
);
amount = depositAmount;
}
if (provider is SimpleSwapExchangeProvider) {
request = SimpleSwapRequest(
if (provider is SimpleSwapExchangeProvider) {
request = SimpleSwapRequest(
from: depositCurrency,
to: receiveCurrency,
amount: depositAmount?.replaceAll(',', '.'),
address: receiveAddress,
refundAddress: depositAddress,
);
amount = depositAmount;
currency = depositCurrency;
}
);
amount = depositAmount;
}
if (provider is XMRTOExchangeProvider) {
request = XMRTOTradeRequest(
from: depositCurrency,
to: receiveCurrency,
amount: depositAmount?.replaceAll(',', '.'),
receiveAmount: receiveAmount?.replaceAll(',', '.'),
address: receiveAddress,
refundAddress: depositAddress,
isBTCRequest: isReceiveAmountEntered);
amount = depositAmount;
currency = depositCurrency;
}
if (provider is XMRTOExchangeProvider) {
request = XMRTOTradeRequest(
from: depositCurrency,
to: receiveCurrency,
amount: depositAmount?.replaceAll(',', '.'),
receiveAmount: receiveAmount?.replaceAll(',', '.'),
address: receiveAddress,
refundAddress: depositAddress,
isBTCRequest: isReceiveAmountEntered);
amount = depositAmount;
}
if (provider is ChangeNowExchangeProvider) {
request = ChangeNowRequest(
from: depositCurrency,
to: receiveCurrency,
fromAmount: depositAmount?.replaceAll(',', '.'),
toAmount: receiveAmount?.replaceAll(',', '.'),
refundAddress: depositAddress,
address: receiveAddress,
isReverse: isReverse);
amount = isReverse ? receiveAmount : depositAmount;
currency = depositCurrency;
}
if (provider is ChangeNowExchangeProvider) {
request = ChangeNowRequest(
from: depositCurrency,
to: receiveCurrency,
fromAmount: depositAmount?.replaceAll(',', '.'),
toAmount: receiveAmount?.replaceAll(',', '.'),
refundAddress: depositAddress,
address: receiveAddress,
isReverse: isReverse);
amount = isReverse ? receiveAmount : depositAmount;
}
if (provider is MorphTokenExchangeProvider) {
request = MorphTokenRequest(
from: depositCurrency,
to: receiveCurrency,
amount: depositAmount?.replaceAll(',', '.'),
refundAddress: depositAddress,
address: receiveAddress);
amount = depositAmount;
currency = depositCurrency;
}
if (provider is MorphTokenExchangeProvider) {
request = MorphTokenRequest(
from: depositCurrency,
to: receiveCurrency,
amount: depositAmount?.replaceAll(',', '.'),
refundAddress: depositAddress,
address: receiveAddress);
amount = depositAmount;
}
amount = amount.replaceAll(',', '.');
amount = amount.replaceAll(',', '.');
if (limitsState is LimitsLoadedSuccessfully && amount != null) {
if (double.parse(amount) < limits.min) {
tradeState = TradeIsCreatedFailure(
title: provider.title,
error: S.current.error_text_minimal_limit('${provider.description}',
'${limits.min}', currency.toString()));
} else if (limits.max != null && double.parse(amount) > limits.max) {
tradeState = TradeIsCreatedFailure(
title: provider.title,
error: S.current.error_text_maximum_limit('${provider.description}',
'${limits.max}', currency.toString()));
} else {
try {
tradeState = TradeIsCreating();
final trade = await provider.createTrade(
request: request, isFixedRateMode: isFixedRateMode);
trade.walletId = wallet.id;
tradesStore.setTrade(trade);
await trades.add(trade);
tradeState = TradeIsCreatedSuccessfully(trade: trade);
} catch (e) {
tradeState =
TradeIsCreatedFailure(title: provider.title, error: e.toString());
if (limitsState is LimitsLoadedSuccessfully && amount != null) {
if (double.parse(amount) < limits.min) {
continue;
} else if (limits.max != null && double.parse(amount) > limits.max) {
continue;
} else {
try {
tradeState = TradeIsCreating();
final trade = await provider.createTrade(
request: request, isFixedRateMode: isFixedRateMode);
trade.walletId = wallet.id;
tradesStore.setTrade(trade);
await trades.add(trade);
tradeState = TradeIsCreatedSuccessfully(trade: trade);
/// return after the first successful trade
return;
} catch (e) {
continue;
}
}
}
} else {
tradeState = TradeIsCreatedFailure(
title: provider.title,
error: S.current
.error_text_limits_loading_failed('${provider.description}'));
}
/// if the code reached here then none of the providers succeeded
tradeState = TradeIsCreatedFailure(
title: S.current.trade_not_created,
error: S.current.none_of_selected_providers_can_exchange);
}
@action
@ -414,7 +506,7 @@ abstract class ExchangeViewModelBase with Store {
final providers = providerList
.where((provider) => provider.pairList
.where((pair) =>
pair.from == depositCurrency && pair.to == receiveCurrency)
pair.from == (from ?? depositCurrency) && pair.to == (to ?? receiveCurrency))
.isNotEmpty)
.toList();
@ -422,27 +514,8 @@ abstract class ExchangeViewModelBase with Store {
}
void _onPairChange() {
final isPairExist = provider.pairList
.where((pair) =>
pair.from == depositCurrency && pair.to == receiveCurrency)
.isNotEmpty;
if (isPairExist) {
final provider =
_providerForPair(from: depositCurrency, to: receiveCurrency);
if (provider != null) {
changeProvider(provider: provider);
}
} else {
depositAmount = '';
receiveAmount = '';
}
}
ExchangeProvider _providerForPair({CryptoCurrency from, CryptoCurrency to}) {
final providers = _providersForPair(from: from, to: to);
return providers.isNotEmpty ? providers[0] : null;
depositAmount = '';
receiveAmount = '';
}
void _initialPairBasedOnWallet() {
@ -473,6 +546,45 @@ abstract class ExchangeViewModelBase with Store {
isReceiveAmountEditable = false;
}*/
//isReceiveAmountEditable = false;
isReceiveAmountEditable = provider is ChangeNowExchangeProvider || provider is SimpleSwapExchangeProvider;
// isReceiveAmountEditable = selectedProviders.any((provider) => provider is ChangeNowExchangeProvider);
// isReceiveAmountEditable = provider is ChangeNowExchangeProvider || provider is SimpleSwapExchangeProvider;
isReceiveAmountEditable = true;
}
@action
void addExchangeProvider(ExchangeProvider provider) {
selectedProviders.add(provider);
}
@action
void removeExchangeProvider(ExchangeProvider provider) {
selectedProviders.remove(provider);
}
@action
void saveSelectedProviders() {
depositAmount = '';
receiveAmount = '';
isFixedRateMode = false;
_defineIsReceiveAmountEditable();
loadLimits();
final Map<String, dynamic> exchangeProvidersSelection = json
.decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map<String, dynamic>;
exchangeProvidersSelection.updateAll((key, dynamic value) => false);
for (var provider in selectedProviders) {
exchangeProvidersSelection[provider.title] = true;
}
sharedPreferences.setString(
PreferencesKey.exchangeProvidersSelection,
json.encode(exchangeProvidersSelection),
);
}
bool get isAvailableInSelected {
final providersForPair = providersForCurrentPair();
return selectedProviders.any((element) => element.isAvailable && providersForPair.contains(element));
}
}

View file

@ -366,7 +366,7 @@
"trade_for_not_created" : "Handel für ${title} wird nicht erstellt.",
"trade_not_created" : "Handel nicht erstellt.",
"trade_not_created" : "Handel nicht erstellt",
"trade_id_not_found" : "Handel ${tradeId} von ${title} nicht gefunden.",
"trade_not_found" : "Handel nicht gefunden.",
@ -586,7 +586,7 @@
"use_card_info_two": "Tegoeden worden omgezet naar USD wanneer ze op de prepaid-rekening staan, niet in digitale valuta.",
"use_card_info_three": "Gebruik de digitale kaart online of met contactloze betaalmethoden.",
"optionally_order_card": "Optioneel een fysieke kaart bestellen.",
"hide_details" : "Details verbergen",
"hide_details" : "Details verbergen",
"show_details" : "Toon details",
"upto": "tot ${value}",
"discount": "Bespaar ${value}%",
@ -634,5 +634,10 @@
"contact_support": "Support kontaktieren",
"gift_cards_unavailable": "Geschenkkarten können derzeit nur über Monero, Bitcoin und Litecoin erworben werden",
"introducing_cake_pay": "Einführung von Cake Pay!",
"cake_pay_learn_more": "Karten sofort in der App kaufen und einlösen!\nWischen Sie nach rechts, um mehr zu erfahren!"
"cake_pay_learn_more": "Karten sofort in der App kaufen und einlösen!\nWischen Sie nach rechts, um mehr zu erfahren!",
"automatic": "Automatisch",
"fixed_pair_not_supported": "Dieses feste Paar wird von den ausgewählten Vermittlungsstellen nicht unterstützt",
"variable_pair_not_supported": "Dieses Variablenpaar wird von den ausgewählten Börsen nicht unterstützt",
"none_of_selected_providers_can_exchange": "Keiner der ausgewählten Anbieter kann diesen Austausch vornehmen",
"choose_one": "Wähle ein"
}

View file

@ -366,7 +366,7 @@
"trade_for_not_created" : "Trade for ${title} is not created.",
"trade_not_created" : "Trade not created.",
"trade_not_created" : "Trade not created",
"trade_id_not_found" : "Trade ${tradeId} of ${title} not found.",
"trade_not_found" : "Trade not found.",
@ -634,5 +634,10 @@
"contact_support": "Contact Support",
"gift_cards_unavailable": "Gift cards are available for purchase only with Monero, Bitcoin, and Litecoin at this time",
"introducing_cake_pay": "Introducing Cake Pay!",
"cake_pay_learn_more": "Instantly purchase and redeem cards in the app!\nSwipe right to learn more!"
"cake_pay_learn_more": "Instantly purchase and redeem cards in the app!\nSwipe right to learn more!",
"automatic": "Automatic",
"fixed_pair_not_supported": "This fixed pair is not supported with the selected exchanges",
"variable_pair_not_supported": "This variable pair is not supported with the selected exchanges",
"none_of_selected_providers_can_exchange": "None of the selected providers can make this exchange",
"choose_one": "Choose one"
}

View file

@ -366,7 +366,7 @@
"trade_for_not_created" : "Comercio por ${title} no se crea.",
"trade_not_created" : "Comercio no se crea.",
"trade_not_created" : "Comercio no se crea",
"trade_id_not_found" : "Comercio ${tradeId} de ${title} no encontrado.",
"trade_not_found" : "Comercio no encontrado.",
@ -634,5 +634,10 @@
"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",
"introducing_cake_pay": "¡Presentamos Cake Pay!",
"cake_pay_learn_more": "¡Compre y canjee tarjetas al instante en la aplicación!\n¡Desliza hacia la derecha para obtener más información!"
"cake_pay_learn_more": "¡Compre y canjee tarjetas al instante en la aplicación!\n¡Desliza hacia la derecha para obtener más información!",
"automatic": "Automático",
"fixed_pair_not_supported": "Este par fijo no es compatible con los intercambios seleccionados",
"variable_pair_not_supported": "Este par de variables no es compatible con los intercambios seleccionados",
"none_of_selected_providers_can_exchange": "Ninguno de los proveedores seleccionados puede realizar este intercambio",
"choose_one": "Elige uno"
}

View file

@ -364,7 +364,7 @@
"trade_for_not_created" : "L'échange pour ${title} n'est pas créé.",
"trade_not_created" : "Échange non créé.",
"trade_not_created" : "Échange non créé",
"trade_id_not_found" : "Échange ${tradeId} de ${title} introuvable.",
"trade_not_found" : "Échange introuvable.",
@ -632,5 +632,10 @@
"contact_support": "Contacter l'assistance",
"gift_cards_unavailable": "Les cartes-cadeaux ne sont disponibles à l'achat que via Monero, Bitcoin et Litecoin pour le moment",
"introducing_cake_pay": "Présentation de Cake Pay!",
"cake_pay_learn_more": "Achetez et échangez instantanément des cartes dans l'application !\nBalayez vers la droite pour en savoir plus !"
"cake_pay_learn_more": "Achetez et échangez instantanément des cartes dans l'application !\nBalayez vers la droite pour en savoir plus !",
"automatic": "Automatique",
"fixed_pair_not_supported": "Cette paire fixe n'est pas prise en charge avec les échanges sélectionnés",
"variable_pair_not_supported": "Cette paire de variables n'est pas prise en charge avec les échanges sélectionnés",
"none_of_selected_providers_can_exchange": "Aucun des prestataires sélectionnés ne peut effectuer cet échange",
"choose_one": "Choisissez-en un"
}

View file

@ -366,7 +366,7 @@
"trade_for_not_created" : "के लिए व्यापार ${title} निर्मित नहीं है.",
"trade_not_created" : "व्यापार नहीं बनाया गया.",
"trade_not_created" : "व्यापार नहीं बनाया गया",
"trade_id_not_found" : "व्यापार ${tradeId} of ${title} नहीं मिला.",
"trade_not_found" : "व्यापार नहीं मिला",
@ -634,5 +634,10 @@
"contact_support": "सहायता से संपर्क करें",
"gift_cards_unavailable": "उपहार कार्ड इस समय केवल मोनेरो, बिटकॉइन और लिटकोइन के माध्यम से खरीदने के लिए उपलब्ध हैं",
"introducing_cake_pay": "परिचय Cake Pay!",
"cake_pay_learn_more": "ऐप में तुरंत कार्ड खरीदें और रिडीम करें!\nअधिक जानने के लिए दाएं स्वाइप करें!"
"cake_pay_learn_more": "ऐप में तुरंत कार्ड खरीदें और रिडीम करें!\nअधिक जानने के लिए दाएं स्वाइप करें!",
"automatic": "स्वचालित",
"fixed_pair_not_supported": "यह निश्चित जोड़ी चयनित एक्सचेंजों के साथ समर्थित नहीं है",
"variable_pair_not_supported": "यह परिवर्तनीय जोड़ी चयनित एक्सचेंजों के साथ समर्थित नहीं है",
"none_of_selected_providers_can_exchange": "चयनित प्रदाताओं में से कोई भी इस एक्सचेंज को नहीं बना सकता",
"choose_one": "एक का चयन"
}

View file

@ -366,7 +366,7 @@
"trade_for_not_created" : "Razmjena za ${title} nije izrađena.",
"trade_not_created" : "Razmjena nije izrađena.",
"trade_not_created" : "Razmjena nije izrađena",
"trade_id_not_found" : "Razmjena ${tradeId} za ${title} nije pronađena.",
"trade_not_found" : "Razmjena nije pronađena.",
@ -634,5 +634,10 @@
"contact_support": "Kontaktirajte podršku",
"gift_cards_unavailable": "Poklon kartice trenutno su dostupne za kupnju samo putem Monera, Bitcoina i Litecoina",
"introducing_cake_pay": "Predstavljamo Cake Pay!",
"cake_pay_learn_more": "Odmah kupite i iskoristite kartice u aplikaciji!\nPrijeđite prstom udesno da biste saznali više!"
"cake_pay_learn_more": "Odmah kupite i iskoristite kartice u aplikaciji!\nPrijeđite prstom udesno da biste saznali više!",
"automatic": "Automatski",
"fixed_pair_not_supported": "Ovaj fiksni par nije podržan s odabranim burzama",
"variable_pair_not_supported": "Ovaj par varijabli nije podržan s odabranim burzama",
"none_of_selected_providers_can_exchange": "Niti jedan od odabranih pružatelja usluga ne može izvršiti ovu razmjenu",
"choose_one": "Izaberi jedan"
}

View file

@ -366,7 +366,7 @@
"trade_for_not_created" : "Lo scambio per ${title} non è stato creato.",
"trade_not_created" : "Scambio non creato.",
"trade_not_created" : "Scambio non creato",
"trade_id_not_found" : "Scambio ${tradeId} di ${title} not trovato.",
"trade_not_found" : "Scambio non trovato.",
@ -634,5 +634,10 @@
"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",
"introducing_cake_pay": "Presentazione di Cake Pay!",
"cake_pay_learn_more": "Acquista e riscatta istantaneamente le carte nell'app!\nScorri verso destra per saperne di più!"
"cake_pay_learn_more": "Acquista e riscatta istantaneamente le carte nell'app!\nScorri verso destra per saperne di più!",
"automatic": "Automatico",
"fixed_pair_not_supported": "Questa coppia fissa non è supportata con gli scambi selezionati",
"variable_pair_not_supported": "Questa coppia di variabili non è supportata con gli scambi selezionati",
"none_of_selected_providers_can_exchange": "Nessuno dei fornitori selezionati può effettuare questo scambio",
"choose_one": "Scegline uno"
}

View file

@ -612,21 +612,21 @@
"cards": "カード",
"active": "アクティブ",
"redeemed": "償還",
"gift_card_balance_note": "残高が残っているギフトカードがここに表示されます",
"gift_card_redeemed_note": "利用したギフトカードがここに表示されます",
"logout": "ログアウト",
"add_tip": "ヒントを追加",
"percentageOf": "of ${amount}",
"is_percentage": "is",
"search_category": "検索カテゴリ",
"mark_as_redeemed": "償還済みとしてマーク",
"more_options": "その他のオプション",
"awaiting_payment_confirmation": "支払い確認を待っています",
"transaction_sent_notice": "1分経っても画面が進まない場合は、ブロックエクスプローラーとメールアドレスを確認してください。",
"agree": "同意する",
"in_store": "インストア",
"generated_gift_card": "ギフトカードの生成",
"payment_was_received": "お支払いを受け取りました。",
"gift_card_balance_note": "残高が残っているギフトカードがここに表示されます",
"gift_card_redeemed_note": "利用したギフトカードがここに表示されます",
"logout": "ログアウト",
"add_tip": "ヒントを追加",
"percentageOf": "of ${amount}",
"is_percentage": "is",
"search_category": "検索カテゴリ",
"mark_as_redeemed": "償還済みとしてマーク",
"more_options": "その他のオプション",
"awaiting_payment_confirmation": "支払い確認を待っています",
"transaction_sent_notice": "1分経っても画面が進まない場合は、ブロックエクスプローラーとメールアドレスを確認してください。",
"agree": "同意する",
"in_store": "インストア",
"generated_gift_card": "ギフトカードの生成",
"payment_was_received": "お支払いを受け取りました。",
"proceed_after_one_minute": "1分経っても画面が進まない場合は、メールを確認してください。",
"order_id": "注文ID",
"gift_card_is_generated": "ギフトカードが生成されます",
@ -634,5 +634,10 @@
"contact_support": "サポートに連絡する",
"gift_cards_unavailable": "現時点では、ギフトカードはMonero、Bitcoin、Litecoinからのみ購入できます。",
"introducing_cake_pay": "序章Cake Pay",
"cake_pay_learn_more": "アプリですぐにカードを購入して引き換えましょう!\n右にスワイプして詳細をご覧ください。"
"cake_pay_learn_more": "アプリですぐにカードを購入して引き換えましょう!\n右にスワイプして詳細をご覧ください。",
"automatic": "自動",
"fixed_pair_not_supported": "この固定ペアは、選択したエクスチェンジではサポートされていません",
"variable_pair_not_supported": "この変数ペアは、選択した取引所ではサポートされていません",
"none_of_selected_providers_can_exchange": "選択したプロバイダーはいずれもこの交換を行うことができません",
"choose_one": "1 つ選択してください"
}

View file

@ -366,7 +366,7 @@
"trade_for_not_created" : "거래 ${title} 생성되지 않습니다.",
"trade_not_created" : "거래가 생성되지 않았습니다.",
"trade_not_created" : "거래가 생성되지 않았습니다",
"trade_id_not_found" : "무역 ${tradeId} 의 ${title} 찾을 수 없습니다.",
"trade_not_found" : "거래를 찾을 수 없습니다.",
@ -634,5 +634,10 @@
"contact_support": "지원팀에 문의",
"gift_cards_unavailable": "기프트 카드는 현재 Monero, Bitcoin 및 Litecoin을 통해서만 구매할 수 있습니다.",
"introducing_cake_pay": "소개 Cake Pay!",
"cake_pay_learn_more": "앱에서 즉시 카드를 구매하고 사용하세요!\n자세히 알아보려면 오른쪽으로 스와이프하세요!"
"cake_pay_learn_more": "앱에서 즉시 카드를 구매하고 사용하세요!\n자세히 알아보려면 오른쪽으로 스와이프하세요!",
"automatic": "자동적 인",
"fixed_pair_not_supported": "이 고정 쌍은 선택한 교환에서 지원되지 않습니다.",
"variable_pair_not_supported": "이 변수 쌍은 선택한 교환에서 지원되지 않습니다.",
"none_of_selected_providers_can_exchange": "선택한 공급자 중 누구도 이 교환을 할 수 없습니다.",
"choose_one": "하나 선택"
}

View file

@ -366,7 +366,7 @@
"trade_for_not_created" : "Ruilen voor ${title} is niet gemaakt.",
"trade_not_created" : "Handel niet gecreëerd.",
"trade_not_created" : "Handel niet gecreëerd",
"trade_id_not_found" : "Handel ${tradeId} van ${title} niet gevonden.",
"trade_not_found" : "Handel niet gevonden.",
@ -533,7 +533,7 @@
"search_language": "Zoektaal",
"search_currency": "Zoek valuta",
"new_template" : "Nieuwe sjabloon",
"electrum_address_disclaimer": "We generate new addresses each time you use one, but previous addresses continue to work",
"electrum_address_disclaimer": "We genereren nieuwe adressen elke keer dat u er een gebruikt, maar eerdere adressen blijven werken",
"wallet_name_exists": "Portemonnee met die naam bestaat al",
"market_place": "Marktplaats",
"cake_pay_title": "Cake Pay-cadeaubonnen",
@ -634,5 +634,10 @@
"contact_support": "Contact opnemen met ondersteuning",
"gift_cards_unavailable": "Cadeaubonnen kunnen momenteel alleen worden gekocht via Monero, Bitcoin en Litecoin",
"introducing_cake_pay": "Introductie van Cake Pay!",
"cake_pay_learn_more": "Koop en wissel direct kaarten in de app!\nSwipe naar rechts voor meer informatie!"
"cake_pay_learn_more": "Koop en wissel direct kaarten in de app!\nSwipe naar rechts voor meer informatie!",
"automatic": "automatisch",
"fixed_pair_not_supported": "Dit vaste paar wordt niet ondersteund bij de geselecteerde exchanges",
"variable_pair_not_supported": "Dit variabelenpaar wordt niet ondersteund met de geselecteerde uitwisselingen",
"none_of_selected_providers_can_exchange": "Geen van de geselecteerde providers kan deze uitwisseling maken",
"choose_one": "Kies er een"
}

View file

@ -366,7 +366,7 @@
"trade_for_not_created" : "Zamienić się za ${title} nie jest tworzony.",
"trade_not_created" : "Handel nie utworzony.",
"trade_not_created" : "Handel nie utworzony",
"trade_id_not_found" : "Handel ${tradeId} of ${title} nie znaleziono.",
"trade_not_found" : "Nie znaleziono handlu.",
@ -634,5 +634,10 @@
"contact_support": "Skontaktuj się z pomocą techniczną",
"gift_cards_unavailable": "Karty podarunkowe można obecnie kupić tylko za pośrednictwem Monero, Bitcoin i Litecoin",
"introducing_cake_pay": "Przedstawiamy Ciasto Pay!",
"cake_pay_learn_more": "Natychmiast kupuj i realizuj karty w aplikacji!\nPrzesuń w prawo, aby dowiedzieć się więcej!"
"cake_pay_learn_more": "Natychmiast kupuj i realizuj karty w aplikacji!\nPrzesuń w prawo, aby dowiedzieć się więcej!",
"automatic": "Automatyczny",
"fixed_pair_not_supported": "Ta stała para nie jest obsługiwana na wybranych giełdach",
"variable_pair_not_supported": "Ta para zmiennych nie jest obsługiwana na wybranych giełdach",
"none_of_selected_providers_can_exchange": "Żaden z wybranych dostawców nie może dokonać tej wymiany",
"choose_one": "Wybierz jeden"
}

View file

@ -366,7 +366,7 @@
"trade_for_not_created" : "A troca por ${title} não foi criada.",
"trade_not_created" : "Troca não criada.",
"trade_not_created" : "Troca não criada",
"trade_id_not_found" : "A troca ${tradeId} de ${title} não foi encontrada.",
"trade_not_found" : "Troca não encontrada.",
@ -634,5 +634,10 @@
"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",
"introducing_cake_pay": "Apresentando o Cake Pay!",
"cake_pay_learn_more": "Compre e resgate cartões instantaneamente no aplicativo!\nDeslize para a direita para saber mais!"
"cake_pay_learn_more": "Compre e resgate cartões instantaneamente no aplicativo!\nDeslize para a direita para saber mais!",
"automatic": "Automático",
"fixed_pair_not_supported": "Este par fixo não é compatível com as exchanges selecionadas",
"variable_pair_not_supported": "Este par de variáveis não é compatível com as trocas selecionadas",
"none_of_selected_providers_can_exchange": "Nenhum dos provedores selecionados pode fazer esta troca",
"choose_one": "Escolha um"
}

View file

@ -366,7 +366,7 @@
"trade_for_not_created" : "Сделка для ${title} не создана.",
"trade_not_created" : "Сделка не создана.",
"trade_not_created" : "Сделка не создана",
"trade_id_not_found" : "Сделка ${tradeId} ${title} не найдена.",
"trade_not_found" : "Trade not found.",
@ -634,5 +634,10 @@
"contact_support": "Связаться со службой поддержки",
"gift_cards_unavailable": "В настоящее время подарочные карты можно приобрести только через Monero, Bitcoin и Litecoin.",
"introducing_cake_pay": "Представляем Cake Pay!",
"cake_pay_learn_more": "Мгновенно покупайте и погашайте карты в приложении!\nПроведите вправо, чтобы узнать больше!"
"cake_pay_learn_more": "Мгновенно покупайте и погашайте карты в приложении!\nПроведите вправо, чтобы узнать больше!",
"automatic": "автоматический",
"fixed_pair_not_supported": "Эта фиксированная пара не поддерживается выбранными биржами.",
"variable_pair_not_supported": "Эта пара переменных не поддерживается выбранными биржами.",
"none_of_selected_providers_can_exchange": "Ни один из выбранных провайдеров не может совершить этот обмен",
"choose_one": "Выбери один"
}

View file

@ -365,7 +365,7 @@
"trade_for_not_created" : "Операція для ${title} не створена.",
"trade_not_created" : "Операція не створена.",
"trade_not_created" : "Операція не створена",
"trade_id_not_found" : "Операція ${tradeId} ${title} не знайдена.",
"trade_not_found" : "Операція не знайдена.",
@ -633,5 +633,10 @@
"contact_support": "Звернутися до служби підтримки",
"gift_cards_unavailable": "Наразі подарункові картки можна придбати лише через Monero, Bitcoin і Litecoin",
"introducing_cake_pay": "Представляємо Cake Pay!",
"cake_pay_learn_more": "Миттєва купівля та погашення карток в додатку!\nПроведіть праворуч, щоб дізнатися більше!"
"cake_pay_learn_more": "Миттєва купівля та погашення карток в додатку!\nПроведіть праворуч, щоб дізнатися більше!",
"automatic": "Автоматичний",
"fixed_pair_not_supported": "Ця фіксована пара не підтримується вибраними біржами",
"variable_pair_not_supported": "Ця пара змінних не підтримується вибраними біржами",
"none_of_selected_providers_can_exchange": "Жоден із вибраних провайдерів не може здійснити цей обмін",
"choose_one": "Вибери один"
}

View file

@ -366,7 +366,7 @@
"trade_for_not_created" : "交易 ${title} 未创建.",
"trade_not_created" : "未建立交易.",
"trade_not_created" : "未建立交易",
"trade_id_not_found" : "交易方式 ${tradeId} 的 ${title} 未找到.",
"trade_not_found" : "找不到交易.",
@ -632,5 +632,10 @@
"contact_support": "联系支持",
"gift_cards_unavailable": "目前只能通过门罗币、比特币和莱特币购买礼品卡",
"introducing_cake_pay": "介绍 Cake Pay!",
"cake_pay_learn_more": "立即在应用程序中购买和兑换卡!\n向右滑动了解更多"
"cake_pay_learn_more": "立即在应用程序中购买和兑换卡!\n向右滑动了解更多",
"automatic": "自动的",
"fixed_pair_not_supported": "所选交易所不支持此固定货币对",
"variable_pair_not_supported": "所选交易所不支持此变量对",
"none_of_selected_providers_can_exchange": "选定的供应商都不能进行此交换",
"choose_one": "选一个"
}