diff --git a/assets/images/cake_pay_icon.png b/assets/images/cake_pay_icon.png new file mode 100644 index 000000000..6b4304306 Binary files /dev/null and b/assets/images/cake_pay_icon.png differ diff --git a/lib/buy/buy_provider.dart b/lib/buy/buy_provider.dart index f57cc5472..689c86fea 100644 --- a/lib/buy/buy_provider.dart +++ b/lib/buy/buy_provider.dart @@ -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'; diff --git a/lib/buy/buy_provider_description.dart b/lib/buy/buy_provider_description.dart index 07c7ff08b..d395c9df1 100644 --- a/lib/buy/buy_provider_description.dart +++ b/lib/buy/buy_provider_description.dart @@ -1,21 +1,22 @@ import 'package:cw_core/enumerable_item.dart'; -class BuyProviderDescription extends EnumerableItem - with Serializable { - const BuyProviderDescription({required String title, required int raw}) +class BuyProviderDescription extends EnumerableItem with Serializable { + 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'); } -} \ No newline at end of file + } +} diff --git a/lib/buy/moonpay/moonpay_provider.dart b/lib/buy/moonpay/moonpay_provider.dart index 1f9ab1bb1..92f9e5046 100644 --- a/lib/buy/moonpay/moonpay_provider.dart +++ b/lib/buy/moonpay/moonpay_provider.dart @@ -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, diff --git a/lib/buy/order.dart b/lib/buy/order.dart deleted file mode 100644 index 5a677d291..000000000 --- a/lib/buy/order.dart +++ /dev/null @@ -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); -} diff --git a/lib/buy/wyre/wyre_buy_provider.dart b/lib/buy/wyre/wyre_buy_provider.dart index 3f9844df6..f85c04406 100644 --- a/lib/buy/wyre/wyre_buy_provider.dart +++ b/lib/buy/wyre/wyre_buy_provider.dart @@ -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, diff --git a/lib/cake_pay/src/models/cake_pay_card.dart b/lib/cake_pay/src/models/cake_pay_card.dart index f65d0d311..a64921e0f 100644 --- a/lib/cake_pay/src/models/cake_pay_card.dart +++ b/lib/cake_pay/src/models/cake_pay_card.dart @@ -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']); diff --git a/lib/cake_pay/src/models/cake_pay_order.dart b/lib/cake_pay/src/models/cake_pay_order.dart index ddbeb73aa..d3a2ecfda 100644 --- a/lib/cake_pay/src/models/cake_pay_order.dart +++ b/lib/cake_pay/src/models/cake_pay_order.dart @@ -15,8 +15,11 @@ class CakePayOrder { final List 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 map) { + + final cards = map['cards'] as List; + final firstCard = cards.isNotEmpty ? cards.first as Map : {}; + return CakePayOrder( orderId: map['order_id'] as String, - cards: (map['cards'] as List) + cards: cards .map((x) => OrderCard.fromMap(x as Map)) .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)); } + + 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 map) { return CryptoPaymentData( price: map['price'] as String, + amount: (map['amount_from'] as double?)?.toString() ?? '', paymentUrls: PaymentUrl.fromMap(map['paymentUrls'] as Map?), address: map['address'] as String, ); diff --git a/lib/cake_pay/src/services/cake_pay_api.dart b/lib/cake_pay/src/services/cake_pay_api.dart index f8e3849d8..13aa1b7d5 100644 --- a/lib/cake_pay/src/services/cake_pay_api.dart +++ b/lib/cake_pay/src/services/cake_pay_api.dart @@ -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 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; + + return CakePayOrder.fromMap(bodyJson); + } + ///Simulate Payment Future simulatePayment( {required String CSRFToken, diff --git a/lib/cake_pay/src/services/cake_pay_service.dart b/lib/cake_pay/src/services/cake_pay_service.dart index d6f6c826c..8166b52c2 100644 --- a/lib/cake_pay/src/services/cake_pay_service.dart +++ b/lib/cake_pay/src/services/cake_pay_service.dart @@ -109,6 +109,11 @@ class CakePayService { ); } + Future 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 simulatePayment({required String orderId}) async { final token = (await secureStorage.read(key: cakePayUserTokenKey))!; diff --git a/lib/di.dart b/lib/di.dart index ada849917..006e1a753 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -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 setup({ getIt.registerFactory(() => PayjoinTransactionsStore(payjoinSessionSource: _payjoinSessionSource)); getIt.registerSingleton(TradeFilterStore()); + getIt.registerSingleton(OrderFilterStore()); getIt.registerSingleton(TransactionFilterStore(getIt.get())); getIt.registerSingleton(FiatConversionStore()); getIt.registerSingleton(SendTemplateStore(templateSource: _templates)); @@ -545,11 +547,12 @@ Future setup({ balanceViewModel: getIt.get(), appStore: getIt.get(), tradesStore: getIt.get(), + ordersStore: getIt.get(), tradeFilterStore: getIt.get(), + orderFilterStore: getIt.get(), transactionFilterStore: getIt.get(), settingsStore: settingsStore, yatStore: getIt.get(), - ordersStore: getIt.get(), anonpayTransactionsStore: getIt.get(), payjoinTransactionsStore: getIt.get(), sharedPreferences: getIt.get(), @@ -1349,8 +1352,13 @@ Future setup({ getIt.registerFactoryParam((order, _) { final wallet = getIt.get().wallet; + final cakePayService = getIt.get(); - return OrderDetailsViewModel(wallet: wallet!, orderForDetails: order); + return OrderDetailsViewModel( + wallet: wallet!, + orderForDetails: order, + cakePayService: cakePayService, + orders: _ordersSource); }); getIt.registerFactoryParam( @@ -1423,7 +1431,8 @@ Future setup({ (CakePayVendor vendor, _) { return CakePayBuyCardViewModel(vendor: vendor, cakePayService: getIt.get(), - sendViewModel: getIt.get()); + sendViewModel: getIt.get(), + orders: _ordersSource); }); getIt.registerFactory(() => CakePayAccountViewModel(cakePayService: getIt.get())); diff --git a/lib/main.dart b/lib/main.dart index 13322dd64..9ce76113d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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'; diff --git a/lib/order/order.dart b/lib/order/order.dart new file mode 100644 index 000000000..a67546bf0 --- /dev/null +++ b/lib/order/order.dart @@ -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 ?? ''}'; +} diff --git a/lib/order/order_provider.dart b/lib/order/order_provider.dart new file mode 100644 index 000000000..8d29f2621 --- /dev/null +++ b/lib/order/order_provider.dart @@ -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; +} diff --git a/lib/order/order_provider_adapter/cake_pay_order_provider_adapter.dart b/lib/order/order_provider_adapter/cake_pay_order_provider_adapter.dart new file mode 100644 index 000000000..1e0998c55 --- /dev/null +++ b/lib/order/order_provider_adapter/cake_pay_order_provider_adapter.dart @@ -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; + } +} diff --git a/lib/order/order_provider_adapter/moon_pay_order_provider_adapter.dart b/lib/order/order_provider_adapter/moon_pay_order_provider_adapter.dart new file mode 100644 index 000000000..0fb6d86d2 --- /dev/null +++ b/lib/order/order_provider_adapter/moon_pay_order_provider_adapter.dart @@ -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); + } +} diff --git a/lib/order/order_provider_adapter/wyre_order_provider_adapter.dart b/lib/order/order_provider_adapter/wyre_order_provider_adapter.dart new file mode 100644 index 000000000..cc6ea3856 --- /dev/null +++ b/lib/order/order_provider_adapter/wyre_order_provider_adapter.dart @@ -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; +} diff --git a/lib/order/order_provider_description.dart b/lib/order/order_provider_description.dart new file mode 100644 index 000000000..552ddb506 --- /dev/null +++ b/lib/order/order_provider_description.dart @@ -0,0 +1,23 @@ +import 'package:cw_core/enumerable_item.dart'; + +class OrderProviderDescription extends EnumerableItem with Serializable { + 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'); + } + } +} diff --git a/lib/order/order_source_description.dart b/lib/order/order_source_description.dart new file mode 100644 index 000000000..379987317 --- /dev/null +++ b/lib/order/order_source_description.dart @@ -0,0 +1,20 @@ +import 'package:cw_core/enumerable_item.dart'; + +class OrderSourceDescription extends EnumerableItem with Serializable { + 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'); + } + } +} \ No newline at end of file diff --git a/lib/router.dart b/lib/router.dart index 1d04e0c00..6ed7e6040 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -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'; diff --git a/lib/src/screens/dashboard/pages/transactions_page.dart b/lib/src/screens/dashboard/pages/transactions_page.dart index 04ce6ddb0..98edb4d16 100644 --- a/lib/src/screens/dashboard/pages/transactions_page.dart +++ b/lib/src/screens/dashboard/pages/transactions_page.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); diff --git a/lib/src/screens/dashboard/widgets/order_row.dart b/lib/src/screens/dashboard/widgets/order_row.dart index 2e35a1cdb..95de4e1b1 100644 --- a/lib/src/screens/dashboard/widgets/order_row.dart +++ b/lib/src/screens/dashboard/widgets/order_row.dart @@ -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: [ - 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: [ 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: [ + 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, + ), + ), + ); + } } diff --git a/lib/src/screens/order_details/cake_pay_detail_list_card_item.dart b/lib/src/screens/order_details/cake_pay_detail_list_card_item.dart new file mode 100644 index 000000000..4606acba0 --- /dev/null +++ b/lib/src/screens/order_details/cake_pay_detail_list_card_item.dart @@ -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 cards; +} diff --git a/lib/src/screens/order_details/order_details_page.dart b/lib/src/screens/order_details/order_details_page.dart index 10254eaef..c1463a7cb 100644 --- a/lib/src/screens/order_details/order_details_page.dart +++ b/lib/src/screens/order_details/order_details_page.dart @@ -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 { - 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 { @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(context, S.of(context).copied_to_clipboard); }, - child: ListRow( - title: '${item.title}', value: '${item.value}')); + child: ListRow(title: '${item.title}', value: '${item.value}')); } }); }); } - -} \ No newline at end of file +} diff --git a/lib/src/widgets/cake_pay_order_list_card.dart b/lib/src/widgets/cake_pay_order_list_card.dart new file mode 100644 index 000000000..1a82add58 --- /dev/null +++ b/lib/src/widgets/cake_pay_order_list_card.dart @@ -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, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/store/dashboard/order_filter_store.dart b/lib/store/dashboard/order_filter_store.dart new file mode 100644 index 000000000..d7f856d72 --- /dev/null +++ b/lib/store/dashboard/order_filter_store.dart @@ -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 filtered({ + required List 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 []; + return cakePayOrders; + } +} \ No newline at end of file diff --git a/lib/store/dashboard/orders_store.dart b/lib/store/dashboard/orders_store.dart index d91ad3512..d6f2fd629 100644 --- a/lib/store/dashboard/orders_store.dart +++ b/lib/store/dashboard/orders_store.dart @@ -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'; diff --git a/lib/view_model/buy/buy_view_model.dart b/lib/view_model/buy/buy_view_model.dart index 181276ed5..785a75963 100644 --- a/lib/view_model/buy/buy_view_model.dart +++ b/lib/view_model/buy/buy_view_model.dart @@ -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'; diff --git a/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart b/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart index 382f794b9..87ebe63c8 100644 --- a/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart +++ b/lib/view_model/cake_pay/cake_pay_buy_card_view_model.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 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 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 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 confirmSending() async { - final cryptoPaymentData = getPaymentDataFor(selectedPaymentMethod); - if (order == null || cryptoPaymentData == null) return; - + Future 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) { diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 9bf08248d..e893ac8d0 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -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; diff --git a/lib/view_model/dashboard/order_list_item.dart b/lib/view_model/dashboard/order_list_item.dart index 52adf53a6..1e9db9b10 100644 --- a/lib/view_model/dashboard/order_list_item.dart +++ b/lib/view_model/dashboard/order_list_item.dart @@ -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; diff --git a/lib/view_model/order_details_view_model.dart b/lib/view_model/order_details_view_model.dart index a7cd98a21..2edb4db22 100644 --- a/lib/view_model/order_details_view_model.dart +++ b/lib/view_model/order_details_view_model.dart @@ -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(), - 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(), + 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 items; - BuyProvider? _provider; + final CakePayService cakePayService; + final Box orders; + OrderProvider? _provider; + List cards = []; Timer? timer; @action Future _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? ?? []; } + + 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(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) {} + })); + } } -} \ No newline at end of file +}