CW-1138-Add-CakePay-orders-to-the-transaction-history-screen (#2446)

* migrate to new backend API * hide value_type filter * Improve vendor card parsing * Update country.dart * Update lib/cake_pay/src/models/cake_pay_card.dart Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * Update lib/cake_pay/src/models/cake_pay_card.dart Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * address review comments * fix min limit value * Update lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * cake pay order row * order provider adapter * add Cake Pay order filtering * Update cake_pay_buy_card_view_model.dart * fix hidden order balance * Update lib/cake_pay/src/models/cake_pay_card.dart Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * Update lib/cake_pay/src/services/cake_pay_api.dart Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * address review comments --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

Serhii committed Sep 1, 2025 at 19:09 UTC 4b7a89d397ac3af227f34b0d1c030894d777e676
32 files changed +1028 -291
assets/images/cake_pay_icon.png
Binary files /dev/null and b/assets/images/cake_pay_icon.png differ
lib/buy/buy_provider.dart
+1 -1
@@ -1,6 +1,6 @@
1 import 'package:cake_wallet/buy/buy_amount.dart';
2 import 'package:cake_wallet/buy/buy_quote.dart';
3 -import 'package:cake_wallet/buy/order.dart';
3 +import 'package:cake_wallet/order/order.dart';
4 import 'package:cake_wallet/buy/pairs_utils.dart';
5 import 'package:cake_wallet/buy/payment_method.dart';
6 import 'package:cake_wallet/entities/fiat_currency.dart';
lib/buy/buy_provider_description.dart
+16 -15
@@ -1,21 +1,22 @@
1 import 'package:cw_core/enumerable_item.dart';
2
3 -class BuyProviderDescription extends EnumerableItem<int>
4 - with Serializable<int> {
5 - const BuyProviderDescription({required String title, required int raw})
3 +class BuyProviderDescription extends EnumerableItem<int> with Serializable<int> {
4 + const BuyProviderDescription({required String title, required int raw, required this.image})
5 : super(title: title, raw: raw);
6
8 - static const wyre = BuyProviderDescription(title: 'Wyre', raw: 0);
9 - static const moonPay = BuyProviderDescription(title: 'MoonPay', raw: 1);
7 + final String image;
8
11 - static BuyProviderDescription deserialize({required int raw}) {
12 - switch (raw) {
13 - case 0:
14 - return wyre;
15 - case 1:
16 - return moonPay;
17 - default:
18 - throw Exception('Incorrect token $raw for BuyProviderDescription deserialize');
19 - }
9 + static const wyre = BuyProviderDescription(title: 'Wyre', raw: 0, image: '');
10 + static const moonPay = BuyProviderDescription(title: 'MoonPay', raw: 1, image: '');
11 +
12 + static BuyProviderDescription deserialize({required int raw}) {
13 + switch (raw) {
14 + case 0:
15 + return wyre;
16 + case 1:
17 + return moonPay;
18 + default:
19 + throw Exception('Incorrect token $raw for BuyProviderDescription deserialize');
20 }
21 -}
\ No newline at end of file
21 + }
22 +}
lib/buy/moonpay/moonpay_provider.dart
+4 -2
@@ -6,7 +6,8 @@ import 'package:cake_wallet/buy/buy_exception.dart';
6 import 'package:cake_wallet/buy/buy_provider.dart';
7 import 'package:cake_wallet/buy/buy_provider_description.dart';
8 import 'package:cake_wallet/buy/buy_quote.dart';
9 -import 'package:cake_wallet/buy/order.dart';
9 +import 'package:cake_wallet/order/order.dart';
10 +import 'package:cake_wallet/order/order_source_description.dart';
11 import 'package:cake_wallet/buy/pairs_utils.dart';
12 import 'package:cake_wallet/buy/payment_method.dart';
13 import 'package:cake_wallet/entities/fiat_currency.dart';
@@ -329,7 +330,8 @@ class MoonPayProvider extends BuyProvider {
330
331 return Order(
332 id: id,
332 - provider: BuyProviderDescription.moonPay,
333 + source: OrderSourceDescription.buy,
334 + buyProvider: BuyProviderDescription.moonPay,
335 transferId: id,
336 state: state,
337 createdAt: createdAt,
lib/buy/order.dart deleted
-70
@@ -1,70 +0,0 @@
1 -import 'package:cake_wallet/buy/buy_provider_description.dart';
2 -import 'package:cake_wallet/exchange/trade_state.dart';
3 -import 'package:cw_core/format_amount.dart';
4 -import 'package:cw_core/hive_type_ids.dart';
5 -import 'package:hive/hive.dart';
6 -
7 -part 'order.g.dart';
8 -
9 -@HiveType(typeId: Order.typeId)
10 -class Order extends HiveObject {
11 - Order(
12 - {required this.id,
13 - required this.transferId,
14 - required this.createdAt,
15 - required this.amount,
16 - required this.receiveAddress,
17 - required this.walletId,
18 - BuyProviderDescription? provider,
19 - TradeState? state,
20 - this.from,
21 - this.to}) {
22 - if (provider != null) {
23 - providerRaw = provider.raw;
24 - }
25 - if (state != null) {
26 - stateRaw = state.raw;
27 - }
28 - }
29 -
30 - static const typeId = ORDER_TYPE_ID;
31 - static const boxName = 'Orders';
32 - static const boxKey = 'ordersBoxKey';
33 -
34 - @HiveField(0, defaultValue: '')
35 - String id;
36 -
37 - @HiveField(1, defaultValue: '')
38 - String transferId;
39 -
40 - @HiveField(2)
41 - String? from;
42 -
43 - @HiveField(3)
44 - String? to;
45 -
46 - @HiveField(4, defaultValue: '')
47 - late String stateRaw;
48 -
49 - TradeState get state => TradeState.deserialize(raw: stateRaw);
50 -
51 - @HiveField(5)
52 - DateTime createdAt;
53 -
54 - @HiveField(6, defaultValue: '')
55 - String amount;
56 -
57 - @HiveField(7, defaultValue: '')
58 - String receiveAddress;
59 -
60 - @HiveField(8, defaultValue: '')
61 - String walletId;
62 -
63 - @HiveField(9, defaultValue: 0)
64 - late int providerRaw;
65 -
66 - BuyProviderDescription get provider =>
67 - BuyProviderDescription.deserialize(raw: providerRaw);
68 -
69 - String amountFormatted() => formatAmount(amount);
70 -}
lib/buy/wyre/wyre_buy_provider.dart
+4 -2
@@ -1,5 +1,6 @@
1 import 'dart:convert';
2 import 'package:cake_wallet/buy/buy_exception.dart';
3 +import 'package:cake_wallet/order/order_source_description.dart';
4 import 'package:cake_wallet/buy/pairs_utils.dart';
5 import 'package:cake_wallet/entities/fiat_currency.dart';
6 import 'package:cw_core/crypto_currency.dart';
@@ -7,7 +8,7 @@ import 'package:cw_core/utils/proxy_wrapper.dart';
8 import 'package:cake_wallet/buy/buy_amount.dart';
9 import 'package:cake_wallet/buy/buy_provider.dart';
10 import 'package:cake_wallet/buy/buy_provider_description.dart';
10 -import 'package:cake_wallet/buy/order.dart';
11 +import 'package:cake_wallet/order/order.dart';
12 import 'package:cw_core/wallet_base.dart';
13 import 'package:cw_core/wallet_type.dart';
14 import 'package:cake_wallet/exchange/trade_state.dart';
@@ -157,7 +158,8 @@ class WyreBuyProvider extends BuyProvider {
158
159 return Order(
160 id: id,
160 - provider: BuyProviderDescription.wyre,
161 + source: OrderSourceDescription.buy,
162 + buyProvider: BuyProviderDescription.wyre,
163 transferId: transferId,
164 from: from,
165 to: to,
lib/cake_pay/src/models/cake_pay_card.dart
+1
@@ -39,6 +39,7 @@ class CakePayCard {
39 final termsAndConditions = stripHtmlIfNeeded(json['terms_and_conditions'] as String? ?? '');
40 final howToUse = stripHtmlIfNeeded(json['how_to_use'] as String? ?? '');
41 final fiatCurrency = FiatCurrency.deserialize(raw: json['currency_code'] as String? ?? '');
42 +
43 String? minValue = json['min_value'] as String?;
44
45 final parsedMinValueLocal = _toDouble(json['min_value']);
lib/cake_pay/src/models/cake_pay_order.dart
+52 -1
@@ -15,8 +15,11 @@ class CakePayOrder {
15 final List<OrderCard> cards;
16 final String? externalId;
17 final double amountUsd;
18 + final String totalReceiveAmount;
19 + final int quantity;
20 final String status;
21 final String? vouchers;
22 + final String fiatCurrencyCode;
23 final PaymentData paymentData;
24
25 CakePayOrder({
@@ -24,23 +27,68 @@ class CakePayOrder {
27 required this.cards,
28 required this.externalId,
29 required this.amountUsd,
30 + required this.totalReceiveAmount,
31 + required this.quantity,
32 required this.status,
33 required this.vouchers,
34 + required this.fiatCurrencyCode,
35 required this.paymentData,
36 });
37
38 factory CakePayOrder.fromMap(Map<String, dynamic> map) {
39 +
40 + final cards = map['cards'] as List<dynamic>;
41 + final firstCard = cards.isNotEmpty ? cards.first as Map<String, dynamic> : {};
42 +
43 return CakePayOrder(
44 orderId: map['order_id'] as String,
35 - cards: (map['cards'] as List<dynamic>)
45 + cards: cards
46 .map((x) => OrderCard.fromMap(x as Map<String, dynamic>))
47 .toList(),
48 externalId: map['external_id'] as String?,
49 amountUsd: map['amount_usd'] as double,
50 + totalReceiveAmount: firstCard['subtotal'] as String? ?? '',
51 + quantity: firstCard['quantity'] as int,
52 status: map['status'] as String,
53 vouchers: map['vouchers'] as String?,
54 + fiatCurrencyCode: firstCard['currency_code'] as String? ?? '',
55 paymentData: PaymentData.fromMap(map['payment_data'] as Map<String, dynamic>));
56 }
57 +
58 + static CryptoPaymentData? getPaymentDataFor({CakePayPaymentMethod? method,CakePayOrder? order}) {
59 + if (order == null || method == null) return null;
60 +
61 + final data = switch (method) {
62 + CakePayPaymentMethod.BTC => order.paymentData.btc,
63 + CakePayPaymentMethod.XMR => order.paymentData.xmr,
64 + CakePayPaymentMethod.LTC => order.paymentData.ltc,
65 + CakePayPaymentMethod.LTC_MWEB => order.paymentData.ltc_mweb,
66 + _ => null
67 + };
68 +
69 + if (data == null) return null;
70 +
71 + final bip21 = data.paymentUrls?.bip21;
72 + if (bip21 != null && bip21.isNotEmpty) {
73 + final uri = Uri.parse(bip21);
74 + final addr = uri.path;
75 + final price = uri.queryParameters['amount'] ?? data.price;
76 +
77 + return CryptoPaymentData(price: price, address: addr, amount: data.amount, paymentUrls: data.paymentUrls);
78 + }
79 +
80 + return data;
81 + }
82 +
83 + static String getCurrencyCodeFromPaymentMethod(CakePayPaymentMethod method) {
84 + return switch (method) {
85 + CakePayPaymentMethod.BTC => 'BTC',
86 + CakePayPaymentMethod.BTC_LN => 'BTC',
87 + CakePayPaymentMethod.XMR => 'XMR',
88 + CakePayPaymentMethod.LTC => 'LTC',
89 + CakePayPaymentMethod.LTC_MWEB => 'LTC',
90 + };
91 + }
92 }
93
94 class OrderCard {
@@ -112,11 +160,13 @@ class PaymentData {
160
161 class CryptoPaymentData {
162 final String price;
163 + final String amount;
164 final PaymentUrl? paymentUrls;
165 final String address;
166
167 CryptoPaymentData({
168 required this.price,
169 + required this.amount,
170 this.paymentUrls,
171 required this.address,
172 });
@@ -124,6 +174,7 @@ class CryptoPaymentData {
174 factory CryptoPaymentData.fromMap(Map<String, dynamic> map) {
175 return CryptoPaymentData(
176 price: map['price'] as String,
177 + amount: (map['amount_from'] as double?)?.toString() ?? '',
178 paymentUrls: PaymentUrl.fromMap(map['paymentUrls'] as Map<String, dynamic>?),
179 address: map['address'] as String,
180 );
lib/cake_pay/src/services/cake_pay_api.dart
+24 -2
@@ -19,7 +19,7 @@ class CakePayApi {
19 static const authPath = '/api/accounts/auth';
20 static final verifyEmailPath = '/api/accounts/auth/verify';
21 static final logoutPath = '/api/accounts/logout';
22 - static final createOrderPath = '/api/orders/order';
22 + static final orderPath = '/api/orders/order';
23 static final simulatePaymentPath = '/api/orders/simulate-payment';
24
25 /// AuthenticateUser
@@ -104,7 +104,7 @@ class CakePayApi {
104 required bool confirmsVoidedRefund,
105 required bool confirmsTermsAgreed,
106 }) async {
107 - final uri = Uri.https(baseCakePayUri, createOrderPath);
107 + final uri = Uri.https(baseCakePayUri, orderPath);
108
109 final headers = {
110 'Accept': 'application/json',
@@ -155,6 +155,28 @@ class CakePayApi {
155 throw Exception(message);
156 }
157
158 + /// Get Order by ID
159 + Future<CakePayOrder> getOrderById({required String orderId, required String token}) async {
160 + final uri = Uri.https(baseCakePayUri, '$orderPath/$orderId');
161 + final headers = {
162 + 'Accept': 'application/json',
163 + 'Content-Type': 'application/json',
164 + 'Authorization': 'Token $token',
165 + };
166 + final response = await ProxyWrapper().get(
167 + clearnetUri: uri,
168 + headers: headers,
169 + );
170 +
171 + if (response.statusCode != 200) {
172 + throw Exception('Unexpected http status: ${response.statusCode}');
173 + }
174 +
175 + final bodyJson = json.decode(response.body) as Map<String, dynamic>;
176 +
177 + return CakePayOrder.fromMap(bodyJson);
178 + }
179 +
180 ///Simulate Payment
181 Future<String> simulatePayment(
182 {required String CSRFToken,
lib/cake_pay/src/services/cake_pay_service.dart
+5
@@ -109,6 +109,11 @@ class CakePayService {
109 );
110 }
111
112 + Future<CakePayOrder> findOrderById({required String orderId}) async {
113 + final token = (await secureStorage.read(key: cakePayUserTokenKey))!;
114 + return await cakePayApi.getOrderById(orderId: orderId, token: token);
115 + }
116 +
117 ///Simulate Purchase Gift Card
118 Future<String> simulatePayment({required String orderId}) async {
119 final token = (await secureStorage.read(key: cakePayUserTokenKey))!;
lib/di.dart
+13 -4
@@ -10,7 +10,7 @@ import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
10 import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart';
11 import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart';
12 import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
13 -import 'package:cake_wallet/buy/order.dart';
13 +import 'package:cake_wallet/order/order.dart';
14 import 'package:cake_wallet/core/backup_service_v3.dart';
15 import 'package:cake_wallet/core/new_wallet_arguments.dart';
16 import 'package:cake_wallet/buy/robinhood/robinhood_buy_provider.dart';
@@ -43,6 +43,7 @@ import 'package:cake_wallet/src/screens/start_tor/start_tor_page.dart';
43 import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
44 import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
45 import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
46 +import 'package:cake_wallet/store/dashboard/order_filter_store.dart';
47 import 'package:cake_wallet/themes/core/theme_store.dart';
48 import 'package:cake_wallet/view_model/dev/monero_background_sync.dart';
49 import 'package:cake_wallet/view_model/dev/secure_preferences.dart';
@@ -374,6 +375,7 @@ Future<void> setup({
375 getIt.registerFactory(() =>
376 PayjoinTransactionsStore(payjoinSessionSource: _payjoinSessionSource));
377 getIt.registerSingleton<TradeFilterStore>(TradeFilterStore());
378 + getIt.registerSingleton<OrderFilterStore>(OrderFilterStore());
379 getIt.registerSingleton<TransactionFilterStore>(TransactionFilterStore(getIt.get<AppStore>()));
380 getIt.registerSingleton<FiatConversionStore>(FiatConversionStore());
381 getIt.registerSingleton<SendTemplateStore>(SendTemplateStore(templateSource: _templates));
@@ -545,11 +547,12 @@ Future<void> setup({
547 balanceViewModel: getIt.get<BalanceViewModel>(),
548 appStore: getIt.get<AppStore>(),
549 tradesStore: getIt.get<TradesStore>(),
550 + ordersStore: getIt.get<OrdersStore>(),
551 tradeFilterStore: getIt.get<TradeFilterStore>(),
552 + orderFilterStore: getIt.get<OrderFilterStore>(),
553 transactionFilterStore: getIt.get<TransactionFilterStore>(),
554 settingsStore: settingsStore,
555 yatStore: getIt.get<YatStore>(),
552 - ordersStore: getIt.get<OrdersStore>(),
556 anonpayTransactionsStore: getIt.get<AnonpayTransactionsStore>(),
557 payjoinTransactionsStore: getIt.get<PayjoinTransactionsStore>(),
558 sharedPreferences: getIt.get<SharedPreferences>(),
@@ -1349,8 +1352,13 @@ Future<void> setup({
1352
1353 getIt.registerFactoryParam<OrderDetailsViewModel, Order, void>((order, _) {
1354 final wallet = getIt.get<AppStore>().wallet;
1355 + final cakePayService = getIt.get<CakePayService>();
1356
1353 - return OrderDetailsViewModel(wallet: wallet!, orderForDetails: order);
1357 + return OrderDetailsViewModel(
1358 + wallet: wallet!,
1359 + orderForDetails: order,
1360 + cakePayService: cakePayService,
1361 + orders: _ordersSource);
1362 });
1363
1364 getIt.registerFactoryParam<OrderDetailsPage, Order, void>(
@@ -1423,7 +1431,8 @@ Future<void> setup({
1431 (CakePayVendor vendor, _) {
1432 return CakePayBuyCardViewModel(vendor: vendor,
1433 cakePayService: getIt.get<CakePayService>(),
1426 - sendViewModel: getIt.get<SendViewModel>());
1434 + sendViewModel: getIt.get<SendViewModel>(),
1435 + orders: _ordersSource);
1436 });
1437
1438 getIt.registerFactory(() => CakePayAccountViewModel(cakePayService: getIt.get<CakePayService>()));
lib/main.dart
+1 -1
@@ -3,7 +3,7 @@ import 'dart:io';
3 import 'dart:ui';
4 import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
5 import 'package:cake_wallet/app_scroll_behavior.dart';
6 -import 'package:cake_wallet/buy/order.dart';
6 +import 'package:cake_wallet/order/order.dart';
7 import 'package:cake_wallet/core/auth_service.dart';
8 import 'package:cake_wallet/core/background_sync.dart';
9 import 'package:cake_wallet/core/node_switching_service.dart';
lib/order/order.dart new
+108
@@ -0,0 +1,108 @@
1 +import 'package:cake_wallet/buy/buy_provider_description.dart';
2 +import 'package:cake_wallet/exchange/trade_state.dart';
3 +import 'package:cw_core/format_amount.dart';
4 +import 'package:cw_core/hive_type_ids.dart';
5 +import 'package:hive/hive.dart';
6 +
7 +import 'order_provider_description.dart';
8 +import 'order_source_description.dart';
9 +
10 +part 'order.g.dart';
11 +
12 +@HiveType(typeId: Order.typeId)
13 +class Order extends HiveObject {
14 + Order({
15 + required this.id,
16 + required this.transferId,
17 + required this.createdAt,
18 + required this.amount,
19 + this.receiveAmount,
20 + this.quantity,
21 + required this.receiveAddress,
22 + required this.walletId,
23 + this.from,
24 + this.to,
25 + TradeState? state,
26 + OrderSourceDescription source = OrderSourceDescription.buy,
27 + BuyProviderDescription? buyProvider,
28 + OrderProviderDescription? giftCardProvider,
29 + }) {
30 + sourceRaw = source.raw;
31 + if (state != null) stateRaw = state.raw;
32 + if (buyProvider != null) providerRaw = buyProvider.raw;
33 + if (giftCardProvider != null) providerRaw = giftCardProvider.raw;
34 + }
35 +
36 + static const typeId = ORDER_TYPE_ID;
37 + static const boxName = 'Orders';
38 + static const boxKey = 'ordersBoxKey';
39 +
40 + @HiveField(0, defaultValue: '')
41 + String id;
42 +
43 + @HiveField(1, defaultValue: '')
44 + String transferId;
45 +
46 + @HiveField(2)
47 + String? from;
48 +
49 + @HiveField(3)
50 + String? to;
51 +
52 + @HiveField(4, defaultValue: '')
53 + late String stateRaw;
54 +
55 + TradeState get state => TradeState.deserialize(raw: stateRaw);
56 +
57 + @HiveField(5)
58 + DateTime createdAt;
59 +
60 + @HiveField(6, defaultValue: '')
61 + String amount;
62 +
63 + @HiveField(7, defaultValue: '')
64 + String receiveAddress;
65 +
66 + @HiveField(8, defaultValue: '')
67 + String walletId;
68 +
69 + @HiveField(9, defaultValue: 0)
70 + late int providerRaw;
71 +
72 + @HiveField(10, defaultValue: '')
73 + String? receiveAmount;
74 +
75 + @HiveField(11, defaultValue: 0)
76 + late int? sourceRaw;
77 +
78 + @HiveField(12, defaultValue: '')
79 + String? quantity;
80 +
81 + OrderSourceDescription get source => OrderSourceDescription.deserialize(raw: sourceRaw ?? 0);
82 +
83 + BuyProviderDescription? get buyProvider => source == OrderSourceDescription.buy
84 + ? BuyProviderDescription.deserialize(raw: providerRaw)
85 + : null;
86 +
87 + OrderProviderDescription? get orderProvider => source == OrderSourceDescription.order
88 + ? OrderProviderDescription.deserialize(raw: providerRaw)
89 + : null;
90 +
91 + String get providerTitle {
92 + if (source == OrderSourceDescription.buy) {
93 + return buyProvider?.title ?? '';
94 + } else {
95 + return orderProvider?.title ?? '';
96 + }
97 + }
98 +
99 + String get providerIcon {
100 + if (source == OrderSourceDescription.buy) {
101 + return buyProvider?.image ?? '';
102 + } else {
103 + return orderProvider?.image ?? '';
104 + }
105 + }
106 +
107 + String amountFormatted() => formatAmount(amount) + ' ${from ?? ''}';
108 +}
lib/order/order_provider.dart new
+9
@@ -0,0 +1,9 @@
1 +import 'package:cake_wallet/order/order.dart';
2 +
3 +abstract class OrderProvider {
4 + Future<(Order, Object?)> findOrderById(String id);
5 +
6 + String get title;
7 +
8 + String get trackUrl;
9 +}
lib/order/order_provider_adapter/cake_pay_order_provider_adapter.dart new
+67
@@ -0,0 +1,67 @@
1 +import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
2 +import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
3 +import 'package:cake_wallet/exchange/trade_state.dart';
4 +import 'package:cake_wallet/order/order.dart';
5 +import 'package:cake_wallet/order/order_provider.dart';
6 +import 'package:cake_wallet/order/order_provider_description.dart';
7 +import 'package:cake_wallet/order/order_source_description.dart';
8 +import 'package:cw_core/wallet_base.dart';
9 +
10 +class CakePayOrderProviderAdapter implements OrderProvider {
11 + CakePayOrderProviderAdapter({
12 + required this.wallet,
13 + required this.cakePayService,
14 + });
15 +
16 + final WalletBase wallet;
17 + final CakePayService cakePayService;
18 +
19 + @override
20 + String get title => 'Cake Pay';
21 +
22 + @override
23 + String get trackUrl => '';
24 +
25 + @override
26 + Future<(Order, Object?)> findOrderById(String id) async {
27 + final cakePayOrder = await cakePayService.findOrderById(orderId: id);
28 + final cards = cakePayOrder.cards;
29 + final order = Order(
30 + id: cakePayOrder.orderId,
31 + state: determineState(cakePayOrder.status),
32 + transferId: '',
33 + createdAt: DateTime.now(),
34 + amount: '',
35 + receiveAmount: cakePayOrder.totalReceiveAmount,
36 + quantity: cakePayOrder.quantity.toString(),
37 + receiveAddress: '',
38 + walletId: wallet.id,
39 + from: '',
40 + to: cakePayOrder.fiatCurrencyCode,
41 + source: OrderSourceDescription.order,
42 + giftCardProvider: OrderProviderDescription.cakePay,
43 + );
44 + return (order, cards);
45 + }
46 +
47 + TradeState determineState(String state) {
48 + final swapState = switch (state) {
49 + 'new' => TradeState.pending,
50 + 'expired_but_still_pending' => TradeState.overdue,
51 + 'expired' => TradeState.expired,
52 + 'failed' => TradeState.failed,
53 + 'paid' => TradeState.paid,
54 + 'paid_partial' => TradeState.underpaid,
55 + 'pending_purchase' => TradeState.processing,
56 + 'purchase_processing' => TradeState.processing,
57 + 'purchased' => TradeState.confirmed,
58 + 'pending_email' => TradeState.awaiting,
59 + 'complete' => TradeState.complete,
60 + 'pending_refund' => TradeState.refund,
61 + 'refunded' => TradeState.refunded,
62 + _ => TradeState.notFound
63 + };
64 +
65 + return swapState;
66 + }
67 +}
lib/order/order_provider_adapter/moon_pay_order_provider_adapter.dart new
+47
@@ -0,0 +1,47 @@
1 +import 'package:cake_wallet/buy/buy_provider_description.dart';
2 +import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart';
3 +import 'package:cake_wallet/order/order.dart';
4 +import 'package:cake_wallet/order/order_provider.dart';
5 +import 'package:cake_wallet/order/order_source_description.dart';
6 +import 'package:cake_wallet/store/app_store.dart';
7 +import 'package:cw_core/wallet_base.dart';
8 +
9 +class MoonPayOrderProviderAdapter implements OrderProvider {
10 + MoonPayOrderProviderAdapter(
11 + {required AppStore appStore, required WalletBase wallet, bool isTestEnvironment = false})
12 + : _inner = MoonPayProvider(
13 + appStore: appStore,
14 + wallet: wallet,
15 + isTestEnvironment: isTestEnvironment,
16 + ),
17 + _wallet = wallet;
18 +
19 + final MoonPayProvider _inner;
20 + final WalletBase _wallet;
21 +
22 + @override
23 + String get title => 'MoonPay';
24 +
25 + @override
26 + String get trackUrl => _inner.trackUrl;
27 +
28 + @override
29 + Future<(Order, Object?)> findOrderById(String id) async {
30 + final buyOrder = await _inner.findOrderById(id);
31 + final order = Order(
32 + id: buyOrder.id,
33 + transferId: buyOrder.transferId,
34 + createdAt: buyOrder.createdAt,
35 + amount: buyOrder.amount,
36 + receiveAmount: buyOrder.receiveAmount,
37 + receiveAddress: _wallet.walletAddresses.address,
38 + walletId: _wallet.id,
39 + from: buyOrder.from,
40 + to: buyOrder.to,
41 + state: buyOrder.state,
42 + source: OrderSourceDescription.buy,
43 + buyProvider: BuyProviderDescription.moonPay,
44 + );
45 + return (order, null);
46 + }
47 +}
lib/order/order_provider_adapter/wyre_order_provider_adapter.dart new
+23
@@ -0,0 +1,23 @@
1 +import 'package:cake_wallet/buy/wyre/wyre_buy_provider.dart';
2 +import 'package:cake_wallet/order/order.dart';
3 +import 'package:cake_wallet/order/order_provider.dart';
4 +import 'package:cw_core/wallet_base.dart';
5 +
6 +class WyreOrderProviderAdapter implements OrderProvider {
7 + WyreOrderProviderAdapter({required WalletBase wallet})
8 + : _wyreProvider = WyreBuyProvider(wallet: wallet);
9 +
10 + final WyreBuyProvider _wyreProvider;
11 +
12 + @override
13 + Future<(Order, Object?)> findOrderById(String id) async {
14 + final order = await _wyreProvider.findOrderById(id);
15 + return (order, null);
16 + }
17 +
18 + @override
19 + String get title => _wyreProvider.title;
20 +
21 + @override
22 + String get trackUrl => _wyreProvider.trackUrl;
23 +}
lib/order/order_provider_description.dart new
+23
@@ -0,0 +1,23 @@
1 +import 'package:cw_core/enumerable_item.dart';
2 +
3 +class OrderProviderDescription extends EnumerableItem<int> with Serializable<int> {
4 + const OrderProviderDescription({
5 + required String title,
6 + required int raw,
7 + required this.image,
8 + }) : super(title: title, raw: raw);
9 +
10 + final String image;
11 +
12 + static const cakePay = OrderProviderDescription(
13 + title: 'Cake Pay', raw: 0, image: 'assets/images/cake_pay_icon.png');
14 +
15 + static OrderProviderDescription deserialize({required int raw}) {
16 + switch (raw) {
17 + case 0:
18 + return cakePay;
19 + default:
20 + throw Exception('Invalid OrderProviderDescription $raw');
21 + }
22 + }
23 +}
lib/order/order_source_description.dart new
+20
@@ -0,0 +1,20 @@
1 +import 'package:cw_core/enumerable_item.dart';
2 +
3 +class OrderSourceDescription extends EnumerableItem<int> with Serializable<int> {
4 + const OrderSourceDescription({required String title, required int raw})
5 + : super(title: title, raw: raw);
6 +
7 + static const buy = OrderSourceDescription(title: 'Buy', raw: 0);
8 + static const order = OrderSourceDescription(title: 'Order', raw: 1);
9 +
10 + static OrderSourceDescription deserialize({required int raw}) {
11 + switch (raw) {
12 + case 0:
13 + return buy;
14 + case 1:
15 + return order;
16 + default:
17 + throw Exception('Invalid OrderSourceDescription raw value: $raw');
18 + }
19 + }
20 +}
\ No newline at end of file
lib/router.dart
+1 -1
@@ -3,7 +3,7 @@ import 'dart:io';
3 import 'package:cake_wallet/anonpay/anonpay_info_base.dart';
4 import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
5 import 'package:cake_wallet/core/new_wallet_arguments.dart';
6 -import 'package:cake_wallet/buy/order.dart';
6 +import 'package:cake_wallet/order/order.dart';
7 import 'package:cake_wallet/core/new_wallet_type_arguments.dart';
8 import 'package:cake_wallet/core/totp_request_details.dart';
9 import 'package:cake_wallet/di.dart';
lib/src/screens/dashboard/pages/transactions_page.dart
+24 -9
@@ -1,4 +1,6 @@
1 import 'package:cake_wallet/bitcoin/bitcoin.dart';
2 +import 'package:cake_wallet/entities/balance_display_mode.dart';
3 +import 'package:cake_wallet/order/order_source_description.dart';
4 import 'package:cake_wallet/src/screens/dashboard/widgets/anonpay_transaction_row.dart';
5 import 'package:cake_wallet/src/screens/dashboard/widgets/order_row.dart';
6 import 'package:cake_wallet/src/screens/dashboard/widgets/payjoin_transaction_row.dart';
@@ -189,19 +191,32 @@ class TransactionsPage extends StatelessWidget {
191 }
192 if (item is OrderListItem) {
193 final order = item.order;
192 -
194 return Observer(
194 - builder: (_) => OrderRow(
195 + builder: (_) {
196 +
197 + // TODO: Refactor Amount Hiding Logic it is not working properly for Orders and Trades
198 + final hide = dashboardViewModel.balanceViewModel.displayMode ==
199 + BalanceDisplayMode.hiddenBalance;
200 +
201 + final formattedAmount = hide ? '---' : order.amountFormatted();
202 + final formattedReceiveAmount = hide ? '---' : order.receiveAmount;
203 +
204 + return OrderRow(
205 key: item.key,
206 onTap: () => Navigator.of(context)
207 .pushNamed(Routes.orderDetails, arguments: order),
198 - provider: order.provider,
199 - from: order.from!,
200 - to: order.to!,
201 - createdAtFormattedDate:
202 - DateFormat('HH:mm').format(order.createdAt),
203 - formattedAmount: item.orderFormattedAmount,
204 - ),
208 + providerTitle: order.providerTitle,
209 + providerIconPath: order.providerIcon,
210 + from: order.from ?? '',
211 + to: order.to ?? '',
212 + createdAtFormattedDate: DateFormat('HH:mm').format(order.createdAt),
213 + formattedAmount: formattedAmount,
214 + formattedReceiveAmount: dashboardViewModel.balanceViewModel.isFiatDisabled &&
215 + order.source == OrderSourceDescription.order
216 + ? ''
217 + : formattedReceiveAmount,
218 + );
219 + }
220 );
221 }
222 return Container(color: Colors.transparent, height: 1);
lib/src/screens/dashboard/widgets/order_row.dart
+124 -41
@@ -1,77 +1,110 @@
1 -import 'package:cake_wallet/buy/buy_provider_description.dart';
2 -import 'package:cake_wallet/buy/get_buy_provider_icon.dart';
1 +import 'package:cake_wallet/exchange/trade_state.dart';
2 +import 'package:cake_wallet/palette.dart';
3 +import 'package:cake_wallet/utils/image_utill.dart';
4 import 'package:flutter/material.dart';
5
6 class OrderRow extends StatelessWidget {
6 - OrderRow({
7 - required this.provider,
7 + const OrderRow({
8 + Key? key,
9 + required this.providerTitle,
10 + required this.providerIconPath,
11 required this.from,
12 required this.to,
13 required this.createdAtFormattedDate,
14 + this.state,
15 this.onTap,
16 this.formattedAmount,
13 - super.key,
14 - });
17 + this.formattedReceiveAmount,
18 + }) : super(key: key);
19
20 final VoidCallback? onTap;
17 - final BuyProviderDescription provider;
21 +
22 + final String providerTitle;
23 + final String providerIconPath;
24 +
25 final String from;
26 final String to;
20 - final String createdAtFormattedDate;
27 + final String? createdAtFormattedDate;
28 final String? formattedAmount;
29 + final String? formattedReceiveAmount;
30 + final TradeState? state;
31
32 @override
33 Widget build(BuildContext context) {
25 - final iconColor = Theme.of(context).colorScheme.onSurfaceVariant;
26 -
27 - final providerIcon = getBuyProviderIcon(provider, iconColor: iconColor);
28 -
34 return InkWell(
35 onTap: onTap,
36 child: Container(
32 - padding: EdgeInsets.fromLTRB(24, 8, 24, 8),
37 + padding: const EdgeInsets.fromLTRB(24, 8, 24, 8),
38 color: Colors.transparent,
39 child: Row(
35 - mainAxisSize: MainAxisSize.max,
40 crossAxisAlignment: CrossAxisAlignment.center,
41 children: [
38 - if (providerIcon != null)
39 - Padding(
40 - padding: EdgeInsets.only(right: 12),
41 - child: providerIcon,
42 - ),
42 + Stack(
43 + clipBehavior: Clip.none,
44 + children: [
45 + _ProviderIcon(path: providerIconPath, title: providerTitle),
46 + if (state != null)
47 + Positioned(
48 + right: 0,
49 + bottom: 2,
50 + child: Container(
51 + height: 8,
52 + width: 8,
53 + decoration: BoxDecoration(
54 + color: _statusColor(context, state!),
55 + borderRadius: BorderRadius.circular(12),
56 + ),
57 + ),
58 + ),
59 + ],
60 + ),
61 + const SizedBox(width: 12),
62 Expanded(
63 child: Column(
64 mainAxisSize: MainAxisSize.min,
65 children: [
47 - Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[
48 - Text(
49 - '$from → $to',
50 - style: Theme.of(context).textTheme.bodyMedium!.copyWith(
51 - fontSize: 16,
52 - fontWeight: FontWeight.w500,
53 - ),
54 - ),
55 - formattedAmount != null
56 - ? Text(
57 - formattedAmount! + ' ' + to,
58 - style: Theme.of(context).textTheme.bodyMedium!.copyWith(
59 - fontSize: 16,
60 - fontWeight: FontWeight.w500,
61 - ),
62 - )
63 - : Container()
64 - ]),
65 - SizedBox(height: 5),
66 Row(
67 mainAxisAlignment: MainAxisAlignment.spaceBetween,
68 children: <Widget>[
69 Text(
70 - createdAtFormattedDate,
71 - style: Theme.of(context).textTheme.bodyMedium!.copyWith(
72 - color: Theme.of(context).colorScheme.onSurfaceVariant,
70 + '$from → $to',
71 + style: Theme.of(context).textTheme.bodyLarge?.copyWith(
72 + fontSize: 16,
73 + fontWeight: FontWeight.w600,
74 + color: Theme.of(context).colorScheme.primary,
75 ),
76 ),
77 + if (formattedAmount != null)
78 + Text(
79 + formattedAmount!,
80 + style: Theme.of(context).textTheme.bodyLarge?.copyWith(
81 + fontSize: 16,
82 + fontWeight: FontWeight.w500,
83 + color: Theme.of(context).colorScheme.onSurface,
84 + ),
85 + ),
86 + ],
87 + ),
88 + const SizedBox(height: 5),
89 + Row(
90 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
91 + children: <Widget>[
92 + if (createdAtFormattedDate != null)
93 + Text(
94 + createdAtFormattedDate!,
95 + style: Theme.of(context).textTheme.bodyMedium?.copyWith(
96 + fontWeight: FontWeight.w500,
97 + color: Theme.of(context).colorScheme.onSurfaceVariant,
98 + ),
99 + ),
100 + if (formattedReceiveAmount != null)
101 + Text(
102 + formattedReceiveAmount!,
103 + style: Theme.of(context).textTheme.bodyMedium?.copyWith(
104 + fontWeight: FontWeight.w500,
105 + color: Theme.of(context).colorScheme.onSurfaceVariant,
106 + ),
107 + ),
108 ],
109 )
110 ],
@@ -82,4 +115,54 @@ class OrderRow extends StatelessWidget {
115 ),
116 );
117 }
118 +
119 + Color _statusColor(BuildContext context, TradeState status) {
120 + switch (status) {
121 + case TradeState.complete:
122 + case TradeState.completed:
123 + case TradeState.finished:
124 + case TradeState.success:
125 + case TradeState.settled:
126 + return PaletteDark.brightGreen;
127 + case TradeState.failed:
128 + case TradeState.expired:
129 + case TradeState.notFound:
130 + return Palette.darkRed;
131 + default:
132 + return const Color(0xffff6600);
133 + }
134 + }
135 +}
136 +
137 +class _ProviderIcon extends StatelessWidget {
138 + const _ProviderIcon({required this.path, required this.title});
139 +
140 + final String path;
141 + final String title;
142 +
143 + @override
144 + Widget build(BuildContext context) {
145 + if (path.isNotEmpty) {
146 + return ClipRRect(
147 + borderRadius: BorderRadius.circular(50),
148 + child: ImageUtil.getImageFromPath(imagePath: path, height: 36, width: 36),
149 + );
150 + }
151 + return Container(
152 + height: 36,
153 + width: 36,
154 + decoration: BoxDecoration(
155 + borderRadius: BorderRadius.circular(50),
156 + color: Theme.of(context).colorScheme.surfaceVariant,
157 + ),
158 + alignment: Alignment.center,
159 + child: Text(
160 + title.isNotEmpty ? title.characters.first.toUpperCase() : '?',
161 + style: Theme.of(context).textTheme.bodyMedium?.copyWith(
162 + fontWeight: FontWeight.w700,
163 + color: Theme.of(context).colorScheme.onSurfaceVariant,
164 + ),
165 + ),
166 + );
167 + }
168 }
lib/src/screens/order_details/cake_pay_detail_list_card_item.dart new
+27
@@ -0,0 +1,27 @@
1 +import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
2 +import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
3 +import 'package:flutter/material.dart';
4 +
5 +class CakePayDetailsListCardItem extends StandartListItem {
6 + CakePayDetailsListCardItem({
7 + required String title,
8 + required String value,
9 + required this.id,
10 + required this.createdAt,
11 + this.price,
12 + this.quantity,
13 + required this.from,
14 + required this.to,
15 + required this.onTap,
16 + required this.cards,
17 + }) : super(title: title, value: value);
18 +
19 + final String id;
20 + final String createdAt;
21 + final String? price;
22 + final String? quantity;
23 + final String from;
24 + final String to;
25 + final void Function(BuildContext) onTap;
26 + final List<OrderCard> cards;
27 +}
lib/src/screens/order_details/order_details_page.dart
+35 -12
@@ -1,4 +1,9 @@
1 +import 'package:cake_wallet/src/screens/order_details/cake_pay_detail_list_card_item.dart';
2 +import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart';
3 +import 'package:cake_wallet/src/widgets/cake_pay_order_list_card.dart';
4 import 'package:cake_wallet/src/widgets/standard_list.dart';
5 +import 'package:cake_wallet/src/widgets/standard_list_status_row.dart';
6 +import 'package:cake_wallet/themes/core/material_base_theme.dart';
7 import 'package:cake_wallet/utils/show_bar.dart';
8 import 'package:cake_wallet/view_model/order_details_view_model.dart';
9 import 'package:flutter/material.dart';
@@ -19,24 +24,25 @@ class OrderDetailsPage extends BasePage {
24 final OrderDetailsViewModel orderDetailsViewModel;
25
26 @override
22 - Widget body(BuildContext context) =>
23 - OrderDetailsPageBody(orderDetailsViewModel);
27 + Widget body(BuildContext context) => OrderDetailsPageBody(orderDetailsViewModel, currentTheme);
28 }
29
30 class OrderDetailsPageBody extends StatefulWidget {
27 - OrderDetailsPageBody(this.orderDetailsViewModel);
31 + OrderDetailsPageBody(this.orderDetailsViewModel, this.currentTheme);
32
33 final OrderDetailsViewModel orderDetailsViewModel;
34 + final MaterialThemeBase currentTheme;
35
36 @override
37 OrderDetailsPageBodyState createState() =>
33 - OrderDetailsPageBodyState(orderDetailsViewModel);
38 + OrderDetailsPageBodyState(orderDetailsViewModel, currentTheme);
39 }
40
41 class OrderDetailsPageBodyState extends State<OrderDetailsPageBody> {
37 - OrderDetailsPageBodyState(this.orderDetailsViewModel);
42 + OrderDetailsPageBodyState(this.orderDetailsViewModel, this.currentTheme);
43
44 final OrderDetailsViewModel orderDetailsViewModel;
45 + final MaterialThemeBase currentTheme;
46
47 @override
48 void dispose() {
@@ -47,28 +53,45 @@ class OrderDetailsPageBodyState extends State<OrderDetailsPageBody> {
53 @override
54 Widget build(BuildContext context) {
55 return Observer(builder: (_) {
56 + final itemsCount = orderDetailsViewModel.items.length;
57 return SectionStandardList(
58 sectionCount: 1,
52 - itemCounter: (int _) => orderDetailsViewModel.items.length,
59 + itemCounter: (int _) => itemsCount,
60 itemBuilder: (__, index) {
61 final item = orderDetailsViewModel.items[index];
62
63 + if (item is DetailsListStatusItem) {
64 + return StandardListStatusRow(title: item.title, value: item.value);
65 + }
66 +
67 + if (item is CakePayDetailsListCardItem) {
68 + final cardImagePath = (item.cards.isNotEmpty) ? item.cards.first.cardImagePath : null;
69 +
70 + return CakePayOrderListCard(
71 + id: item.id,
72 + create: item.createdAt,
73 + price: item.price,
74 + quantity: item.quantity,
75 + pair: '${item.from} → ${item.to}',
76 + currentTheme: widget.currentTheme.type,
77 + onTap: item.onTap,
78 + backgroundImage: cardImagePath,
79 + );
80 + }
81 +
82 if (item is TrackTradeListItem) {
83 return GestureDetector(
84 onTap: item.onTap,
59 - child: ListRow(
60 - title: '${item.title}', value: '${item.value}'));
85 + child: ListRow(title: '${item.title}', value: '${item.value}'));
86 } else {
87 return GestureDetector(
88 onTap: () {
89 Clipboard.setData(ClipboardData(text: '${item.value}'));
90 showBar<void>(context, S.of(context).copied_to_clipboard);
91 },
67 - child: ListRow(
68 - title: '${item.title}', value: '${item.value}'));
92 + child: ListRow(title: '${item.title}', value: '${item.value}'));
93 }
94 });
95 });
96 }
73 -
74 -}
\ No newline at end of file
97 +}
lib/src/widgets/cake_pay_order_list_card.dart new
+188
@@ -0,0 +1,188 @@
1 +import 'package:flutter/material.dart';
2 +import 'package:cake_wallet/themes/core/material_base_theme.dart';
3 +
4 +class CakePayOrderListCard extends StatelessWidget {
5 + const CakePayOrderListCard({
6 + Key? key,
7 + required this.id,
8 + required this.create,
9 + this.quantity,
10 + this.price,
11 + required this.pair,
12 + required this.onTap,
13 + required this.currentTheme,
14 + this.backgroundImage,
15 + this.aspectRatio = 16 / 9,
16 + }) : super(key: key);
17 +
18 + final String id;
19 + final String create;
20 + final String? quantity;
21 + final String? price;
22 + final String pair;
23 + final ThemeType currentTheme;
24 + final void Function(BuildContext) onTap;
25 + final String? backgroundImage;
26 + final double aspectRatio;
27 +
28 + ImageProvider? _imageProviderFor(String? path) {
29 + if (path == null || path.isEmpty) return null;
30 + if (path.startsWith('http://') || path.startsWith('https://')) return NetworkImage(path);
31 + return AssetImage(path);
32 + }
33 +
34 + Color _labelBackground(BuildContext context) {
35 + final surface = Theme.of(context).colorScheme.surface;
36 + return surface.withOpacity(0.75);
37 + }
38 +
39 + Color _titleLabelBackground(BuildContext context) {
40 + final surface = Theme.of(context).colorScheme.surface;
41 + return surface.withOpacity(0.85);
42 + }
43 +
44 + Widget _labelBox(
45 + BuildContext context, {
46 + required String text,
47 + EdgeInsets padding = const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
48 + double radius = 10,
49 + Color? background,
50 + TextStyle? style,
51 + }) {
52 + return Container(
53 + padding: padding,
54 + decoration: BoxDecoration(
55 + color: background ?? _labelBackground(context),
56 + borderRadius: BorderRadius.circular(radius),
57 + ),
58 + child: Text(
59 + text,
60 + overflow: TextOverflow.ellipsis,
61 + maxLines: 1,
62 + style: style ??
63 + Theme.of(context).textTheme.bodyMedium?.copyWith(
64 + fontWeight: FontWeight.w600,
65 + color: Theme.of(context).colorScheme.onSurface,
66 + ),
67 + ),
68 + );
69 + }
70 +
71 + @override
72 + Widget build(BuildContext context) {
73 + final image = _imageProviderFor(backgroundImage);
74 +
75 + return Padding(
76 + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
77 + child: InkWell(
78 + borderRadius: BorderRadius.circular(16),
79 + onTap: () => onTap(context),
80 + child: ClipRRect(
81 + borderRadius: BorderRadius.circular(16),
82 + child: AspectRatio(
83 + aspectRatio: aspectRatio,
84 + child: Stack(
85 + fit: StackFit.expand,
86 + children: [
87 + if (image != null)
88 + Image(image: image, fit: BoxFit.cover)
89 + else
90 + Container(color: Theme.of(context).colorScheme.surfaceContainerHighest),
91 + if (image != null)
92 + Container(
93 + decoration: const BoxDecoration(
94 + gradient: LinearGradient(
95 + begin: Alignment.topCenter,
96 + end: Alignment.bottomCenter,
97 + stops: [0.0, 0.6, 1.0],
98 + colors: [Colors.transparent, Colors.black26, Colors.black54],
99 + ),
100 + ),
101 + ),
102 + Padding(
103 + padding: const EdgeInsets.all(14.0),
104 + child: Column(
105 + crossAxisAlignment: CrossAxisAlignment.start,
106 + children: [
107 + Row(
108 + mainAxisAlignment: MainAxisAlignment.spaceBetween,
109 + children: [
110 + Column(
111 + crossAxisAlignment: CrossAxisAlignment.start,
112 + children: [
113 + _labelBox(
114 + context,
115 + text: 'Create • $create',
116 + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
117 + radius: 8,
118 + style: Theme.of(context).textTheme.bodySmall?.copyWith(
119 + fontWeight: FontWeight.w700,
120 + fontSize: 10,
121 + color: Theme.of(context).colorScheme.onSurface,
122 + ),
123 + ),
124 + const SizedBox(height: 4),
125 + _labelBox(
126 + context,
127 + text: 'Qty • $quantity Total • $price',
128 + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
129 + radius: 8,
130 + style: Theme.of(context).textTheme.bodySmall?.copyWith(
131 + fontWeight: FontWeight.w700,
132 + fontSize: 10,
133 + color: Theme.of(context).colorScheme.onSurface,
134 + ),
135 + ),
136 + ],
137 + ),
138 + _labelBox(
139 + context,
140 + text: pair,
141 + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
142 + radius: 12,
143 + background: _titleLabelBackground(context),
144 + style: Theme.of(context).textTheme.titleMedium?.copyWith(
145 + fontWeight: FontWeight.w800,
146 + fontSize: 16,
147 + color: Theme.of(context).colorScheme.onSurface,
148 + ),
149 + ),
150 + ],
151 + ),
152 + const Spacer(),
153 + _labelBox(
154 + context,
155 + text: 'Order • $id',
156 + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
157 + radius: 10,
158 + style: Theme.of(context).textTheme.bodyMedium?.copyWith(
159 + fontWeight: FontWeight.w800,
160 + fontSize: 12,
161 + color: Theme.of(context).colorScheme.onSurface,
162 + ),
163 + ),
164 + ],
165 + ),
166 + ),
167 + IgnorePointer(
168 + child: Container(
169 + decoration: BoxDecoration(
170 + borderRadius: BorderRadius.circular(16),
171 + border: Border.all(
172 + color: Theme.of(context)
173 + .colorScheme
174 + .onSurface
175 + .withOpacity(currentTheme == ThemeType.dark ? 0.08 : 0.12),
176 + width: 1,
177 + ),
178 + ),
179 + ),
180 + ),
181 + ],
182 + ),
183 + ),
184 + ),
185 + ),
186 + );
187 + }
188 +}
lib/store/dashboard/order_filter_store.dart new
+46
@@ -0,0 +1,46 @@
1 +import 'package:cake_wallet/order/order_provider_description.dart';
2 +import 'package:cake_wallet/order/order_source_description.dart';
3 +import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
4 +import 'package:cw_core/wallet_base.dart';
5 +import 'package:mobx/mobx.dart';
6 +
7 +part 'order_filter_store.g.dart';
8 +
9 +class OrderFilterStore = OrderFilterStoreBase with _$OrderFilterStore;
10 +
11 +abstract class OrderFilterStoreBase with Store {
12 + OrderFilterStoreBase() : displayCakePay = true;
13 +
14 + @observable
15 + bool displayCakePay;
16 +
17 + @computed
18 + bool get displayAllOrders => displayCakePay;
19 +
20 + @action
21 + void toggleDisplayOrder(OrderProviderDescription provider) {
22 + switch (provider) {
23 + case OrderProviderDescription.cakePay:
24 + displayCakePay = !displayCakePay;
25 + break;
26 + }
27 + }
28 +
29 + List<OrderListItem> filtered({
30 + required List<OrderListItem> orders,
31 + required WalletBase wallet,
32 + }) {
33 + final walletOrders =
34 + orders.where((item) => item.order.walletId == wallet.id).toList();
35 +
36 + final cakePayOrders = walletOrders.where((item) {
37 + final order = item.order;
38 + final isOrderSource = order.source == OrderSourceDescription.order;
39 + final isCakePay = order.orderProvider == OrderProviderDescription.cakePay;
40 + return isOrderSource && isCakePay;
41 + }).toList();
42 +
43 + if (!displayCakePay) return <OrderListItem>[];
44 + return cakePayOrders;
45 + }
46 +}
\ No newline at end of file
lib/store/dashboard/orders_store.dart
+1 -1
@@ -1,5 +1,5 @@
1 import 'dart:async';
2 -import 'package:cake_wallet/buy/order.dart';
2 +import 'package:cake_wallet/order/order.dart';
3 import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
4 import 'package:flutter/foundation.dart';
5 import 'package:hive/hive.dart';
lib/view_model/buy/buy_view_model.dart
+1 -1
@@ -8,7 +8,7 @@ import 'package:cw_core/wallet_type.dart';
8 import 'package:cake_wallet/store/settings_store.dart';
9 import 'package:cake_wallet/view_model/buy/buy_item.dart';
10 import 'package:hive/hive.dart';
11 -import 'package:cake_wallet/buy/order.dart';
11 +import 'package:cake_wallet/order/order.dart';
12 import 'package:cake_wallet/store/dashboard/orders_store.dart';
13 import 'package:mobx/mobx.dart';
14 import 'package:cw_core/wallet_base.dart';
lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart
+43 -36
@@ -1,14 +1,20 @@
1 import 'dart:async';
2
3 +import 'package:cake_wallet/buy/buy_provider_description.dart';
4 +import 'package:cake_wallet/order/order_provider_description.dart';
5 +import 'package:cake_wallet/order/order.dart';
6 +import 'package:cake_wallet/order/order_source_description.dart';
7 import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
8 import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
9 import 'package:cake_wallet/cake_pay/src/models/cake_pay_vendor.dart';
10 import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
11 import 'package:cake_wallet/core/execution_state.dart';
12 +import 'package:cake_wallet/exchange/trade_state.dart';
13 import 'package:cake_wallet/utils/feature_flag.dart';
14 import 'package:cake_wallet/view_model/send/send_view_model.dart';
15 import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
16 import 'package:cw_core/wallet_type.dart';
17 +import 'package:hive/hive.dart';
18 import 'package:mobx/mobx.dart';
19
20 part 'cake_pay_buy_card_view_model.g.dart';
@@ -17,8 +23,12 @@ class CakePayBuyCardViewModel = CakePayBuyCardViewModelBase with _$CakePayBuyCar
23
24 abstract class CakePayBuyCardViewModelBase with Store {
25 CakePayBuyCardViewModelBase(
20 - {required this.vendor, required CakePayService cakePayService, required this.sendViewModel})
21 - : _cakePayService = cakePayService, walletType = sendViewModel.walletType,
26 + {required this.vendor,
27 + required CakePayService cakePayService,
28 + required this.sendViewModel,
29 + required this.orders})
30 + : _cakePayService = cakePayService,
31 + walletType = sendViewModel.walletType,
32 amount = vendor.card!.denominationItems.isNotEmpty
33 ? vendor.card!.denominationItems.first.value
34 : 0,
@@ -41,6 +51,7 @@ abstract class CakePayBuyCardViewModelBase with Store {
51 final double min;
52 final CakePayCard card;
53 final WalletType walletType;
54 + final Box<Order> orders;
55
56 CakePayOrder? order;
57 Timer? _timer;
@@ -56,7 +67,7 @@ abstract class CakePayBuyCardViewModelBase with Store {
67 bool get isDenominationSelected =>
68 card.denominationItems.isNotEmpty &&
69 card.denominationItems.any((item) => item.value == amount);
59 -
70 +
71 Future<bool> get isUserLogged async => await _cakePayService.isLogged();
72
73 @observable
@@ -99,7 +110,10 @@ abstract class CakePayBuyCardViewModelBase with Store {
110 case WalletType.bitcoin:
111 return [CakePayPaymentMethod.BTC];
112 case WalletType.litecoin:
102 - return [CakePayPaymentMethod.LTC,if (sendViewModel.isMwebEnabled) CakePayPaymentMethod.LTC_MWEB];
113 + return [
114 + CakePayPaymentMethod.LTC,
115 + if (sendViewModel.isMwebEnabled) CakePayPaymentMethod.LTC_MWEB
116 + ];
117 case WalletType.monero:
118 return [CakePayPaymentMethod.XMR];
119 default:
@@ -124,31 +138,6 @@ abstract class CakePayBuyCardViewModelBase with Store {
138 amount = double.parse(input.replaceAll(',', '.'));
139 }
140
127 - CryptoPaymentData? getPaymentDataFor(CakePayPaymentMethod? method) {
128 - if (order == null || method == null) return null;
129 -
130 - final data = switch (method) {
131 - CakePayPaymentMethod.BTC => order?.paymentData.btc,
132 - CakePayPaymentMethod.XMR => order?.paymentData.xmr,
133 - CakePayPaymentMethod.LTC => order?.paymentData.ltc,
134 - CakePayPaymentMethod.LTC_MWEB => order?.paymentData.ltc_mweb,
135 - _ => null
136 - };
137 -
138 - if (data == null) return null;
139 -
140 - final bip21 = data.paymentUrls?.bip21;
141 - if (bip21 != null && bip21.isNotEmpty) {
142 - final uri = Uri.parse(bip21);
143 - final addr = uri.path;
144 - final price = uri.queryParameters['amount'] ?? data.price;
145 -
146 - return CryptoPaymentData(price: price, address: addr);
147 - }
148 -
149 - return data;
150 - }
151 -
141 @action
142 Future<void> createOrder() async {
143 if (walletType != WalletType.bitcoin &&
@@ -166,8 +155,29 @@ abstract class CakePayBuyCardViewModelBase with Store {
155 confirmsVoidedRefund: confirmsVoidedRefund,
156 confirmsTermsAgreed: confirmsTermsAgreed,
157 );
169 - await confirmSending();
158 + final paymentData =
159 + CakePayOrder.getPaymentDataFor(method: selectedPaymentMethod, order: order); // TODO should hande other currencies if added
160 + if (paymentData == null || order == null)
161 + throw Exception('Payment data or order is not available.');
162 +
163 + await confirmSending(paymentData);
164 expirationTime = order!.paymentData.expirationTime;
165 +
166 + final orderRecord = Order(
167 + id: order!.orderId,
168 + state: TradeState.deserialize(raw: order!.status),
169 + transferId: order!.externalId ?? '',
170 + from: CakePayOrder.getCurrencyCodeFromPaymentMethod(selectedPaymentMethod!),
171 + to: order!.fiatCurrencyCode,
172 + createdAt: DateTime.now(),
173 + amount: paymentData.amount ?? '',
174 + receiveAmount: order!.totalReceiveAmount,
175 + quantity: order!.quantity.toString(),
176 + receiveAddress: paymentData.address ?? '',
177 + source: OrderSourceDescription.order,
178 + giftCardProvider: OrderProviderDescription.cakePay,
179 + walletId: sendViewModel.wallet.id);
180 + orders.add(orderRecord);
181 updateRemainingTime();
182 _startExpirationTimer();
183 } catch (e) {
@@ -177,15 +187,12 @@ abstract class CakePayBuyCardViewModelBase with Store {
187 }
188
189 @action
180 - Future<void> confirmSending() async {
181 - final cryptoPaymentData = getPaymentDataFor(selectedPaymentMethod);
182 - if (order == null || cryptoPaymentData == null) return;
183 -
190 + Future<void> confirmSending(CryptoPaymentData paymentData) async {
191 try {
192 sendViewModel.clearOutputs();
193 final output = sendViewModel.outputs.first;
187 - output.address = cryptoPaymentData.address;
188 - output.setCryptoAmount(cryptoPaymentData.price);
194 + output.address = paymentData.address;
195 + output.setCryptoAmount(paymentData.price);
196
197 await sendViewModel.createTransaction();
198 } catch (e) {
lib/view_model/dashboard/dashboard_view_model.dart
+13 -1
@@ -13,7 +13,9 @@ import 'package:cake_wallet/entities/service_status.dart';
13 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
14 import 'package:cake_wallet/generated/i18n.dart';
15 import 'package:cake_wallet/monero/monero.dart';
16 +import 'package:cake_wallet/order/order_provider_description.dart';
17 import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
18 +import 'package:cake_wallet/store/dashboard/order_filter_store.dart';
19 import 'package:cake_wallet/utils/device_info.dart';
20 import 'package:cake_wallet/utils/show_pop_up.dart';
21 import 'package:cw_core/utils/proxy_wrapper.dart';
@@ -72,6 +74,7 @@ abstract class DashboardViewModelBase with Store {
74 required this.appStore,
75 required this.tradesStore,
76 required this.tradeFilterStore,
77 + required this.orderFilterStore,
78 required this.transactionFilterStore,
79 required this.settingsStore,
80 required this.yatStore,
@@ -110,6 +113,13 @@ abstract class DashboardViewModelBase with Store {
113 // caption: S.current.transactions_by_date,
114 // onChanged: null),
115 ],
116 + 'Orders': [
117 + FilterItem(
118 + value: () => orderFilterStore.displayCakePay,
119 + caption: 'Cake Pay',
120 + onChanged: () =>
121 + orderFilterStore.toggleDisplayOrder(OrderProviderDescription.cakePay)),
122 + ],
123 S.current.trades: [
124 FilterItem(
125 value: () => tradeFilterStore.displayAllTrades,
@@ -452,7 +462,7 @@ abstract class DashboardViewModelBase with Store {
462 _items.addAll(
463 transactionFilterStore.filtered(transactions: [...transactions, ...anonpayTransactions]));
464 _items.addAll(tradeFilterStore.filtered(trades: trades, wallet: wallet));
455 - _items.addAll(orders);
465 + _items.addAll(orderFilterStore.filtered(orders: orders, wallet: wallet));
466
467 if (payjoinTransactions.isNotEmpty) {
468 final _payjoinTransactions = payjoinTransactions;
@@ -807,6 +817,8 @@ abstract class DashboardViewModelBase with Store {
817
818 TradeFilterStore tradeFilterStore;
819
820 + OrderFilterStore orderFilterStore;
821 +
822 AnonpayTransactionsStore anonpayTransactionsStore;
823
824 TransactionFilterStore transactionFilterStore;
lib/view_model/dashboard/order_list_item.dart
+6 -8
@@ -1,4 +1,4 @@
1 -import 'package:cake_wallet/buy/order.dart';
1 +import 'package:cake_wallet/order/order.dart';
2 import 'package:cake_wallet/store/settings_store.dart';
3 import 'package:cake_wallet/view_model/dashboard/action_list_item.dart';
4 import 'package:cake_wallet/entities/balance_display_mode.dart';
@@ -15,13 +15,11 @@ class OrderListItem extends ActionListItem {
15
16 BalanceDisplayMode get displayMode => settingsStore.balanceDisplayMode;
17
18 - String get orderFormattedAmount {
19 - return order.amount != null
20 - ? displayMode == BalanceDisplayMode.hiddenBalance
21 - ? '---'
22 - : order.amountFormatted()
23 - : order.amount;
24 - }
18 + String get orderFormattedAmount =>
19 + displayMode == BalanceDisplayMode.hiddenBalance ? '---' : order.amountFormatted();
20 +
21 + String get orderFormattedReceiveAddress =>
22 + displayMode == BalanceDisplayMode.hiddenBalance ? '---' : order.receiveAmount ?? '';
23
24 @override
25 DateTime get date => order.createdAt;
lib/view_model/order_details_view_model.dart
+101 -83
@@ -1,41 +1,61 @@
1 import 'dart:async';
2 -import 'package:cake_wallet/buy/buy_provider.dart';
2 +
3 import 'package:cake_wallet/buy/buy_provider_description.dart';
4 -import 'package:cake_wallet/buy/order.dart';
4 +import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
5 +import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
6 +import 'package:cake_wallet/core/utilities.dart';
7 +import 'package:cake_wallet/generated/i18n.dart';
8 +import 'package:cake_wallet/order/order.dart';
9 +import 'package:cake_wallet/order/order_provider.dart';
10 +import 'package:cake_wallet/order/order_provider_adapter/cake_pay_order_provider_adapter.dart';
11 +import 'package:cake_wallet/order/order_provider_adapter/wyre_order_provider_adapter.dart';
12 +import 'package:cake_wallet/order/order_provider_description.dart';
13 +import 'package:cake_wallet/order/order_source_description.dart';
14 +import 'package:cake_wallet/src/screens/order_details/cake_pay_detail_list_card_item.dart';
15 +import 'package:cake_wallet/src/screens/trade_details/track_trade_list_item.dart';
16 +import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart';
17 +import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
18 import 'package:cake_wallet/utils/date_formatter.dart';
19 +import 'package:cake_wallet/utils/show_bar.dart';
20 import 'package:cw_core/utils/print_verbose.dart';
21 +import 'package:cw_core/wallet_base.dart';
22 +import 'package:flutter/material.dart';
23 +import 'package:flutter/services.dart';
24 +import 'package:hive/hive.dart';
25 import 'package:mobx/mobx.dart';
8 -import 'package:cake_wallet/generated/i18n.dart';
9 -import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
10 -import 'package:cake_wallet/src/screens/trade_details/track_trade_list_item.dart';
26 import 'package:url_launcher/url_launcher.dart';
12 -import 'package:cw_core/wallet_base.dart';
13 -import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart';
14 -import 'package:cake_wallet/buy/wyre/wyre_buy_provider.dart';
27
28 part 'order_details_view_model.g.dart';
29
18 -class OrderDetailsViewModel = OrderDetailsViewModelBase
19 - with _$OrderDetailsViewModel;
30 +class OrderDetailsViewModel = OrderDetailsViewModelBase with _$OrderDetailsViewModel;
31
32 abstract class OrderDetailsViewModelBase with Store {
22 - OrderDetailsViewModelBase({required WalletBase wallet, required Order orderForDetails})
23 - : items = ObservableList<StandartListItem>(),
24 - order = orderForDetails {
25 - if (order.provider != null) {
26 - switch (order.provider) {
27 - case BuyProviderDescription.wyre:
28 - _provider = WyreBuyProvider(wallet: wallet);
29 - break;
30 - case BuyProviderDescription.moonPay:
31 - // _provider = MoonPayProvider(wallet: wallet);// TODO: CW-521
32 - break;
33 - }
33 + OrderDetailsViewModelBase({
34 + required WalletBase wallet,
35 + required Order orderForDetails,
36 + required this.cakePayService,
37 + required this.orders,
38 + }) : items = ObservableList<StandartListItem>(),
39 + order = orderForDetails {
40 + switch (order.source) {
41 + case OrderSourceDescription.buy:
42 + if (order.buyProvider == BuyProviderDescription.wyre) {
43 + _provider = WyreOrderProviderAdapter(wallet: wallet);
44 + } else if (order.buyProvider == BuyProviderDescription.moonPay) {
45 + //_provider = MoonPayOrderProviderAdapter(appStore: null /* provide if needed */, wallet: wallet);
46 + }
47 + break;
48 +
49 + case OrderSourceDescription.order:
50 + if (order.orderProvider == OrderProviderDescription.cakePay) {
51 + _provider = CakePayOrderProviderAdapter(wallet: wallet, cakePayService: cakePayService);
52 + }
53 + break;
54 }
55
56 _updateItems();
57 _updateOrder();
38 - timer = Timer.periodic(Duration(seconds: 20), (_) async => _updateOrder());
58 + timer = Timer.periodic(const Duration(seconds: 20), (_) async => _updateOrder());
59 }
60
61 @observable
@@ -44,27 +64,37 @@ abstract class OrderDetailsViewModelBase with Store {
64 @observable
65 ObservableList<StandartListItem> items;
66
47 - BuyProvider? _provider;
67 + final CakePayService cakePayService;
68 + final Box<Order> orders;
69 + OrderProvider? _provider;
70 + List<OrderCard> cards = [];
71
72 Timer? timer;
73
74 @action
75 Future<void> _updateOrder() async {
76 try {
54 - if (_provider != null && (_provider is MoonPayProvider || _provider is WyreBuyProvider)) {
55 - final updatedOrder = _provider is MoonPayProvider
56 - ? await (_provider as MoonPayProvider).findOrderById(order.id)
57 - : await (_provider as WyreBuyProvider).findOrderById(order.id);
58 - updatedOrder.from = order.from;
59 - updatedOrder.to = order.to;
60 - updatedOrder.receiveAddress = order.receiveAddress;
61 - updatedOrder.walletId = order.walletId;
62 - if (order.provider != null) {
63 - updatedOrder.providerRaw = order.provider.raw;
64 - }
77 + if (_provider == null) return;
78 +
79 + final updatedOrderObj = await _provider!.findOrderById(order.id);
80 + final updatedOrder = updatedOrderObj.$1;
81 +
82 + if (_provider is CakePayOrderProviderAdapter) {
83 + cards = updatedOrderObj.$2 as List<OrderCard>? ?? [];
84 + }
85 +
86 + final existing = orders.values.firstWhereOrNull((e) => e.id == updatedOrder.id);
87 + if (existing != null) {
88 + existing.stateRaw = updatedOrder.stateRaw;
89 + await existing.save();
90 +
91 + order = existing;
92 + } else {
93 + await orders.add(updatedOrder);
94 order = updatedOrder;
66 - _updateItems();
95 }
96 +
97 + _updateItems();
98 } catch (e) {
99 printV(e.toString());
100 }
@@ -73,55 +103,43 @@ abstract class OrderDetailsViewModelBase with Store {
103 void _updateItems() {
104 final dateFormat = DateFormatter.withCurrentLocal();
105 items.clear();
76 - items.addAll([
77 - StandartListItem(
78 - title: 'Transfer ID',
79 - value: order.transferId),
80 - StandartListItem(
81 - title: S.current.trade_details_state,
82 - value: order.state != null
83 - ? order.state.toString()
84 - : S.current.trade_details_fetching),
85 - ]);
106
107 items.add(
88 - StandartListItem(
89 - title: 'Buy provider',
90 - value: order.provider.title)
91 - );
92 -
93 - if (_provider != null && (_provider is MoonPayProvider || _provider is WyreBuyProvider)) {
94 -
95 - final trackUrl = _provider is MoonPayProvider
96 - ? (_provider as MoonPayProvider).trackUrl
97 - : (_provider as WyreBuyProvider).trackUrl;
98 -
99 - if (trackUrl.isNotEmpty ?? false) {
100 - final buildURL = trackUrl + '${order.transferId}';
101 - items.add(
102 - TrackTradeListItem(
103 - title: S.current.track,
104 - value: buildURL,
105 - onTap: () async {
106 - try {
107 - final uri = Uri.parse(buildURL);
108 - if (await canLaunchUrl(uri))
109 - await launchUrl(uri, mode: LaunchMode.externalApplication);
110 - } catch (e) {}
111 - }));
112 - }
108 + DetailsListStatusItem(title: S.current.trade_details_state, value: order.state.toString()));
109 +
110 + if (_provider is CakePayOrderProviderAdapter) {
111 + items.add(CakePayDetailsListCardItem(
112 + title: '',
113 + value: '',
114 + id: order.id,
115 + createdAt: dateFormat.format(order.createdAt),
116 + price: order.receiveAmount,
117 + quantity: order.quantity,
118 + from: order.from ?? '',
119 + to: order.to ?? '',
120 + cards: cards,
121 + onTap: (BuildContext context) {
122 + Clipboard.setData(ClipboardData(text: '${order.id}'));
123 + showBar<void>(context, S.of(context).copied_to_clipboard);
124 + },
125 + ));
126 }
127
115 - items.add(
116 - StandartListItem(
117 - title: S.current.trade_details_created_at,
118 - value: dateFormat.format(order.createdAt).toString())
119 - );
120 -
121 - items.add(
122 - StandartListItem(
123 - title: S.current.trade_details_pair,
124 - value: '${order.from} → ${order.to}')
125 - );
128 + items.add(StandartListItem(title: 'Order provider', value: _provider?.title ?? ''));
129 +
130 + final trackUrl = _provider?.trackUrl ?? '';
131 + if (trackUrl.isNotEmpty) {
132 + final buildURL = trackUrl + '${order.transferId}';
133 + items.add(TrackTradeListItem(
134 + title: S.current.track,
135 + value: buildURL,
136 + onTap: () async {
137 + try {
138 + final uri = Uri.parse(buildURL);
139 + if (await canLaunchUrl(uri))
140 + await launchUrl(uri, mode: LaunchMode.externalApplication);
141 + } catch (e) {}
142 + }));
143 + }
144 }
127 -}
\ No newline at end of file
145 +}