CW-539-DFX-off-ramp-sell-provider (#1229)

* buy_provider_types refactoring * refactor MoonPay and sell option flow * dfx sell flow * add default sell provider flow * localization * Update other_settings_page.dart * [skip ci] update localization * [skip ci] providers fixes * [skip ci] ui fixes * refactor sell and buy flow * handle dfx availability by country * PR fixes

Serhii committed Dec 28, 2023 at 21:20 UTC 914565eb724bbda6a6cc9c8ba166f90761115f67
49 files changed +622 -369
assets/images/moonpay_dark.png
Binary files /dev/null and b/assets/images/moonpay_dark.png differ
assets/images/moonpay_light.png
Binary files /dev/null and b/assets/images/moonpay_light.png differ
cw_haven/pubspec.lock
-9
@@ -623,15 +623,6 @@ packages:
623 url: "https://pub.dev"
624 source: hosted
625 version: "1.0.1"
626 - tor:
627 - dependency: transitive
628 - description:
629 - path: "."
630 - ref: main
631 - resolved-ref: "09ba92cb11d4e3cacf97256e57863b805f79f2e5"
632 - url: "https://github.com/cake-tech/tor.git"
633 - source: git
634 - version: "0.0.1"
626 typed_data:
627 dependency: transitive
628 description:
lib/buy/buy_exception.dart
+4 -4
@@ -2,11 +2,11 @@ import 'package:flutter/foundation.dart';
2 import 'package:cake_wallet/buy/buy_provider_description.dart';
3
4 class BuyException implements Exception {
5 - BuyException({required this.description, required this.text});
5 + BuyException({required this.title, required this.content});
6
7 - final BuyProviderDescription description;
8 - final String text;
7 + final String title;
8 + final String content;
9
10 @override
11 - String toString() => '${description.title}: $text';
11 + String toString() => '$title: $content';
12 }
\ No newline at end of file
lib/buy/buy_provider.dart
+20 -12
@@ -1,27 +1,35 @@
1 import 'package:cake_wallet/buy/buy_amount.dart';
2 -import 'package:cake_wallet/buy/buy_provider_description.dart';
2 import 'package:cake_wallet/buy/order.dart';
3 import 'package:cw_core/wallet_base.dart';
5 -import 'package:cw_core/wallet_type.dart';
4 +import 'package:flutter/material.dart';
5
6 abstract class BuyProvider {
8 - BuyProvider({required this.wallet, required this.isTestEnvironment});
7 + BuyProvider({
8 + required this.wallet,
9 + required this.isTestEnvironment,
10 + });
11
12 final WalletBase wallet;
13 final bool isTestEnvironment;
14
15 String get title;
14 - BuyProviderDescription get description;
15 - String get trackUrl;
16
17 - WalletType get walletType => wallet.type;
18 - String get walletAddress => wallet.walletAddresses.address;
19 - String get walletId => wallet.id;
17 + String get buyOptionDescription;
18 +
19 + String get sellOptionDescription;
20 +
21 + String get lightIcon;
22 +
23 + String get darkIcon;
24
25 @override
26 String toString() => title;
27
24 - Future<String> requestUrl(String amount, String sourceCurrency);
25 - Future<Order> findOrderById(String id);
26 - Future<BuyAmount> calculateAmount(String amount, String sourceCurrency);
27 -}
\ No newline at end of file
28 + Future<void> launchProvider(BuildContext context, bool? isBuyAction);
29 +
30 + Future<String> requestUrl(String amount, String sourceCurrency) => throw UnimplementedError();
31 +
32 + Future<Order> findOrderById(String id) => throw UnimplementedError();
33 +
34 + Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) => throw UnimplementedError();
35 +}
lib/buy/dfx/dfx_buy_provider.dart
+44 -20
@@ -1,5 +1,6 @@
1 import 'dart:convert';
2
3 +import 'package:cake_wallet/buy/buy_provider.dart';
4 import 'package:cake_wallet/generated/i18n.dart';
5 import 'package:cake_wallet/routes.dart';
6 import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
@@ -11,10 +12,9 @@ import 'package:flutter/material.dart';
12 import 'package:http/http.dart' as http;
13 import 'package:url_launcher/url_launcher.dart';
14
14 -class DFXBuyProvider {
15 - DFXBuyProvider({required WalletBase wallet}) : this._wallet = wallet;
16 -
17 - final WalletBase _wallet;
15 +class DFXBuyProvider extends BuyProvider {
16 + DFXBuyProvider({required WalletBase wallet, bool isTestEnvironment = false})
17 + : super(wallet: wallet, isTestEnvironment: isTestEnvironment);
18
19 static const _baseUrl = 'api.dfx.swiss';
20 static const _authPath = '/v1/auth/signMessage';
@@ -22,8 +22,23 @@ class DFXBuyProvider {
22 static const _signInPath = '/v1/auth/signIn';
23 static const walletName = 'CakeWallet';
24
25 + @override
26 + String get title => 'DFX Connect';
27 +
28 + @override
29 + String get buyOptionDescription => S.current.dfx_option_description;
30 +
31 + @override
32 + String get sellOptionDescription => S.current.dfx_option_description;
33 +
34 + @override
35 + String get lightIcon => 'assets/images/dfx_light.png';
36 +
37 + @override
38 + String get darkIcon => 'assets/images/dfx_dark.png';
39 +
40 String get assetOut {
26 - switch (_wallet.type) {
41 + switch (wallet.type) {
42 case WalletType.bitcoin:
43 return 'BTC';
44 case WalletType.bitcoinCash:
@@ -35,12 +50,12 @@ class DFXBuyProvider {
50 case WalletType.ethereum:
51 return 'ETH';
52 default:
38 - throw Exception("WalletType is not available for DFX ${_wallet.type}");
53 + throw Exception("WalletType is not available for DFX ${wallet.type}");
54 }
55 }
56
57 String get blockchain {
43 - switch (_wallet.type) {
58 + switch (wallet.type) {
59 case WalletType.bitcoin:
60 case WalletType.bitcoinCash:
61 case WalletType.litecoin:
@@ -50,12 +65,12 @@ class DFXBuyProvider {
65 case WalletType.ethereum:
66 return 'Ethereum';
67 default:
53 - throw Exception("WalletType is not available for DFX ${_wallet.type}");
68 + throw Exception("WalletType is not available for DFX ${wallet.type}");
69 }
70 }
71
72 Future<String> getSignMessage() async {
58 - final walletAddress = _wallet.walletAddresses.address;
73 + final walletAddress = wallet.walletAddresses.address;
74 final uri = Uri.https(_baseUrl, _authPath, {'address': walletAddress});
75
76 var response = await http.get(uri, headers: {'accept': 'application/json'});
@@ -71,7 +86,7 @@ class DFXBuyProvider {
86
87 Future<String> signUp() async {
88 final signMessage = getSignature(await getSignMessage());
74 - final walletAddress = _wallet.walletAddresses.address;
89 + final walletAddress = wallet.walletAddresses.address;
90
91 final requestBody = jsonEncode({
92 'wallet': walletName,
@@ -86,6 +101,10 @@ class DFXBuyProvider {
101 if (response.statusCode == 201) {
102 final responseBody = jsonDecode(response.body);
103 return responseBody['accessToken'] as String;
104 + } else if (response.statusCode == 403) {
105 + final responseBody = jsonDecode(response.body);
106 + final message = responseBody['message'] ?? 'Service unavailable in your country';
107 + throw Exception(message);
108 } else {
109 throw Exception(
110 'Failed to sign up. Status: ${response.statusCode} ${response.body}');
@@ -94,7 +113,7 @@ class DFXBuyProvider {
113
114 Future<String> signIn() async {
115 final signMessage = getSignature(await getSignMessage());
97 - final walletAddress = _wallet.walletAddresses.address;
116 + final walletAddress = wallet.walletAddresses.address;
117
118 final requestBody = jsonEncode({
119 'address': walletAddress,
@@ -108,6 +127,10 @@ class DFXBuyProvider {
127 if (response.statusCode == 201) {
128 final responseBody = jsonDecode(response.body);
129 return responseBody['accessToken'] as String;
130 + } else if (response.statusCode == 403) {
131 + final responseBody = jsonDecode(response.body);
132 + final message = responseBody['message'] ?? 'Service unavailable in your country';
133 + throw Exception(message);
134 } else {
135 throw Exception(
136 'Failed to sign in. Status: ${response.statusCode} ${response.body}');
@@ -115,24 +138,25 @@ class DFXBuyProvider {
138 }
139
140 String getSignature(String message) {
118 - switch (_wallet.type) {
141 + switch (wallet.type) {
142 case WalletType.ethereum:
120 - return _wallet.signMessage(message);
143 + return wallet.signMessage(message);
144 case WalletType.monero:
145 case WalletType.litecoin:
146 case WalletType.bitcoin:
147 case WalletType.bitcoinCash:
125 - return _wallet.signMessage(message,
126 - address: _wallet.walletAddresses.address);
148 + return wallet.signMessage(message,
149 + address: wallet.walletAddresses.address);
150 default:
128 - throw Exception("WalletType is not available for DFX ${_wallet.type}");
151 + throw Exception("WalletType is not available for DFX ${wallet.type}");
152 }
153 }
154
132 - Future<void> launchProvider(BuildContext context) async {
155 + Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
156 try {
157 final assetOut = this.assetOut;
158 final blockchain = this.blockchain;
159 + final actionType = isBuyAction == true ? '/buy' : '/sell';
160
161 String accessToken;
162
@@ -146,7 +170,7 @@ class DFXBuyProvider {
170 }
171 }
172
149 - final uri = Uri.https('services.dfx.swiss', '/buy', {
173 + final uri = Uri.https('services.dfx.swiss', actionType, {
174 'session': accessToken,
175 'lang': 'en',
176 'asset-out': assetOut,
@@ -156,8 +180,8 @@ class DFXBuyProvider {
180
181 if (await canLaunchUrl(uri)) {
182 if (DeviceInfo.instance.isMobile) {
159 - Navigator.of(context).pushNamed(Routes.webViewPage,
160 - arguments: [S.of(context).buy, uri]);
183 + Navigator.of(context)
184 + .pushNamed(Routes.webViewPage, arguments: ["DFX Connect", uri]);
185 } else {
186 await launchUrl(uri, mode: LaunchMode.externalApplication);
187 }
lib/buy/moonpay/moonpay_buy_provider.dart
+89 -28
@@ -1,9 +1,14 @@
1 import 'dart:convert';
2 import 'package:cake_wallet/palette.dart';
3 +import 'package:cake_wallet/routes.dart';
4 +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
5 import 'package:cake_wallet/store/settings_store.dart';
6 import 'package:cake_wallet/themes/theme_base.dart';
7 +import 'package:cake_wallet/utils/device_info.dart';
8 import 'package:crypto/crypto.dart';
9 import 'package:cake_wallet/buy/buy_exception.dart';
10 +import 'package:cake_wallet/generated/i18n.dart';
11 +import 'package:flutter/material.dart';
12 import 'package:http/http.dart';
13 import 'package:cake_wallet/buy/buy_amount.dart';
14 import 'package:cake_wallet/buy/buy_provider.dart';
@@ -14,33 +19,41 @@ import 'package:cw_core/wallet_type.dart';
19 import 'package:cake_wallet/exchange/trade_state.dart';
20 import 'package:cake_wallet/.secrets.g.dart' as secrets;
21 import 'package:cw_core/crypto_currency.dart';
22 +import 'package:url_launcher/url_launcher.dart';
23
24 class MoonPaySellProvider {
19 - MoonPaySellProvider({this.isTest = false})
20 - : baseUrl = isTest ? _baseTestUrl : _baseProductUrl;
25 + MoonPaySellProvider({required SettingsStore settingsStore,
26 + required WalletBase wallet, this.isTest = false})
27 + : baseUrl = isTest ? _baseTestUrl : _baseProductUrl,
28 + this._settingsStore = settingsStore,
29 + this._wallet = wallet;
30 +
31 + final SettingsStore _settingsStore;
32 + final WalletBase _wallet;
33
34 static const _baseTestUrl = 'sell-sandbox.moonpay.com';
35 static const _baseProductUrl = 'sell.moonpay.com';
36 +
37 static String themeToMoonPayTheme(ThemeBase theme) {
38 switch (theme.type) {
39 case ThemeType.bright:
27 - return 'light';
40 case ThemeType.light:
41 return 'light';
42 case ThemeType.dark:
43 return 'dark';
44 }
45 }
34 - static String get _apiKey => secrets.moonPayApiKey;
35 - static String get _secretKey => secrets.moonPaySecretKey;
46 +
47 + static String get _apiKey => secrets.moonPayApiKey;
48 + static String get _secretKey => secrets.moonPaySecretKey;
49 final bool isTest;
50 final String baseUrl;
51
39 - Future<Uri> requestUrl(
40 - {required CryptoCurrency currency,
41 - required String refundWalletAddress,
42 - required SettingsStore settingsStore}) async {
43 -
52 + Future<Uri> requestUrl({
53 + required CryptoCurrency currency,
54 + required String refundWalletAddress,
55 + required SettingsStore settingsStore,
56 + }) async {
57 final customParams = {
58 'theme': themeToMoonPayTheme(settingsStore.currentTheme),
59 'language': settingsStore.languageCode,
@@ -50,11 +63,15 @@ class MoonPaySellProvider {
63 };
64
65 final originalUri = Uri.https(
53 - baseUrl, '', <String, dynamic>{
66 + baseUrl,
67 + '',
68 + <String, dynamic>{
69 'apiKey': _apiKey,
70 'defaultBaseCurrencyCode': currency.toString().toLowerCase(),
56 - 'refundWalletAddress': refundWalletAddress
57 - }..addAll(customParams));
71 + 'refundWalletAddress': refundWalletAddress,
72 + }..addAll(customParams),
73 + );
74 +
75 final messageBytes = utf8.encode('?${originalUri.query}');
76 final key = utf8.encode(_secretKey);
77 final hmac = Hmac(sha256, key);
@@ -70,6 +87,38 @@ class MoonPaySellProvider {
87 final signedUri = originalUri.replace(queryParameters: query);
88 return signedUri;
89 }
90 +
91 + Future<void> launchProvider(BuildContext context) async {
92 + try {
93 + final uri = await requestUrl(
94 + currency: _wallet.currency,
95 + refundWalletAddress: _wallet.walletAddresses.address,
96 + settingsStore: _settingsStore,
97 + );
98 +
99 + if (await canLaunchUrl(uri)) {
100 + if (DeviceInfo.instance.isMobile) {
101 + Navigator.of(context).pushNamed(Routes.webViewPage, arguments: ['MoonPay', uri]);
102 + } else {
103 + await launchUrl(uri, mode: LaunchMode.externalApplication);
104 + }
105 + } else {
106 + throw Exception('Could not launch URL');
107 + }
108 + } catch (e) {
109 + await showDialog<void>(
110 + context: context,
111 + builder: (BuildContext context) {
112 + return AlertWithOneAction(
113 + alertTitle: 'MoonPay',
114 + alertContent: 'The MoonPay service is currently unavailable: $e',
115 + buttonText: S.of(context).ok,
116 + buttonAction: () => Navigator.of(context).pop(),
117 + );
118 + },
119 + );
120 + }
121 + }
122 }
123
124 class MoonPayBuyProvider extends BuyProvider {
@@ -88,20 +137,24 @@ class MoonPayBuyProvider extends BuyProvider {
137 static const _secretKey = secrets.moonPaySecretKey;
138
139 @override
91 - String get title => 'MoonPay';
140 + String get title => 'Moon Pay';
141
142 @override
94 - BuyProviderDescription get description => BuyProviderDescription.moonPay;
143 + String get buyOptionDescription => '';
144
96 - String get currencyCode =>
97 - walletTypeToCryptoCurrency(walletType).title.toLowerCase();
145 + @override
146 + String get lightIcon => 'assets/images/moonpay_light.png';
147
148 @override
149 + String get darkIcon => 'assets/images/moonpay_dark.png';
150 +
151 + String get currencyCode =>
152 + walletTypeToCryptoCurrency(wallet.type).title.toLowerCase();
153 +
154 String get trackUrl => baseUrl + '/transaction_receipt?transactionId=';
155
156 String baseUrl;
157
104 - @override
158 Future<String> requestUrl(String amount, String sourceCurrency) async {
159 final enabledPaymentMethods =
160 'credit_debit_card%2Capple_pay%2Cgoogle_pay%2Csamsung_pay'
@@ -109,7 +162,7 @@ class MoonPayBuyProvider extends BuyProvider {
162
163 final suffix = '?apiKey=' + _apiKey + '&currencyCode=' +
164 currencyCode + '&enabledPaymentMethods=' + enabledPaymentMethods +
112 - '&walletAddress=' + walletAddress +
165 + '&walletAddress=' + wallet.walletAddresses.address +
166 '&baseCurrencyCode=' + sourceCurrency.toLowerCase() +
167 '&baseCurrencyAmount=' + amount + '&lockAmount=true' +
168 '&showAllCurrencies=false' + '&showWalletAddressForm=false';
@@ -127,7 +180,6 @@ class MoonPayBuyProvider extends BuyProvider {
180 return isTestEnvironment ? originalUrl : urlWithSignature;
181 }
182
130 - @override
183 Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) async {
184 final url = _apiUrl + _currenciesSuffix + '/$currencyCode' +
185 _quoteSuffix + '/?apiKey=' + _apiKey +
@@ -138,8 +190,8 @@ class MoonPayBuyProvider extends BuyProvider {
190
191 if (response.statusCode != 200) {
192 throw BuyException(
141 - description: description,
142 - text: 'Quote is not found!');
193 + title: buyOptionDescription,
194 + content: 'Quote is not found!');
195 }
196
197 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
@@ -153,7 +205,6 @@ class MoonPayBuyProvider extends BuyProvider {
205 minAmount: minSourceAmount);
206 }
207
156 - @override
208 Future<Order> findOrderById(String id) async {
209 final url = _apiUrl + _transactionsSuffix + '/$id' +
210 '?apiKey=' + _apiKey;
@@ -162,8 +213,8 @@ class MoonPayBuyProvider extends BuyProvider {
213
214 if (response.statusCode != 200) {
215 throw BuyException(
165 - description: description,
166 - text: 'Transaction $id is not found!');
216 + title: buyOptionDescription,
217 + content: 'Transaction $id is not found!');
218 }
219
220 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
@@ -175,13 +226,13 @@ class MoonPayBuyProvider extends BuyProvider {
226
227 return Order(
228 id: id,
178 - provider: description,
229 + provider: BuyProviderDescription.moonPay,
230 transferId: id,
231 state: state,
232 createdAt: createdAt,
233 amount: amount.toString(),
183 - receiveAddress: walletAddress,
184 - walletId: walletId
234 + receiveAddress: wallet.walletAddresses.address,
235 + walletId: wallet.id
236 );
237 }
238
@@ -201,4 +252,14 @@ class MoonPayBuyProvider extends BuyProvider {
252
253 return isBuyEnable;
254 }
255 +
256 + @override
257 + // TODO: implement sellOptionDescription
258 + String get sellOptionDescription => throw UnimplementedError();
259 +
260 + @override
261 + Future<void> launchProvider(BuildContext context, bool? isBuyAction) {
262 + // TODO: implement launchProvider
263 + throw UnimplementedError();
264 + }
265 }
\ No newline at end of file
lib/buy/onramper/onramper_buy_provider.dart
+35 -17
@@ -1,4 +1,5 @@
1 import 'package:cake_wallet/.secrets.g.dart' as secrets;
2 +import 'package:cake_wallet/buy/buy_provider.dart';
3 import 'package:cake_wallet/generated/i18n.dart';
4 import 'package:cake_wallet/routes.dart';
5 import 'package:cake_wallet/store/settings_store.dart';
@@ -9,20 +10,34 @@ import 'package:cw_core/wallet_base.dart';
10 import 'package:flutter/material.dart';
11 import 'package:url_launcher/url_launcher.dart';
12
12 -class OnRamperBuyProvider {
13 - OnRamperBuyProvider({required SettingsStore settingsStore, required WalletBase wallet})
14 - : this._settingsStore = settingsStore,
15 - this._wallet = wallet;
13 +class OnRamperBuyProvider extends BuyProvider {
14 + OnRamperBuyProvider(this._settingsStore,
15 + {required WalletBase wallet, bool isTestEnvironment = false})
16 + : super(wallet: wallet, isTestEnvironment: isTestEnvironment);
17 +
18 + static const _baseUrl = 'buy.onramper.com';
19
20 final SettingsStore _settingsStore;
18 - final WalletBase _wallet;
21
20 - static const _baseUrl = 'buy.onramper.com';
22 + @override
23 + String get title => 'Onramper';
24 +
25 + @override
26 + String get buyOptionDescription => S.current.onramper_option_description;
27 +
28 + @override
29 + String get sellOptionDescription => S.current.onramper_option_description;
30 +
31 + @override
32 + String get lightIcon => 'assets/images/onramper_light.png';
33 +
34 + @override
35 + String get darkIcon => 'assets/images/onramper_dark.png';
36
37 String get _apiKey => secrets.onramperApiKey;
38
39 String get _normalizeCryptoCurrency {
25 - switch (_wallet.currency) {
40 + switch (wallet.currency) {
41 case CryptoCurrency.ltc:
42 return "LTC_LITECOIN";
43 case CryptoCurrency.xmr:
@@ -32,7 +47,7 @@ class OnRamperBuyProvider {
47 case CryptoCurrency.nano:
48 return "XNO_NANO";
49 default:
35 - return _wallet.currency.title;
50 + return wallet.currency.title;
51 }
52 }
53
@@ -40,7 +55,7 @@ class OnRamperBuyProvider {
55 return color.value.toRadixString(16).replaceAll(RegExp(r'^ff'), "");
56 }
57
43 - Uri requestUrl(BuildContext context) {
58 + Uri requestOnramperUrl(BuildContext context) {
59 String primaryColor,
60 secondaryColor,
61 primaryTextColor,
@@ -50,9 +65,10 @@ class OnRamperBuyProvider {
65
66 primaryColor = getColorStr(Theme.of(context).primaryColor);
67 secondaryColor = getColorStr(Theme.of(context).colorScheme.background);
53 - primaryTextColor = getColorStr(Theme.of(context).extension<CakeTextTheme>()!.titleColor);
54 - secondaryTextColor =
55 - getColorStr(Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor);
68 + primaryTextColor =
69 + getColorStr(Theme.of(context).extension<CakeTextTheme>()!.titleColor);
70 + secondaryTextColor = getColorStr(
71 + Theme.of(context).extension<CakeTextTheme>()!.secondaryTextColor);
72 containerColor = getColorStr(Theme.of(context).colorScheme.background);
73 cardColor = getColorStr(Theme.of(context).cardColor);
74
@@ -60,12 +76,13 @@ class OnRamperBuyProvider {
76 cardColor = getColorStr(Colors.white);
77 }
78
63 - final networkName = _wallet.currency.fullName?.toUpperCase().replaceAll(" ", "");
79 + final networkName =
80 + wallet.currency.fullName?.toUpperCase().replaceAll(" ", "");
81
82 return Uri.https(_baseUrl, '', <String, dynamic>{
83 'apiKey': _apiKey,
84 'defaultCrypto': _normalizeCryptoCurrency,
68 - 'networkWallets': '${networkName}:${_wallet.walletAddresses.address}',
85 + 'networkWallets': '${networkName}:${wallet.walletAddresses.address}',
86 'supportSell': "false",
87 'supportSwap': "false",
88 'primaryColor': primaryColor,
@@ -77,10 +94,11 @@ class OnRamperBuyProvider {
94 });
95 }
96
80 - Future<void> launchProvider(BuildContext context) async {
81 - final uri = requestUrl(context);
97 + Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
98 + final uri = requestOnramperUrl(context);
99 if (DeviceInfo.instance.isMobile) {
83 - Navigator.of(context).pushNamed(Routes.webViewPage, arguments: [S.of(context).buy, uri]);
100 + Navigator.of(context)
101 + .pushNamed(Routes.webViewPage, arguments: [S.of(context).buy, uri]);
102 } else {
103 await launchUrl(uri);
104 }
lib/buy/robinhood/robinhood_buy_provider.dart
+35 -22
@@ -1,6 +1,10 @@
1 import 'dart:convert';
2
3 import 'package:cake_wallet/.secrets.g.dart' as secrets;
4 +import 'package:cake_wallet/buy/buy_amount.dart';
5 +import 'package:cake_wallet/buy/buy_provider.dart';
6 +import 'package:cake_wallet/buy/buy_provider_description.dart';
7 +import 'package:cake_wallet/buy/order.dart';
8 import 'package:cake_wallet/generated/i18n.dart';
9 import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
10 import 'package:cake_wallet/utils/show_pop_up.dart';
@@ -10,40 +14,48 @@ import 'package:flutter/material.dart';
14 import 'package:http/http.dart' as http;
15 import 'package:url_launcher/url_launcher.dart';
16
13 -class RobinhoodBuyProvider {
14 - RobinhoodBuyProvider({required WalletBase wallet}) : this._wallet = wallet;
15 -
16 - final WalletBase _wallet;
17 +class RobinhoodBuyProvider extends BuyProvider{
18 + RobinhoodBuyProvider({required WalletBase wallet, bool isTestEnvironment = false})
19 + : super(wallet: wallet, isTestEnvironment: isTestEnvironment);
20
21 static const _baseUrl = 'applink.robinhood.com';
22 static const _cIdBaseUrl = 'exchange-helper.cakewallet.com';
23
24 +
25 + @override
26 + String get title => 'Robinhood Connect';
27 +
28 + @override
29 + String get buyOptionDescription => S.current.robinhood_option_description;
30 +
31 + @override
32 + String get sellOptionDescription => S.current.robinhood_option_description;
33 +
34 + @override
35 + String get lightIcon => 'assets/images/robinhood_light.png';
36 +
37 + @override
38 + String get darkIcon => 'assets/images/robinhood_dark.png';
39 +
40 String get _applicationId => secrets.robinhoodApplicationId;
41
42 String get _apiSecret => secrets.robinhoodCIdApiSecret;
43
25 - bool get isAvailable => [
26 - WalletType.bitcoin,
27 - WalletType.bitcoinCash,
28 - WalletType.litecoin,
29 - WalletType.ethereum
30 - ].contains(_wallet.type);
31 -
44 String getSignature(String message) {
33 - switch (_wallet.type) {
45 + switch (wallet.type) {
46 case WalletType.ethereum:
35 - return _wallet.signMessage(message);
47 + return wallet.signMessage(message);
48 case WalletType.litecoin:
49 case WalletType.bitcoin:
50 case WalletType.bitcoinCash:
39 - return _wallet.signMessage(message, address: _wallet.walletAddresses.address);
51 + return wallet.signMessage(message, address: wallet.walletAddresses.address);
52 default:
41 - throw Exception("WalletType is not available for Robinhood ${_wallet.type}");
53 + throw Exception("WalletType is not available for Robinhood ${wallet.type}");
54 }
55 }
56
57 Future<String> getConnectId() async {
46 - final walletAddress = _wallet.walletAddresses.address;
58 + final walletAddress = wallet.walletAddresses.address;
59 final valid_until = (DateTime.now().millisecondsSinceEpoch / 1000).round() + 10;
60 final message = "$_apiSecret:${valid_until}";
61
@@ -64,22 +76,22 @@ class RobinhoodBuyProvider {
76 }
77 }
78
67 - Future<Uri> requestUrl() async {
79 + Future<Uri> requestProviderUrl() async {
80 final connectId = await getConnectId();
69 - final networkName = _wallet.currency.fullName?.toUpperCase().replaceAll(" ", "_");
81 + final networkName = wallet.currency.fullName?.toUpperCase().replaceAll(" ", "_");
82
83 return Uri.https(_baseUrl, '/u/connect', <String, dynamic>{
84 'applicationId': _applicationId,
85 'connectId': connectId,
74 - 'walletAddress': _wallet.walletAddresses.address,
75 - 'userIdentifier': _wallet.walletAddresses.address,
86 + 'walletAddress': wallet.walletAddresses.address,
87 + 'userIdentifier': wallet.walletAddresses.address,
88 'supportedNetworks': networkName
89 });
90 }
91
80 - Future<void> launchProvider(BuildContext context) async {
92 + Future<void> launchProvider(BuildContext context, bool? isBuyAction) async {
93 try {
82 - final uri = await requestUrl();
94 + final uri = await requestProviderUrl();
95 await launchUrl(uri, mode: LaunchMode.externalApplication);
96 } catch (_) {
97 await showPopUp<void>(
@@ -93,4 +105,5 @@ class RobinhoodBuyProvider {
105 });
106 }
107 }
108 +
109 }
lib/buy/wyre/wyre_buy_provider.dart
+32 -19
@@ -1,5 +1,6 @@
1 import 'dart:convert';
2 import 'package:cake_wallet/buy/buy_exception.dart';
3 +import 'package:flutter/src/widgets/framework.dart';
4 import 'package:http/http.dart';
5 import 'package:cake_wallet/buy/buy_amount.dart';
6 import 'package:cake_wallet/buy/buy_provider.dart';
@@ -35,16 +36,20 @@ class WyreBuyProvider extends BuyProvider {
36 String get title => 'Wyre';
37
38 @override
38 - BuyProviderDescription get description => BuyProviderDescription.wyre;
39 + String get buyOptionDescription => '';
40
41 @override
42 + String get lightIcon => 'assets/images/robinhood_light.png';
43 +
44 + @override
45 + String get darkIcon => 'assets/images/robinhood_dark.png';
46 +
47 String get trackUrl => isTestEnvironment
48 ? _trackTestUrl
49 : _trackProductUrl;
50
51 String baseApiUrl;
52
47 - @override
53 Future<String> requestUrl(String amount, String sourceCurrency) async {
54 final timestamp = DateTime.now().millisecondsSinceEpoch.toString();
55 final url = baseApiUrl + _ordersSuffix + _reserveSuffix +
@@ -53,8 +58,8 @@ class WyreBuyProvider extends BuyProvider {
58 final body = {
59 'amount': amount,
60 'sourceCurrency': sourceCurrency,
56 - 'destCurrency': walletTypeToCryptoCurrency(walletType).title,
57 - 'dest': walletTypeToString(walletType).toLowerCase() + ':' + walletAddress,
61 + 'destCurrency': walletTypeToCryptoCurrency(wallet.type).title,
62 + 'dest': walletTypeToString(wallet.type).toLowerCase() + ':' + wallet.walletAddresses.address,
63 'referrerAccountId': _accountId,
64 'lockFields': ['amount', 'sourceCurrency', 'destCurrency', 'dest']
65 };
@@ -68,8 +73,8 @@ class WyreBuyProvider extends BuyProvider {
73
74 if (response.statusCode != 200) {
75 throw BuyException(
71 - description: description,
72 - text: 'Url $url is not found!');
76 + title: buyOptionDescription,
77 + content: 'Url $url is not found!');
78 }
79
80 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
@@ -77,14 +82,13 @@ class WyreBuyProvider extends BuyProvider {
82 return urlFromResponse;
83 }
84
80 - @override
85 Future<BuyAmount> calculateAmount(String amount, String sourceCurrency) async {
86 final quoteUrl = _baseProductApiUrl + _ordersSuffix + _quoteSuffix;
87 final body = {
88 'amount': amount,
89 'sourceCurrency': sourceCurrency,
86 - 'destCurrency': walletTypeToCryptoCurrency(walletType).title,
87 - 'dest': walletTypeToString(walletType).toLowerCase() + ':' + walletAddress,
90 + 'destCurrency': walletTypeToCryptoCurrency(wallet.type).title,
91 + 'dest': walletTypeToString(wallet.type).toLowerCase() + ':' + wallet.walletAddresses.address,
92 'accountId': _accountId,
93 'country': _countryCode
94 };
@@ -99,8 +103,8 @@ class WyreBuyProvider extends BuyProvider {
103
104 if (response.statusCode != 200) {
105 throw BuyException(
102 - description: description,
103 - text: 'Quote is not found!');
106 + title: buyOptionDescription,
107 + content: 'Quote is not found!');
108 }
109
110 final responseJSON = json.decode(response.body) as Map<String, dynamic>;
@@ -111,7 +115,6 @@ class WyreBuyProvider extends BuyProvider {
115 return BuyAmount(sourceAmount: sourceAmount, destAmount: destAmount, achSourceAmount: achAmount);
116 }
117
114 - @override
118 Future<Order> findOrderById(String id) async {
119 final orderUrl = baseApiUrl + _ordersSuffix + '/$id';
120 final orderUri = Uri.parse(orderUrl);
@@ -119,8 +122,8 @@ class WyreBuyProvider extends BuyProvider {
122
123 if (orderResponse.statusCode != 200) {
124 throw BuyException(
122 - description: description,
123 - text: 'Order $id is not found!');
125 + title: buyOptionDescription,
126 + content: 'Order $id is not found!');
127 }
128
129 final orderResponseJSON =
@@ -141,8 +144,8 @@ class WyreBuyProvider extends BuyProvider {
144
145 if (transferResponse.statusCode != 200) {
146 throw BuyException(
144 - description: description,
145 - text: 'Transfer $transferId is not found!');
147 + title: buyOptionDescription,
148 + content: 'Transfer $transferId is not found!');
149 }
150
151 final transferResponseJSON =
@@ -151,15 +154,25 @@ class WyreBuyProvider extends BuyProvider {
154
155 return Order(
156 id: id,
154 - provider: description,
157 + provider: BuyProviderDescription.wyre,
158 transferId: transferId,
159 from: from,
160 to: to,
161 state: state,
162 createdAt: createdAt,
163 amount: amount.toString(),
161 - receiveAddress: walletAddress,
162 - walletId: walletId
164 + receiveAddress: wallet.walletAddresses.address,
165 + walletId: wallet.id
166 );
167 }
168 +
169 + @override
170 + Future<void> launchProvider(BuildContext context, bool? isBuyAction) {
171 + // TODO: implement launchProvider
172 + throw UnimplementedError();
173 + }
174 +
175 + @override
176 + // TODO: implement sellOptionDescription
177 + String get sellOptionDescription => throw UnimplementedError();
178 }
\ No newline at end of file
lib/di.dart
+8 -2
@@ -1,6 +1,7 @@
1 import 'package:cake_wallet/anonpay/anonpay_api.dart';
2 import 'package:cake_wallet/anonpay/anonpay_info_base.dart';
3 import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
4 +import 'package:cake_wallet/buy/moonpay/moonpay_buy_provider.dart';
5 import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
6 import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
7 import 'package:cake_wallet/buy/payfura/payfura_buy_provider.dart';
@@ -796,8 +797,12 @@ Future<void> setup({
797 getIt
798 .registerFactory<DFXBuyProvider>(() => DFXBuyProvider(wallet: getIt.get<AppStore>().wallet!));
799
800 + getIt.registerFactory<MoonPaySellProvider>(() => MoonPaySellProvider(
801 + settingsStore: getIt.get<AppStore>().settingsStore,
802 + wallet: getIt.get<AppStore>().wallet!));
803 +
804 getIt.registerFactory<OnRamperBuyProvider>(() => OnRamperBuyProvider(
800 - settingsStore: getIt.get<AppStore>().settingsStore,
805 + getIt.get<AppStore>().settingsStore,
806 wallet: getIt.get<AppStore>().wallet!,
807 ));
808
@@ -941,7 +946,8 @@ Future<void> setup({
946
947 getIt.registerFactory(() => BuyAmountViewModel());
948
944 - getIt.registerFactory(() => BuyOptionsPage(getIt.get<DashboardViewModel>()));
949 + getIt.registerFactoryParam<BuySellOptionsPage, bool, void>(
950 + (isBuyOption, _) => BuySellOptionsPage(getIt.get<DashboardViewModel>(), isBuyOption));
951
952 getIt.registerFactory(() {
953 final wallet = getIt.get<AppStore>().wallet;
lib/entities/buy_provider_types.dart
+87 -32
@@ -1,57 +1,112 @@
1 -import 'package:cake_wallet/generated/i18n.dart';
1 +import 'package:cake_wallet/buy/buy_provider.dart';
2 +import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart';
3 +import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
4 +import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart';
5 +import 'package:cake_wallet/di.dart';
6 import 'package:cw_core/wallet_type.dart';
7
8 enum BuyProviderType {
5 - AskEachTime,
6 - Robinhood,
7 - Onramper,
8 - DFX;
9 + askEachTime,
10 + robinhood,
11 + dfx,
12 + onramper,
13 +}
14
10 - @override
11 - String toString() {
15 +extension BuyProviderTypeName on BuyProviderType {
16 + String get name {
17 switch (this) {
13 - case BuyProviderType.AskEachTime:
14 - return S.current.ask_each_time;
15 - case BuyProviderType.Robinhood:
16 - return "Robinhood";
17 - case BuyProviderType.Onramper:
18 - return "Onramper";
19 - case BuyProviderType.DFX:
20 - return "DFX";
18 + case BuyProviderType.askEachTime:
19 + return 'Ask each time';
20 + case BuyProviderType.robinhood:
21 + return 'Robinhood Connect';
22 + case BuyProviderType.dfx:
23 + return 'DFX Connect';
24 + case BuyProviderType.onramper:
25 + return 'Onramper';
26 + default:
27 + return this.toString().split('.').last;
28 + }
29 + }
30 +
31 + String get id {
32 + switch (this) {
33 + case BuyProviderType.askEachTime:
34 + return 'ask_each_time_provider';
35 + case BuyProviderType.robinhood:
36 + return 'robinhood_connect_provider';
37 + case BuyProviderType.dfx:
38 + return 'dfx_connect_provider';
39 + case BuyProviderType.onramper:
40 + return 'onramper_provider';
41 + default:
42 + return this.toString().split('.').last.replaceAll('.', '_')
43 + .toLowerCase() + '_provider';
44 }
45 }
46 +}
47
24 - static List<BuyProviderType> getAvailableProviders(WalletType walletType) {
48 +class BuyProviderHelper {
49 + static List<BuyProviderType> getAvailableBuyProviderTypes(
50 + WalletType walletType) {
51 switch (walletType) {
52 case WalletType.nano:
53 case WalletType.banano:
28 - return [
29 - BuyProviderType.AskEachTime,
30 - BuyProviderType.Onramper
31 - ];
54 + return [BuyProviderType.askEachTime, BuyProviderType.onramper];
55 case WalletType.monero:
56 return [
34 - BuyProviderType.AskEachTime,
35 - BuyProviderType.Onramper,
36 - BuyProviderType.DFX
57 + BuyProviderType.askEachTime,
58 + BuyProviderType.onramper,
59 + BuyProviderType.dfx
60 ];
61 case WalletType.bitcoin:
62 case WalletType.ethereum:
63 return [
41 - BuyProviderType.AskEachTime,
42 - BuyProviderType.Onramper,
43 - BuyProviderType.DFX,
44 - BuyProviderType.Robinhood
64 + BuyProviderType.askEachTime,
65 + BuyProviderType.onramper,
66 + BuyProviderType.dfx,
67 + BuyProviderType.robinhood
68 + ];
69 + case WalletType.litecoin:
70 + case WalletType.bitcoinCash:
71 + return [
72 + BuyProviderType.askEachTime,
73 + BuyProviderType.onramper,
74 + BuyProviderType.robinhood
75 ];
76 + default:
77 + return [];
78 + }
79 + }
80 +
81 + static List<BuyProviderType> getAvailableSellProviderTypes(
82 + WalletType walletType) {
83 + switch (walletType) {
84 + case WalletType.nano:
85 + case WalletType.banano:
86 + return [BuyProviderType.askEachTime];
87 + case WalletType.monero:
88 + return [BuyProviderType.askEachTime, BuyProviderType.dfx];
89 + case WalletType.bitcoin:
90 + case WalletType.ethereum:
91 + return [BuyProviderType.askEachTime, BuyProviderType.dfx];
92 case WalletType.litecoin:
93 case WalletType.bitcoinCash:
48 - return [
49 - BuyProviderType.AskEachTime,
50 - BuyProviderType.Onramper,
51 - BuyProviderType.Robinhood
52 - ];
94 + return [BuyProviderType.askEachTime];
95 default:
96 return [];
97 }
98 }
99 +
100 + static BuyProvider? getProviderByType(BuyProviderType type) {
101 + switch (type) {
102 + case BuyProviderType.robinhood:
103 + return getIt.get<RobinhoodBuyProvider>();
104 + case BuyProviderType.dfx:
105 + return getIt.get<DFXBuyProvider>();
106 + case BuyProviderType.onramper:
107 + return getIt.get<OnRamperBuyProvider>();
108 + case BuyProviderType.askEachTime:
109 + return null;
110 + }
111 + }
112 }
lib/entities/main_actions.dart
+34 -81
@@ -1,18 +1,9 @@
1 -import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart';
2 -import 'package:cake_wallet/buy/moonpay/moonpay_buy_provider.dart';
3 -import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
4 -import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart';
5 -import 'package:cake_wallet/di.dart';
6 -import 'package:cake_wallet/entities/buy_provider_types.dart';
1 import 'package:cake_wallet/generated/i18n.dart';
2 import 'package:cake_wallet/routes.dart';
3 import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
10 -import 'package:cake_wallet/utils/device_info.dart';
4 import 'package:cake_wallet/utils/show_pop_up.dart';
5 import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
13 -import 'package:cw_core/wallet_type.dart';
6 import 'package:flutter/material.dart';
15 -import 'package:url_launcher/url_launcher.dart';
7
8 class MainActions {
9 final String Function(BuildContext context) name;
@@ -46,53 +37,22 @@ class MainActions {
37 canShow: (viewModel) => viewModel.hasBuyAction,
38 onTap: (BuildContext context, DashboardViewModel viewModel) async {
39 if (!viewModel.isEnabledBuyAction) {
49 - await _showErrorDialog(context, S.of(context).unsupported_asset);
40 + await _showErrorDialog(
41 + context, S.of(context).buy, S.of(context).unsupported_asset);
42 return;
43 }
44
45 final defaultBuyProvider = viewModel.defaultBuyProvider;
46 try {
55 - await _launchProviderByType(context, defaultBuyProvider);
47 + defaultBuyProvider != null
48 + ? await defaultBuyProvider.launchProvider(context, true)
49 + : await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: true);
50 } catch (e) {
57 - await _showErrorDialog(context, e.toString());
51 + await _showErrorDialog(context, defaultBuyProvider.toString(), e.toString());
52 }
53 },
54 );
55
62 - static Future<void> _launchProviderByType(BuildContext context, BuyProviderType providerType) async {
63 - switch (providerType) {
64 - case BuyProviderType.AskEachTime:
65 - Navigator.pushNamed(context, Routes.buy);
66 - break;
67 - case BuyProviderType.Onramper:
68 - await getIt.get<OnRamperBuyProvider>().launchProvider(context);
69 - break;
70 - case BuyProviderType.Robinhood:
71 - await getIt.get<RobinhoodBuyProvider>().launchProvider(context);
72 - break;
73 - case BuyProviderType.DFX:
74 - await getIt.get<DFXBuyProvider>().launchProvider(context);
75 - break;
76 - default:
77 - throw UnsupportedError('Unsupported buy provider type');
78 - }
79 - }
80 -
81 -
82 - static Future<void> _showErrorDialog(BuildContext context, String errorMessage) async {
83 - await showPopUp<void>(
84 - context: context,
85 - builder: (BuildContext context) {
86 - return AlertWithOneAction(
87 - alertTitle: S.of(context).buy,
88 - alertContent: errorMessage,
89 - buttonText: S.of(context).ok,
90 - buttonAction: () => Navigator.of(context).pop(),
91 - );
92 - },
93 - );
94 - }
95 -
56 static MainActions receiveAction = MainActions._(
57 name: (context) => S.of(context).receive,
58 image: 'assets/images/received.png',
@@ -127,42 +87,35 @@ class MainActions {
87 isEnabled: (viewModel) => viewModel.isEnabledSellAction,
88 canShow: (viewModel) => viewModel.hasSellAction,
89 onTap: (BuildContext context, DashboardViewModel viewModel) async {
130 - final walletType = viewModel.type;
131 -
132 - switch (walletType) {
133 - case WalletType.bitcoin:
134 - case WalletType.litecoin:
135 - case WalletType.ethereum:
136 - case WalletType.polygon:
137 - case WalletType.bitcoinCash:
138 - if (viewModel.isEnabledSellAction) {
139 - final moonPaySellProvider = MoonPaySellProvider();
140 - final uri = await moonPaySellProvider.requestUrl(
141 - currency: viewModel.wallet.currency,
142 - refundWalletAddress: viewModel.wallet.walletAddresses.address,
143 - settingsStore: viewModel.settingsStore,
144 - );
145 - if (DeviceInfo.instance.isMobile) {
146 - Navigator.of(context).pushNamed(Routes.webViewPage,
147 - arguments: [S.of(context).sell, uri]);
148 - } else {
149 - await launchUrl(uri);
150 - }
151 - }
90 + if (!viewModel.isEnabledSellAction) {
91 + await _showErrorDialog(
92 + context, S.of(context).sell, S.of(context).unsupported_asset);
93 + return;
94 + }
95
153 - break;
154 - default:
155 - await showPopUp<void>(
156 - context: context,
157 - builder: (BuildContext context) {
158 - return AlertWithOneAction(
159 - alertTitle: S.of(context).sell,
160 - alertContent: S.of(context).unsupported_asset,
161 - buttonText: S.of(context).ok,
162 - buttonAction: () => Navigator.of(context).pop());
163 - },
164 - );
96 + final defaultSellProvider = viewModel.defaultSellProvider;
97 + try {
98 + defaultSellProvider != null
99 + ? await defaultSellProvider.launchProvider(context, false)
100 + : await Navigator.of(context).pushNamed(Routes.buySellPage, arguments: false);
101 + } catch (e) {
102 + await _showErrorDialog(context, defaultSellProvider.toString(), e.toString());
103 }
104 },
105 );
168 -}
106 +
107 + static Future<void> _showErrorDialog(
108 + BuildContext context, String title, String errorMessage) async {
109 + await showPopUp<void>(
110 + context: context,
111 + builder: (BuildContext context) {
112 + return AlertWithOneAction(
113 + alertTitle: title,
114 + alertContent: errorMessage,
115 + buttonText: S.of(context).ok,
116 + buttonAction: () => Navigator.of(context).pop(),
117 + );
118 + },
119 + );
120 + }
121 +}
\ No newline at end of file
lib/router.dart
+4 -2
@@ -390,8 +390,10 @@ Route<dynamic> createRoute(RouteSettings settings) {
390 return MaterialPageRoute<void>(
391 builder: (_) => getIt.get<OrderDetailsPage>(param1: settings.arguments as Order));
392
393 - case Routes.buy:
394 - return MaterialPageRoute<void>(builder: (_) => getIt.get<BuyOptionsPage>());
393 + case Routes.buySellPage:
394 + final args = settings.arguments as bool;
395 + return MaterialPageRoute<void>(
396 + builder: (_) => getIt.get<BuySellOptionsPage>(param1: args));
397
398 case Routes.buyWebView:
399 final args = settings.arguments as List;
lib/routes.dart
+1 -1
@@ -55,7 +55,7 @@ class Routes {
55 static const supportLiveChat = '/support/live_chat';
56 static const supportOtherLinks = '/support/other';
57 static const orderDetails = '/order_details';
58 - static const buy = '/buy';
58 + static const buySellPage = '/buy_sell_page';
59 static const buyWebView = '/buy_web_view';
60 static const unspentCoinsList = '/unspent_coins_list';
61 static const unspentCoinsDetails = '/unspent_coins_details';
lib/src/screens/buy/buy_options_page.dart
+39 -69
@@ -1,7 +1,4 @@
1 -import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart';
2 -import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
3 -import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart';
4 -import 'package:cake_wallet/di.dart';
1 +import 'package:cake_wallet/buy/buy_provider.dart';
2 import 'package:cake_wallet/entities/buy_provider_types.dart';
3 import 'package:cake_wallet/generated/i18n.dart';
4 import 'package:cake_wallet/src/screens/base_page.dart';
@@ -11,19 +8,14 @@ import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
8 import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
9 import 'package:flutter/material.dart';
10
14 -class BuyOptionsPage extends BasePage {
15 - BuyOptionsPage(this.dashboardViewModel);
11 +class BuySellOptionsPage extends BasePage {
12 + BuySellOptionsPage(this.dashboardViewModel, this.isBuyAction);
13
14 final DashboardViewModel dashboardViewModel;
18 - final iconDarkRobinhood = 'assets/images/robinhood_dark.png';
19 - final iconLightRobinhood = 'assets/images/robinhood_light.png';
20 - final iconDarkOnramper = 'assets/images/onramper_dark.png';
21 - final iconLightOnramper = 'assets/images/onramper_light.png';
22 - final iconDarkDFX = 'assets/images/dfx_dark.png';
23 - final iconLightDFX = 'assets/images/dfx_light.png';
15 + final bool isBuyAction;
16
17 @override
26 - String get title => S.current.buy;
18 + String get title => isBuyAction ? S.current.buy : S.current.sell;
19
20 @override
21 AppBarStyle get appBarStyle => AppBarStyle.regular;
@@ -32,17 +24,9 @@ class BuyOptionsPage extends BasePage {
24 Widget body(BuildContext context) {
25 final isLightMode =
26 Theme.of(context).extension<OptionTileTheme>()?.useDarkImage ?? false;
35 - final iconRobinhood = Image.asset(
36 - isLightMode ? iconLightRobinhood : iconDarkRobinhood,
37 - height: 40,
38 - width: 40);
39 - final iconOnramper = Image.asset(
40 - isLightMode ? iconLightOnramper : iconDarkOnramper,
41 - height: 40,
42 - width: 40);
43 - final iconDFX = Image.asset(isLightMode ? iconLightDFX : iconDarkDFX,
44 - height: 40, width: 40);
45 - final availableProviders = dashboardViewModel.availableProviders;
27 + final availableProviders = isBuyAction
28 + ? dashboardViewModel.availableBuyProviders
29 + : dashboardViewModel.availableSellProviders;
30
31 return Container(
32 child: Center(
@@ -50,57 +34,43 @@ class BuyOptionsPage extends BasePage {
34 constraints: BoxConstraints(maxWidth: 330),
35 child: Column(
36 children: [
53 - if (availableProviders.contains(BuyProviderType.Onramper))
54 - Padding(
55 - padding: EdgeInsets.only(top: 24),
56 - child: OptionTile(
57 - image: iconOnramper,
58 - title: "Onramper",
59 - description: S.of(context).onramper_option_description,
60 - onPressed: () async => await getIt
61 - .get<OnRamperBuyProvider>()
62 - .launchProvider(context),
63 - ),
64 - ),
65 - if (availableProviders.contains(BuyProviderType.Robinhood))
66 - Padding(
67 - padding: EdgeInsets.only(top: 24),
68 - child: OptionTile(
69 - image: iconRobinhood,
70 - title: "Robinhood Connect",
71 - description: S.of(context).robinhood_option_description,
72 - onPressed: () async => await getIt
73 - .get<RobinhoodBuyProvider>()
74 - .launchProvider(context),
75 - ),
76 - ),
77 - if (availableProviders.contains(BuyProviderType.DFX))
78 - Padding(
37 + ...availableProviders.map((provider) {
38 + final icon = Image.asset(
39 + isLightMode ? provider.lightIcon : provider.darkIcon,
40 + height: 40,
41 + width: 40,
42 + );
43 +
44 + return Padding(
45 padding: EdgeInsets.only(top: 24),
46 child: OptionTile(
81 - image: iconDFX,
82 - title: "DFX Connect",
83 - description: S.of(context).dfx_option_description,
84 - onPressed: () async => await getIt
85 - .get<DFXBuyProvider>()
86 - .launchProvider(context),
47 + image: icon,
48 + title: provider.toString(),
49 + description: isBuyAction
50 + ? provider.buyOptionDescription
51 + : provider.sellOptionDescription,
52 + onPressed: () =>
53 + provider.launchProvider(context, isBuyAction),
54 ),
88 - ),
55 + );
56 + }).toList(),
57 Spacer(),
90 - Padding(
91 - padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
92 - child: Text(
93 - S.of(context).select_buy_provider_notice,
94 - textAlign: TextAlign.center,
95 - style: TextStyle(
96 - fontSize: 14,
97 - fontWeight: FontWeight.normal,
98 - color: Theme.of(context)
99 - .extension<TransactionTradeTheme>()!
100 - .detailsTitlesColor,
58 + Padding(
59 + padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
60 + child: Text(
61 + isBuyAction
62 + ? S.of(context).select_buy_provider_notice
63 + : S.of(context).select_sell_provider_notice,
64 + textAlign: TextAlign.center,
65 + style: TextStyle(
66 + fontSize: 14,
67 + fontWeight: FontWeight.normal,
68 + color: Theme.of(context)
69 + .extension<TransactionTradeTheme>()!
70 + .detailsTitlesColor,
71 + ),
72 ),
73 ),
103 - ),
74 ],
75 ),
76 ),
lib/src/screens/buy/widgets/buy_list_item.dart
+3 -4
@@ -29,11 +29,10 @@ class BuyListItem extends StatelessWidget {
29
30 @override
31 Widget build(BuildContext context) {
32 - final isSelected = selectedProvider?.description == provider.description;
32 + final isSelected = selectedProvider?.buyOptionDescription == provider.buyOptionDescription;
33 final iconColor = isSelected ? Colors.white : Colors.black;
34
35 - final providerIcon = getBuyProviderIcon(provider.description,
36 - iconColor: iconColor)!;
35 + final providerIcon = Image.asset('assets/images/wyre-icon.png', width: 36, height: 36);
36
37 final backgroundColor = isSelected
38 ? Palette.greyBlueCraiola
@@ -76,7 +75,7 @@ class BuyListItem extends StatelessWidget {
75 padding: EdgeInsets.only(right: 10),
76 child: providerIcon),
77 Text(
79 - provider.description.title,
78 + provider.title,
79 style: TextStyle(
80 color: secondaryTextColor,
81 fontSize: 20,
lib/src/screens/settings/other_settings_page.dart
+10 -2
@@ -43,10 +43,18 @@ class OtherSettingsPage extends BasePage {
43 if(_otherSettingsViewModel.isEnabledBuyAction)
44 SettingsPickerCell(
45 title: S.current.default_buy_provider,
46 - items: _otherSettingsViewModel.availableBuyProviders,
46 + items: _otherSettingsViewModel.availableBuyProvidersTypes,
47 displayItem: _otherSettingsViewModel.getBuyProviderType,
48 selectedItem: _otherSettingsViewModel.buyProviderType,
49 - onItemSelected: _otherSettingsViewModel.onBuyProviderTypeSelected,
49 + onItemSelected: _otherSettingsViewModel.onBuyProviderTypeSelected
50 + ),
51 + if(_otherSettingsViewModel.isEnabledSellAction)
52 + SettingsPickerCell(
53 + title: S.current.default_sell_provider,
54 + items: _otherSettingsViewModel.availableSellProvidersTypes,
55 + displayItem: _otherSettingsViewModel.getSellProviderType,
56 + selectedItem: _otherSettingsViewModel.sellProviderType,
57 + onItemSelected: _otherSettingsViewModel.onSellProviderTypeSelected,
58 ),
59 SettingsCellWithArrow(
60 title: S.current.settings_terms_and_conditions,
lib/store/settings_store.dart
+41 -15
@@ -2,6 +2,7 @@ import 'dart:io';
2
3 import 'package:cake_wallet/bitcoin/bitcoin.dart';
4 import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
5 +import 'package:cake_wallet/buy/buy_provider.dart';
6 import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
7 import 'package:cake_wallet/entities/buy_provider_types.dart';
8 import 'package:cake_wallet/entities/cake_2fa_preset_options.dart';
@@ -147,7 +148,9 @@ abstract class SettingsStoreBase with Store {
148 initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings,
149 currentSyncMode = initialSyncMode,
150 currentSyncAll = initialSyncAll,
150 - priority = ObservableMap<WalletType, TransactionPriority>() {
151 + priority = ObservableMap<WalletType, TransactionPriority>(),
152 + defaultBuyProviders = ObservableMap<WalletType, BuyProviderType>(),
153 + defaultSellProviders = ObservableMap<WalletType, BuyProviderType>() {
154 //this.nodes = ObservableMap<WalletType, Node>.of(nodes);
155
156 if (initialMoneroTransactionPriority != null) {
@@ -181,9 +184,25 @@ abstract class SettingsStoreBase with Store {
184 initializeTrocadorProviderStates();
185
186 WalletType.values.forEach((walletType) {
184 - final key = 'defaultBuyProvider_${walletType.toString()}';
185 - final providerIndex = sharedPreferences.getInt(key);
186 - defaultBuyProviders[walletType] = providerIndex != null ? BuyProviderType.values[providerIndex] : BuyProviderType.AskEachTime;
187 + final key = 'buyProvider_${walletType.toString()}';
188 + final providerId = sharedPreferences.getString(key);
189 + if (providerId != null) {
190 + defaultBuyProviders[walletType] = BuyProviderType.values
191 + .firstWhere((provider) => provider.id == providerId, orElse: () => BuyProviderType.askEachTime);
192 + } else {
193 + defaultBuyProviders[walletType] = BuyProviderType.askEachTime;
194 + }
195 + });
196 +
197 + WalletType.values.forEach((walletType) {
198 + final key = 'sellProvider_${walletType.toString()}';
199 + final providerId = sharedPreferences.getString(key);
200 + if (providerId != null) {
201 + defaultSellProviders[walletType] = BuyProviderType.values
202 + .firstWhere((provider) => provider.id == providerId, orElse: () => BuyProviderType.askEachTime);
203 + } else {
204 + defaultSellProviders[walletType] = BuyProviderType.askEachTime;
205 + }
206 });
207
208 reaction(
@@ -196,6 +215,20 @@ abstract class SettingsStoreBase with Store {
215 (bool shouldShowYatPopup) =>
216 sharedPreferences.setBool(PreferencesKey.shouldShowYatPopup, shouldShowYatPopup));
217
218 + defaultBuyProviders.observe((change) {
219 + final String key = 'buyProvider_${change.key.toString()}';
220 + if (change.newValue != null) {
221 + sharedPreferences.setString(key, change.newValue!.id);
222 + }
223 + });
224 +
225 + defaultSellProviders.observe((change) {
226 + final String key = 'sellProvider_${change.key.toString()}';
227 + if (change.newValue != null) {
228 + sharedPreferences.setString(key, change.newValue!.id);
229 + }
230 + });
231 +
232 priority.observe((change) {
233 final String? key;
234 switch (change.key) {
@@ -251,16 +284,6 @@ abstract class SettingsStoreBase with Store {
284 (bool disableSell) =>
285 sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell));
286
254 - reaction(
255 - (_) => defaultBuyProviders.asObservable(),
256 - (ObservableMap<WalletType, BuyProviderType> providers) {
257 - providers.forEach((walletType, provider) {
258 - final key = 'defaultBuyProvider_${walletType.toString()}';
259 - sharedPreferences.setInt(key, provider.index);
260 - });
261 - }
262 - );
263 -
287 reaction(
288 (_) => walletListOrder,
289 (WalletListOrderType walletListOrder) =>
@@ -594,7 +617,10 @@ abstract class SettingsStoreBase with Store {
617 ObservableMap<String, bool> trocadorProviderStates = ObservableMap<String, bool>();
618
619 @observable
597 - ObservableMap<WalletType, BuyProviderType> defaultBuyProviders = ObservableMap<WalletType, BuyProviderType>();
620 + ObservableMap<WalletType, BuyProviderType> defaultBuyProviders;
621 +
622 + @observable
623 + ObservableMap<WalletType, BuyProviderType> defaultSellProviders;
624
625 @observable
626 SortBalanceBy sortBalanceBy;
lib/view_model/buy/buy_view_model.dart
+1 -2
@@ -61,8 +61,7 @@ abstract class BuyViewModelBase with Store {
61 String _url = '';
62
63 try {
64 - _url = await selectedProvider
65 - !.requestUrl(doubleAmount.toString(), fiatCurrency.title);
64 + _url = await selectedProvider!.requestUrl(doubleAmount.toString(), fiatCurrency.title);
65 } catch (e) {
66 print(e.toString());
67 }
lib/view_model/dashboard/dashboard_view_model.dart
+27 -8
@@ -1,5 +1,6 @@
1 import 'dart:convert';
2
3 +import 'package:cake_wallet/buy/buy_provider.dart';
4 import 'package:cake_wallet/core/key_service.dart';
5 import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
6 import 'package:cake_wallet/entities/balance_display_mode.dart';
@@ -41,6 +42,7 @@ import 'package:cw_core/wallet_type.dart';
42 import 'package:eth_sig_util/util/utils.dart';
43 import 'package:flutter/services.dart';
44 import 'package:mobx/mobx.dart';
45 +import 'package:cake_wallet/entities/buy_provider_types.dart';
46
47 part 'dashboard_view_model.g.dart';
48
@@ -295,13 +297,31 @@ abstract class DashboardViewModelBase with Store {
297
298 Map<String, List<FilterItem>> filterItems;
299
298 - BuyProviderType get defaultBuyProvider =>
299 - settingsStore.defaultBuyProviders[wallet.type] ?? BuyProviderType.AskEachTime;
300 + BuyProvider? get defaultBuyProvider => BuyProviderHelper.getProviderByType(
301 + settingsStore.defaultBuyProviders[wallet.type] ?? BuyProviderType.askEachTime);
302 +
303 + BuyProvider? get defaultSellProvider => BuyProviderHelper.getProviderByType(
304 + settingsStore.defaultSellProviders[wallet.type] ?? BuyProviderType.askEachTime);
305
306 bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled;
307
303 - List<BuyProviderType> get availableProviders =>
304 - BuyProviderType.getAvailableProviders(wallet.type);
308 + List<BuyProvider> get availableBuyProviders {
309 + final providerTypes = BuyProviderHelper.getAvailableBuyProviderTypes(wallet.type);
310 + return providerTypes
311 + .map((type) => BuyProviderHelper.getProviderByType(type))
312 + .where((provider) => provider != null)
313 + .cast<BuyProvider>()
314 + .toList();
315 + }
316 +
317 + List<BuyProvider> get availableSellProviders {
318 + final providerTypes = BuyProviderHelper.getAvailableSellProviderTypes(wallet.type);
319 + return providerTypes
320 + .map((type) => BuyProviderHelper.getProviderByType(type))
321 + .where((provider) => provider != null)
322 + .cast<BuyProvider>()
323 + .toList();
324 + }
325
326 bool get shouldShowYatPopup => settingsStore.shouldShowYatPopup;
327
@@ -315,16 +335,15 @@ abstract class DashboardViewModelBase with Store {
335 bool hasExchangeAction;
336
337 @computed
318 - bool get isEnabledBuyAction => !settingsStore.disableBuy && wallet.type != WalletType.haven;
338 + bool get isEnabledBuyAction =>
339 + !settingsStore.disableBuy && availableBuyProviders.isNotEmpty;
340
341 @observable
342 bool hasBuyAction;
343
344 @computed
345 bool get isEnabledSellAction =>
325 - !settingsStore.disableSell &&
326 - wallet.type != WalletType.haven &&
327 - wallet.type != WalletType.monero;
346 + !settingsStore.disableSell && availableSellProviders.isNotEmpty;
347
348 @observable
349 bool hasSellAction;
lib/view_model/order_details_view_model.dart
+24 -15
@@ -50,8 +50,10 @@ abstract class OrderDetailsViewModelBase with Store {
50 @action
51 Future<void> _updateOrder() async {
52 try {
53 - if (_provider != null) {
54 - final updatedOrder = await _provider!.findOrderById(order.id);
53 + if (_provider != null && (_provider is MoonPayBuyProvider || _provider is WyreBuyProvider)) {
54 + final updatedOrder = _provider is MoonPayBuyProvider
55 + ? await (_provider as MoonPayBuyProvider).findOrderById(order.id)
56 + : await (_provider as WyreBuyProvider).findOrderById(order.id);
57 updatedOrder.from = order.from;
58 updatedOrder.to = order.to;
59 updatedOrder.receiveAddress = order.receiveAddress;
@@ -87,19 +89,26 @@ abstract class OrderDetailsViewModelBase with Store {
89 value: order.provider.title)
90 );
91
90 - if (_provider?.trackUrl.isNotEmpty ?? false) {
91 - final buildURL = _provider!.trackUrl + '${order.transferId}';
92 - items.add(
93 - TrackTradeListItem(
94 - title: 'Track',
95 - value: buildURL,
96 - onTap: () {
97 - try {
98 - launch(buildURL);
99 - } catch (e) {}
100 - }
101 - )
102 - );
92 + if (_provider != null && (_provider is MoonPayBuyProvider || _provider is WyreBuyProvider)) {
93 +
94 + final trackUrl = _provider is MoonPayBuyProvider
95 + ? (_provider as MoonPayBuyProvider).trackUrl
96 + : (_provider as WyreBuyProvider).trackUrl;
97 +
98 + if (trackUrl.isNotEmpty ?? false) {
99 + final buildURL = trackUrl + '${order.transferId}';
100 + items.add(
101 + TrackTradeListItem(
102 + title: 'Track',
103 + value: buildURL,
104 + onTap: () {
105 + try {
106 + launch(buildURL);
107 + } catch (e) {}
108 + }
109 + )
110 + );
111 + }
112 }
113
114 items.add(
lib/view_model/settings/other_settings_view_model.dart
+32 -5
@@ -1,6 +1,7 @@
1 import 'package:cake_wallet/bitcoin/bitcoin.dart';
2 import 'package:cake_wallet/entities/buy_provider_types.dart';
3 import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
4 +import 'package:cake_wallet/generated/i18n.dart';
5 import 'package:cake_wallet/store/settings_store.dart';
6 import 'package:cw_core/balance.dart';
7 import 'package:cw_core/transaction_history.dart';
@@ -59,12 +60,24 @@ abstract class OtherSettingsViewModelBase with Store {
60 bool get isEnabledBuyAction =>
61 !_settingsStore.disableBuy && _wallet.type != WalletType.haven;
62
62 - List<BuyProviderType> get availableBuyProviders =>
63 - BuyProviderType.getAvailableProviders(walletType);
63 + @computed
64 + bool get isEnabledSellAction =>
65 + !_settingsStore.disableSell && _wallet.type != WalletType.haven;
66 +
67 + List<BuyProviderType> get availableBuyProvidersTypes {
68 + return BuyProviderHelper.getAvailableBuyProviderTypes(walletType);
69 + }
70 +
71 + List<BuyProviderType> get availableSellProvidersTypes =>
72 + BuyProviderHelper.getAvailableSellProviderTypes(walletType);
73
74 BuyProviderType get buyProviderType =>
75 _settingsStore.defaultBuyProviders[walletType] ??
67 - BuyProviderType.AskEachTime;
76 + BuyProviderType.askEachTime;
77 +
78 + BuyProviderType get sellProviderType =>
79 + _settingsStore.defaultSellProviders[walletType] ??
80 + BuyProviderType.askEachTime;
81
82 String getDisplayPriority(dynamic priority) {
83 final _priority = priority as TransactionPriority;
@@ -81,13 +94,27 @@ abstract class OtherSettingsViewModelBase with Store {
94
95 String getBuyProviderType(dynamic buyProviderType) {
96 final _buyProviderType = buyProviderType as BuyProviderType;
97 + return _buyProviderType == BuyProviderType.askEachTime
98 + ? S.current.ask_each_time
99 + : _buyProviderType.name;
100 + }
101
85 - return _buyProviderType.toString();
102 + String getSellProviderType(dynamic sellProviderType) {
103 + final _sellProviderType = sellProviderType as BuyProviderType;
104 + return _sellProviderType == BuyProviderType.askEachTime
105 + ? S.current.ask_each_time
106 + : _sellProviderType.name;
107 }
108
109 void onDisplayPrioritySelected(TransactionPriority priority) =>
110 _settingsStore.priority[_wallet.type] = priority;
111
91 - void onBuyProviderTypeSelected(BuyProviderType buyProviderType) =>
112 + @action
113 + BuyProviderType onBuyProviderTypeSelected(BuyProviderType buyProviderType) =>
114 _settingsStore.defaultBuyProviders[walletType] = buyProviderType;
115 +
116 + @action
117 + BuyProviderType onSellProviderTypeSelected(
118 + BuyProviderType sellProviderType) =>
119 + _settingsStore.defaultSellProviders[walletType] = sellProviderType;
120 }
res/values/strings_ar.arb
+2
@@ -754,6 +754,8 @@
754 "dfx_option_description": "ﺎﺑﻭﺭﻭﺃ ﻲﻓ ﺕﺎﻛﺮﺸﻟﺍﻭ ﺔﺋﺰﺠﺘﻟﺍ ءﻼﻤﻌﻟ .ﻲﻓﺎﺿﺇ KYC ﻥﻭﺪﺑ ﻭﺭﻮﻳ 990 ﻰﻟﺇ ﻞﺼﻳ ﺎﻣ .ﻱﺮﺴﻳﻮﺴﻟﺍ",
755 "polygonscan_history": "ﻥﺎﻜﺴﻧﻮﺠﻴﻟﻮﺑ ﺦﻳﺭﺎﺗ",
756 "wallet_seed_legacy": "بذرة محفظة قديمة",
757 + "default_sell_provider": " ﻲﺿﺍﺮﺘﻓﻻﺍ ﻊﻴﺒﻟﺍ ﺩﻭﺰﻣ",
758 + "select_sell_provider_notice": ".ﻖﻴﺒﻄﺘﻟﺍ ﺕﺍﺩﺍﺪﻋﺇ ﻲﻓ ﻚﺑ ﺹﺎﺨﻟﺍ ﻲﺿﺍﺮﺘﻓﻻﺍ ﻊﻴﺒﻟﺍ ﺩﻭﺰﻣ ﻦﻴﻴﻌﺗ ﻖﻳﺮﻃ ﻦﻋ ﺔﺷﺎﺸﻟﺍ ﻩﺬﻫ ﻲﻄﺨﺗ",
759 "custom_drag": "مخصص (عقد وسحب)",
760 "switchToEVMCompatibleWallet": " (Ethereum، Polygon) ﻯﺮﺧﺃ ﺓﺮﻣ ﺔﻟﻭﺎﺤﻤﻟﺍﻭ EVM ﻊﻣ ﺔﻘﻓﺍﻮﺘﻣ ﺔﻈﻔﺤﻣ ﻰﻟﺇ ﻞﻳﺪﺒﺘﻟﺍ ﻰﺟﺮﻳ"
761 }
res/values/strings_bg.arb
+2
@@ -750,6 +750,8 @@
750 "dfx_option_description": "Купете крипто с EUR и CHF. До 990 € без допълнителен KYC. За клиенти на дребно и корпоративни клиенти в Европа",
751 "polygonscan_history": "История на PolygonScan",
752 "wallet_seed_legacy": "Наследено портфейл семе",
753 + "default_sell_provider": "Доставчик за продажба по подразбиране",
754 + "select_sell_provider_notice": "Изберете доставчик на продажба по-горе. Можете да пропуснете този екран, като зададете своя доставчик на продажба по подразбиране в настройките на приложението.",
755 "custom_drag": "Персонализиране (задръжте и плъзнете)",
756 "switchToEVMCompatibleWallet": "Моля, превключете към портфейл, съвместим с EVM, и опитайте отново (Ethereum, Polygon)"
757 }
res/values/strings_cs.arb
+2
@@ -750,6 +750,8 @@
750 "dfx_option_description": "Nakupujte kryptoměny za EUR a CHF. Až 990 € bez dalších KYC. Pro maloobchodní a firemní zákazníky v Evropě",
751 "polygonscan_history": "Historie PolygonScan",
752 "wallet_seed_legacy": "Starší semeno peněženky",
753 + "default_sell_provider": "Výchozí poskytovatel prodeje",
754 + "select_sell_provider_notice": "Výše vyberte poskytovatele prodeje. Tuto obrazovku můžete přeskočit nastavením výchozího poskytovatele prodeje v nastavení aplikace.",
755 "custom_drag": "Custom (Hold and Drag)",
756 "switchToEVMCompatibleWallet": "Přepněte na peněženku kompatibilní s EVM a zkuste to znovu (Ethereum, Polygon)"
757 }
res/values/strings_de.arb
+2
@@ -758,6 +758,8 @@
758 "dfx_option_description": "Krypto mit EUR und CHF kaufen. Bis zu 990€ ohne zusätzliches KYC. Für Privat- und Firmenkunden in Europa",
759 "polygonscan_history": "PolygonScan-Verlauf",
760 "wallet_seed_legacy": "Legacy Wallet Seed",
761 + "default_sell_provider": "Standard-Verkaufsanbieter",
762 + "select_sell_provider_notice": "Wählen Sie oben einen Verkaufsanbieter aus. Sie können diesen Bildschirm überspringen, indem Sie in den App-Einstellungen Ihren Standard-Verkaufsanbieter festlegen.",
763 "custom_drag": "Custom (Hold and Drag)",
764 "switchToEVMCompatibleWallet": "Bitte wechseln Sie zu einem EVM-kompatiblen Wallet und versuchen Sie es erneut (Ethereum, Polygon)"
765 }
res/values/strings_en.arb
+2
@@ -759,6 +759,8 @@
759 "dfx_option_description": "Buy crypto with EUR & CHF. Up to 990€ without additional KYC. For retail and corporate customers in Europe",
760 "polygonscan_history": "PolygonScan history",
761 "wallet_seed_legacy": "Legacy wallet seed",
762 + "default_sell_provider": "Default Sell Provider",
763 + "select_sell_provider_notice": "Select a sell provider above. You can skip this screen by setting your default sell provider in app settings.",
764 "custom_drag": "Custom (Hold and Drag)",
765 "switchToEVMCompatibleWallet": "Please switch to an EVM compatible wallet and try again (Ethereum, Polygon)"
766 }
res/values/strings_es.arb
+2
@@ -758,6 +758,8 @@
758 "dfx_option_description": "Compre criptomonedas con EUR y CHF. Hasta 990€ sin KYC adicional. Para clientes minoristas y corporativos en Europa",
759 "polygonscan_history": "Historial de PolygonScan",
760 "wallet_seed_legacy": "Semilla de billetera heredada",
761 + "default_sell_provider": "Proveedor de venta predeterminado",
762 + "select_sell_provider_notice": "Seleccione un proveedor de venta arriba. Puede omitir esta pantalla configurando su proveedor de venta predeterminado en la configuración de la aplicación.",
763 "custom_drag": "Custom (mantenía y arrastre)",
764 "switchToEVMCompatibleWallet": "Cambie a una billetera compatible con EVM e inténtelo nuevamente (Ethereum, Polygon)"
765 }
res/values/strings_fr.arb
+2
@@ -758,6 +758,8 @@
758 "dfx_option_description": "Achetez des crypto-monnaies avec EUR et CHF. Jusqu'à 990€ sans KYC supplémentaire. Pour les clients particuliers et entreprises en Europe",
759 "polygonscan_history": "Historique de PolygonScan",
760 "wallet_seed_legacy": "Graine de portefeuille hérité",
761 + "default_sell_provider": "Fournisseur de vente par défaut",
762 + "select_sell_provider_notice": "Sélectionnez un fournisseur de vente ci-dessus. Vous pouvez ignorer cet écran en définissant votre fournisseur de vente par défaut dans les paramètres de l'application.",
763 "custom_drag": "Custom (maintenir et traîner)",
764 "switchToEVMCompatibleWallet": "Veuillez passer à un portefeuille compatible EVM et réessayer (Ethereum, Polygon)"
765 }
res/values/strings_ha.arb
+2
@@ -736,6 +736,8 @@
736 "dfx_option_description": "Sayi crypto tare da EUR & CHF. Har zuwa € 990 ba tare da ƙarin KYC ba. Don 'yan kasuwa da abokan ciniki na kamfanoni a Turai",
737 "polygonscan_history": "PolygonScan tarihin kowane zamani",
738 "wallet_seed_legacy": "Tallarin walat walat",
739 + "default_sell_provider": "Tsohuwar Mai Bayar Siyarwa",
740 + "select_sell_provider_notice": "Zaɓi mai bada siyarwa a sama. Kuna iya tsallake wannan allon ta saita mai bada siyar da ku a cikin saitunan app.",
741 "custom_drag": "Al'ada (riƙe da ja)",
742 "switchToEVMCompatibleWallet": "Da fatan za a canza zuwa walat ɗin EVM mai jituwa kuma a sake gwadawa (Ethereum, Polygon)"
743 }
res/values/strings_hi.arb
+2
@@ -758,6 +758,8 @@
758 "dfx_option_description": "EUR और CHF के साथ क्रिप्टो खरीदें। अतिरिक्त केवाईसी के बिना 990€ तक। यूरोप में खुदरा और कॉर्पोरेट ग्राहकों के लिए",
759 "polygonscan_history": "पॉलीगॉनस्कैन इतिहास",
760 "wallet_seed_legacy": "विरासत बटुए बीज",
761 + "default_sell_provider": "डिफ़ॉल्ट विक्रय प्रदाता",
762 + "select_sell_provider_notice": "ऊपर एक विक्रय प्रदाता का चयन करें। आप ऐप सेटिंग में अपना डिफ़ॉल्ट विक्रय प्रदाता सेट करके इस स्क्रीन को छोड़ सकते हैं।",
763 "custom_drag": "कस्टम (पकड़ और खींचें)",
764 "switchToEVMCompatibleWallet": "कृपया ईवीएम संगत वॉलेट पर स्विच करें और पुनः प्रयास करें (एथेरियम, पॉलीगॉन)"
765 }
res/values/strings_hr.arb
+2
@@ -756,6 +756,8 @@
756 "dfx_option_description": "Kupujte kripto s EUR i CHF. Do 990 € bez dodatnog KYC-a. Za maloprodajne i poslovne korisnike u Europi",
757 "polygonscan_history": "Povijest PolygonScan",
758 "wallet_seed_legacy": "Sjeme naslijeđenog novčanika",
759 + "default_sell_provider": "Zadani dobavljač prodaje",
760 + "select_sell_provider_notice": "Gore odaberite pružatelja usluga prodaje. Ovaj zaslon možete preskočiti postavljanjem zadanog pružatelja usluga prodaje u postavkama aplikacije.",
761 "custom_drag": "Prilagođeni (držite i povucite)",
762 "switchToEVMCompatibleWallet": "Prijeđite na novčanik kompatibilan s EVM-om i pokušajte ponovno (Ethereum, Polygon)"
763 }
res/values/strings_id.arb
+2
@@ -746,6 +746,8 @@
746 "dfx_option_description": "Beli kripto dengan EUR & CHF. Hingga 990€ tanpa KYC tambahan. Untuk pelanggan ritel dan korporat di Eropa",
747 "polygonscan_history": "Sejarah PolygonScan",
748 "wallet_seed_legacy": "Biji dompet warisan",
749 + "default_sell_provider": "Penyedia Penjualan Default",
750 + "select_sell_provider_notice": "Pilih penyedia jual di atas. Anda dapat melewati layar ini dengan mengatur penyedia penjualan default Anda di pengaturan aplikasi.",
751 "custom_drag": "Khusus (tahan dan seret)",
752 "switchToEVMCompatibleWallet": "Silakan beralih ke dompet yang kompatibel dengan EVM dan coba lagi (Ethereum, Polygon)"
753 }
res/values/strings_it.arb
+2
@@ -758,6 +758,8 @@
758 "dfx_option_description": "Acquista criptovalute con EUR e CHF. Fino a 990€ senza KYC aggiuntivi. Per clienti al dettaglio e aziendali in Europa",
759 "polygonscan_history": "Cronologia PolygonScan",
760 "wallet_seed_legacy": "Seme di portafoglio legacy",
761 + "default_sell_provider": "Fornitore di vendita predefinito",
762 + "select_sell_provider_notice": "Seleziona un fornitore di vendita sopra. Puoi saltare questa schermata impostando il tuo fornitore di vendita predefinito nelle impostazioni dell'app.",
763 "custom_drag": "Custom (Hold and Drag)",
764 "switchToEVMCompatibleWallet": "Passa a un portafoglio compatibile con EVM e riprova (Ethereum, Polygon)"
765 }
res/values/strings_ja.arb
+2
@@ -758,6 +758,8 @@
758 "dfx_option_description": "EUR と CHF で暗号通貨を購入します。追加のKYCなしで最大990ユーロ。ヨーロッパの小売および法人顧客向け",
759 "polygonscan_history": "ポリゴンスキャン履歴",
760 "wallet_seed_legacy": "レガシーウォレットシード",
761 + "default_sell_provider": "デフォルトの販売プロバイダー",
762 + "select_sell_provider_notice": "上記の販売プロバイダーを選択してください。アプリ設定でデフォルトの販売プロバイダーを設定することで、この画面をスキップできます。",
763 "custom_drag": "カスタム(ホールドとドラッグ)",
764 "switchToEVMCompatibleWallet": "EVM 互換のウォレットに切り替えて再試行してください (イーサリアム、ポリゴン)"
765 }
res/values/strings_ko.arb
+2
@@ -756,6 +756,8 @@
756 "dfx_option_description": "EUR 및 CHF로 암호화폐를 구매하세요. 추가 KYC 없이 최대 990€. 유럽의 소매 및 기업 고객용",
757 "polygonscan_history": "다각형 스캔 기록",
758 "wallet_seed_legacy": "레거시 지갑 시드",
759 + "default_sell_provider": "기본 판매 공급자",
760 + "select_sell_provider_notice": "위에서 판매 공급자를 선택하세요. 앱 설정에서 기본 판매 공급자를 설정하면 이 화면을 건너뛸 수 있습니다.",
761 "custom_drag": "사용자 정의 (홀드 앤 드래그)",
762 "switchToEVMCompatibleWallet": "EVM 호환 지갑으로 전환 후 다시 시도해 주세요. (이더리움, 폴리곤)"
763 }
res/values/strings_my.arb
+2
@@ -756,6 +756,8 @@
756 "dfx_option_description": "EUR & CHF ဖြင့် crypto ကိုဝယ်ပါ။ အပို KYC မပါဘဲ 990€ အထိ။ ဥရောပရှိ လက်လီရောင်းချသူများနှင့် ကော်ပိုရိတ်ဖောက်သည်များအတွက်",
757 "polygonscan_history": "PolygonScan မှတ်တမ်း",
758 "wallet_seed_legacy": "အမွေအနှစ်ပိုက်ဆံအိတ်မျိုးစေ့",
759 + "default_sell_provider": "ပုံသေရောင်းချပေးသူ",
760 + "select_sell_provider_notice": "အထက်ဖော်ပြပါ အရောင်းဝန်ဆောင်မှုပေးသူကို ရွေးပါ။ အက်ပ်ဆက်တင်များတွင် သင်၏မူလရောင်းချပေးသူကို သတ်မှတ်ခြင်းဖြင့် ဤစခရင်ကို ကျော်နိုင်သည်။",
761 "custom_drag": "စိတ်ကြိုက် (Drag)",
762 "switchToEVMCompatibleWallet": "ကျေးဇူးပြု၍ EVM တွဲဖက်သုံးနိုင်သော ပိုက်ဆံအိတ်သို့ ပြောင်းပြီး ထပ်စမ်းကြည့်ပါ (Ethereum၊ Polygon)"
763 }
res/values/strings_nl.arb
+2
@@ -758,6 +758,8 @@
758 "dfx_option_description": "Koop crypto met EUR & CHF. Tot 990€ zonder extra KYC. Voor particuliere en zakelijke klanten in Europa",
759 "polygonscan_history": "PolygonScan-geschiedenis",
760 "wallet_seed_legacy": "Legacy portemonnee zaad",
761 + "default_sell_provider": "Standaard verkoopaanbieder",
762 + "select_sell_provider_notice": "Selecteer hierboven een verkoopaanbieder. U kunt dit scherm overslaan door uw standaardverkoopprovider in te stellen in de app-instellingen.",
763 "custom_drag": "Custom (vasthouden en slepen)",
764 "switchToEVMCompatibleWallet": "Schakel over naar een EVM-compatibele portemonnee en probeer het opnieuw (Ethereum, Polygon)"
765 }
res/values/strings_pl.arb
+2
@@ -758,6 +758,8 @@
758 "dfx_option_description": "Kupuj kryptowaluty za EUR i CHF. Do 990 € bez dodatkowego KYC. Dla klientów detalicznych i korporacyjnych w Europie",
759 "polygonscan_history": "Historia PolygonScan",
760 "wallet_seed_legacy": "Dziedziczne ziarno portfela",
761 + "default_sell_provider": "Domyślny dostawca sprzedaży",
762 + "select_sell_provider_notice": "Wybierz dostawcę sprzedaży powyżej. Możesz pominąć ten ekran, ustawiając domyślnego dostawcę sprzedaży w ustawieniach aplikacji.",
763 "custom_drag": "Niestandardowe (trzymaj i przeciągnij)",
764 "switchToEVMCompatibleWallet": "Przejdź na portfel zgodny z EVM i spróbuj ponownie (Ethereum, Polygon)"
765 }
res/values/strings_pt.arb
+2
@@ -757,6 +757,8 @@
757 "dfx_option_description": "Compre criptografia com EUR e CHF. Até 990€ sem KYC adicional. Para clientes de varejo e corporativos na Europa",
758 "polygonscan_history": "História do PolygonScan",
759 "wallet_seed_legacy": "Semente de carteira herdada",
760 + "default_sell_provider": "Provedor de venda padrão",
761 + "select_sell_provider_notice": "Selecione um fornecedor de venda acima. Você pode pular esta tela definindo seu provedor de venda padrão nas configurações do aplicativo.",
762 "custom_drag": "Personalizado (segure e arraste)",
763 "switchToEVMCompatibleWallet": "Mude para uma carteira compatível com EVM e tente novamente (Ethereum, Polygon)"
764 }
res/values/strings_ru.arb
+2
@@ -758,6 +758,8 @@
758 "dfx_option_description": "Покупайте криптовалюту за EUR и CHF. До 990€ без дополнительного KYC. Для розничных и корпоративных клиентов в Европе",
759 "polygonscan_history": "История PolygonScan",
760 "wallet_seed_legacy": "Наследие семя кошелька",
761 + "default_sell_provider": "Поставщик продаж по умолчанию",
762 + "select_sell_provider_notice": "Выберите поставщика услуг продажи выше. Вы можете пропустить этот экран, установив поставщика услуг продаж по умолчанию в настройках приложения.",
763 "custom_drag": "Пользователь (удерживайте и перетаскивайте)",
764 "switchToEVMCompatibleWallet": "Пожалуйста, переключитесь на кошелек, совместимый с EVM, и повторите попытку (Ethereum, Polygon)."
765 }
res/values/strings_th.arb
+2
@@ -756,6 +756,8 @@
756 "dfx_option_description": "ซื้อ crypto ด้วย EUR และ CHF สูงถึง 990€ โดยไม่มี KYC เพิ่มเติม สำหรับลูกค้ารายย่อยและลูกค้าองค์กรในยุโรป",
757 "polygonscan_history": "ประวัติ PolygonScan",
758 "wallet_seed_legacy": "เมล็ดกระเป๋าเงินมรดก",
759 + "default_sell_provider": "ผู้ให้บริการการขายเริ่มต้น",
760 + "select_sell_provider_notice": "เลือกผู้ให้บริการการขายด้านบน คุณสามารถข้ามหน้าจอนี้ได้โดยการตั้งค่าผู้ให้บริการการขายเริ่มต้นในการตั้งค่าแอป",
761 "custom_drag": "กำหนดเอง (ค้างและลาก)",
762 "switchToEVMCompatibleWallet": "โปรดเปลี่ยนไปใช้กระเป๋าเงินที่รองรับ EVM แล้วลองอีกครั้ง (Ethereum, Polygon)"
763 }
res/values/strings_tl.arb
+2
@@ -752,6 +752,8 @@
752 "dfx_option_description": "Bumili ng crypto gamit ang EUR at CHF. Hanggang 990€ nang walang karagdagang KYC. Para sa retail at corporate na mga customer sa Europe",
753 "polygonscan_history": "Kasaysayan ng PolygonScan",
754 "wallet_seed_legacy": "Legacy wallet seed",
755 + "default_sell_provider": "Default na Sell Provider",
756 + "select_sell_provider_notice": "Pumili ng provider ng nagbebenta sa itaas. Maaari mong laktawan ang screen na ito sa pamamagitan ng pagtatakda ng iyong default na sell provider sa mga setting ng app.",
757 "custom_drag": "Pasadyang (hawakan at i -drag)",
758 "switchToEVMCompatibleWallet": "Mangyaring lumipat sa isang EVM compatible na wallet at subukang muli (Ethereum, Polygon)"
759 }
res/values/strings_tr.arb
+2
@@ -756,6 +756,8 @@
756 "dfx_option_description": "EUR ve CHF ile kripto satın alın. Ek KYC olmadan 990 €'ya kadar. Avrupa'daki perakende ve kurumsal müşteriler için",
757 "polygonscan_history": "PolygonScan geçmişi",
758 "wallet_seed_legacy": "Eski cüzdan tohumu",
759 + "default_sell_provider": "Varsayılan Satış Sağlayıcısı",
760 + "select_sell_provider_notice": "Yukarıdan bir satış sağlayıcısı seçin. Uygulama ayarlarında varsayılan satış sağlayıcınızı ayarlayarak bu ekranı atlayabilirsiniz.",
761 "custom_drag": "Özel (Bekle ve Sürükle)",
762 "switchToEVMCompatibleWallet": "Lütfen EVM uyumlu bir cüzdana geçin ve tekrar deneyin (Ethereum, Polygon)"
763 }
res/values/strings_uk.arb
+2
@@ -758,6 +758,8 @@
758 "seed_language_chinese_traditional": "Китайський (традиційний)",
759 "polygonscan_history": "Історія PolygonScan",
760 "wallet_seed_legacy": "Спадець насіння гаманця",
761 + "default_sell_provider": "Постачальник продажу за замовчуванням",
762 + "select_sell_provider_notice": "Виберіть вище постачальника послуг продажу. Ви можете пропустити цей екран, встановивши постачальника послуг продажу за умовчанням у налаштуваннях програми.",
763 "custom_drag": "На замовлення (утримуйте та перетягується)",
764 "switchToEVMCompatibleWallet": "Перейдіть на гаманець, сумісний з EVM, і повторіть спробу (Ethereum, Polygon)"
765 }
res/values/strings_ur.arb
+2
@@ -750,6 +750,8 @@
750 "dfx_option_description": "EUR ﺭﻭﺍ CHF ﯽﻓﺎﺿﺍ ۔ﮟﯾﺪﯾﺮﺧ ﻮﭩﭘﺮﮐ ﮫﺗﺎﺳ ﮯﮐ KYC ﮯﯿﻟ ﮯﮐ ﻦﯿﻓﺭﺎﺻ ﭧﯾﺭﻮﭘﺭﺎﮐ ﺭﻭﺍ ﮦﺩﺭﻮﺧ ﮟ",
751 "polygonscan_history": "ﺦﯾﺭﺎﺗ ﯽﮐ ﻦﯿﮑﺳﺍ ﻥﻮﮔ ﯽﻟﻮﭘ",
752 "wallet_seed_legacy": "میراثی پرس کا بیج",
753 + "default_sell_provider": " ﮦﺪﻨﻨﮐ ﻢﮨﺍﺮﻓ ﻞﯿﺳ ﭧﻟﺎﻔﯾﮈ",
754 + "select_sell_provider_notice": "۔ﮟﯿﮨ ﮯﺘﮑﺳ ﮌﻮﮭﭼ ﻮﮐ ﻦﯾﺮﮑﺳﺍ ﺱﺍ ﺮﮐ ﮮﺩ ﺐﯿﺗﺮﺗ ﻮﮐ ﮦﺪﻨﻨﮐ ﻢﮨﺍﺮﻓ ﻞﯿﺳ ﭧﻟﺎﻔﯾﮈ ﮯﻨﭘﺍ ﮟﯿﻣ ﺕﺎﺒ",
755 "custom_drag": "کسٹم (ہولڈ اینڈ ڈریگ)",
756 "switchToEVMCompatibleWallet": "(Ethereum, Polygon) ﮟﯾﺮﮐ ﺶﺷﻮﮐ ﮦﺭﺎﺑﻭﺩ ﺭﻭﺍ ﮟﯾﺮﮐ ﭻﺋﻮﺳ ﺮﭘ ﭧﯿﻟﺍﻭ ﮯﻟﺍﻭ ﮯﻨﮭﮐﺭ ﺖﻘﺑﺎﻄﻣ "
757 }
res/values/strings_yo.arb
+2
@@ -752,6 +752,8 @@
752 "dfx_option_description": "Ra crypto pẹlu EUR & CHF. Titi di 990 € laisi afikun KYC. Fun soobu ati awọn onibara ile-iṣẹ ni Yuroopu",
753 "polygonscan_history": "PolygonScan itan",
754 "wallet_seed_legacy": "Irugbin akole",
755 + "default_sell_provider": "Aiyipada Olupese Tita",
756 + "select_sell_provider_notice": "Yan olupese ti o ta loke. O le foju iboju yii nipa tito olupese iṣẹ tita aiyipada rẹ ni awọn eto app.",
757 "custom_drag": "Aṣa (mu ati fa)",
758 "switchToEVMCompatibleWallet": "Jọwọ yipada si apamọwọ ibaramu EVM ki o tun gbiyanju lẹẹkansi (Ethereum, Polygon)"
759 }
res/values/strings_zh.arb
+2
@@ -757,6 +757,8 @@
757 "dfx_option_description": "用欧元和瑞士法郎购买加密货币。高达 990 欧元,无需额外 KYC。对于欧洲的零售和企业客户",
758 "polygonscan_history": "多边形扫描历史",
759 "wallet_seed_legacy": "旧的钱包种子",
760 + "default_sell_provider": "默认销售提供商",
761 + "select_sell_provider_notice": "选择上面的销售提供商。您可以通过在应用程序设置中设置默认销售提供商来跳过此屏幕。",
762 "custom_drag": "定制(保持和拖动)",
763 "switchToEVMCompatibleWallet": "请切换到 EVM 兼容钱包并重试(以太坊、Polygon)"
764 }