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 + '¤cyCode=' +
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
}