From c50eeee58b417768a60c82281b25a9309dc25011 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Thu, 1 Sep 2022 16:12:38 +0200 Subject: [PATCH] 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 --- lib/di.dart | 4 +- lib/entities/preferences_key.dart | 2 + .../changenow_exchange_provider.dart | 3 + lib/exchange/exchange_provider.dart | 1 + .../exchange_provider_description.dart | 19 +- .../morphtoken_exchange_provider.dart | 3 + .../sideshift_exchange_provider.dart | 3 + .../simpleswap_exchange_provider.dart | 3 + .../xmrto/xmrto_exchange_provider.dart | 3 + lib/src/screens/exchange/exchange_page.dart | 26 +- .../widgets/present_provider_picker.dart | 73 ++-- .../exchange_trade/exchange_confirm_page.dart | 21 +- lib/src/widgets/check_box_picker.dart | 166 ++++++++ .../exchange/exchange_trade_view_model.dart | 2 +- .../exchange/exchange_view_model.dart | 382 +++++++++++------- res/values/strings_de.arb | 11 +- res/values/strings_en.arb | 9 +- res/values/strings_es.arb | 9 +- res/values/strings_fr.arb | 9 +- res/values/strings_hi.arb | 9 +- res/values/strings_hr.arb | 9 +- res/values/strings_it.arb | 9 +- res/values/strings_ja.arb | 37 +- res/values/strings_ko.arb | 9 +- res/values/strings_nl.arb | 11 +- res/values/strings_pl.arb | 9 +- res/values/strings_pt.arb | 9 +- res/values/strings_ru.arb | 9 +- res/values/strings_uk.arb | 9 +- res/values/strings_zh.arb | 9 +- 30 files changed, 627 insertions(+), 251 deletions(-) create mode 100644 lib/src/widgets/check_box_picker.dart diff --git a/lib/di.dart b/lib/di.dart index bd911335d..081aec364 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -485,7 +485,9 @@ Future setup( _tradesSource, getIt.get(), getIt.get(), - getIt.get().settingsStore)); + getIt.get().settingsStore, + getIt.get(), + )); getIt.registerFactory(() => ExchangeTradeViewModel( wallet: getIt.get().wallet, diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index f4a0008a2..6cf7e5608 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -26,4 +26,6 @@ class PreferencesKey { static String moneroWalletUpdateV1Key(String name) => '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}'; + + static const exchangeProvidersSelection = 'exchange-providers-selection'; } diff --git a/lib/exchange/changenow/changenow_exchange_provider.dart b/lib/exchange/changenow/changenow_exchange_provider.dart index c6594516d..df5afe5ab 100644 --- a/lib/exchange/changenow/changenow_exchange_provider.dart +++ b/lib/exchange/changenow/changenow_exchange_provider.dart @@ -39,6 +39,9 @@ class ChangeNowExchangeProvider extends ExchangeProvider { @override bool get isAvailable => true; + @override + bool get isEnabled => true; + @override ExchangeProviderDescription get description => ExchangeProviderDescription.changeNow; diff --git a/lib/exchange/exchange_provider.dart b/lib/exchange/exchange_provider.dart index 30a359fda..a960c0ad7 100644 --- a/lib/exchange/exchange_provider.dart +++ b/lib/exchange/exchange_provider.dart @@ -13,6 +13,7 @@ abstract class ExchangeProvider { List pairList; ExchangeProviderDescription description; bool get isAvailable; + bool get isEnabled; @override String toString() => title; diff --git a/lib/exchange/exchange_provider_description.dart b/lib/exchange/exchange_provider_description.dart index 7b3509fe1..651d0502e 100644 --- a/lib/exchange/exchange_provider_description.dart +++ b/lib/exchange/exchange_provider_description.dart @@ -2,20 +2,23 @@ import 'package:cw_core/enumerable_item.dart'; class ExchangeProviderDescription extends EnumerableItem with Serializable { - 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) { diff --git a/lib/exchange/morphtoken/morphtoken_exchange_provider.dart b/lib/exchange/morphtoken/morphtoken_exchange_provider.dart index cea92a1bb..4b3e6f646 100644 --- a/lib/exchange/morphtoken/morphtoken_exchange_provider.dart +++ b/lib/exchange/morphtoken/morphtoken_exchange_provider.dart @@ -63,6 +63,9 @@ class MorphTokenExchangeProvider extends ExchangeProvider { @override bool get isAvailable => true; + @override + bool get isEnabled => true; + @override ExchangeProviderDescription get description => ExchangeProviderDescription.morphToken; diff --git a/lib/exchange/sideshift/sideshift_exchange_provider.dart b/lib/exchange/sideshift/sideshift_exchange_provider.dart index 1f06de94e..878ffd528 100644 --- a/lib/exchange/sideshift/sideshift_exchange_provider.dart +++ b/lib/exchange/sideshift/sideshift_exchange_provider.dart @@ -244,6 +244,9 @@ class SideShiftExchangeProvider extends ExchangeProvider { @override bool get isAvailable => true; + @override + bool get isEnabled => true; + @override String get title => 'SideShift'; diff --git a/lib/exchange/simpleswap/simpleswap_exchange_provider.dart b/lib/exchange/simpleswap/simpleswap_exchange_provider.dart index 78e83dd4d..cf6fb3d38 100644 --- a/lib/exchange/simpleswap/simpleswap_exchange_provider.dart +++ b/lib/exchange/simpleswap/simpleswap_exchange_provider.dart @@ -197,6 +197,9 @@ class SimpleSwapExchangeProvider extends ExchangeProvider { @override bool get isAvailable => true; + @override + bool get isEnabled => true; + @override String get title => 'SimpleSwap'; diff --git a/lib/exchange/xmrto/xmrto_exchange_provider.dart b/lib/exchange/xmrto/xmrto_exchange_provider.dart index 12c6c8587..8f08d5a5b 100644 --- a/lib/exchange/xmrto/xmrto_exchange_provider.dart +++ b/lib/exchange/xmrto/xmrto_exchange_provider.dart @@ -44,6 +44,9 @@ class XMRTOExchangeProvider extends ExchangeProvider { @override bool get isAvailable => _isAvailable; + @override + bool get isEnabled => true; + @override ExchangeProviderDescription get description => ExchangeProviderDescription.xmrto; diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 3fce3029a..7e4aa6089 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -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(() { diff --git a/lib/src/screens/exchange/widgets/present_provider_picker.dart b/lib/src/screens/exchange/widgets/present_provider_picker.dart index 51738986a..cd3653ee5 100644 --- a/lib/src/screens/exchange/widgets/present_provider_picker.dart +++ b/lib/src/screens/exchange/widgets/present_provider_picker.dart @@ -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 = []; - 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( - builder: (BuildContext popUpContext) => Picker( - items: items, - images: images, - selectedAtIndex: selectedItem, + void _presentProviderPicker(BuildContext context) async { + await showPopUp( + 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( 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(); } } diff --git a/lib/src/screens/exchange_trade/exchange_confirm_page.dart b/lib/src/screens/exchange_trade/exchange_confirm_page.dart index 8fd4883df..661ed95a3 100644 --- a/lib/src/screens/exchange_trade/exchange_confirm_page.dart +++ b/lib/src/screens/exchange_trade/exchange_confirm_page.dart @@ -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: [ 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( diff --git a/lib/src/widgets/check_box_picker.dart b/lib/src/widgets/check_box_picker.dart new file mode 100644 index 000000000..b0d435caf --- /dev/null +++ b/lib/src/widgets/check_box_picker.dart @@ -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 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 { + CheckBoxPickerState(this.items); + + final List items; + + ScrollController controller = ScrollController(); + + @override + Widget build(BuildContext context) { + return AlertBackground( + child: Stack( + alignment: Alignment.center, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + 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: [ + (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; +} diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index 16523a8ce..6ca7ebeda 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -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 diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index f0ff5ee9e..d91c393ab 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -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(); + + final Map exchangeProvidersSelection = json + .decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map; + + /// 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 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 currentTradeAvailableProviders; + + @observable + ObservableList selectedProviders; + @observable List 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 exchangeProvidersSelection = json + .decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map; + + 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)); } } diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index d82b55911..4d4a6a1dd 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -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" } diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 077e81772..958e777f5 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -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" } diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 79f70a7b9..cd1177fa7 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -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" } diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 545583786..ec6782cac 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -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" } diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 4d56e3430..50fd5a2d2 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -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": "एक का चयन" } diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index db2531d51..3dbbc03be 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -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" } diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index db0459295..2fbdbe66c 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -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" } diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index c0f70cea7..79102b251 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -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 つ選択してください" } diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index da94dc1bb..42c2202c4 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -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": "하나 선택" } diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 75a7475e5..72b69dfab 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -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" } diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index d6c626913..9c6fbce26 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -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" } diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index cdc8e86c0..1f61e5a20 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -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" } diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 44dad0323..46ea635f8 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -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": "Выбери один" } diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index cd5d574a9..16b38de88 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -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": "Вибери один" } diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index f7375bdfb..170646ec9 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -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": "选一个" }