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

Omar Hatem committed Sep 1, 2022 at 16:12 UTC c50eeee58b417768a60c82281b25a9309dc25011
30 files changed +629 -253
lib/di.dart
+3 -1
@@ -485,7 +485,9 @@ Future setup(
485 _tradesSource,
486 getIt.get<ExchangeTemplateStore>(),
487 getIt.get<TradesStore>(),
488 - getIt.get<AppStore>().settingsStore));
488 + getIt.get<AppStore>().settingsStore,
489 + getIt.get<SharedPreferences>(),
490 + ));
491
492 getIt.registerFactory(() => ExchangeTradeViewModel(
493 wallet: getIt.get<AppStore>().wallet,
lib/entities/preferences_key.dart
+2
@@ -26,4 +26,6 @@ class PreferencesKey {
26
27 static String moneroWalletUpdateV1Key(String name)
28 => '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}';
29 +
30 + static const exchangeProvidersSelection = 'exchange-providers-selection';
31 }
lib/exchange/changenow/changenow_exchange_provider.dart
+3
@@ -39,6 +39,9 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
39 @override
40 bool get isAvailable => true;
41
42 + @override
43 + bool get isEnabled => true;
44 +
45 @override
46 ExchangeProviderDescription get description =>
47 ExchangeProviderDescription.changeNow;
lib/exchange/exchange_provider.dart
+1
@@ -13,6 +13,7 @@ abstract class ExchangeProvider {
13 List<ExchangePair> pairList;
14 ExchangeProviderDescription description;
15 bool get isAvailable;
16 + bool get isEnabled;
17
18 @override
19 String toString() => title;
lib/exchange/exchange_provider_description.dart
+11 -8
@@ -2,20 +2,23 @@ import 'package:cw_core/enumerable_item.dart';
2
3 class ExchangeProviderDescription extends EnumerableItem<int>
4 with Serializable<int> {
5 - const ExchangeProviderDescription({String title, int raw})
5 + const ExchangeProviderDescription({String title, int raw, this.horizontalLogo = false, this.image})
6 : super(title: title, raw: raw);
7
8 - static const xmrto = ExchangeProviderDescription(title: 'XMR.TO', raw: 0);
8 + final bool horizontalLogo;
9 + final String image;
10 +
11 + static const xmrto = ExchangeProviderDescription(title: 'XMR.TO', raw: 0, image: 'assets/images/xmrto.png');
12 static const changeNow =
10 - ExchangeProviderDescription(title: 'ChangeNOW', raw: 1);
13 + ExchangeProviderDescription(title: 'ChangeNOW', raw: 1, image: 'assets/images/changenow.png');
14 static const morphToken =
12 - ExchangeProviderDescription(title: 'MorphToken', raw: 2);
15 + ExchangeProviderDescription(title: 'MorphToken', raw: 2, image: 'assets/images/morph.png');
16
17 static const sideShift =
15 - ExchangeProviderDescription(title: 'SideShift', raw: 3);
16 -
17 - static const simpleSwap =
18 - ExchangeProviderDescription(title: 'SimpleSwap', raw: 4);
18 + ExchangeProviderDescription(title: 'SideShift', raw: 3, image: 'assets/images/sideshift.png');
19 +
20 + static const simpleSwap =
21 + ExchangeProviderDescription(title: 'SimpleSwap', raw: 4, image: 'assets/images/simpleSwap.png');
22
23 static ExchangeProviderDescription deserialize({int raw}) {
24 switch (raw) {
lib/exchange/morphtoken/morphtoken_exchange_provider.dart
+3
@@ -63,6 +63,9 @@ class MorphTokenExchangeProvider extends ExchangeProvider {
63 @override
64 bool get isAvailable => true;
65
66 + @override
67 + bool get isEnabled => true;
68 +
69 @override
70 ExchangeProviderDescription get description =>
71 ExchangeProviderDescription.morphToken;
lib/exchange/sideshift/sideshift_exchange_provider.dart
+3
@@ -244,6 +244,9 @@ class SideShiftExchangeProvider extends ExchangeProvider {
244 @override
245 bool get isAvailable => true;
246
247 + @override
248 + bool get isEnabled => true;
249 +
250 @override
251 String get title => 'SideShift';
252
lib/exchange/simpleswap/simpleswap_exchange_provider.dart
+3
@@ -197,6 +197,9 @@ class SimpleSwapExchangeProvider extends ExchangeProvider {
197 @override
198 bool get isAvailable => true;
199
200 + @override
201 + bool get isEnabled => true;
202 +
203 @override
204 String get title => 'SimpleSwap';
205
lib/exchange/xmrto/xmrto_exchange_provider.dart
+3
@@ -44,6 +44,9 @@ class XMRTOExchangeProvider extends ExchangeProvider {
44 @override
45 bool get isAvailable => _isAvailable;
46
47 + @override
48 + bool get isEnabled => true;
49 +
50 @override
51 ExchangeProviderDescription get description =>
52 ExchangeProviderDescription.xmrto;
lib/src/screens/exchange/exchange_page.dart
+11 -15
@@ -354,8 +354,12 @@ class ExchangePage extends BasePage {
354 padding: EdgeInsets.only(bottom: 15),
355 child: Observer(builder: (_) {
356 final description = exchangeViewModel.isFixedRateMode
357 + ? exchangeViewModel.isAvailableInSelected
358 ? S.of(context).amount_is_guaranteed
358 - : S.of(context).amount_is_estimate;
359 + : S.of(context).fixed_pair_not_supported
360 + : exchangeViewModel.isAvailableInSelected
361 + ? S.of(context).amount_is_estimate
362 + : S.of(context).variable_pair_not_supported;
363 return Center(
364 child: Text(
365 description,
@@ -399,8 +403,8 @@ class ExchangePage extends BasePage {
403 },
404 color: Theme.of(context).accentTextTheme.body2.color,
405 textColor: Colors.white,
402 - isLoading:
403 - exchangeViewModel.tradeState is TradeIsCreating)),
406 + isDisabled: exchangeViewModel.selectedProviders.isEmpty,
407 + isLoading: exchangeViewModel.tradeState is TradeIsCreating)),
408 ]),
409 )),
410 ));
@@ -518,13 +522,6 @@ class ExchangePage extends BasePage {
522 exchangeViewModel.changeReceiveCurrency(
523 currency: CryptoCurrency.fromString(template.receiveCurrency));
524
521 - switch (template.provider) {
522 - case 'ChangeNOW':
523 - exchangeViewModel.changeProvider(
524 - provider: exchangeViewModel.providerList[0]);
525 - break;
526 - }
527 -
525 exchangeViewModel.changeDepositAmount(amount: template.amount);
526 exchangeViewModel.depositAddress = template.depositAddress;
527 exchangeViewModel.receiveAddress = template.receiveAddress;
@@ -744,11 +741,10 @@ class ExchangePage extends BasePage {
741 });
742
743 _receiveAmountFocus.addListener(() {
747 - if(receiveAmountController.text.isNotEmpty){
748 - exchangeViewModel.isFixedRateMode = true;
749 - }
750 - exchangeViewModel.changeReceiveAmount(
751 - amount: receiveAmountController.text);
744 + if (_receiveAmountFocus.hasFocus) {
745 + exchangeViewModel.isFixedRateMode = true;
746 + }
747 + exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text);
748 });
749
750 _depositAmountFocus.addListener(() {
lib/src/screens/exchange/widgets/present_provider_picker.dart
+30 -43
@@ -1,11 +1,9 @@
1 import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
2 +import 'package:cake_wallet/src/widgets/check_box_picker.dart';
3 import 'package:cake_wallet/utils/show_pop_up.dart';
4 import 'package:flutter/material.dart';
4 -import 'package:cake_wallet/exchange/exchange_provider_description.dart';
5 -import 'package:cake_wallet/exchange/exchange_provider.dart';
5 import 'package:flutter_mobx/flutter_mobx.dart';
6 import 'package:cake_wallet/generated/i18n.dart';
8 -import 'package:cake_wallet/src/widgets/picker.dart';
7 import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart';
8
9 class PresentProviderPicker extends StatelessWidget {
@@ -38,11 +36,16 @@ class PresentProviderPicker extends StatelessWidget {
36 fontWeight: FontWeight.w600,
37 color: Colors.white)),
38 Observer(
41 - builder: (_) => Text('${exchangeViewModel.provider.title}',
42 - style: TextStyle(
43 - fontSize: 10.0,
44 - fontWeight: FontWeight.w500,
45 - color: Theme.of(context).textTheme.headline.color)))
39 + builder: (_) => Text(
40 + exchangeViewModel.selectedProviders.isEmpty
41 + ? S.of(context).choose_one
42 + : exchangeViewModel.selectedProviders.length > 1
43 + ? S.of(context).automatic
44 + : exchangeViewModel.selectedProviders.first.title,
45 + style: TextStyle(
46 + fontSize: 10.0,
47 + fontWeight: FontWeight.w500,
48 + color: Theme.of(context).textTheme.headline.color)))
49 ],
50 ),
51 SizedBox(width: 5),
@@ -54,41 +57,19 @@ class PresentProviderPicker extends StatelessWidget {
57 ));
58 }
59
57 - void _presentProviderPicker(BuildContext context) {
58 - final items = exchangeViewModel.providersForCurrentPair();
59 - final selectedItem = items.indexOf(exchangeViewModel.provider);
60 - final images = <Image>[];
61 - String description;
62 -
63 - for (var provider in items) {
64 - switch (provider.description) {
65 - case ExchangeProviderDescription.xmrto:
66 - images.add(Image.asset('assets/images/xmr_btc.png'));
67 - break;
68 - case ExchangeProviderDescription.changeNow:
69 - images.add(Image.asset('assets/images/change_now.png'));
70 - break;
71 - case ExchangeProviderDescription.morphToken:
72 - images.add(Image.asset('assets/images/morph_icon.png'));
73 - break;
74 - case ExchangeProviderDescription.sideShift:
75 - images.add(Image.asset('assets/images/sideshift.png', width: 20));
76 - break;
77 - case ExchangeProviderDescription.simpleSwap:
78 - images.add(Image.asset('assets/images/simpleSwap.png', width: 20));
79 - break;
80 - }
81 - }
82 -
83 - showPopUp<void>(
84 - builder: (BuildContext popUpContext) => Picker(
85 - items: items,
86 - images: images,
87 - selectedAtIndex: selectedItem,
60 + void _presentProviderPicker(BuildContext context) async {
61 + await showPopUp<void>(
62 + builder: (BuildContext popUpContext) => CheckBoxPicker(
63 + items: exchangeViewModel.providerList
64 + .map((e) => CheckBoxItem(
65 + e.title,
66 + exchangeViewModel.selectedProviders.contains(e),
67 + isDisabled: !exchangeViewModel.providersForCurrentPair().contains(e),
68 + ))
69 + .toList(),
70 title: S.of(context).change_exchange_provider,
89 - description: description,
90 - onItemSelected: (ExchangeProvider provider) {
91 - if (!provider.isAvailable) {
71 + onChanged: (int index, bool value) {
72 + if (!exchangeViewModel.providerList[index].isAvailable) {
73 showPopUp<void>(
74 builder: (BuildContext popUpContext) => AlertWithOneAction(
75 alertTitle: 'Error',
@@ -98,8 +79,14 @@ class PresentProviderPicker extends StatelessWidget {
79 context: context);
80 return;
81 }
101 - exchangeViewModel.changeProvider(provider: provider);
82 + if (value) {
83 + exchangeViewModel.addExchangeProvider(exchangeViewModel.providerList[index]);
84 + } else {
85 + exchangeViewModel.removeExchangeProvider(exchangeViewModel.providerList[index]);
86 + }
87 }),
88 context: context);
89 +
90 + exchangeViewModel.saveSelectedProviders();
91 }
92 }
lib/src/screens/exchange_trade/exchange_confirm_page.dart
+19 -2
@@ -1,3 +1,4 @@
1 +import 'package:cake_wallet/exchange/exchange_provider_description.dart';
2 import 'package:cake_wallet/store/dashboard/trades_store.dart';
3 import 'package:cake_wallet/utils/show_bar.dart';
4 import 'package:flutter/material.dart';
@@ -56,7 +57,7 @@ class ExchangeConfirmPage extends BasePage {
57 crossAxisAlignment: CrossAxisAlignment.center,
58 children: <Widget>[
59 Text(
59 - S.of(context).trade_id,
60 + "${trade.provider.title} ${S.of(context).trade_id}",
61 style: TextStyle(
62 fontSize: 12.0,
63 fontWeight: FontWeight.w500,
@@ -101,7 +102,23 @@ class ExchangeConfirmPage extends BasePage {
102 ],
103 ),
104 ),
104 - Flexible(child: Offstage()),
105 + Flexible(
106 + child: Center(
107 + child: Row(
108 + mainAxisAlignment: MainAxisAlignment.center,
109 + children: [
110 + (trade.provider.image?.isNotEmpty ?? false)
111 + ? Image.asset(trade.provider.image, height: 50)
112 + : const SizedBox(),
113 + if (!trade.provider.horizontalLogo)
114 + Padding(
115 + padding: const EdgeInsets.symmetric(horizontal: 8.0),
116 + child: Text(trade.provider.title),
117 + ),
118 + ],
119 + ),
120 + ),
121 + ),
122 ],
123 )),
124 PrimaryButton(
lib/src/widgets/check_box_picker.dart new
+166
@@ -0,0 +1,166 @@
1 +import 'dart:ui';
2 +import 'package:cake_wallet/palette.dart';
3 +import 'package:flutter/cupertino.dart';
4 +import 'package:flutter/material.dart';
5 +import 'package:cake_wallet/src/widgets/alert_background.dart';
6 +import 'package:cake_wallet/src/widgets/alert_close_button.dart';
7 +import 'package:cake_wallet/generated/i18n.dart';
8 +
9 +class CheckBoxPicker extends StatefulWidget {
10 + CheckBoxPicker({
11 + @required this.items,
12 + @required this.onChanged,
13 + this.title,
14 + this.displayItem,
15 + this.isSeparated = true,
16 + });
17 +
18 + final List<CheckBoxItem> items;
19 + final String title;
20 + final Widget Function(CheckBoxItem) displayItem;
21 + final bool isSeparated;
22 + final Function(int, bool) onChanged;
23 +
24 + @override
25 + CheckBoxPickerState createState() => CheckBoxPickerState(items);
26 +}
27 +
28 +class CheckBoxPickerState extends State<CheckBoxPicker> {
29 + CheckBoxPickerState(this.items);
30 +
31 + final List<CheckBoxItem> items;
32 +
33 + ScrollController controller = ScrollController();
34 +
35 + @override
36 + Widget build(BuildContext context) {
37 + return AlertBackground(
38 + child: Stack(
39 + alignment: Alignment.center,
40 + children: <Widget>[
41 + Column(
42 + mainAxisSize: MainAxisSize.min,
43 + children: <Widget>[
44 + if (widget.title?.isNotEmpty ?? false)
45 + Container(
46 + padding: EdgeInsets.symmetric(horizontal: 24),
47 + child: Text(
48 + widget.title,
49 + textAlign: TextAlign.center,
50 + style: TextStyle(
51 + fontSize: 18,
52 + fontFamily: 'Lato',
53 + fontWeight: FontWeight.bold,
54 + decoration: TextDecoration.none,
55 + color: Colors.white,
56 + ),
57 + ),
58 + ),
59 + Padding(
60 + padding: EdgeInsets.only(left: 24, right: 24, top: 24),
61 + child: ClipRRect(
62 + borderRadius: BorderRadius.all(Radius.circular(30)),
63 + child: Container(
64 + color: Theme.of(context).accentTextTheme.title.color,
65 + child: ConstrainedBox(
66 + constraints: BoxConstraints(
67 + maxHeight: MediaQuery.of(context).size.height * 0.65,
68 + ),
69 + child: Column(
70 + mainAxisSize: MainAxisSize.min,
71 + children: [
72 + Flexible(
73 + child: Stack(
74 + alignment: Alignment.center,
75 + children: <Widget>[
76 + (items?.length ?? 0) > 3
77 + ? Scrollbar(
78 + controller: controller,
79 + child: itemsList(),
80 + )
81 + : itemsList(),
82 + ],
83 + ),
84 + ),
85 + ],
86 + ),
87 + ),
88 + ),
89 + ),
90 + )
91 + ],
92 + ),
93 + AlertCloseButton(),
94 + ],
95 + ),
96 + );
97 + }
98 +
99 + Widget itemsList() {
100 + return Container(
101 + color: Theme.of(context).accentTextTheme.headline6.backgroundColor,
102 + child: ListView.separated(
103 + padding: EdgeInsets.zero,
104 + controller: controller,
105 + shrinkWrap: true,
106 + separatorBuilder: (context, index) => widget.isSeparated
107 + ? Divider(
108 + color: Theme.of(context).accentTextTheme.title.backgroundColor,
109 + height: 1,
110 + )
111 + : const SizedBox(),
112 + itemCount: items == null || items.isEmpty ? 0 : items.length,
113 + itemBuilder: (context, index) => buildItem(index),
114 + ),
115 + );
116 + }
117 +
118 + Widget buildItem(int index) {
119 + final item = items[index];
120 +
121 + return GestureDetector(
122 + onTap: () {
123 + Navigator.of(context).pop();
124 + },
125 + child: Container(
126 + height: 55,
127 + color: Theme.of(context).accentTextTheme.headline6.color,
128 + padding: EdgeInsets.only(left: 24, right: 24),
129 + child: CheckboxListTile(
130 + value: item.value,
131 + activeColor: item.value
132 + ? Palette.blueCraiola
133 + : Theme.of(context).accentTextTheme.subhead.decorationColor,
134 + checkColor: Colors.white,
135 + title: widget.displayItem?.call(item) ??
136 + Text(
137 + item.title,
138 + style: TextStyle(
139 + fontSize: 14,
140 + fontFamily: 'Lato',
141 + fontWeight: FontWeight.w600,
142 + color: item.isDisabled
143 + ? Colors.grey.withOpacity(0.5)
144 + : Theme.of(context).primaryTextTheme.title.color,
145 + decoration: TextDecoration.none,
146 + ),
147 + ),
148 + onChanged: (bool value) {
149 + item.value = value;
150 + widget.onChanged(index, value);
151 + setState(() {});
152 + },
153 + controlAffinity: ListTileControlAffinity.leading,
154 + ),
155 + ),
156 + );
157 + }
158 +}
159 +
160 +class CheckBoxItem {
161 + CheckBoxItem(this.title, this.value, {this.isDisabled = false});
162 +
163 + final String title;
164 + final bool isDisabled;
165 + bool value;
166 +}
lib/view_model/exchange/exchange_trade_view_model.dart
+1 -1
@@ -116,7 +116,7 @@ abstract class ExchangeTradeViewModelBase with Store {
116 items?.clear();
117
118 items.add(ExchangeTradeItem(
119 - title: S.current.id, data: '${trade.id}', isCopied: true));
119 + title: "${trade.provider.title} ${S.current.id}", data: '${trade.id}', isCopied: true));
120
121 if (trade.extraId != null) {
122 final title = trade.from == CryptoCurrency.xrp
lib/view_model/exchange/exchange_view_model.dart
+249 -137
@@ -1,3 +1,7 @@
1 +import 'dart:collection';
2 +import 'dart:convert';
3 +
4 +import 'package:cake_wallet/entities/preferences_key.dart';
5 import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart';
6 import 'package:cake_wallet/exchange/sideshift/sideshift_request.dart';
7 import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart';
@@ -27,6 +31,7 @@ import 'package:cake_wallet/exchange/morphtoken/morphtoken_exchange_provider.dar
31 import 'package:cake_wallet/exchange/morphtoken/morphtoken_request.dart';
32 import 'package:cake_wallet/store/templates/exchange_template_store.dart';
33 import 'package:cake_wallet/exchange/exchange_template.dart';
34 +import 'package:shared_preferences/shared_preferences.dart';
35
36 part 'exchange_view_model.g.dart';
37
@@ -34,10 +39,24 @@ class ExchangeViewModel = ExchangeViewModelBase with _$ExchangeViewModel;
39
40 abstract class ExchangeViewModelBase with Store {
41 ExchangeViewModelBase(this.wallet, this.trades, this._exchangeTemplateStore,
37 - this.tradesStore, this._settingsStore) {
42 + this.tradesStore, this._settingsStore, this.sharedPreferences) {
43 const excludeDepositCurrencies = [CryptoCurrency.xhv];
44 const excludeReceiveCurrencies = [CryptoCurrency.xlm, CryptoCurrency.xrp, CryptoCurrency.bnb, CryptoCurrency.xhv];
45 providerList = [ChangeNowExchangeProvider(), SideShiftExchangeProvider(), SimpleSwapExchangeProvider()];
46 +
47 + currentTradeAvailableProviders = SplayTreeMap<double, ExchangeProvider>();
48 +
49 + final Map<String, dynamic> exchangeProvidersSelection = json
50 + .decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map<String, dynamic>;
51 +
52 + /// if the provider is not in the user settings (user's first time or newly added provider)
53 + /// then use its default value decided by us
54 + selectedProviders = ObservableList.of(providerList.where(
55 + (element) => exchangeProvidersSelection[element.title] == null
56 + ? element.isEnabled
57 + : (exchangeProvidersSelection[element.title] as bool))
58 + .toList());
59 +
60 _initialPairBasedOnWallet();
61 isDepositAddressEnabled = !(depositCurrency == wallet.currency);
62 isReceiveAddressEnabled = !(receiveCurrency == wallet.currency);
@@ -48,7 +67,7 @@ abstract class ExchangeViewModelBase with Store {
67 ? wallet.walletAddresses.address : '';
68 limitsState = LimitsInitialState();
69 tradeState = ExchangeTradeStateInitial();
51 - _cryptoNumberFormat = NumberFormat()..maximumFractionDigits = 12;
70 + _cryptoNumberFormat = NumberFormat()..maximumFractionDigits = wallet.type == WalletType.bitcoin ? 8 : 12;
71 provider = providersForCurrentPair().first;
72 final initialProvider = provider;
73 provider.checkIsAvailable().then((bool isAvailable) {
@@ -79,10 +98,20 @@ abstract class ExchangeViewModelBase with Store {
98 final Box<Trade> trades;
99 final ExchangeTemplateStore _exchangeTemplateStore;
100 final TradesStore tradesStore;
101 + final SharedPreferences sharedPreferences;
102
103 @observable
104 ExchangeProvider provider;
105
106 + /// Maps in dart are not sorted by default
107 + /// SplayTreeMap is a map sorted by keys
108 + /// will use it to sort available providers
109 + /// depending on the amount they yield for the current trade
110 + SplayTreeMap<double, ExchangeProvider> currentTradeAvailableProviders;
111 +
112 + @observable
113 + ObservableList<ExchangeProvider> selectedProviders;
114 +
115 @observable
116 List<ExchangeProvider> providerList;
117
@@ -147,17 +176,7 @@ abstract class ExchangeViewModelBase with Store {
176
177 NumberFormat _cryptoNumberFormat;
178
150 - SettingsStore _settingsStore;
151 -
152 - @action
153 - void changeProvider({ExchangeProvider provider}) {
154 - this.provider = provider;
155 - depositAmount = '';
156 - receiveAmount = '';
157 - isFixedRateMode = false;
158 - _defineIsReceiveAmountEditable();
159 - loadLimits();
160 - }
179 + final SettingsStore _settingsStore;
180
181 @action
182 void changeDepositCurrency({CryptoCurrency currency}) {
@@ -188,20 +207,46 @@ abstract class ExchangeViewModelBase with Store {
207 return;
208 }
209
191 - final _amount = double.parse(amount.replaceAll(',', '.')) ?? 0;
192 -
193 - provider
194 - .calculateAmount(
195 - from: receiveCurrency,
196 - to: depositCurrency,
197 - amount: _amount,
198 - isFixedRateMode: isFixedRateMode,
199 - isReceiveAmount: true)
200 - .then((amount) => _cryptoNumberFormat
210 + final _enteredAmount = double.parse(amount.replaceAll(',', '.')) ?? 0;
211 +
212 + currentTradeAvailableProviders.clear();
213 + for (var provider in selectedProviders) {
214 + provider
215 + .calculateAmount(
216 + from: receiveCurrency,
217 + to: depositCurrency,
218 + amount: _enteredAmount,
219 + isFixedRateMode: isFixedRateMode,
220 + isReceiveAmount: true)
221 + .then((amount) {
222 +
223 + final from = isFixedRateMode
224 + ? receiveCurrency
225 + : depositCurrency;
226 + final to = isFixedRateMode
227 + ? depositCurrency
228 + : receiveCurrency;
229 +
230 + provider.fetchLimits(
231 + from: from,
232 + to: to,
233 + isFixedRateMode: isFixedRateMode,
234 + ).then((limits) {
235 + /// if the entered amount doesn't exceed the limits of this provider
236 + if ((limits.max ?? double.maxFinite) >= _enteredAmount
237 + && (limits.min ?? 0) <= _enteredAmount) {
238 + /// add this provider as its valid for this trade
239 + /// will be sorted ascending already since
240 + /// we seek the least deposit amount
241 + currentTradeAvailableProviders[amount] = provider;
242 + }
243 + return amount;
244 + }).then((amount) => depositAmount = _cryptoNumberFormat
245 .format(amount)
246 .toString()
203 - .replaceAll(RegExp('\\,'), ''))
204 - .then((amount) => depositAmount = amount);
247 + .replaceAll(RegExp('\\,'), ''));
248 + });
249 + }
250 }
251
252 @action
@@ -215,23 +260,56 @@ abstract class ExchangeViewModelBase with Store {
260 return;
261 }
262
218 - final _amount = double.parse(amount.replaceAll(',', '.')) ?? 0;
219 - provider
220 - .calculateAmount(
221 - from: depositCurrency,
222 - to: receiveCurrency,
223 - amount: _amount,
224 - isFixedRateMode: isFixedRateMode,
225 - isReceiveAmount: false)
226 - .then((amount) => _cryptoNumberFormat
263 + final _enteredAmount = double.tryParse(amount.replaceAll(',', '.')) ?? 0;
264 +
265 + currentTradeAvailableProviders.clear();
266 + for (var provider in selectedProviders) {
267 + provider
268 + .calculateAmount(
269 + from: depositCurrency,
270 + to: receiveCurrency,
271 + amount: _enteredAmount,
272 + isFixedRateMode: isFixedRateMode,
273 + isReceiveAmount: false)
274 + .then((amount) {
275 +
276 + final from = isFixedRateMode
277 + ? receiveCurrency
278 + : depositCurrency;
279 + final to = isFixedRateMode
280 + ? depositCurrency
281 + : receiveCurrency;
282 +
283 + provider.fetchLimits(
284 + from: from,
285 + to: to,
286 + isFixedRateMode: isFixedRateMode,
287 + ).then((limits) {
288 +
289 + /// if the entered amount doesn't exceed the limits of this provider
290 + if ((limits.max ?? double.maxFinite) >= _enteredAmount
291 + && (limits.min ?? 0) <= _enteredAmount) {
292 + /// add this provider as its valid for this trade
293 + /// subtract from maxFinite so the provider
294 + /// with the largest amount would be sorted ascending
295 + currentTradeAvailableProviders[double.maxFinite - amount] = provider;
296 + }
297 + return amount;
298 + }).then((amount) => receiveAmount =
299 + receiveAmount = _cryptoNumberFormat
300 .format(amount)
301 .toString()
229 - .replaceAll(RegExp('\\,'), ''))
230 - .then((amount) => receiveAmount = amount);
302 + .replaceAll(RegExp('\\,'), ''));
303 + });
304 + }
305 }
306
307 @action
308 Future loadLimits() async {
309 + if (selectedProviders.isEmpty) {
310 + return;
311 + }
312 +
313 limitsState = LimitsIsLoading();
314
315 try {
@@ -241,10 +319,29 @@ abstract class ExchangeViewModelBase with Store {
319 final to = isFixedRateMode
320 ? depositCurrency
321 : receiveCurrency;
244 - limits = await provider.fetchLimits(
322 +
323 + limits = await selectedProviders.first.fetchLimits(
324 from: from,
325 to: to,
326 isFixedRateMode: isFixedRateMode);
327 +
328 + /// if the first provider limits is bounded then check with other providers
329 + /// for the highest maximum limit
330 + if (limits.max != null) {
331 + for (int i = 1;i < selectedProviders.length;i++) {
332 + final Limits tempLimits = await selectedProviders[i].fetchLimits(
333 + from: from,
334 + to: to,
335 + isFixedRateMode: isFixedRateMode);
336 +
337 + /// set the limits with the maximum provider limit
338 + /// if there is a provider with null max then it's the maximum limit
339 + if ((tempLimits.max ?? double.maxFinite) > limits.max) {
340 + limits = tempLimits;
341 + }
342 + }
343 + }
344 +
345 limitsState = LimitsLoadedSuccessfully(limits: limits);
346 } catch (e) {
347 limitsState = LimitsLoadedFailure(error: e.toString());
@@ -255,102 +352,97 @@ abstract class ExchangeViewModelBase with Store {
352 Future createTrade() async {
353 TradeRequest request;
354 String amount;
258 - CryptoCurrency currency;
355
260 - if (provider is SideShiftExchangeProvider) {
261 - request = SideShiftRequest(
356 + for (var provider in currentTradeAvailableProviders.values) {
357 + if (!(await provider.checkIsAvailable())) {
358 + continue;
359 + }
360 +
361 + if (provider is SideShiftExchangeProvider) {
362 + request = SideShiftRequest(
363 depositMethod: depositCurrency,
364 settleMethod: receiveCurrency,
365 depositAmount: depositAmount?.replaceAll(',', '.'),
366 settleAddress: receiveAddress,
367 refundAddress: depositAddress,
267 - );
268 - amount = depositAmount;
269 - currency = depositCurrency;
270 - }
368 + );
369 + amount = depositAmount;
370 + }
371
272 - if (provider is SimpleSwapExchangeProvider) {
273 - request = SimpleSwapRequest(
372 + if (provider is SimpleSwapExchangeProvider) {
373 + request = SimpleSwapRequest(
374 from: depositCurrency,
375 to: receiveCurrency,
376 amount: depositAmount?.replaceAll(',', '.'),
377 address: receiveAddress,
378 refundAddress: depositAddress,
279 - );
280 - amount = depositAmount;
281 - currency = depositCurrency;
282 - }
379 + );
380 + amount = depositAmount;
381 + }
382
284 - if (provider is XMRTOExchangeProvider) {
285 - request = XMRTOTradeRequest(
286 - from: depositCurrency,
287 - to: receiveCurrency,
288 - amount: depositAmount?.replaceAll(',', '.'),
289 - receiveAmount: receiveAmount?.replaceAll(',', '.'),
290 - address: receiveAddress,
291 - refundAddress: depositAddress,
292 - isBTCRequest: isReceiveAmountEntered);
293 - amount = depositAmount;
294 - currency = depositCurrency;
295 - }
383 + if (provider is XMRTOExchangeProvider) {
384 + request = XMRTOTradeRequest(
385 + from: depositCurrency,
386 + to: receiveCurrency,
387 + amount: depositAmount?.replaceAll(',', '.'),
388 + receiveAmount: receiveAmount?.replaceAll(',', '.'),
389 + address: receiveAddress,
390 + refundAddress: depositAddress,
391 + isBTCRequest: isReceiveAmountEntered);
392 + amount = depositAmount;
393 + }
394
297 - if (provider is ChangeNowExchangeProvider) {
298 - request = ChangeNowRequest(
299 - from: depositCurrency,
300 - to: receiveCurrency,
301 - fromAmount: depositAmount?.replaceAll(',', '.'),
302 - toAmount: receiveAmount?.replaceAll(',', '.'),
303 - refundAddress: depositAddress,
304 - address: receiveAddress,
305 - isReverse: isReverse);
306 - amount = isReverse ? receiveAmount : depositAmount;
307 - currency = depositCurrency;
308 - }
395 + if (provider is ChangeNowExchangeProvider) {
396 + request = ChangeNowRequest(
397 + from: depositCurrency,
398 + to: receiveCurrency,
399 + fromAmount: depositAmount?.replaceAll(',', '.'),
400 + toAmount: receiveAmount?.replaceAll(',', '.'),
401 + refundAddress: depositAddress,
402 + address: receiveAddress,
403 + isReverse: isReverse);
404 + amount = isReverse ? receiveAmount : depositAmount;
405 + }
406
310 - if (provider is MorphTokenExchangeProvider) {
311 - request = MorphTokenRequest(
312 - from: depositCurrency,
313 - to: receiveCurrency,
314 - amount: depositAmount?.replaceAll(',', '.'),
315 - refundAddress: depositAddress,
316 - address: receiveAddress);
317 - amount = depositAmount;
318 - currency = depositCurrency;
319 - }
407 + if (provider is MorphTokenExchangeProvider) {
408 + request = MorphTokenRequest(
409 + from: depositCurrency,
410 + to: receiveCurrency,
411 + amount: depositAmount?.replaceAll(',', '.'),
412 + refundAddress: depositAddress,
413 + address: receiveAddress);
414 + amount = depositAmount;
415 + }
416
321 - amount = amount.replaceAll(',', '.');
322 -
323 - if (limitsState is LimitsLoadedSuccessfully && amount != null) {
324 - if (double.parse(amount) < limits.min) {
325 - tradeState = TradeIsCreatedFailure(
326 - title: provider.title,
327 - error: S.current.error_text_minimal_limit('${provider.description}',
328 - '${limits.min}', currency.toString()));
329 - } else if (limits.max != null && double.parse(amount) > limits.max) {
330 - tradeState = TradeIsCreatedFailure(
331 - title: provider.title,
332 - error: S.current.error_text_maximum_limit('${provider.description}',
333 - '${limits.max}', currency.toString()));
334 - } else {
335 - try {
336 - tradeState = TradeIsCreating();
337 - final trade = await provider.createTrade(
338 - request: request, isFixedRateMode: isFixedRateMode);
339 - trade.walletId = wallet.id;
340 - tradesStore.setTrade(trade);
341 - await trades.add(trade);
342 - tradeState = TradeIsCreatedSuccessfully(trade: trade);
343 - } catch (e) {
344 - tradeState =
345 - TradeIsCreatedFailure(title: provider.title, error: e.toString());
417 + amount = amount.replaceAll(',', '.');
418 +
419 + if (limitsState is LimitsLoadedSuccessfully && amount != null) {
420 + if (double.parse(amount) < limits.min) {
421 + continue;
422 + } else if (limits.max != null && double.parse(amount) > limits.max) {
423 + continue;
424 + } else {
425 + try {
426 + tradeState = TradeIsCreating();
427 + final trade = await provider.createTrade(
428 + request: request, isFixedRateMode: isFixedRateMode);
429 + trade.walletId = wallet.id;
430 + tradesStore.setTrade(trade);
431 + await trades.add(trade);
432 + tradeState = TradeIsCreatedSuccessfully(trade: trade);
433 + /// return after the first successful trade
434 + return;
435 + } catch (e) {
436 + continue;
437 + }
438 }
439 }
348 - } else {
349 - tradeState = TradeIsCreatedFailure(
350 - title: provider.title,
351 - error: S.current
352 - .error_text_limits_loading_failed('${provider.description}'));
440 }
441 +
442 + /// if the code reached here then none of the providers succeeded
443 + tradeState = TradeIsCreatedFailure(
444 + title: S.current.trade_not_created,
445 + error: S.current.none_of_selected_providers_can_exchange);
446 }
447
448 @action
@@ -414,7 +506,7 @@ abstract class ExchangeViewModelBase with Store {
506 final providers = providerList
507 .where((provider) => provider.pairList
508 .where((pair) =>
417 - pair.from == depositCurrency && pair.to == receiveCurrency)
509 + pair.from == (from ?? depositCurrency) && pair.to == (to ?? receiveCurrency))
510 .isNotEmpty)
511 .toList();
512
@@ -422,27 +514,8 @@ abstract class ExchangeViewModelBase with Store {
514 }
515
516 void _onPairChange() {
425 - final isPairExist = provider.pairList
426 - .where((pair) =>
427 - pair.from == depositCurrency && pair.to == receiveCurrency)
428 - .isNotEmpty;
429 -
430 - if (isPairExist) {
431 - final provider =
432 - _providerForPair(from: depositCurrency, to: receiveCurrency);
433 -
434 - if (provider != null) {
435 - changeProvider(provider: provider);
436 - }
437 - } else {
438 - depositAmount = '';
439 - receiveAmount = '';
440 - }
441 - }
442 -
443 - ExchangeProvider _providerForPair({CryptoCurrency from, CryptoCurrency to}) {
444 - final providers = _providersForPair(from: from, to: to);
445 - return providers.isNotEmpty ? providers[0] : null;
517 + depositAmount = '';
518 + receiveAmount = '';
519 }
520
521 void _initialPairBasedOnWallet() {
@@ -473,6 +546,45 @@ abstract class ExchangeViewModelBase with Store {
546 isReceiveAmountEditable = false;
547 }*/
548 //isReceiveAmountEditable = false;
476 - isReceiveAmountEditable = provider is ChangeNowExchangeProvider || provider is SimpleSwapExchangeProvider;
549 + // isReceiveAmountEditable = selectedProviders.any((provider) => provider is ChangeNowExchangeProvider);
550 + // isReceiveAmountEditable = provider is ChangeNowExchangeProvider || provider is SimpleSwapExchangeProvider;
551 + isReceiveAmountEditable = true;
552 + }
553 +
554 + @action
555 + void addExchangeProvider(ExchangeProvider provider) {
556 + selectedProviders.add(provider);
557 + }
558 +
559 + @action
560 + void removeExchangeProvider(ExchangeProvider provider) {
561 + selectedProviders.remove(provider);
562 + }
563 +
564 + @action
565 + void saveSelectedProviders() {
566 + depositAmount = '';
567 + receiveAmount = '';
568 + isFixedRateMode = false;
569 + _defineIsReceiveAmountEditable();
570 + loadLimits();
571 +
572 + final Map<String, dynamic> exchangeProvidersSelection = json
573 + .decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map<String, dynamic>;
574 +
575 + exchangeProvidersSelection.updateAll((key, dynamic value) => false);
576 + for (var provider in selectedProviders) {
577 + exchangeProvidersSelection[provider.title] = true;
578 + }
579 +
580 + sharedPreferences.setString(
581 + PreferencesKey.exchangeProvidersSelection,
582 + json.encode(exchangeProvidersSelection),
583 + );
584 + }
585 +
586 + bool get isAvailableInSelected {
587 + final providersForPair = providersForCurrentPair();
588 + return selectedProviders.any((element) => element.isAvailable && providersForPair.contains(element));
589 }
590 }
res/values/strings_de.arb
+8 -3
@@ -366,7 +366,7 @@
366
367
368 "trade_for_not_created" : "Handel für ${title} wird nicht erstellt.",
369 - "trade_not_created" : "Handel nicht erstellt.",
369 + "trade_not_created" : "Handel nicht erstellt",
370 "trade_id_not_found" : "Handel ${tradeId} von ${title} nicht gefunden.",
371 "trade_not_found" : "Handel nicht gefunden.",
372
@@ -586,7 +586,7 @@
586 "use_card_info_two": "Tegoeden worden omgezet naar USD wanneer ze op de prepaid-rekening staan, niet in digitale valuta.",
587 "use_card_info_three": "Gebruik de digitale kaart online of met contactloze betaalmethoden.",
588 "optionally_order_card": "Optioneel een fysieke kaart bestellen.",
589 - "hide_details" : "Details verbergen",
589 + "hide_details" : "Details verbergen",
590 "show_details" : "Toon details",
591 "upto": "tot ${value}",
592 "discount": "Bespaar ${value}%",
@@ -634,5 +634,10 @@
634 "contact_support": "Support kontaktieren",
635 "gift_cards_unavailable": "Geschenkkarten können derzeit nur über Monero, Bitcoin und Litecoin erworben werden",
636 "introducing_cake_pay": "Einführung von Cake Pay!",
637 - "cake_pay_learn_more": "Karten sofort in der App kaufen und einlösen!\nWischen Sie nach rechts, um mehr zu erfahren!"
637 + "cake_pay_learn_more": "Karten sofort in der App kaufen und einlösen!\nWischen Sie nach rechts, um mehr zu erfahren!",
638 + "automatic": "Automatisch",
639 + "fixed_pair_not_supported": "Dieses feste Paar wird von den ausgewählten Vermittlungsstellen nicht unterstützt",
640 + "variable_pair_not_supported": "Dieses Variablenpaar wird von den ausgewählten Börsen nicht unterstützt",
641 + "none_of_selected_providers_can_exchange": "Keiner der ausgewählten Anbieter kann diesen Austausch vornehmen",
642 + "choose_one": "Wähle ein"
643 }
res/values/strings_en.arb
+7 -2
@@ -366,7 +366,7 @@
366
367
368 "trade_for_not_created" : "Trade for ${title} is not created.",
369 - "trade_not_created" : "Trade not created.",
369 + "trade_not_created" : "Trade not created",
370 "trade_id_not_found" : "Trade ${tradeId} of ${title} not found.",
371 "trade_not_found" : "Trade not found.",
372
@@ -634,5 +634,10 @@
634 "contact_support": "Contact Support",
635 "gift_cards_unavailable": "Gift cards are available for purchase only with Monero, Bitcoin, and Litecoin at this time",
636 "introducing_cake_pay": "Introducing Cake Pay!",
637 - "cake_pay_learn_more": "Instantly purchase and redeem cards in the app!\nSwipe right to learn more!"
637 + "cake_pay_learn_more": "Instantly purchase and redeem cards in the app!\nSwipe right to learn more!",
638 + "automatic": "Automatic",
639 + "fixed_pair_not_supported": "This fixed pair is not supported with the selected exchanges",
640 + "variable_pair_not_supported": "This variable pair is not supported with the selected exchanges",
641 + "none_of_selected_providers_can_exchange": "None of the selected providers can make this exchange",
642 + "choose_one": "Choose one"
643 }
res/values/strings_es.arb
+7 -2
@@ -366,7 +366,7 @@
366
367
368 "trade_for_not_created" : "Comercio por ${title} no se crea.",
369 - "trade_not_created" : "Comercio no se crea.",
369 + "trade_not_created" : "Comercio no se crea",
370 "trade_id_not_found" : "Comercio ${tradeId} de ${title} no encontrado.",
371 "trade_not_found" : "Comercio no encontrado.",
372
@@ -634,5 +634,10 @@
634 "contact_support": "Contactar con Soporte",
635 "gift_cards_unavailable": "Las tarjetas de regalo están disponibles para comprar solo a través de Monero, Bitcoin y Litecoin en este momento",
636 "introducing_cake_pay": "¡Presentamos Cake Pay!",
637 - "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!"
637 + "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!",
638 + "automatic": "Automático",
639 + "fixed_pair_not_supported": "Este par fijo no es compatible con los intercambios seleccionados",
640 + "variable_pair_not_supported": "Este par de variables no es compatible con los intercambios seleccionados",
641 + "none_of_selected_providers_can_exchange": "Ninguno de los proveedores seleccionados puede realizar este intercambio",
642 + "choose_one": "Elige uno"
643 }
res/values/strings_fr.arb
+7 -2
@@ -364,7 +364,7 @@
364
365
366 "trade_for_not_created" : "L'échange pour ${title} n'est pas créé.",
367 - "trade_not_created" : "Échange non créé.",
367 + "trade_not_created" : "Échange non créé",
368 "trade_id_not_found" : "Échange ${tradeId} de ${title} introuvable.",
369 "trade_not_found" : "Échange introuvable.",
370
@@ -632,5 +632,10 @@
632 "contact_support": "Contacter l'assistance",
633 "gift_cards_unavailable": "Les cartes-cadeaux ne sont disponibles à l'achat que via Monero, Bitcoin et Litecoin pour le moment",
634 "introducing_cake_pay": "Présentation de Cake Pay!",
635 - "cake_pay_learn_more": "Achetez et échangez instantanément des cartes dans l'application !\nBalayez vers la droite pour en savoir plus !"
635 + "cake_pay_learn_more": "Achetez et échangez instantanément des cartes dans l'application !\nBalayez vers la droite pour en savoir plus !",
636 + "automatic": "Automatique",
637 + "fixed_pair_not_supported": "Cette paire fixe n'est pas prise en charge avec les échanges sélectionnés",
638 + "variable_pair_not_supported": "Cette paire de variables n'est pas prise en charge avec les échanges sélectionnés",
639 + "none_of_selected_providers_can_exchange": "Aucun des prestataires sélectionnés ne peut effectuer cet échange",
640 + "choose_one": "Choisissez-en un"
641 }
res/values/strings_hi.arb
+7 -2
@@ -366,7 +366,7 @@
366
367
368 "trade_for_not_created" : "के लिए व्यापार ${title} निर्मित नहीं है.",
369 - "trade_not_created" : "व्यापार नहीं बनाया गया.",
369 + "trade_not_created" : "व्यापार नहीं बनाया गया",
370 "trade_id_not_found" : "व्यापार ${tradeId} of ${title} नहीं मिला.",
371 "trade_not_found" : "व्यापार नहीं मिला",
372
@@ -634,5 +634,10 @@
634 "contact_support": "सहायता से संपर्क करें",
635 "gift_cards_unavailable": "उपहार कार्ड इस समय केवल मोनेरो, बिटकॉइन और लिटकोइन के माध्यम से खरीदने के लिए उपलब्ध हैं",
636 "introducing_cake_pay": "परिचय Cake Pay!",
637 - "cake_pay_learn_more": "ऐप में तुरंत कार्ड खरीदें और रिडीम करें!\nअधिक जानने के लिए दाएं स्वाइप करें!"
637 + "cake_pay_learn_more": "ऐप में तुरंत कार्ड खरीदें और रिडीम करें!\nअधिक जानने के लिए दाएं स्वाइप करें!",
638 + "automatic": "स्वचालित",
639 + "fixed_pair_not_supported": "यह निश्चित जोड़ी चयनित एक्सचेंजों के साथ समर्थित नहीं है",
640 + "variable_pair_not_supported": "यह परिवर्तनीय जोड़ी चयनित एक्सचेंजों के साथ समर्थित नहीं है",
641 + "none_of_selected_providers_can_exchange": "चयनित प्रदाताओं में से कोई भी इस एक्सचेंज को नहीं बना सकता",
642 + "choose_one": "एक का चयन"
643 }
res/values/strings_hr.arb
+7 -2
@@ -366,7 +366,7 @@
366
367
368 "trade_for_not_created" : "Razmjena za ${title} nije izrađena.",
369 - "trade_not_created" : "Razmjena nije izrađena.",
369 + "trade_not_created" : "Razmjena nije izrađena",
370 "trade_id_not_found" : "Razmjena ${tradeId} za ${title} nije pronađena.",
371 "trade_not_found" : "Razmjena nije pronađena.",
372
@@ -634,5 +634,10 @@
634 "contact_support": "Kontaktirajte podršku",
635 "gift_cards_unavailable": "Poklon kartice trenutno su dostupne za kupnju samo putem Monera, Bitcoina i Litecoina",
636 "introducing_cake_pay": "Predstavljamo Cake Pay!",
637 - "cake_pay_learn_more": "Odmah kupite i iskoristite kartice u aplikaciji!\nPrijeđite prstom udesno da biste saznali više!"
637 + "cake_pay_learn_more": "Odmah kupite i iskoristite kartice u aplikaciji!\nPrijeđite prstom udesno da biste saznali više!",
638 + "automatic": "Automatski",
639 + "fixed_pair_not_supported": "Ovaj fiksni par nije podržan s odabranim burzama",
640 + "variable_pair_not_supported": "Ovaj par varijabli nije podržan s odabranim burzama",
641 + "none_of_selected_providers_can_exchange": "Niti jedan od odabranih pružatelja usluga ne može izvršiti ovu razmjenu",
642 + "choose_one": "Izaberi jedan"
643 }
res/values/strings_it.arb
+7 -2
@@ -366,7 +366,7 @@
366
367
368 "trade_for_not_created" : "Lo scambio per ${title} non è stato creato.",
369 - "trade_not_created" : "Scambio non creato.",
369 + "trade_not_created" : "Scambio non creato",
370 "trade_id_not_found" : "Scambio ${tradeId} di ${title} not trovato.",
371 "trade_not_found" : "Scambio non trovato.",
372
@@ -634,5 +634,10 @@
634 "contact_support": "Contatta l'assistenza",
635 "gift_cards_unavailable": "Le carte regalo sono disponibili per l'acquisto solo tramite Monero, Bitcoin e Litecoin in questo momento",
636 "introducing_cake_pay": "Presentazione di Cake Pay!",
637 - "cake_pay_learn_more": "Acquista e riscatta istantaneamente le carte nell'app!\nScorri verso destra per saperne di più!"
637 + "cake_pay_learn_more": "Acquista e riscatta istantaneamente le carte nell'app!\nScorri verso destra per saperne di più!",
638 + "automatic": "Automatico",
639 + "fixed_pair_not_supported": "Questa coppia fissa non è supportata con gli scambi selezionati",
640 + "variable_pair_not_supported": "Questa coppia di variabili non è supportata con gli scambi selezionati",
641 + "none_of_selected_providers_can_exchange": "Nessuno dei fornitori selezionati può effettuare questo scambio",
642 + "choose_one": "Scegline uno"
643 }
res/values/strings_ja.arb
+21 -16
@@ -612,21 +612,21 @@
612 "cards": "カード",
613 "active": "アクティブ",
614 "redeemed": "償還",
615 - "gift_card_balance_note": "残高が残っているギフトカードがここに表示されます",
616 - "gift_card_redeemed_note": "利用したギフトカードがここに表示されます",
617 - "logout": "ログアウト",
618 - "add_tip": "ヒントを追加",
619 - "percentageOf": "of ${amount}",
620 - "is_percentage": "is",
621 - "search_category": "検索カテゴリ",
622 - "mark_as_redeemed": "償還済みとしてマーク",
623 - "more_options": "その他のオプション",
624 - "awaiting_payment_confirmation": "支払い確認を待っています",
625 - "transaction_sent_notice": "1分経っても画面が進まない場合は、ブロックエクスプローラーとメールアドレスを確認してください。",
626 - "agree": "同意する",
627 - "in_store": "インストア",
628 - "generated_gift_card": "ギフトカードの生成",
629 - "payment_was_received": "お支払いを受け取りました。",
615 + "gift_card_balance_note": "残高が残っているギフトカードがここに表示されます",
616 + "gift_card_redeemed_note": "利用したギフトカードがここに表示されます",
617 + "logout": "ログアウト",
618 + "add_tip": "ヒントを追加",
619 + "percentageOf": "of ${amount}",
620 + "is_percentage": "is",
621 + "search_category": "検索カテゴリ",
622 + "mark_as_redeemed": "償還済みとしてマーク",
623 + "more_options": "その他のオプション",
624 + "awaiting_payment_confirmation": "支払い確認を待っています",
625 + "transaction_sent_notice": "1分経っても画面が進まない場合は、ブロックエクスプローラーとメールアドレスを確認してください。",
626 + "agree": "同意する",
627 + "in_store": "インストア",
628 + "generated_gift_card": "ギフトカードの生成",
629 + "payment_was_received": "お支払いを受け取りました。",
630 "proceed_after_one_minute": "1分経っても画面が進まない場合は、メールを確認してください。",
631 "order_id": "注文ID",
632 "gift_card_is_generated": "ギフトカードが生成されます",
@@ -634,5 +634,10 @@
634 "contact_support": "サポートに連絡する",
635 "gift_cards_unavailable": "現時点では、ギフトカードはMonero、Bitcoin、Litecoinからのみ購入できます。",
636 "introducing_cake_pay": "序章Cake Pay!",
637 - "cake_pay_learn_more": "アプリですぐにカードを購入して引き換えましょう!\n右にスワイプして詳細をご覧ください。"
637 + "cake_pay_learn_more": "アプリですぐにカードを購入して引き換えましょう!\n右にスワイプして詳細をご覧ください。",
638 + "automatic": "自動",
639 + "fixed_pair_not_supported": "この固定ペアは、選択したエクスチェンジではサポートされていません",
640 + "variable_pair_not_supported": "この変数ペアは、選択した取引所ではサポートされていません",
641 + "none_of_selected_providers_can_exchange": "選択したプロバイダーはいずれもこの交換を行うことができません",
642 + "choose_one": "1 つ選択してください"
643 }
res/values/strings_ko.arb
+7 -2
@@ -366,7 +366,7 @@
366
367
368 "trade_for_not_created" : "거래 ${title} 생성되지 않습니다.",
369 - "trade_not_created" : "거래가 생성되지 않았습니다.",
369 + "trade_not_created" : "거래가 생성되지 않았습니다",
370 "trade_id_not_found" : "무역 ${tradeId} 의 ${title} 찾을 수 없습니다.",
371 "trade_not_found" : "거래를 찾을 수 없습니다.",
372
@@ -634,5 +634,10 @@
634 "contact_support": "지원팀에 문의",
635 "gift_cards_unavailable": "기프트 카드는 현재 Monero, Bitcoin 및 Litecoin을 통해서만 구매할 수 있습니다.",
636 "introducing_cake_pay": "소개 Cake Pay!",
637 - "cake_pay_learn_more": "앱에서 즉시 카드를 구매하고 사용하세요!\n자세히 알아보려면 오른쪽으로 스와이프하세요!"
637 + "cake_pay_learn_more": "앱에서 즉시 카드를 구매하고 사용하세요!\n자세히 알아보려면 오른쪽으로 스와이프하세요!",
638 + "automatic": "자동적 인",
639 + "fixed_pair_not_supported": "이 고정 쌍은 선택한 교환에서 지원되지 않습니다.",
640 + "variable_pair_not_supported": "이 변수 쌍은 선택한 교환에서 지원되지 않습니다.",
641 + "none_of_selected_providers_can_exchange": "선택한 공급자 중 누구도 이 교환을 할 수 없습니다.",
642 + "choose_one": "하나 선택"
643 }
res/values/strings_nl.arb
+8 -3
@@ -366,7 +366,7 @@
366
367
368 "trade_for_not_created" : "Ruilen voor ${title} is niet gemaakt.",
369 - "trade_not_created" : "Handel niet gecreëerd.",
369 + "trade_not_created" : "Handel niet gecreëerd",
370 "trade_id_not_found" : "Handel ${tradeId} van ${title} niet gevonden.",
371 "trade_not_found" : "Handel niet gevonden.",
372
@@ -533,7 +533,7 @@
533 "search_language": "Zoektaal",
534 "search_currency": "Zoek valuta",
535 "new_template" : "Nieuwe sjabloon",
536 - "electrum_address_disclaimer": "We generate new addresses each time you use one, but previous addresses continue to work",
536 + "electrum_address_disclaimer": "We genereren nieuwe adressen elke keer dat u er een gebruikt, maar eerdere adressen blijven werken",
537 "wallet_name_exists": "Portemonnee met die naam bestaat al",
538 "market_place": "Marktplaats",
539 "cake_pay_title": "Cake Pay-cadeaubonnen",
@@ -634,5 +634,10 @@
634 "contact_support": "Contact opnemen met ondersteuning",
635 "gift_cards_unavailable": "Cadeaubonnen kunnen momenteel alleen worden gekocht via Monero, Bitcoin en Litecoin",
636 "introducing_cake_pay": "Introductie van Cake Pay!",
637 - "cake_pay_learn_more": "Koop en wissel direct kaarten in de app!\nSwipe naar rechts voor meer informatie!"
637 + "cake_pay_learn_more": "Koop en wissel direct kaarten in de app!\nSwipe naar rechts voor meer informatie!",
638 + "automatic": "automatisch",
639 + "fixed_pair_not_supported": "Dit vaste paar wordt niet ondersteund bij de geselecteerde exchanges",
640 + "variable_pair_not_supported": "Dit variabelenpaar wordt niet ondersteund met de geselecteerde uitwisselingen",
641 + "none_of_selected_providers_can_exchange": "Geen van de geselecteerde providers kan deze uitwisseling maken",
642 + "choose_one": "Kies er een"
643 }
res/values/strings_pl.arb
+7 -2
@@ -366,7 +366,7 @@
366
367
368 "trade_for_not_created" : "Zamienić się za ${title} nie jest tworzony.",
369 - "trade_not_created" : "Handel nie utworzony.",
369 + "trade_not_created" : "Handel nie utworzony",
370 "trade_id_not_found" : "Handel ${tradeId} of ${title} nie znaleziono.",
371 "trade_not_found" : "Nie znaleziono handlu.",
372
@@ -634,5 +634,10 @@
634 "contact_support": "Skontaktuj się z pomocą techniczną",
635 "gift_cards_unavailable": "Karty podarunkowe można obecnie kupić tylko za pośrednictwem Monero, Bitcoin i Litecoin",
636 "introducing_cake_pay": "Przedstawiamy Ciasto Pay!",
637 - "cake_pay_learn_more": "Natychmiast kupuj i realizuj karty w aplikacji!\nPrzesuń w prawo, aby dowiedzieć się więcej!"
637 + "cake_pay_learn_more": "Natychmiast kupuj i realizuj karty w aplikacji!\nPrzesuń w prawo, aby dowiedzieć się więcej!",
638 + "automatic": "Automatyczny",
639 + "fixed_pair_not_supported": "Ta stała para nie jest obsługiwana na wybranych giełdach",
640 + "variable_pair_not_supported": "Ta para zmiennych nie jest obsługiwana na wybranych giełdach",
641 + "none_of_selected_providers_can_exchange": "Żaden z wybranych dostawców nie może dokonać tej wymiany",
642 + "choose_one": "Wybierz jeden"
643 }
res/values/strings_pt.arb
+7 -2
@@ -366,7 +366,7 @@
366
367
368 "trade_for_not_created" : "A troca por ${title} não foi criada.",
369 - "trade_not_created" : "Troca não criada.",
369 + "trade_not_created" : "Troca não criada",
370 "trade_id_not_found" : "A troca ${tradeId} de ${title} não foi encontrada.",
371 "trade_not_found" : "Troca não encontrada.",
372
@@ -634,5 +634,10 @@
634 "contact_support": "Contatar Suporte",
635 "gift_cards_unavailable": "Os cartões-presente estão disponíveis para compra apenas através do Monero, Bitcoin e Litecoin no momento",
636 "introducing_cake_pay": "Apresentando o Cake Pay!",
637 - "cake_pay_learn_more": "Compre e resgate cartões instantaneamente no aplicativo!\nDeslize para a direita para saber mais!"
637 + "cake_pay_learn_more": "Compre e resgate cartões instantaneamente no aplicativo!\nDeslize para a direita para saber mais!",
638 + "automatic": "Automático",
639 + "fixed_pair_not_supported": "Este par fixo não é compatível com as exchanges selecionadas",
640 + "variable_pair_not_supported": "Este par de variáveis não é compatível com as trocas selecionadas",
641 + "none_of_selected_providers_can_exchange": "Nenhum dos provedores selecionados pode fazer esta troca",
642 + "choose_one": "Escolha um"
643 }
res/values/strings_ru.arb
+7 -2
@@ -366,7 +366,7 @@
366
367
368 "trade_for_not_created" : "Сделка для ${title} не создана.",
369 - "trade_not_created" : "Сделка не создана.",
369 + "trade_not_created" : "Сделка не создана",
370 "trade_id_not_found" : "Сделка ${tradeId} ${title} не найдена.",
371 "trade_not_found" : "Trade not found.",
372
@@ -634,5 +634,10 @@
634 "contact_support": "Связаться со службой поддержки",
635 "gift_cards_unavailable": "В настоящее время подарочные карты можно приобрести только через Monero, Bitcoin и Litecoin.",
636 "introducing_cake_pay": "Представляем Cake Pay!",
637 - "cake_pay_learn_more": "Мгновенно покупайте и погашайте карты в приложении!\nПроведите вправо, чтобы узнать больше!"
637 + "cake_pay_learn_more": "Мгновенно покупайте и погашайте карты в приложении!\nПроведите вправо, чтобы узнать больше!",
638 + "automatic": "автоматический",
639 + "fixed_pair_not_supported": "Эта фиксированная пара не поддерживается выбранными биржами.",
640 + "variable_pair_not_supported": "Эта пара переменных не поддерживается выбранными биржами.",
641 + "none_of_selected_providers_can_exchange": "Ни один из выбранных провайдеров не может совершить этот обмен",
642 + "choose_one": "Выбери один"
643 }
res/values/strings_uk.arb
+7 -2
@@ -365,7 +365,7 @@
365
366
367 "trade_for_not_created" : "Операція для ${title} не створена.",
368 - "trade_not_created" : "Операція не створена.",
368 + "trade_not_created" : "Операція не створена",
369 "trade_id_not_found" : "Операція ${tradeId} ${title} не знайдена.",
370 "trade_not_found" : "Операція не знайдена.",
371
@@ -633,5 +633,10 @@
633 "contact_support": "Звернутися до служби підтримки",
634 "gift_cards_unavailable": "Наразі подарункові картки можна придбати лише через Monero, Bitcoin і Litecoin",
635 "introducing_cake_pay": "Представляємо Cake Pay!",
636 - "cake_pay_learn_more": "Миттєва купівля та погашення карток в додатку!\nПроведіть праворуч, щоб дізнатися більше!"
636 + "cake_pay_learn_more": "Миттєва купівля та погашення карток в додатку!\nПроведіть праворуч, щоб дізнатися більше!",
637 + "automatic": "Автоматичний",
638 + "fixed_pair_not_supported": "Ця фіксована пара не підтримується вибраними біржами",
639 + "variable_pair_not_supported": "Ця пара змінних не підтримується вибраними біржами",
640 + "none_of_selected_providers_can_exchange": "Жоден із вибраних провайдерів не може здійснити цей обмін",
641 + "choose_one": "Вибери один"
642 }
res/values/strings_zh.arb
+7 -2
@@ -366,7 +366,7 @@
366
367
368 "trade_for_not_created" : "交易 ${title} 未创建.",
369 - "trade_not_created" : "未建立交易.",
369 + "trade_not_created" : "未建立交易",
370 "trade_id_not_found" : "交易方式 ${tradeId} 的 ${title} 未找到.",
371 "trade_not_found" : "找不到交易.",
372
@@ -632,5 +632,10 @@
632 "contact_support": "联系支持",
633 "gift_cards_unavailable": "目前只能通过门罗币、比特币和莱特币购买礼品卡",
634 "introducing_cake_pay": "介绍 Cake Pay!",
635 - "cake_pay_learn_more": "立即在应用程序中购买和兑换卡!\n向右滑动了解更多!"
635 + "cake_pay_learn_more": "立即在应用程序中购买和兑换卡!\n向右滑动了解更多!",
636 + "automatic": "自动的",
637 + "fixed_pair_not_supported": "所选交易所不支持此固定货币对",
638 + "variable_pair_not_supported": "所选交易所不支持此变量对",
639 + "none_of_selected_providers_can_exchange": "选定的供应商都不能进行此交换",
640 + "choose_one": "选一个"
641 }