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>
This commit is contained in:
Serhii
2025-09-01 19:09:26 +03:00
committed by GitHub
co-authored by Omar Hatem
parent ec7b9ccb51
commit 4b7a89d397
32 changed files with 1028 additions and 291 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
import 'package:cake_wallet/buy/buy_amount.dart';
import 'package:cake_wallet/buy/buy_quote.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/buy/pairs_utils.dart';
import 'package:cake_wallet/buy/payment_method.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
+16 -15
View File
@@ -1,21 +1,22 @@
import 'package:cw_core/enumerable_item.dart';
class BuyProviderDescription extends EnumerableItem<int>
with Serializable<int> {
const BuyProviderDescription({required String title, required int raw})
class BuyProviderDescription extends EnumerableItem<int> with Serializable<int> {
const BuyProviderDescription({required String title, required int raw, required this.image})
: super(title: title, raw: raw);
static const wyre = BuyProviderDescription(title: 'Wyre', raw: 0);
static const moonPay = BuyProviderDescription(title: 'MoonPay', raw: 1);
final String image;
static BuyProviderDescription deserialize({required int raw}) {
switch (raw) {
case 0:
return wyre;
case 1:
return moonPay;
default:
throw Exception('Incorrect token $raw for BuyProviderDescription deserialize');
}
static const wyre = BuyProviderDescription(title: 'Wyre', raw: 0, image: '');
static const moonPay = BuyProviderDescription(title: 'MoonPay', raw: 1, image: '');
static BuyProviderDescription deserialize({required int raw}) {
switch (raw) {
case 0:
return wyre;
case 1:
return moonPay;
default:
throw Exception('Incorrect token $raw for BuyProviderDescription deserialize');
}
}
}
}
+4 -2
View File
@@ -6,7 +6,8 @@ import 'package:cake_wallet/buy/buy_exception.dart';
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_provider_description.dart';
import 'package:cake_wallet/buy/buy_quote.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/order/order_source_description.dart';
import 'package:cake_wallet/buy/pairs_utils.dart';
import 'package:cake_wallet/buy/payment_method.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
@@ -329,7 +330,8 @@ class MoonPayProvider extends BuyProvider {
return Order(
id: id,
provider: BuyProviderDescription.moonPay,
source: OrderSourceDescription.buy,
buyProvider: BuyProviderDescription.moonPay,
transferId: id,
state: state,
createdAt: createdAt,
-70
View File
@@ -1,70 +0,0 @@
import 'package:cake_wallet/buy/buy_provider_description.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cw_core/format_amount.dart';
import 'package:cw_core/hive_type_ids.dart';
import 'package:hive/hive.dart';
part 'order.g.dart';
@HiveType(typeId: Order.typeId)
class Order extends HiveObject {
Order(
{required this.id,
required this.transferId,
required this.createdAt,
required this.amount,
required this.receiveAddress,
required this.walletId,
BuyProviderDescription? provider,
TradeState? state,
this.from,
this.to}) {
if (provider != null) {
providerRaw = provider.raw;
}
if (state != null) {
stateRaw = state.raw;
}
}
static const typeId = ORDER_TYPE_ID;
static const boxName = 'Orders';
static const boxKey = 'ordersBoxKey';
@HiveField(0, defaultValue: '')
String id;
@HiveField(1, defaultValue: '')
String transferId;
@HiveField(2)
String? from;
@HiveField(3)
String? to;
@HiveField(4, defaultValue: '')
late String stateRaw;
TradeState get state => TradeState.deserialize(raw: stateRaw);
@HiveField(5)
DateTime createdAt;
@HiveField(6, defaultValue: '')
String amount;
@HiveField(7, defaultValue: '')
String receiveAddress;
@HiveField(8, defaultValue: '')
String walletId;
@HiveField(9, defaultValue: 0)
late int providerRaw;
BuyProviderDescription get provider =>
BuyProviderDescription.deserialize(raw: providerRaw);
String amountFormatted() => formatAmount(amount);
}
+4 -2
View File
@@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:cake_wallet/buy/buy_exception.dart';
import 'package:cake_wallet/order/order_source_description.dart';
import 'package:cake_wallet/buy/pairs_utils.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cw_core/crypto_currency.dart';
@@ -7,7 +8,7 @@ import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cake_wallet/buy/buy_amount.dart';
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_provider_description.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/order/order.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
@@ -157,7 +158,8 @@ class WyreBuyProvider extends BuyProvider {
return Order(
id: id,
provider: BuyProviderDescription.wyre,
source: OrderSourceDescription.buy,
buyProvider: BuyProviderDescription.wyre,
transferId: transferId,
from: from,
to: to,
@@ -39,6 +39,7 @@ class CakePayCard {
final termsAndConditions = stripHtmlIfNeeded(json['terms_and_conditions'] as String? ?? '');
final howToUse = stripHtmlIfNeeded(json['how_to_use'] as String? ?? '');
final fiatCurrency = FiatCurrency.deserialize(raw: json['currency_code'] as String? ?? '');
String? minValue = json['min_value'] as String?;
final parsedMinValueLocal = _toDouble(json['min_value']);
+52 -1
View File
@@ -15,8 +15,11 @@ class CakePayOrder {
final List<OrderCard> cards;
final String? externalId;
final double amountUsd;
final String totalReceiveAmount;
final int quantity;
final String status;
final String? vouchers;
final String fiatCurrencyCode;
final PaymentData paymentData;
CakePayOrder({
@@ -24,23 +27,68 @@ class CakePayOrder {
required this.cards,
required this.externalId,
required this.amountUsd,
required this.totalReceiveAmount,
required this.quantity,
required this.status,
required this.vouchers,
required this.fiatCurrencyCode,
required this.paymentData,
});
factory CakePayOrder.fromMap(Map<String, dynamic> map) {
final cards = map['cards'] as List<dynamic>;
final firstCard = cards.isNotEmpty ? cards.first as Map<String, dynamic> : {};
return CakePayOrder(
orderId: map['order_id'] as String,
cards: (map['cards'] as List<dynamic>)
cards: cards
.map((x) => OrderCard.fromMap(x as Map<String, dynamic>))
.toList(),
externalId: map['external_id'] as String?,
amountUsd: map['amount_usd'] as double,
totalReceiveAmount: firstCard['subtotal'] as String? ?? '',
quantity: firstCard['quantity'] as int,
status: map['status'] as String,
vouchers: map['vouchers'] as String?,
fiatCurrencyCode: firstCard['currency_code'] as String? ?? '',
paymentData: PaymentData.fromMap(map['payment_data'] as Map<String, dynamic>));
}
static CryptoPaymentData? getPaymentDataFor({CakePayPaymentMethod? method,CakePayOrder? order}) {
if (order == null || method == null) return null;
final data = switch (method) {
CakePayPaymentMethod.BTC => order.paymentData.btc,
CakePayPaymentMethod.XMR => order.paymentData.xmr,
CakePayPaymentMethod.LTC => order.paymentData.ltc,
CakePayPaymentMethod.LTC_MWEB => order.paymentData.ltc_mweb,
_ => null
};
if (data == null) return null;
final bip21 = data.paymentUrls?.bip21;
if (bip21 != null && bip21.isNotEmpty) {
final uri = Uri.parse(bip21);
final addr = uri.path;
final price = uri.queryParameters['amount'] ?? data.price;
return CryptoPaymentData(price: price, address: addr, amount: data.amount, paymentUrls: data.paymentUrls);
}
return data;
}
static String getCurrencyCodeFromPaymentMethod(CakePayPaymentMethod method) {
return switch (method) {
CakePayPaymentMethod.BTC => 'BTC',
CakePayPaymentMethod.BTC_LN => 'BTC',
CakePayPaymentMethod.XMR => 'XMR',
CakePayPaymentMethod.LTC => 'LTC',
CakePayPaymentMethod.LTC_MWEB => 'LTC',
};
}
}
class OrderCard {
@@ -112,11 +160,13 @@ class PaymentData {
class CryptoPaymentData {
final String price;
final String amount;
final PaymentUrl? paymentUrls;
final String address;
CryptoPaymentData({
required this.price,
required this.amount,
this.paymentUrls,
required this.address,
});
@@ -124,6 +174,7 @@ class CryptoPaymentData {
factory CryptoPaymentData.fromMap(Map<String, dynamic> map) {
return CryptoPaymentData(
price: map['price'] as String,
amount: (map['amount_from'] as double?)?.toString() ?? '',
paymentUrls: PaymentUrl.fromMap(map['paymentUrls'] as Map<String, dynamic>?),
address: map['address'] as String,
);
+24 -2
View File
@@ -19,7 +19,7 @@ class CakePayApi {
static const authPath = '/api/accounts/auth';
static final verifyEmailPath = '/api/accounts/auth/verify';
static final logoutPath = '/api/accounts/logout';
static final createOrderPath = '/api/orders/order';
static final orderPath = '/api/orders/order';
static final simulatePaymentPath = '/api/orders/simulate-payment';
/// AuthenticateUser
@@ -104,7 +104,7 @@ class CakePayApi {
required bool confirmsVoidedRefund,
required bool confirmsTermsAgreed,
}) async {
final uri = Uri.https(baseCakePayUri, createOrderPath);
final uri = Uri.https(baseCakePayUri, orderPath);
final headers = {
'Accept': 'application/json',
@@ -155,6 +155,28 @@ class CakePayApi {
throw Exception(message);
}
/// Get Order by ID
Future<CakePayOrder> getOrderById({required String orderId, required String token}) async {
final uri = Uri.https(baseCakePayUri, '$orderPath/$orderId');
final headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Token $token',
};
final response = await ProxyWrapper().get(
clearnetUri: uri,
headers: headers,
);
if (response.statusCode != 200) {
throw Exception('Unexpected http status: ${response.statusCode}');
}
final bodyJson = json.decode(response.body) as Map<String, dynamic>;
return CakePayOrder.fromMap(bodyJson);
}
///Simulate Payment
Future<String> simulatePayment(
{required String CSRFToken,
@@ -109,6 +109,11 @@ class CakePayService {
);
}
Future<CakePayOrder> findOrderById({required String orderId}) async {
final token = (await secureStorage.read(key: cakePayUserTokenKey))!;
return await cakePayApi.getOrderById(orderId: orderId, token: token);
}
///Simulate Purchase Gift Card
Future<String> simulatePayment({required String orderId}) async {
final token = (await secureStorage.read(key: cakePayUserTokenKey))!;
+13 -4
View File
@@ -10,7 +10,7 @@ import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
import 'package:cake_wallet/buy/dfx/dfx_buy_provider.dart';
import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart';
import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/core/backup_service_v3.dart';
import 'package:cake_wallet/core/new_wallet_arguments.dart';
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';
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
import 'package:cake_wallet/store/dashboard/order_filter_store.dart';
import 'package:cake_wallet/themes/core/theme_store.dart';
import 'package:cake_wallet/view_model/dev/monero_background_sync.dart';
import 'package:cake_wallet/view_model/dev/secure_preferences.dart';
@@ -374,6 +375,7 @@ Future<void> setup({
getIt.registerFactory(() =>
PayjoinTransactionsStore(payjoinSessionSource: _payjoinSessionSource));
getIt.registerSingleton<TradeFilterStore>(TradeFilterStore());
getIt.registerSingleton<OrderFilterStore>(OrderFilterStore());
getIt.registerSingleton<TransactionFilterStore>(TransactionFilterStore(getIt.get<AppStore>()));
getIt.registerSingleton<FiatConversionStore>(FiatConversionStore());
getIt.registerSingleton<SendTemplateStore>(SendTemplateStore(templateSource: _templates));
@@ -545,11 +547,12 @@ Future<void> setup({
balanceViewModel: getIt.get<BalanceViewModel>(),
appStore: getIt.get<AppStore>(),
tradesStore: getIt.get<TradesStore>(),
ordersStore: getIt.get<OrdersStore>(),
tradeFilterStore: getIt.get<TradeFilterStore>(),
orderFilterStore: getIt.get<OrderFilterStore>(),
transactionFilterStore: getIt.get<TransactionFilterStore>(),
settingsStore: settingsStore,
yatStore: getIt.get<YatStore>(),
ordersStore: getIt.get<OrdersStore>(),
anonpayTransactionsStore: getIt.get<AnonpayTransactionsStore>(),
payjoinTransactionsStore: getIt.get<PayjoinTransactionsStore>(),
sharedPreferences: getIt.get<SharedPreferences>(),
@@ -1349,8 +1352,13 @@ Future<void> setup({
getIt.registerFactoryParam<OrderDetailsViewModel, Order, void>((order, _) {
final wallet = getIt.get<AppStore>().wallet;
final cakePayService = getIt.get<CakePayService>();
return OrderDetailsViewModel(wallet: wallet!, orderForDetails: order);
return OrderDetailsViewModel(
wallet: wallet!,
orderForDetails: order,
cakePayService: cakePayService,
orders: _ordersSource);
});
getIt.registerFactoryParam<OrderDetailsPage, Order, void>(
@@ -1423,7 +1431,8 @@ Future<void> setup({
(CakePayVendor vendor, _) {
return CakePayBuyCardViewModel(vendor: vendor,
cakePayService: getIt.get<CakePayService>(),
sendViewModel: getIt.get<SendViewModel>());
sendViewModel: getIt.get<SendViewModel>(),
orders: _ordersSource);
});
getIt.registerFactory(() => CakePayAccountViewModel(cakePayService: getIt.get<CakePayService>()));
+1 -1
View File
@@ -3,7 +3,7 @@ import 'dart:io';
import 'dart:ui';
import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/app_scroll_behavior.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/background_sync.dart';
import 'package:cake_wallet/core/node_switching_service.dart';
+108
View File
@@ -0,0 +1,108 @@
import 'package:cake_wallet/buy/buy_provider_description.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cw_core/format_amount.dart';
import 'package:cw_core/hive_type_ids.dart';
import 'package:hive/hive.dart';
import 'order_provider_description.dart';
import 'order_source_description.dart';
part 'order.g.dart';
@HiveType(typeId: Order.typeId)
class Order extends HiveObject {
Order({
required this.id,
required this.transferId,
required this.createdAt,
required this.amount,
this.receiveAmount,
this.quantity,
required this.receiveAddress,
required this.walletId,
this.from,
this.to,
TradeState? state,
OrderSourceDescription source = OrderSourceDescription.buy,
BuyProviderDescription? buyProvider,
OrderProviderDescription? giftCardProvider,
}) {
sourceRaw = source.raw;
if (state != null) stateRaw = state.raw;
if (buyProvider != null) providerRaw = buyProvider.raw;
if (giftCardProvider != null) providerRaw = giftCardProvider.raw;
}
static const typeId = ORDER_TYPE_ID;
static const boxName = 'Orders';
static const boxKey = 'ordersBoxKey';
@HiveField(0, defaultValue: '')
String id;
@HiveField(1, defaultValue: '')
String transferId;
@HiveField(2)
String? from;
@HiveField(3)
String? to;
@HiveField(4, defaultValue: '')
late String stateRaw;
TradeState get state => TradeState.deserialize(raw: stateRaw);
@HiveField(5)
DateTime createdAt;
@HiveField(6, defaultValue: '')
String amount;
@HiveField(7, defaultValue: '')
String receiveAddress;
@HiveField(8, defaultValue: '')
String walletId;
@HiveField(9, defaultValue: 0)
late int providerRaw;
@HiveField(10, defaultValue: '')
String? receiveAmount;
@HiveField(11, defaultValue: 0)
late int? sourceRaw;
@HiveField(12, defaultValue: '')
String? quantity;
OrderSourceDescription get source => OrderSourceDescription.deserialize(raw: sourceRaw ?? 0);
BuyProviderDescription? get buyProvider => source == OrderSourceDescription.buy
? BuyProviderDescription.deserialize(raw: providerRaw)
: null;
OrderProviderDescription? get orderProvider => source == OrderSourceDescription.order
? OrderProviderDescription.deserialize(raw: providerRaw)
: null;
String get providerTitle {
if (source == OrderSourceDescription.buy) {
return buyProvider?.title ?? '';
} else {
return orderProvider?.title ?? '';
}
}
String get providerIcon {
if (source == OrderSourceDescription.buy) {
return buyProvider?.image ?? '';
} else {
return orderProvider?.image ?? '';
}
}
String amountFormatted() => formatAmount(amount) + ' ${from ?? ''}';
}
+9
View File
@@ -0,0 +1,9 @@
import 'package:cake_wallet/order/order.dart';
abstract class OrderProvider {
Future<(Order, Object?)> findOrderById(String id);
String get title;
String get trackUrl;
}
@@ -0,0 +1,67 @@
import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/order/order_provider.dart';
import 'package:cake_wallet/order/order_provider_description.dart';
import 'package:cake_wallet/order/order_source_description.dart';
import 'package:cw_core/wallet_base.dart';
class CakePayOrderProviderAdapter implements OrderProvider {
CakePayOrderProviderAdapter({
required this.wallet,
required this.cakePayService,
});
final WalletBase wallet;
final CakePayService cakePayService;
@override
String get title => 'Cake Pay';
@override
String get trackUrl => '';
@override
Future<(Order, Object?)> findOrderById(String id) async {
final cakePayOrder = await cakePayService.findOrderById(orderId: id);
final cards = cakePayOrder.cards;
final order = Order(
id: cakePayOrder.orderId,
state: determineState(cakePayOrder.status),
transferId: '',
createdAt: DateTime.now(),
amount: '',
receiveAmount: cakePayOrder.totalReceiveAmount,
quantity: cakePayOrder.quantity.toString(),
receiveAddress: '',
walletId: wallet.id,
from: '',
to: cakePayOrder.fiatCurrencyCode,
source: OrderSourceDescription.order,
giftCardProvider: OrderProviderDescription.cakePay,
);
return (order, cards);
}
TradeState determineState(String state) {
final swapState = switch (state) {
'new' => TradeState.pending,
'expired_but_still_pending' => TradeState.overdue,
'expired' => TradeState.expired,
'failed' => TradeState.failed,
'paid' => TradeState.paid,
'paid_partial' => TradeState.underpaid,
'pending_purchase' => TradeState.processing,
'purchase_processing' => TradeState.processing,
'purchased' => TradeState.confirmed,
'pending_email' => TradeState.awaiting,
'complete' => TradeState.complete,
'pending_refund' => TradeState.refund,
'refunded' => TradeState.refunded,
_ => TradeState.notFound
};
return swapState;
}
}
@@ -0,0 +1,47 @@
import 'package:cake_wallet/buy/buy_provider_description.dart';
import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart';
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/order/order_provider.dart';
import 'package:cake_wallet/order/order_source_description.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cw_core/wallet_base.dart';
class MoonPayOrderProviderAdapter implements OrderProvider {
MoonPayOrderProviderAdapter(
{required AppStore appStore, required WalletBase wallet, bool isTestEnvironment = false})
: _inner = MoonPayProvider(
appStore: appStore,
wallet: wallet,
isTestEnvironment: isTestEnvironment,
),
_wallet = wallet;
final MoonPayProvider _inner;
final WalletBase _wallet;
@override
String get title => 'MoonPay';
@override
String get trackUrl => _inner.trackUrl;
@override
Future<(Order, Object?)> findOrderById(String id) async {
final buyOrder = await _inner.findOrderById(id);
final order = Order(
id: buyOrder.id,
transferId: buyOrder.transferId,
createdAt: buyOrder.createdAt,
amount: buyOrder.amount,
receiveAmount: buyOrder.receiveAmount,
receiveAddress: _wallet.walletAddresses.address,
walletId: _wallet.id,
from: buyOrder.from,
to: buyOrder.to,
state: buyOrder.state,
source: OrderSourceDescription.buy,
buyProvider: BuyProviderDescription.moonPay,
);
return (order, null);
}
}
@@ -0,0 +1,23 @@
import 'package:cake_wallet/buy/wyre/wyre_buy_provider.dart';
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/order/order_provider.dart';
import 'package:cw_core/wallet_base.dart';
class WyreOrderProviderAdapter implements OrderProvider {
WyreOrderProviderAdapter({required WalletBase wallet})
: _wyreProvider = WyreBuyProvider(wallet: wallet);
final WyreBuyProvider _wyreProvider;
@override
Future<(Order, Object?)> findOrderById(String id) async {
final order = await _wyreProvider.findOrderById(id);
return (order, null);
}
@override
String get title => _wyreProvider.title;
@override
String get trackUrl => _wyreProvider.trackUrl;
}
+23
View File
@@ -0,0 +1,23 @@
import 'package:cw_core/enumerable_item.dart';
class OrderProviderDescription extends EnumerableItem<int> with Serializable<int> {
const OrderProviderDescription({
required String title,
required int raw,
required this.image,
}) : super(title: title, raw: raw);
final String image;
static const cakePay = OrderProviderDescription(
title: 'Cake Pay', raw: 0, image: 'assets/images/cake_pay_icon.png');
static OrderProviderDescription deserialize({required int raw}) {
switch (raw) {
case 0:
return cakePay;
default:
throw Exception('Invalid OrderProviderDescription $raw');
}
}
}
+20
View File
@@ -0,0 +1,20 @@
import 'package:cw_core/enumerable_item.dart';
class OrderSourceDescription extends EnumerableItem<int> with Serializable<int> {
const OrderSourceDescription({required String title, required int raw})
: super(title: title, raw: raw);
static const buy = OrderSourceDescription(title: 'Buy', raw: 0);
static const order = OrderSourceDescription(title: 'Order', raw: 1);
static OrderSourceDescription deserialize({required int raw}) {
switch (raw) {
case 0:
return buy;
case 1:
return order;
default:
throw Exception('Invalid OrderSourceDescription raw value: $raw');
}
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ import 'dart:io';
import 'package:cake_wallet/anonpay/anonpay_info_base.dart';
import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/core/new_wallet_arguments.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/core/new_wallet_type_arguments.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/di.dart';
@@ -1,4 +1,6 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/entities/balance_display_mode.dart';
import 'package:cake_wallet/order/order_source_description.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/anonpay_transaction_row.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/order_row.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/payjoin_transaction_row.dart';
@@ -189,19 +191,32 @@ class TransactionsPage extends StatelessWidget {
}
if (item is OrderListItem) {
final order = item.order;
return Observer(
builder: (_) => OrderRow(
builder: (_) {
// TODO: Refactor Amount Hiding Logic it is not working properly for Orders and Trades
final hide = dashboardViewModel.balanceViewModel.displayMode ==
BalanceDisplayMode.hiddenBalance;
final formattedAmount = hide ? '---' : order.amountFormatted();
final formattedReceiveAmount = hide ? '---' : order.receiveAmount;
return OrderRow(
key: item.key,
onTap: () => Navigator.of(context)
.pushNamed(Routes.orderDetails, arguments: order),
provider: order.provider,
from: order.from!,
to: order.to!,
createdAtFormattedDate:
DateFormat('HH:mm').format(order.createdAt),
formattedAmount: item.orderFormattedAmount,
),
providerTitle: order.providerTitle,
providerIconPath: order.providerIcon,
from: order.from ?? '',
to: order.to ?? '',
createdAtFormattedDate: DateFormat('HH:mm').format(order.createdAt),
formattedAmount: formattedAmount,
formattedReceiveAmount: dashboardViewModel.balanceViewModel.isFiatDisabled &&
order.source == OrderSourceDescription.order
? ''
: formattedReceiveAmount,
);
}
);
}
return Container(color: Colors.transparent, height: 1);
+124 -41
View File
@@ -1,77 +1,110 @@
import 'package:cake_wallet/buy/buy_provider_description.dart';
import 'package:cake_wallet/buy/get_buy_provider_icon.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/utils/image_utill.dart';
import 'package:flutter/material.dart';
class OrderRow extends StatelessWidget {
OrderRow({
required this.provider,
const OrderRow({
Key? key,
required this.providerTitle,
required this.providerIconPath,
required this.from,
required this.to,
required this.createdAtFormattedDate,
this.state,
this.onTap,
this.formattedAmount,
super.key,
});
this.formattedReceiveAmount,
}) : super(key: key);
final VoidCallback? onTap;
final BuyProviderDescription provider;
final String providerTitle;
final String providerIconPath;
final String from;
final String to;
final String createdAtFormattedDate;
final String? createdAtFormattedDate;
final String? formattedAmount;
final String? formattedReceiveAmount;
final TradeState? state;
@override
Widget build(BuildContext context) {
final iconColor = Theme.of(context).colorScheme.onSurfaceVariant;
final providerIcon = getBuyProviderIcon(provider, iconColor: iconColor);
return InkWell(
onTap: onTap,
child: Container(
padding: EdgeInsets.fromLTRB(24, 8, 24, 8),
padding: const EdgeInsets.fromLTRB(24, 8, 24, 8),
color: Colors.transparent,
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (providerIcon != null)
Padding(
padding: EdgeInsets.only(right: 12),
child: providerIcon,
),
Stack(
clipBehavior: Clip.none,
children: [
_ProviderIcon(path: providerIconPath, title: providerTitle),
if (state != null)
Positioned(
right: 0,
bottom: 2,
child: Container(
height: 8,
width: 8,
decoration: BoxDecoration(
color: _statusColor(context, state!),
borderRadius: BorderRadius.circular(12),
),
),
),
],
),
const SizedBox(width: 12),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[
Text(
'$from → $to',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
formattedAmount != null
? Text(
formattedAmount! + ' ' + to,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 16,
fontWeight: FontWeight.w500,
),
)
: Container()
]),
SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
createdAtFormattedDate,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
'$from → $to',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.primary,
),
),
if (formattedAmount != null)
Text(
formattedAmount!,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
const SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
if (createdAtFormattedDate != null)
Text(
createdAtFormattedDate!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
if (formattedReceiveAmount != null)
Text(
formattedReceiveAmount!,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
)
],
@@ -82,4 +115,54 @@ class OrderRow extends StatelessWidget {
),
);
}
Color _statusColor(BuildContext context, TradeState status) {
switch (status) {
case TradeState.complete:
case TradeState.completed:
case TradeState.finished:
case TradeState.success:
case TradeState.settled:
return PaletteDark.brightGreen;
case TradeState.failed:
case TradeState.expired:
case TradeState.notFound:
return Palette.darkRed;
default:
return const Color(0xffff6600);
}
}
}
class _ProviderIcon extends StatelessWidget {
const _ProviderIcon({required this.path, required this.title});
final String path;
final String title;
@override
Widget build(BuildContext context) {
if (path.isNotEmpty) {
return ClipRRect(
borderRadius: BorderRadius.circular(50),
child: ImageUtil.getImageFromPath(imagePath: path, height: 36, width: 36),
);
}
return Container(
height: 36,
width: 36,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50),
color: Theme.of(context).colorScheme.surfaceVariant,
),
alignment: Alignment.center,
child: Text(
title.isNotEmpty ? title.characters.first.toUpperCase() : '?',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w700,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
);
}
}
@@ -0,0 +1,27 @@
import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
import 'package:flutter/material.dart';
class CakePayDetailsListCardItem extends StandartListItem {
CakePayDetailsListCardItem({
required String title,
required String value,
required this.id,
required this.createdAt,
this.price,
this.quantity,
required this.from,
required this.to,
required this.onTap,
required this.cards,
}) : super(title: title, value: value);
final String id;
final String createdAt;
final String? price;
final String? quantity;
final String from;
final String to;
final void Function(BuildContext) onTap;
final List<OrderCard> cards;
}
@@ -1,4 +1,9 @@
import 'package:cake_wallet/src/screens/order_details/cake_pay_detail_list_card_item.dart';
import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart';
import 'package:cake_wallet/src/widgets/cake_pay_order_list_card.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/src/widgets/standard_list_status_row.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/view_model/order_details_view_model.dart';
import 'package:flutter/material.dart';
@@ -19,24 +24,25 @@ class OrderDetailsPage extends BasePage {
final OrderDetailsViewModel orderDetailsViewModel;
@override
Widget body(BuildContext context) =>
OrderDetailsPageBody(orderDetailsViewModel);
Widget body(BuildContext context) => OrderDetailsPageBody(orderDetailsViewModel, currentTheme);
}
class OrderDetailsPageBody extends StatefulWidget {
OrderDetailsPageBody(this.orderDetailsViewModel);
OrderDetailsPageBody(this.orderDetailsViewModel, this.currentTheme);
final OrderDetailsViewModel orderDetailsViewModel;
final MaterialThemeBase currentTheme;
@override
OrderDetailsPageBodyState createState() =>
OrderDetailsPageBodyState(orderDetailsViewModel);
OrderDetailsPageBodyState(orderDetailsViewModel, currentTheme);
}
class OrderDetailsPageBodyState extends State<OrderDetailsPageBody> {
OrderDetailsPageBodyState(this.orderDetailsViewModel);
OrderDetailsPageBodyState(this.orderDetailsViewModel, this.currentTheme);
final OrderDetailsViewModel orderDetailsViewModel;
final MaterialThemeBase currentTheme;
@override
void dispose() {
@@ -47,28 +53,45 @@ class OrderDetailsPageBodyState extends State<OrderDetailsPageBody> {
@override
Widget build(BuildContext context) {
return Observer(builder: (_) {
final itemsCount = orderDetailsViewModel.items.length;
return SectionStandardList(
sectionCount: 1,
itemCounter: (int _) => orderDetailsViewModel.items.length,
itemCounter: (int _) => itemsCount,
itemBuilder: (__, index) {
final item = orderDetailsViewModel.items[index];
if (item is DetailsListStatusItem) {
return StandardListStatusRow(title: item.title, value: item.value);
}
if (item is CakePayDetailsListCardItem) {
final cardImagePath = (item.cards.isNotEmpty) ? item.cards.first.cardImagePath : null;
return CakePayOrderListCard(
id: item.id,
create: item.createdAt,
price: item.price,
quantity: item.quantity,
pair: '${item.from} → ${item.to}',
currentTheme: widget.currentTheme.type,
onTap: item.onTap,
backgroundImage: cardImagePath,
);
}
if (item is TrackTradeListItem) {
return GestureDetector(
onTap: item.onTap,
child: ListRow(
title: '${item.title}', value: '${item.value}'));
child: ListRow(title: '${item.title}', value: '${item.value}'));
} else {
return GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: '${item.value}'));
showBar<void>(context, S.of(context).copied_to_clipboard);
},
child: ListRow(
title: '${item.title}', value: '${item.value}'));
child: ListRow(title: '${item.title}', value: '${item.value}'));
}
});
});
}
}
}
@@ -0,0 +1,188 @@
import 'package:flutter/material.dart';
import 'package:cake_wallet/themes/core/material_base_theme.dart';
class CakePayOrderListCard extends StatelessWidget {
const CakePayOrderListCard({
Key? key,
required this.id,
required this.create,
this.quantity,
this.price,
required this.pair,
required this.onTap,
required this.currentTheme,
this.backgroundImage,
this.aspectRatio = 16 / 9,
}) : super(key: key);
final String id;
final String create;
final String? quantity;
final String? price;
final String pair;
final ThemeType currentTheme;
final void Function(BuildContext) onTap;
final String? backgroundImage;
final double aspectRatio;
ImageProvider? _imageProviderFor(String? path) {
if (path == null || path.isEmpty) return null;
if (path.startsWith('http://') || path.startsWith('https://')) return NetworkImage(path);
return AssetImage(path);
}
Color _labelBackground(BuildContext context) {
final surface = Theme.of(context).colorScheme.surface;
return surface.withOpacity(0.75);
}
Color _titleLabelBackground(BuildContext context) {
final surface = Theme.of(context).colorScheme.surface;
return surface.withOpacity(0.85);
}
Widget _labelBox(
BuildContext context, {
required String text,
EdgeInsets padding = const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
double radius = 10,
Color? background,
TextStyle? style,
}) {
return Container(
padding: padding,
decoration: BoxDecoration(
color: background ?? _labelBackground(context),
borderRadius: BorderRadius.circular(radius),
),
child: Text(
text,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: style ??
Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
),
),
);
}
@override
Widget build(BuildContext context) {
final image = _imageProviderFor(backgroundImage);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () => onTap(context),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: AspectRatio(
aspectRatio: aspectRatio,
child: Stack(
fit: StackFit.expand,
children: [
if (image != null)
Image(image: image, fit: BoxFit.cover)
else
Container(color: Theme.of(context).colorScheme.surfaceContainerHighest),
if (image != null)
Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: [0.0, 0.6, 1.0],
colors: [Colors.transparent, Colors.black26, Colors.black54],
),
),
),
Padding(
padding: const EdgeInsets.all(14.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_labelBox(
context,
text: 'Create • $create',
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
radius: 8,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w700,
fontSize: 10,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 4),
_labelBox(
context,
text: 'Qty • $quantity Total • $price',
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
radius: 8,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w700,
fontSize: 10,
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
_labelBox(
context,
text: pair,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
radius: 12,
background: _titleLabelBackground(context),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w800,
fontSize: 16,
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
const Spacer(),
_labelBox(
context,
text: 'Order • $id',
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
radius: 10,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w800,
fontSize: 12,
color: Theme.of(context).colorScheme.onSurface,
),
),
],
),
),
IgnorePointer(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(currentTheme == ThemeType.dark ? 0.08 : 0.12),
width: 1,
),
),
),
),
],
),
),
),
),
);
}
}
@@ -0,0 +1,46 @@
import 'package:cake_wallet/order/order_provider_description.dart';
import 'package:cake_wallet/order/order_source_description.dart';
import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:mobx/mobx.dart';
part 'order_filter_store.g.dart';
class OrderFilterStore = OrderFilterStoreBase with _$OrderFilterStore;
abstract class OrderFilterStoreBase with Store {
OrderFilterStoreBase() : displayCakePay = true;
@observable
bool displayCakePay;
@computed
bool get displayAllOrders => displayCakePay;
@action
void toggleDisplayOrder(OrderProviderDescription provider) {
switch (provider) {
case OrderProviderDescription.cakePay:
displayCakePay = !displayCakePay;
break;
}
}
List<OrderListItem> filtered({
required List<OrderListItem> orders,
required WalletBase wallet,
}) {
final walletOrders =
orders.where((item) => item.order.walletId == wallet.id).toList();
final cakePayOrders = walletOrders.where((item) {
final order = item.order;
final isOrderSource = order.source == OrderSourceDescription.order;
final isCakePay = order.orderProvider == OrderProviderDescription.cakePay;
return isOrderSource && isCakePay;
}).toList();
if (!displayCakePay) return <OrderListItem>[];
return cakePayOrders;
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
import 'dart:async';
import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
+1 -1
View File
@@ -8,7 +8,7 @@ import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/view_model/buy/buy_item.dart';
import 'package:hive/hive.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/store/dashboard/orders_store.dart';
import 'package:mobx/mobx.dart';
import 'package:cw_core/wallet_base.dart';
@@ -1,14 +1,20 @@
import 'dart:async';
import 'package:cake_wallet/buy/buy_provider_description.dart';
import 'package:cake_wallet/order/order_provider_description.dart';
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/order/order_source_description.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_card.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_vendor.dart';
import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/utils/feature_flag.dart';
import 'package:cake_wallet/view_model/send/send_view_model.dart';
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
part 'cake_pay_buy_card_view_model.g.dart';
@@ -17,8 +23,12 @@ class CakePayBuyCardViewModel = CakePayBuyCardViewModelBase with _$CakePayBuyCar
abstract class CakePayBuyCardViewModelBase with Store {
CakePayBuyCardViewModelBase(
{required this.vendor, required CakePayService cakePayService, required this.sendViewModel})
: _cakePayService = cakePayService, walletType = sendViewModel.walletType,
{required this.vendor,
required CakePayService cakePayService,
required this.sendViewModel,
required this.orders})
: _cakePayService = cakePayService,
walletType = sendViewModel.walletType,
amount = vendor.card!.denominationItems.isNotEmpty
? vendor.card!.denominationItems.first.value
: 0,
@@ -41,6 +51,7 @@ abstract class CakePayBuyCardViewModelBase with Store {
final double min;
final CakePayCard card;
final WalletType walletType;
final Box<Order> orders;
CakePayOrder? order;
Timer? _timer;
@@ -56,7 +67,7 @@ abstract class CakePayBuyCardViewModelBase with Store {
bool get isDenominationSelected =>
card.denominationItems.isNotEmpty &&
card.denominationItems.any((item) => item.value == amount);
Future<bool> get isUserLogged async => await _cakePayService.isLogged();
@observable
@@ -99,7 +110,10 @@ abstract class CakePayBuyCardViewModelBase with Store {
case WalletType.bitcoin:
return [CakePayPaymentMethod.BTC];
case WalletType.litecoin:
return [CakePayPaymentMethod.LTC,if (sendViewModel.isMwebEnabled) CakePayPaymentMethod.LTC_MWEB];
return [
CakePayPaymentMethod.LTC,
if (sendViewModel.isMwebEnabled) CakePayPaymentMethod.LTC_MWEB
];
case WalletType.monero:
return [CakePayPaymentMethod.XMR];
default:
@@ -124,31 +138,6 @@ abstract class CakePayBuyCardViewModelBase with Store {
amount = double.parse(input.replaceAll(',', '.'));
}
CryptoPaymentData? getPaymentDataFor(CakePayPaymentMethod? method) {
if (order == null || method == null) return null;
final data = switch (method) {
CakePayPaymentMethod.BTC => order?.paymentData.btc,
CakePayPaymentMethod.XMR => order?.paymentData.xmr,
CakePayPaymentMethod.LTC => order?.paymentData.ltc,
CakePayPaymentMethod.LTC_MWEB => order?.paymentData.ltc_mweb,
_ => null
};
if (data == null) return null;
final bip21 = data.paymentUrls?.bip21;
if (bip21 != null && bip21.isNotEmpty) {
final uri = Uri.parse(bip21);
final addr = uri.path;
final price = uri.queryParameters['amount'] ?? data.price;
return CryptoPaymentData(price: price, address: addr);
}
return data;
}
@action
Future<void> createOrder() async {
if (walletType != WalletType.bitcoin &&
@@ -166,8 +155,29 @@ abstract class CakePayBuyCardViewModelBase with Store {
confirmsVoidedRefund: confirmsVoidedRefund,
confirmsTermsAgreed: confirmsTermsAgreed,
);
await confirmSending();
final paymentData =
CakePayOrder.getPaymentDataFor(method: selectedPaymentMethod, order: order); // TODO should hande other currencies if added
if (paymentData == null || order == null)
throw Exception('Payment data or order is not available.');
await confirmSending(paymentData);
expirationTime = order!.paymentData.expirationTime;
final orderRecord = Order(
id: order!.orderId,
state: TradeState.deserialize(raw: order!.status),
transferId: order!.externalId ?? '',
from: CakePayOrder.getCurrencyCodeFromPaymentMethod(selectedPaymentMethod!),
to: order!.fiatCurrencyCode,
createdAt: DateTime.now(),
amount: paymentData.amount ?? '',
receiveAmount: order!.totalReceiveAmount,
quantity: order!.quantity.toString(),
receiveAddress: paymentData.address ?? '',
source: OrderSourceDescription.order,
giftCardProvider: OrderProviderDescription.cakePay,
walletId: sendViewModel.wallet.id);
orders.add(orderRecord);
updateRemainingTime();
_startExpirationTimer();
} catch (e) {
@@ -177,15 +187,12 @@ abstract class CakePayBuyCardViewModelBase with Store {
}
@action
Future<void> confirmSending() async {
final cryptoPaymentData = getPaymentDataFor(selectedPaymentMethod);
if (order == null || cryptoPaymentData == null) return;
Future<void> confirmSending(CryptoPaymentData paymentData) async {
try {
sendViewModel.clearOutputs();
final output = sendViewModel.outputs.first;
output.address = cryptoPaymentData.address;
output.setCryptoAmount(cryptoPaymentData.price);
output.address = paymentData.address;
output.setCryptoAmount(paymentData.price);
await sendViewModel.createTransaction();
} catch (e) {
@@ -13,7 +13,9 @@ import 'package:cake_wallet/entities/service_status.dart';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/order/order_provider_description.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/store/dashboard/order_filter_store.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
@@ -72,6 +74,7 @@ abstract class DashboardViewModelBase with Store {
required this.appStore,
required this.tradesStore,
required this.tradeFilterStore,
required this.orderFilterStore,
required this.transactionFilterStore,
required this.settingsStore,
required this.yatStore,
@@ -110,6 +113,13 @@ abstract class DashboardViewModelBase with Store {
// caption: S.current.transactions_by_date,
// onChanged: null),
],
'Orders': [
FilterItem(
value: () => orderFilterStore.displayCakePay,
caption: 'Cake Pay',
onChanged: () =>
orderFilterStore.toggleDisplayOrder(OrderProviderDescription.cakePay)),
],
S.current.trades: [
FilterItem(
value: () => tradeFilterStore.displayAllTrades,
@@ -452,7 +462,7 @@ abstract class DashboardViewModelBase with Store {
_items.addAll(
transactionFilterStore.filtered(transactions: [...transactions, ...anonpayTransactions]));
_items.addAll(tradeFilterStore.filtered(trades: trades, wallet: wallet));
_items.addAll(orders);
_items.addAll(orderFilterStore.filtered(orders: orders, wallet: wallet));
if (payjoinTransactions.isNotEmpty) {
final _payjoinTransactions = payjoinTransactions;
@@ -807,6 +817,8 @@ abstract class DashboardViewModelBase with Store {
TradeFilterStore tradeFilterStore;
OrderFilterStore orderFilterStore;
AnonpayTransactionsStore anonpayTransactionsStore;
TransactionFilterStore transactionFilterStore;
@@ -1,4 +1,4 @@
import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/view_model/dashboard/action_list_item.dart';
import 'package:cake_wallet/entities/balance_display_mode.dart';
@@ -15,13 +15,11 @@ class OrderListItem extends ActionListItem {
BalanceDisplayMode get displayMode => settingsStore.balanceDisplayMode;
String get orderFormattedAmount {
return order.amount != null
? displayMode == BalanceDisplayMode.hiddenBalance
? '---'
: order.amountFormatted()
: order.amount;
}
String get orderFormattedAmount =>
displayMode == BalanceDisplayMode.hiddenBalance ? '---' : order.amountFormatted();
String get orderFormattedReceiveAddress =>
displayMode == BalanceDisplayMode.hiddenBalance ? '---' : order.receiveAmount ?? '';
@override
DateTime get date => order.createdAt;
+101 -83
View File
@@ -1,41 +1,61 @@
import 'dart:async';
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_provider_description.dart';
import 'package:cake_wallet/buy/order.dart';
import 'package:cake_wallet/utils/date_formatter.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/cake_pay/src/models/cake_pay_order.dart';
import 'package:cake_wallet/cake_pay/src/services/cake_pay_service.dart';
import 'package:cake_wallet/core/utilities.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/order/order_provider.dart';
import 'package:cake_wallet/order/order_provider_adapter/cake_pay_order_provider_adapter.dart';
import 'package:cake_wallet/order/order_provider_adapter/wyre_order_provider_adapter.dart';
import 'package:cake_wallet/order/order_provider_description.dart';
import 'package:cake_wallet/order/order_source_description.dart';
import 'package:cake_wallet/src/screens/order_details/cake_pay_detail_list_card_item.dart';
import 'package:cake_wallet/src/screens/trade_details/track_trade_list_item.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
import 'package:cake_wallet/utils/date_formatter.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart';
import 'package:cake_wallet/buy/wyre/wyre_buy_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:url_launcher/url_launcher.dart';
part 'order_details_view_model.g.dart';
class OrderDetailsViewModel = OrderDetailsViewModelBase
with _$OrderDetailsViewModel;
class OrderDetailsViewModel = OrderDetailsViewModelBase with _$OrderDetailsViewModel;
abstract class OrderDetailsViewModelBase with Store {
OrderDetailsViewModelBase({required WalletBase wallet, required Order orderForDetails})
: items = ObservableList<StandartListItem>(),
order = orderForDetails {
if (order.provider != null) {
switch (order.provider) {
case BuyProviderDescription.wyre:
_provider = WyreBuyProvider(wallet: wallet);
break;
case BuyProviderDescription.moonPay:
// _provider = MoonPayProvider(wallet: wallet);// TODO: CW-521
break;
}
OrderDetailsViewModelBase({
required WalletBase wallet,
required Order orderForDetails,
required this.cakePayService,
required this.orders,
}) : items = ObservableList<StandartListItem>(),
order = orderForDetails {
switch (order.source) {
case OrderSourceDescription.buy:
if (order.buyProvider == BuyProviderDescription.wyre) {
_provider = WyreOrderProviderAdapter(wallet: wallet);
} else if (order.buyProvider == BuyProviderDescription.moonPay) {
//_provider = MoonPayOrderProviderAdapter(appStore: null /* provide if needed */, wallet: wallet);
}
break;
case OrderSourceDescription.order:
if (order.orderProvider == OrderProviderDescription.cakePay) {
_provider = CakePayOrderProviderAdapter(wallet: wallet, cakePayService: cakePayService);
}
break;
}
_updateItems();
_updateOrder();
timer = Timer.periodic(Duration(seconds: 20), (_) async => _updateOrder());
timer = Timer.periodic(const Duration(seconds: 20), (_) async => _updateOrder());
}
@observable
@@ -44,27 +64,37 @@ abstract class OrderDetailsViewModelBase with Store {
@observable
ObservableList<StandartListItem> items;
BuyProvider? _provider;
final CakePayService cakePayService;
final Box<Order> orders;
OrderProvider? _provider;
List<OrderCard> cards = [];
Timer? timer;
@action
Future<void> _updateOrder() async {
try {
if (_provider != null && (_provider is MoonPayProvider || _provider is WyreBuyProvider)) {
final updatedOrder = _provider is MoonPayProvider
? await (_provider as MoonPayProvider).findOrderById(order.id)
: await (_provider as WyreBuyProvider).findOrderById(order.id);
updatedOrder.from = order.from;
updatedOrder.to = order.to;
updatedOrder.receiveAddress = order.receiveAddress;
updatedOrder.walletId = order.walletId;
if (order.provider != null) {
updatedOrder.providerRaw = order.provider.raw;
}
order = updatedOrder;
_updateItems();
if (_provider == null) return;
final updatedOrderObj = await _provider!.findOrderById(order.id);
final updatedOrder = updatedOrderObj.$1;
if (_provider is CakePayOrderProviderAdapter) {
cards = updatedOrderObj.$2 as List<OrderCard>? ?? [];
}
final existing = orders.values.firstWhereOrNull((e) => e.id == updatedOrder.id);
if (existing != null) {
existing.stateRaw = updatedOrder.stateRaw;
await existing.save();
order = existing;
} else {
await orders.add(updatedOrder);
order = updatedOrder;
}
_updateItems();
} catch (e) {
printV(e.toString());
}
@@ -73,55 +103,43 @@ abstract class OrderDetailsViewModelBase with Store {
void _updateItems() {
final dateFormat = DateFormatter.withCurrentLocal();
items.clear();
items.addAll([
StandartListItem(
title: 'Transfer ID',
value: order.transferId),
StandartListItem(
title: S.current.trade_details_state,
value: order.state != null
? order.state.toString()
: S.current.trade_details_fetching),
]);
items.add(
StandartListItem(
title: 'Buy provider',
value: order.provider.title)
);
DetailsListStatusItem(title: S.current.trade_details_state, value: order.state.toString()));
if (_provider != null && (_provider is MoonPayProvider || _provider is WyreBuyProvider)) {
final trackUrl = _provider is MoonPayProvider
? (_provider as MoonPayProvider).trackUrl
: (_provider as WyreBuyProvider).trackUrl;
if (trackUrl.isNotEmpty ?? false) {
final buildURL = trackUrl + '${order.transferId}';
items.add(
TrackTradeListItem(
title: S.current.track,
value: buildURL,
onTap: () async {
try {
final uri = Uri.parse(buildURL);
if (await canLaunchUrl(uri))
await launchUrl(uri, mode: LaunchMode.externalApplication);
} catch (e) {}
}));
}
if (_provider is CakePayOrderProviderAdapter) {
items.add(CakePayDetailsListCardItem(
title: '',
value: '',
id: order.id,
createdAt: dateFormat.format(order.createdAt),
price: order.receiveAmount,
quantity: order.quantity,
from: order.from ?? '',
to: order.to ?? '',
cards: cards,
onTap: (BuildContext context) {
Clipboard.setData(ClipboardData(text: '${order.id}'));
showBar<void>(context, S.of(context).copied_to_clipboard);
},
));
}
items.add(
StandartListItem(
title: S.current.trade_details_created_at,
value: dateFormat.format(order.createdAt).toString())
);
items.add(StandartListItem(title: 'Order provider', value: _provider?.title ?? ''));
items.add(
StandartListItem(
title: S.current.trade_details_pair,
value: '${order.from} → ${order.to}')
);
final trackUrl = _provider?.trackUrl ?? '';
if (trackUrl.isNotEmpty) {
final buildURL = trackUrl + '${order.transferId}';
items.add(TrackTradeListItem(
title: S.current.track,
value: buildURL,
onTap: () async {
try {
final uri = Uri.parse(buildURL);
if (await canLaunchUrl(uri))
await launchUrl(uri, mode: LaunchMode.externalApplication);
} catch (e) {}
}));
}
}
}
}