CW-532-DFX-buy-provider (#1209)

* dfx buy provider ui * fix signing flow * fixed provider determination based on wallet type and app settings * update localization files * minor fix * Fix BTC, LTC und BCH signMessages * Add signMessage to monero * open dfx in webview * Update dfx_buy_provider.dart * Revert merge conflict * Update bitcoin_flutter ref --------- Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com>

Serhii committed Dec 8, 2023 at 16:05 UTC 2138c35e38680f021b2f23b716a65d5e0e9673c5
50 files changed +456 -109
assets/images/dfx_dark.png
Binary files /dev/null and b/assets/images/dfx_dark.png differ
assets/images/dfx_light.png
Binary files /dev/null and b/assets/images/dfx_light.png differ
cw_bitcoin/lib/electrum_wallet.dart
+2 -3
@@ -725,8 +725,7 @@ abstract class ElectrumWalletBase
725 final index = address != null
726 ? walletAddresses.addresses.firstWhere((element) => element.address == address).index
727 : null;
728 - return index == null
729 - ? base64Encode(hd.sign(message))
730 - : base64Encode(hd.derive(index).sign(message));
728 + final HD = index == null ? hd : hd.derive(index);
729 + return base64Encode(HD.signMessage(message));
730 }
731 }
cw_bitcoin/pubspec.yaml
+1 -1
@@ -22,7 +22,7 @@ dependencies:
22 bitcoin_flutter:
23 git:
24 url: https://github.com/cake-tech/bitcoin_flutter.git
25 - ref: cake-update-v3
25 + ref: cake-update-v4
26 bitbox:
27 git:
28 url: https://github.com/cake-tech/bitbox-flutter.git
cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart
+3 -5
@@ -302,10 +302,8 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
302 final index = address != null
303 ? walletAddresses.addresses
304 .firstWhere((element) => element.address == AddressUtils.toLegacyAddress(address))
305 - .index
306 - : null;
307 - return index == null
308 - ? base64Encode(hd.sign(message))
309 - : base64Encode(hd.derive(index).sign(message));
305 + .index : null;
306 + final HD = index == null ? hd : hd.derive(index);
307 + return base64Encode(HD.signMessage(message));
308 }
309 }
cw_bitcoin_cash/pubspec.yaml
+1 -1
@@ -24,7 +24,7 @@ dependencies:
24 bitcoin_flutter:
25 git:
26 url: https://github.com/cake-tech/bitcoin_flutter.git
27 - ref: cake-update-v3
27 + ref: cake-update-v4
28 bitbox:
29 git:
30 url: https://github.com/cake-tech/bitbox-flutter.git
cw_monero/ios/Classes/monero_api.cpp
+9
@@ -926,6 +926,8 @@ extern "C"
926 return m_wallet->trustedDaemon();
927 }
928
929 + // Coin Control //
930 +
931 CoinsInfoRow* coin(int index)
932 {
933 if (index >= 0 && index < m_coins_info.size()) {
@@ -1020,6 +1022,13 @@ extern "C"
1022 m_coins->thaw(index);
1023 }
1024
1025 + // Sign Messages //
1026 +
1027 + char *sign_message(char *message, char *address = "")
1028 + {
1029 + return strdup(get_current_wallet()->signMessage(std::string(message), std::string(address)).c_str());
1030 + }
1031 +
1032 #ifdef __cplusplus
1033 }
1034 #endif
cw_monero/ios/Classes/monero_api.h
+2 -1
@@ -32,7 +32,8 @@ void store(char *path);
32
33 void set_trusted_daemon(bool arg);
34 bool trusted_daemon();
35 +char *sign_message(char *message, char *address);
36
37 #ifdef __cplusplus
38 }
38 -#endif
\ No newline at end of file
39 +#endif
cw_monero/lib/api/signatures.dart
+2
@@ -149,3 +149,5 @@ typedef coin = Pointer<CoinsInfoRow> Function(Int32 index);
149 typedef freeze_coin = Void Function(Int32 index);
150
151 typedef thaw_coin = Void Function(Int32 index);
152 +
153 +typedef sign_message = Pointer<Utf8> Function(Pointer<Utf8> message, Pointer<Utf8> address);
cw_monero/lib/api/types.dart
+2
@@ -149,3 +149,5 @@ typedef GetCoin = Pointer<CoinsInfoRow> Function(int);
149 typedef FreezeCoin = void Function(int);
150
151 typedef ThawCoin = void Function(int);
152 +
153 +typedef SignMessage = Pointer<Utf8> Function(Pointer<Utf8>, Pointer<Utf8>);
cw_monero/lib/api/wallet.dart
+18 -4
@@ -8,7 +8,6 @@ import 'package:cw_monero/api/types.dart';
8 import 'package:cw_monero/api/monero_api.dart';
9 import 'package:cw_monero/api/exceptions/setup_wallet_exception.dart';
10 import 'package:flutter/foundation.dart';
11 -import 'package:flutter/services.dart';
11
12 int _boolToInt(bool value) => value ? 1 : 0;
13
@@ -128,6 +127,10 @@ final trustedDaemonNative = moneroApi
127 .lookup<NativeFunction<trusted_daemon>>('trusted_daemon')
128 .asFunction<TrustedDaemon>();
129
130 +final signMessageNative = moneroApi
131 + .lookup<NativeFunction<sign_message>>('sign_message')
132 + .asFunction<SignMessage>();
133 +
134 int getSyncingHeight() => getSyncingHeightNative();
135
136 bool isNeededToRefresh() => isNeededToRefreshNative() != 0;
@@ -296,7 +299,7 @@ class SyncListener {
299
300 final bchHeight = await getNodeHeightOrUpdate(syncHeight);
301
299 - if (_lastKnownBlockHeight == syncHeight || syncHeight == null) {
302 + if (_lastKnownBlockHeight == syncHeight) {
303 return;
304 }
305
@@ -311,7 +314,7 @@ class SyncListener {
314 }
315
316 // 1. Actual new height; 2. Blocks left to finish; 3. Progress in percents;
314 - onNewBlock?.call(syncHeight, left, ptc);
317 + onNewBlock.call(syncHeight, left, ptc);
318 });
319 }
320
@@ -382,4 +385,15 @@ String getSubaddressLabel(int accountIndex, int addressIndex) {
385
386 Future setTrustedDaemon(bool trusted) async => setTrustedDaemonNative(_boolToInt(trusted));
387
385 -Future<bool> trustedDaemon() async => trustedDaemonNative() != 0;
\ No newline at end of file
388 +Future<bool> trustedDaemon() async => trustedDaemonNative() != 0;
389 +
390 +String signMessage(String message, {String address = ""}) {
391 + final messagePointer = message.toNativeUtf8();
392 + final addressPointer = address.toNativeUtf8();
393 +
394 + final signature = convertUTF8ToString(pointer: signMessageNative(messagePointer, addressPointer));
395 + calloc.free(messagePointer);
396 + calloc.free(addressPointer);
397 +
398 + return signature;
399 +}
cw_monero/lib/monero_wallet.dart
+6
@@ -651,4 +651,10 @@ abstract class MoneroWalletBase
651
652 @override
653 void setExceptionHandler(void Function(FlutterErrorDetails) onError) => _onError = onError;
654 +
655 + @override
656 + String signMessage(String message, {String? address}) {
657 + final useAddress = address ?? "";
658 + return monero_wallet.signMessage(message, address: useAddress);
659 + }
660 }
cw_monero/macos/Classes/monero_api.cpp
+7
@@ -1020,6 +1020,13 @@ extern "C"
1020 m_coins->thaw(index);
1021 }
1022
1023 + // Sign Messages //
1024 +
1025 + char *sign_message(char *message, char *address = "")
1026 + {
1027 + return strdup(get_current_wallet()->signMessage(std::string(message), std::string(address)).c_str());
1028 + }
1029 +
1030 #ifdef __cplusplus
1031 }
1032 #endif
cw_monero/macos/Classes/monero_api.h
+2 -1
@@ -32,7 +32,8 @@ void store(char *path);
32
33 void set_trusted_daemon(bool arg);
34 bool trusted_daemon();
35 +char *sign_message(char *message, char *address);
36
37 #ifdef __cplusplus
38 }
38 -#endif
\ No newline at end of file
39 +#endif
lib/buy/dfx/dfx_buy_provider.dart new
+179
@@ -0,0 +1,179 @@
1 +import 'dart:convert';
2 +
3 +import 'package:cake_wallet/generated/i18n.dart';
4 +import 'package:cake_wallet/routes.dart';
5 +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
6 +import 'package:cake_wallet/utils/device_info.dart';
7 +import 'package:cake_wallet/utils/show_pop_up.dart';
8 +import 'package:cw_core/wallet_base.dart';
9 +import 'package:cw_core/wallet_type.dart';
10 +import 'package:flutter/material.dart';
11 +import 'package:http/http.dart' as http;
12 +import 'package:url_launcher/url_launcher.dart';
13 +
14 +class DFXBuyProvider {
15 + DFXBuyProvider({required WalletBase wallet}) : this._wallet = wallet;
16 +
17 + final WalletBase _wallet;
18 +
19 + static const _baseUrl = 'api.dfx.swiss';
20 + static const _authPath = '/v1/auth/signMessage';
21 + static const _signUpPath = '/v1/auth/signUp';
22 + static const _signInPath = '/v1/auth/signIn';
23 + static const walletName = 'CakeWallet';
24 +
25 + String get assetOut {
26 + switch (_wallet.type) {
27 + case WalletType.bitcoin:
28 + return 'BTC';
29 + case WalletType.bitcoinCash:
30 + return 'BCH';
31 + case WalletType.litecoin:
32 + return 'LTC';
33 + case WalletType.monero:
34 + return 'XMR';
35 + case WalletType.ethereum:
36 + return 'ETH';
37 + default:
38 + throw Exception("WalletType is not available for DFX ${_wallet.type}");
39 + }
40 + }
41 +
42 + String get blockchain {
43 + switch (_wallet.type) {
44 + case WalletType.bitcoin:
45 + case WalletType.bitcoinCash:
46 + case WalletType.litecoin:
47 + return 'Bitcoin';
48 + case WalletType.monero:
49 + return 'Monero';
50 + case WalletType.ethereum:
51 + return 'Ethereum';
52 + default:
53 + throw Exception("WalletType is not available for DFX ${_wallet.type}");
54 + }
55 + }
56 +
57 + Future<String> getSignMessage() async {
58 + final walletAddress = _wallet.walletAddresses.address;
59 + final uri = Uri.https(_baseUrl, _authPath, {'address': walletAddress});
60 +
61 + var response = await http.get(uri, headers: {'accept': 'application/json'});
62 +
63 + if (response.statusCode == 200) {
64 + final responseBody = jsonDecode(response.body);
65 + return responseBody['message'] as String;
66 + } else {
67 + throw Exception(
68 + 'Failed to get sign message. Status: ${response.statusCode} ${response.body}');
69 + }
70 + }
71 +
72 + Future<String> signUp() async {
73 + final signMessage = getSignature(await getSignMessage());
74 + final walletAddress = _wallet.walletAddresses.address;
75 +
76 + final requestBody = jsonEncode({
77 + 'wallet': walletName,
78 + 'address': walletAddress,
79 + 'signature': signMessage,
80 + });
81 +
82 + final uri = Uri.https(_baseUrl, _signUpPath);
83 + var response = await http.post(uri,
84 + headers: {'Content-Type': 'application/json'}, body: requestBody);
85 +
86 + if (response.statusCode == 201) {
87 + final responseBody = jsonDecode(response.body);
88 + return responseBody['accessToken'] as String;
89 + } else {
90 + throw Exception(
91 + 'Failed to sign up. Status: ${response.statusCode} ${response.body}');
92 + }
93 + }
94 +
95 + Future<String> signIn() async {
96 + final signMessage = getSignature(await getSignMessage());
97 + final walletAddress = _wallet.walletAddresses.address;
98 +
99 + final requestBody = jsonEncode({
100 + 'address': walletAddress,
101 + 'signature': signMessage,
102 + });
103 +
104 + final uri = Uri.https(_baseUrl, _signInPath);
105 + var response = await http.post(uri,
106 + headers: {'Content-Type': 'application/json'}, body: requestBody);
107 +
108 + if (response.statusCode == 201) {
109 + final responseBody = jsonDecode(response.body);
110 + return responseBody['accessToken'] as String;
111 + } else {
112 + throw Exception(
113 + 'Failed to sign in. Status: ${response.statusCode} ${response.body}');
114 + }
115 + }
116 +
117 + String getSignature(String message) {
118 + switch (_wallet.type) {
119 + case WalletType.ethereum:
120 + return _wallet.signMessage(message);
121 + case WalletType.monero:
122 + case WalletType.litecoin:
123 + case WalletType.bitcoin:
124 + case WalletType.bitcoinCash:
125 + return _wallet.signMessage(message,
126 + address: _wallet.walletAddresses.address);
127 + default:
128 + throw Exception("WalletType is not available for DFX ${_wallet.type}");
129 + }
130 + }
131 +
132 + Future<void> launchProvider(BuildContext context) async {
133 + try {
134 + final assetOut = this.assetOut;
135 + final blockchain = this.blockchain;
136 +
137 + String accessToken;
138 +
139 + try {
140 + accessToken = await signUp();
141 + } on Exception catch (e) {
142 + if (e.toString().contains('409')) {
143 + accessToken = await signIn();
144 + } else {
145 + rethrow;
146 + }
147 + }
148 +
149 + final uri = Uri.https('services.dfx.swiss', '/buy', {
150 + 'session': accessToken,
151 + 'lang': 'en',
152 + 'asset-out': assetOut,
153 + 'blockchain': blockchain,
154 + 'asset-in': 'EUR',
155 + });
156 +
157 + if (await canLaunchUrl(uri)) {
158 + if (DeviceInfo.instance.isMobile) {
159 + Navigator.of(context).pushNamed(Routes.webViewPage,
160 + arguments: [S.of(context).buy, uri]);
161 + } else {
162 + await launchUrl(uri, mode: LaunchMode.externalApplication);
163 + }
164 + } else {
165 + throw Exception('Could not launch URL');
166 + }
167 + } catch (e) {
168 + await showPopUp<void>(
169 + context: context,
170 + builder: (BuildContext context) {
171 + return AlertWithOneAction(
172 + alertTitle: "DFX Connect",
173 + alertContent: S.of(context).buy_provider_unavailable + ': $e',
174 + buttonText: S.of(context).ok,
175 + buttonAction: () => Navigator.of(context).pop());
176 + });
177 + }
178 + }
179 +}
lib/di.dart
+5 -1
@@ -227,6 +227,7 @@ import 'package:cake_wallet/core/wallet_loading_service.dart';
227 import 'package:cw_core/crypto_currency.dart';
228 import 'package:cake_wallet/entities/qr_view_data.dart';
229
230 +import 'buy/dfx/dfx_buy_provider.dart';
231 import 'core/totp_request_details.dart';
232 import 'src/screens/settings/desktop_settings/desktop_settings_page.dart';
233
@@ -795,6 +796,9 @@ Future<void> setup({
796 getIt.registerFactory<RobinhoodBuyProvider>(
797 () => RobinhoodBuyProvider(wallet: getIt.get<AppStore>().wallet!));
798
799 + getIt.registerFactory<DFXBuyProvider>(
800 + () => DFXBuyProvider(wallet: getIt.get<AppStore>().wallet!));
801 +
802 getIt.registerFactory<OnRamperBuyProvider>(() => OnRamperBuyProvider(
803 settingsStore: getIt.get<AppStore>().settingsStore,
804 wallet: getIt.get<AppStore>().wallet!,
@@ -940,7 +944,7 @@ Future<void> setup({
944
945 getIt.registerFactory(() => BuyAmountViewModel());
946
943 - getIt.registerFactory(() => BuyOptionsPage());
947 + getIt.registerFactory(() => BuyOptionsPage(getIt.get<DashboardViewModel>()));
948
949 getIt.registerFactory(() {
950 final wallet = getIt.get<AppStore>().wallet;
lib/entities/buy_provider_types.dart
+39 -1
@@ -1,9 +1,11 @@
1 import 'package:cake_wallet/generated/i18n.dart';
2 +import 'package:cw_core/wallet_type.dart';
3
4 enum BuyProviderType {
5 AskEachTime,
6 Robinhood,
6 - Onramper;
7 + Onramper,
8 + DFX;
9
10 @override
11 String toString() {
@@ -14,6 +16,42 @@ enum BuyProviderType {
16 return "Robinhood";
17 case BuyProviderType.Onramper:
18 return "Onramper";
19 + case BuyProviderType.DFX:
20 + return "DFX";
21 + }
22 + }
23 +
24 + static List<BuyProviderType> getAvailableProviders(WalletType walletType) {
25 + switch (walletType) {
26 + case WalletType.nano:
27 + case WalletType.banano:
28 + return [
29 + BuyProviderType.AskEachTime,
30 + BuyProviderType.Onramper
31 + ];
32 + case WalletType.monero:
33 + return [
34 + BuyProviderType.AskEachTime,
35 + BuyProviderType.Onramper,
36 + BuyProviderType.DFX
37 + ];
38 + case WalletType.bitcoin:
39 + case WalletType.ethereum:
40 + return [
41 + BuyProviderType.AskEachTime,
42 + BuyProviderType.Onramper,
43 + BuyProviderType.DFX,
44 + BuyProviderType.Robinhood
45 + ];
46 + case WalletType.litecoin:
47 + case WalletType.bitcoinCash:
48 + return [
49 + BuyProviderType.AskEachTime,
50 + BuyProviderType.Onramper,
51 + BuyProviderType.Robinhood
52 + ];
53 + default:
54 + return [];
55 }
56 }
57 }
lib/entities/main_actions.dart
+44 -37
@@ -1,3 +1,4 @@
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';
@@ -44,48 +45,54 @@ class MainActions {
45 isEnabled: (viewModel) => viewModel.isEnabledBuyAction,
46 canShow: (viewModel) => viewModel.hasBuyAction,
47 onTap: (BuildContext context, DashboardViewModel viewModel) async {
47 - final defaultBuyProvider = viewModel.defaultBuyProvider;
48 - final walletType = viewModel.type;
49 -
50 - if (!viewModel.isEnabledBuyAction) return;
48 + if (!viewModel.isEnabledBuyAction) {
49 + await _showErrorDialog(context, S.of(context).unsupported_asset);
50 + return;
51 + }
52
52 - switch (walletType) {
53 - case WalletType.bitcoin:
54 - case WalletType.litecoin:
55 - case WalletType.ethereum:
56 - case WalletType.polygon:
57 - case WalletType.bitcoinCash:
58 - switch (defaultBuyProvider) {
59 - case BuyProviderType.AskEachTime:
60 - Navigator.pushNamed(context, Routes.buy);
61 - break;
62 - case BuyProviderType.Onramper:
63 - await getIt.get<OnRamperBuyProvider>().launchProvider(context);
64 - break;
65 - case BuyProviderType.Robinhood:
66 - await getIt.get<RobinhoodBuyProvider>().launchProvider(context);
67 - break;
68 - }
69 - break;
70 - case WalletType.nano:
71 - case WalletType.banano:
72 - case WalletType.monero:
73 - await getIt.get<OnRamperBuyProvider>().launchProvider(context);
74 - break;
75 - default:
76 - await showPopUp<void>(
77 - context: context,
78 - builder: (BuildContext context) {
79 - return AlertWithOneAction(
80 - alertTitle: S.of(context).buy,
81 - alertContent: S.of(context).unsupported_asset,
82 - buttonText: S.of(context).ok,
83 - buttonAction: () => Navigator.of(context).pop());
84 - });
53 + final defaultBuyProvider = viewModel.defaultBuyProvider;
54 + try {
55 + await _launchProviderByType(context, defaultBuyProvider);
56 + } catch (e) {
57 + await _showErrorDialog(context, e.toString());
58 }
59 },
60 );
61
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 +
96 static MainActions receiveAction = MainActions._(
97 name: (context) => S.of(context).receive,
98 image: 'assets/images/received.png',
lib/src/screens/buy/buy_options_page.dart
+58 -24
@@ -1,18 +1,26 @@
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';
5 +import 'package:cake_wallet/entities/buy_provider_types.dart';
6 import 'package:cake_wallet/generated/i18n.dart';
7 import 'package:cake_wallet/src/screens/base_page.dart';
8 import 'package:cake_wallet/src/widgets/option_tile.dart';
9 import 'package:cake_wallet/themes/extensions/option_tile_theme.dart';
10 import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
11 +import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
12 import 'package:flutter/material.dart';
13
14 class BuyOptionsPage extends BasePage {
15 + BuyOptionsPage(this.dashboardViewModel);
16 +
17 + 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';
24
25 @override
26 String get title => S.current.buy;
@@ -22,11 +30,19 @@ class BuyOptionsPage extends BasePage {
30
31 @override
32 Widget body(BuildContext context) {
25 - final isLightMode = Theme.of(context).extension<OptionTileTheme>()?.useDarkImage ?? false;
26 - final iconRobinhood =
27 - Image.asset(isLightMode ? iconLightRobinhood : iconDarkRobinhood, height: 40, width: 40);
28 - final iconOnramper =
29 - Image.asset(isLightMode ? iconLightOnramper : iconDarkOnramper, height: 40, width: 40);
33 + final isLightMode =
34 + 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;
46
47 return Container(
48 child: Center(
@@ -34,26 +50,42 @@ class BuyOptionsPage extends BasePage {
50 constraints: BoxConstraints(maxWidth: 330),
51 child: Column(
52 children: [
37 - Padding(
38 - padding: EdgeInsets.only(top: 24),
39 - child: OptionTile(
40 - image: iconOnramper,
41 - title: "Onramper",
42 - description: S.of(context).onramper_option_description,
43 - onPressed: () async =>
44 - await getIt.get<OnRamperBuyProvider>().launchProvider(context),
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 ),
46 - ),
47 - Padding(
48 - padding: EdgeInsets.only(top: 24),
49 - child: OptionTile(
50 - image: iconRobinhood,
51 - title: "Robinhood Connect",
52 - description: S.of(context).robinhood_option_description,
53 - onPressed: () async =>
54 - await getIt.get<RobinhoodBuyProvider>().launchProvider(context),
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(
79 + padding: EdgeInsets.only(top: 24),
80 + 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),
87 + ),
88 ),
56 - ),
89 Spacer(),
90 Padding(
91 padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
@@ -63,7 +95,9 @@ class BuyOptionsPage extends BasePage {
95 style: TextStyle(
96 fontSize: 14,
97 fontWeight: FontWeight.normal,
66 - color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
98 + color: Theme.of(context)
99 + .extension<TransactionTradeTheme>()!
100 + .detailsTitlesColor,
101 ),
102 ),
103 ),
lib/src/screens/settings/other_settings_page.dart
+2 -3
@@ -1,5 +1,3 @@
1 -import 'package:cake_wallet/buy/buy_provider.dart';
2 -import 'package:cake_wallet/entities/buy_provider_types.dart';
1 import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
2 import 'package:cake_wallet/generated/i18n.dart';
3 import 'package:cake_wallet/routes.dart';
@@ -42,9 +40,10 @@ class OtherSettingsPage extends BasePage {
40 handler: (BuildContext context) =>
41 Navigator.of(context).pushNamed(Routes.changeRep),
42 ),
43 + if(_otherSettingsViewModel.isEnabledBuyAction)
44 SettingsPickerCell(
45 title: S.current.default_buy_provider,
47 - items: BuyProviderType.values,
46 + items: _otherSettingsViewModel.availableBuyProviders,
47 displayItem: _otherSettingsViewModel.getBuyProviderType,
48 selectedItem: _otherSettingsViewModel.buyProviderType,
49 onItemSelected: _otherSettingsViewModel.onBuyProviderTypeSelected,
lib/store/settings_store.dart
+18 -10
@@ -36,7 +36,6 @@ import 'package:cake_wallet/monero/monero.dart';
36 import 'package:cake_wallet/entities/action_list_display_mode.dart';
37 import 'package:cake_wallet/entities/fiat_api_mode.dart';
38 import 'package:cw_core/set_app_secure_native.dart';
39 -
39 part 'settings_store.g.dart';
40
41 class SettingsStore = SettingsStoreBase with _$SettingsStore;
@@ -123,7 +122,6 @@ abstract class SettingsStoreBase with Store {
122 isAppSecure = initialAppSecure,
123 disableBuy = initialDisableBuy,
124 disableSell = initialDisableSell,
126 - defaultBuyProvider = initialDefaultBuyProvider,
125 shouldShowMarketPlaceInDashboard = initialShouldShowMarketPlaceInDashboard,
126 exchangeStatus = initialExchangeStatus,
127 currentTheme = initialTheme,
@@ -178,6 +176,12 @@ abstract class SettingsStoreBase with Store {
176
177 initializeTrocadorProviderStates();
178
179 + WalletType.values.forEach((walletType) {
180 + final key = 'defaultBuyProvider_${walletType.toString()}';
181 + final providerIndex = sharedPreferences.getInt(key);
182 + defaultBuyProviders[walletType] = providerIndex != null ? BuyProviderType.values[providerIndex] : BuyProviderType.AskEachTime;
183 + });
184 +
185 reaction(
186 (_) => fiatCurrency,
187 (FiatCurrency fiatCurrency) => sharedPreferences.setString(
@@ -244,9 +248,14 @@ abstract class SettingsStoreBase with Store {
248 sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell));
249
250 reaction(
247 - (_) => defaultBuyProvider,
248 - (BuyProviderType defaultBuyProvider) =>
249 - sharedPreferences.setInt(PreferencesKey.defaultBuyProvider, defaultBuyProvider.index));
251 + (_) => defaultBuyProviders.asObservable(),
252 + (ObservableMap<WalletType, BuyProviderType> providers) {
253 + providers.forEach((walletType, provider) {
254 + final key = 'defaultBuyProvider_${walletType.toString()}';
255 + sharedPreferences.setInt(key, provider.index);
256 + });
257 + }
258 + );
259
260 reaction(
261 (_) => autoGenerateSubaddressStatus,
@@ -333,6 +342,7 @@ abstract class SettingsStoreBase with Store {
342
343 reaction((_) => totpSecretKey,
344 (String totpKey) => sharedPreferences.setString(PreferencesKey.totpSecretKey, totpKey));
345 +
346 reaction(
347 (_) => numberOfFailedTokenTrials,
348 (int failedTokenTrail) =>
@@ -494,9 +504,6 @@ abstract class SettingsStoreBase with Store {
504 @observable
505 bool disableSell;
506
497 - @observable
498 - BuyProviderType defaultBuyProvider;
499 -
507 @observable
508 bool allowBiometricalAuthentication;
509
@@ -566,6 +573,9 @@ abstract class SettingsStoreBase with Store {
573 @observable
574 ObservableMap<String, bool> trocadorProviderStates = ObservableMap<String, bool>();
575
576 + @observable
577 + ObservableMap<WalletType, BuyProviderType> defaultBuyProviders = ObservableMap<WalletType, BuyProviderType>();
578 +
579 @observable
580 SortBalanceBy sortBalanceBy;
581
@@ -1003,8 +1013,6 @@ abstract class SettingsStoreBase with Store {
1013 isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure;
1014 disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? disableBuy;
1015 disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? disableSell;
1006 - defaultBuyProvider =
1007 - BuyProviderType.values[sharedPreferences.getInt(PreferencesKey.defaultBuyProvider) ?? 0];
1016 allowBiometricalAuthentication =
1017 sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey) ??
1018 allowBiometricalAuthentication;
lib/view_model/dashboard/dashboard_view_model.dart
+5 -1
@@ -282,10 +282,14 @@ abstract class DashboardViewModelBase with Store {
282
283 Map<String, List<FilterItem>> filterItems;
284
285 - BuyProviderType get defaultBuyProvider => settingsStore.defaultBuyProvider;
285 + BuyProviderType get defaultBuyProvider =>
286 + settingsStore.defaultBuyProviders[wallet.type] ??
287 + BuyProviderType.AskEachTime;
288
289 bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled;
290
291 + List<BuyProviderType> get availableProviders => BuyProviderType.getAvailableProviders(wallet.type);
292 +
293 bool get shouldShowYatPopup => settingsStore.shouldShowYatPopup;
294
295 @action
lib/view_model/settings/other_settings_view_model.dart
+20 -15
@@ -13,14 +13,15 @@ import 'package:package_info/package_info.dart';
13
14 part 'other_settings_view_model.g.dart';
15
16 -class OtherSettingsViewModel = OtherSettingsViewModelBase with _$OtherSettingsViewModel;
16 +class OtherSettingsViewModel = OtherSettingsViewModelBase
17 + with _$OtherSettingsViewModel;
18
19 abstract class OtherSettingsViewModelBase with Store {
20 OtherSettingsViewModelBase(this._settingsStore, this._wallet)
21 : walletType = _wallet.type,
22 currentVersion = '' {
22 - PackageInfo.fromPlatform()
23 - .then((PackageInfo packageInfo) => currentVersion = packageInfo.version);
23 + PackageInfo.fromPlatform().then(
24 + (PackageInfo packageInfo) => currentVersion = packageInfo.version);
25
26 final priority = _settingsStore.priority[_wallet.type];
27 final priorities = priorityForWalletType(_wallet.type);
@@ -31,7 +32,8 @@ abstract class OtherSettingsViewModelBase with Store {
32 }
33
34 final WalletType walletType;
34 - final WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> _wallet;
35 + final WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
36 + TransactionInfo> _wallet;
37
38 @observable
39 String currentVersion;
@@ -50,15 +52,19 @@ abstract class OtherSettingsViewModelBase with Store {
52 }
53
54 @computed
53 - bool get changeRepresentativeEnabled {
54 - if (_wallet.type == WalletType.nano || _wallet.type == WalletType.banano) {
55 - return true;
56 - }
55 + bool get changeRepresentativeEnabled =>
56 + _wallet.type == WalletType.nano || _wallet.type == WalletType.banano;
57
58 - return false;
59 - }
60 -
61 - BuyProviderType get buyProviderType { return _settingsStore.defaultBuyProvider; }
58 + @computed
59 + bool get isEnabledBuyAction =>
60 + !_settingsStore.disableBuy && _wallet.type != WalletType.haven;
61 +
62 + List<BuyProviderType> get availableBuyProviders =>
63 + BuyProviderType.getAvailableProviders(walletType);
64 +
65 + BuyProviderType get buyProviderType =>
66 + _settingsStore.defaultBuyProviders[walletType] ??
67 + BuyProviderType.AskEachTime;
68
69 String getDisplayPriority(dynamic priority) {
70 final _priority = priority as TransactionPriority;
@@ -73,7 +79,7 @@ abstract class OtherSettingsViewModelBase with Store {
79 return priority.toString();
80 }
81
76 - String getBuyProviderType (dynamic buyProviderType) {
82 + String getBuyProviderType(dynamic buyProviderType) {
83 final _buyProviderType = buyProviderType as BuyProviderType;
84
85 return _buyProviderType.toString();
@@ -83,6 +89,5 @@ abstract class OtherSettingsViewModelBase with Store {
89 _settingsStore.priority[_wallet.type] = priority;
90
91 void onBuyProviderTypeSelected(BuyProviderType buyProviderType) =>
86 - _settingsStore.defaultBuyProvider = buyProviderType;
87 -
92 + _settingsStore.defaultBuyProviders[walletType] = buyProviderType;
93 }
pubspec_base.yaml
+1 -1
@@ -96,7 +96,7 @@ dependencies:
96 bitcoin_flutter:
97 git:
98 url: https://github.com/cake-tech/bitcoin_flutter.git
99 - ref: cake-update-v3
99 + ref: cake-update-v4
100 fluttertoast: 8.1.4
101 # tor:
102 # git:
res/values/strings_ar.arb
+1
@@ -746,5 +746,6 @@
746 "seed_language_czech": "التشيكية",
747 "seed_language_korean": "الكورية",
748 "seed_language_chinese_traditional": "تقاليد صينية)",
749 + "dfx_option_description": "ﺎﺑﻭﺭﻭﺃ ﻲﻓ ﺕﺎﻛﺮﺸﻟﺍﻭ ﺔﺋﺰﺠﺘﻟﺍ ءﻼﻤﻌﻟ .ﻲﻓﺎﺿﺇ KYC ﻥﻭﺪﺑ ﻭﺭﻮﻳ 990 ﻰﻟﺇ ﻞﺼﻳ ﺎﻣ .ﻱﺮﺴﻳﻮﺴﻟﺍ",
750 "polygonscan_history": "ﻥﺎﻜﺴﻧﻮﺠﻴﻟﻮﺑ ﺦﻳﺭﺎﺗ"
751 }
res/values/strings_bg.arb
+1
@@ -742,5 +742,6 @@
742 "seed_language_czech": "Чех",
743 "seed_language_korean": "Корейски",
744 "seed_language_chinese_traditional": "Традиционен китайски)",
745 + "dfx_option_description": "Купете крипто с EUR и CHF. До 990 € без допълнителен KYC. За клиенти на дребно и корпоративни клиенти в Европа",
746 "polygonscan_history": "История на PolygonScan"
747 }
res/values/strings_cs.arb
+1
@@ -742,5 +742,6 @@
742 "seed_language_czech": "čeština",
743 "seed_language_korean": "korejština",
744 "seed_language_chinese_traditional": "Číňan (tradiční)",
745 + "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ě",
746 "polygonscan_history": "Historie PolygonScan"
747 }
res/values/strings_de.arb
+1
@@ -750,5 +750,6 @@
750 "seed_language_czech": "Tschechisch",
751 "seed_language_korean": "Koreanisch",
752 "seed_language_chinese_traditional": "Chinesisch (Traditionell)",
753 + "dfx_option_description": "Krypto mit EUR und CHF kaufen. Bis zu 990€ ohne zusätzliches KYC. Für Privat- und Firmenkunden in Europa",
754 "polygonscan_history": "PolygonScan-Verlauf"
755 }
res/values/strings_en.arb
+1
@@ -751,5 +751,6 @@
751 "seed_language_czech": "Czech",
752 "seed_language_korean": "Korean",
753 "seed_language_chinese_traditional": "Chinese (Traditional)",
754 + "dfx_option_description": "Buy crypto with EUR & CHF. Up to 990€ without additional KYC. For retail and corporate customers in Europe",
755 "polygonscan_history": "PolygonScan history"
756 }
res/values/strings_es.arb
+2
@@ -749,6 +749,8 @@
749 "seedtype_polyseed": "Polieta (16 palabras)",
750 "seed_language_czech": "checo",
751 "seed_language_korean": "coreano",
752 + "seed_language_chinese_traditional": "Chino tradicional)",
753 + "dfx_option_description": "Compre criptomonedas con EUR y CHF. Hasta 990€ sin KYC adicional. Para clientes minoristas y corporativos en Europa",
754 "seed_language_chinese_traditional": "Chino (tradicional)",
755 "polygonscan_history": "Historial de PolygonScan"
756 }
res/values/strings_fr.arb
+1
@@ -750,5 +750,6 @@
750 "seed_language_czech": "tchèque",
751 "seed_language_korean": "coréen",
752 "seed_language_chinese_traditional": "Chinois (Traditionnel)",
753 + "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",
754 "polygonscan_history": "Historique de PolygonScan"
755 }
res/values/strings_ha.arb
+1
@@ -728,5 +728,6 @@
728 "seed_language_czech": "Czech",
729 "seed_language_korean": "Yaren Koriya",
730 "seed_language_chinese_traditional": "Sinanci (na gargajiya)",
731 + "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",
732 "polygonscan_history": "PolygonScan tarihin kowane zamani"
733 }
res/values/strings_hi.arb
+1
@@ -750,5 +750,6 @@
750 "seed_language_czech": "चेक",
751 "seed_language_korean": "कोरियाई",
752 "seed_language_chinese_traditional": "चीनी पारंपरिक)",
753 + "dfx_option_description": "EUR और CHF के साथ क्रिप्टो खरीदें। अतिरिक्त केवाईसी के बिना 990€ तक। यूरोप में खुदरा और कॉर्पोरेट ग्राहकों के लिए",
754 "polygonscan_history": "पॉलीगॉनस्कैन इतिहास"
755 }
res/values/strings_hr.arb
+2
@@ -747,6 +747,8 @@
747 "seedtype_polyseed": "Poliseed (16 riječi)",
748 "seed_language_czech": "češki",
749 "seed_language_korean": "korejski",
750 + "seed_language_chinese_traditional": "Kinesko tradicionalno)",
751 + "dfx_option_description": "Kupujte kripto s EUR i CHF. Do 990 € bez dodatnog KYC-a. Za maloprodajne i poslovne korisnike u Europi",
752 "seed_language_chinese_traditional": "Kinesko (tradicionalno)",
753 "polygonscan_history": "Povijest PolygonScan"
754 }
res/values/strings_id.arb
+2
@@ -737,6 +737,8 @@
737 "seedtype_polyseed": "Polyseed (16 kata)",
738 "seed_language_czech": "Ceko",
739 "seed_language_korean": "Korea",
740 + "seed_language_chinese_traditional": "Cina tradisional)",
741 + "dfx_option_description": "Beli kripto dengan EUR & CHF. Hingga 990€ tanpa KYC tambahan. Untuk pelanggan ritel dan korporat di Eropa",
742 "seed_language_chinese_traditional": "Cina (tradisional)",
743 "polygonscan_history": "Sejarah PolygonScan"
744 }
res/values/strings_it.arb
+1
@@ -750,5 +750,6 @@
750 "seed_language_czech": "ceco",
751 "seed_language_korean": "coreano",
752 "seed_language_chinese_traditional": "Cinese tradizionale)",
753 + "dfx_option_description": "Acquista criptovalute con EUR e CHF. Fino a 990€ senza KYC aggiuntivi. Per clienti al dettaglio e aziendali in Europa",
754 "polygonscan_history": "Cronologia PolygonScan"
755 }
res/values/strings_ja.arb
+1
@@ -750,5 +750,6 @@
750 "seed_language_czech": "チェコ",
751 "seed_language_korean": "韓国語",
752 "seed_language_chinese_traditional": "中国の伝統的な)",
753 + "dfx_option_description": "EUR と CHF で暗号通貨を購入します。追加のKYCなしで最大990ユーロ。ヨーロッパの小売および法人顧客向け",
754 "polygonscan_history": "ポリゴンスキャン履歴"
755 }
res/values/strings_ko.arb
+1
@@ -748,5 +748,6 @@
748 "seed_language_czech": "체코 사람",
749 "seed_language_korean": "한국인",
750 "seed_language_chinese_traditional": "중국 전통)",
751 + "dfx_option_description": "EUR 및 CHF로 암호화폐를 구매하세요. 추가 KYC 없이 최대 990€. 유럽의 소매 및 기업 고객용",
752 "polygonscan_history": "다각형 스캔 기록"
753 }
res/values/strings_my.arb
+1
@@ -748,5 +748,6 @@
748 "seed_language_czech": "ချက်",
749 "seed_language_korean": "ကိုးရီးယား",
750 "seed_language_chinese_traditional": "တရုတ်ရိုးရာ)",
751 + "dfx_option_description": "EUR & CHF ဖြင့် crypto ကိုဝယ်ပါ။ အပို KYC မပါဘဲ 990€ အထိ။ ဥရောပရှိ လက်လီရောင်းချသူများနှင့် ကော်ပိုရိတ်ဖောက်သည်များအတွက်",
752 "polygonscan_history": "PolygonScan မှတ်တမ်း"
753 }
res/values/strings_nl.arb
+2
@@ -749,6 +749,8 @@
749 "seedtype_polyseed": "Polyseed (16 woorden)",
750 "seed_language_czech": "Tsjechisch",
751 "seed_language_korean": "Koreaans",
752 + "seed_language_chinese_traditional": "Chinese traditionele)",
753 + "dfx_option_description": "Koop crypto met EUR & CHF. Tot 990€ zonder extra KYC. Voor particuliere en zakelijke klanten in Europa",
754 "seed_language_chinese_traditional": "Chinese (traditionele)",
755 "polygonscan_history": "PolygonScan-geschiedenis"
756 }
res/values/strings_pl.arb
+1
@@ -750,5 +750,6 @@
750 "seed_language_czech": "Czech",
751 "seed_language_korean": "koreański",
752 "seed_language_chinese_traditional": "Chiński tradycyjny)",
753 + "dfx_option_description": "Kupuj kryptowaluty za EUR i CHF. Do 990 € bez dodatkowego KYC. Dla klientów detalicznych i korporacyjnych w Europie",
754 "polygonscan_history": "Historia PolygonScan"
755 }
res/values/strings_pt.arb
+1
@@ -749,5 +749,6 @@
749 "seed_language_czech": "Tcheco",
750 "seed_language_korean": "coreano",
751 "seed_language_chinese_traditional": "Chinês tradicional)",
752 + "dfx_option_description": "Compre criptografia com EUR e CHF. Até 990€ sem KYC adicional. Para clientes de varejo e corporativos na Europa",
753 "polygonscan_history": "História do PolygonScan"
754 }
res/values/strings_ru.arb
+1
@@ -750,5 +750,6 @@
750 "seed_language_czech": "Чешский",
751 "seed_language_korean": "Корейский",
752 "seed_language_chinese_traditional": "Китайский традиционный)",
753 + "dfx_option_description": "Покупайте криптовалюту за EUR и CHF. До 990€ без дополнительного KYC. Для розничных и корпоративных клиентов в Европе",
754 "polygonscan_history": "История PolygonScan"
755 }
res/values/strings_th.arb
+1
@@ -748,5 +748,6 @@
748 "seed_language_czech": "ภาษาเช็ก",
749 "seed_language_korean": "เกาหลี",
750 "seed_language_chinese_traditional": "จีน (ดั้งเดิม)",
751 + "dfx_option_description": "ซื้อ crypto ด้วย EUR และ CHF สูงถึง 990€ โดยไม่มี KYC เพิ่มเติม สำหรับลูกค้ารายย่อยและลูกค้าองค์กรในยุโรป",
752 "polygonscan_history": "ประวัติ PolygonScan"
753 }
res/values/strings_tl.arb
+1
@@ -744,5 +744,6 @@
744 "seed_language_czech": "Czech",
745 "seed_language_korean": "Korean",
746 "seed_language_chinese_traditional": "Intsik (tradisyonal)",
747 + "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",
748 "polygonscan_history": "Kasaysayan ng PolygonScan"
749 }
res/values/strings_tr.arb
+1
@@ -748,5 +748,6 @@
748 "seed_language_czech": "Çek",
749 "seed_language_korean": "Koreli",
750 "seed_language_chinese_traditional": "Çin geleneği)",
751 + "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",
752 "polygonscan_history": "PolygonScan geçmişi"
753 }
res/values/strings_uk.arb
+1
@@ -749,6 +749,7 @@
749 "seedtype_polyseed": "Полісей (16 слів)",
750 "seed_language_czech": "Чеський",
751 "seed_language_korean": "Корейський",
752 + "dfx_option_description": "Купуйте криптовалюту за EUR і CHF. До 990 євро без додаткового KYC. Для роздрібних і корпоративних клієнтів у Європі",
753 "seed_language_chinese_traditional": "Китайський (традиційний)",
754 "polygonscan_history": "Історія PolygonScan"
755 }
res/values/strings_ur.arb
+1
@@ -742,5 +742,6 @@
742 "seed_language_czech": "چیک",
743 "seed_language_korean": "کورین",
744 "seed_language_chinese_traditional": "چینی (روایتی)",
745 + "dfx_option_description": "EUR ﺭﻭﺍ CHF ﯽﻓﺎﺿﺍ ۔ﮟﯾﺪﯾﺮﺧ ﻮﭩﭘﺮﮐ ﮫﺗﺎﺳ ﮯﮐ KYC ﮯﯿﻟ ﮯﮐ ﻦﯿﻓﺭﺎﺻ ﭧﯾﺭﻮﭘﺭﺎﮐ ﺭﻭﺍ ﮦﺩﺭﻮﺧ ﮟ",
746 "polygonscan_history": "ﺦﯾﺭﺎﺗ ﯽﮐ ﻦﯿﮑﺳﺍ ﻥﻮﮔ ﯽﻟﻮﭘ"
747 }
res/values/strings_yo.arb
+1
@@ -744,5 +744,6 @@
744 "seed_language_czech": "Czech",
745 "seed_language_korean": "Ara ẹni",
746 "seed_language_chinese_traditional": "Kannada (ibile)",
747 + "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",
748 "polygonscan_history": "PolygonScan itan"
749 }
res/values/strings_zh.arb
+1
@@ -749,5 +749,6 @@
749 "seed_language_czech": "捷克",
750 "seed_language_korean": "韩国人",
751 "seed_language_chinese_traditional": "中国传统的)",
752 + "dfx_option_description": "用欧元和瑞士法郎购买加密货币。高达 990 欧元,无需额外 KYC。对于欧洲的零售和企业客户",
753 "polygonscan_history": "多边形扫描历史"
754 }