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
@@ -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,
),
),
),
),
],
),
),
),
),
);
}
}