mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-24 06:48:38 +02:00
Transaction details (New UI) (#3070)
* merge * strings * layout fixes * add modal for tx details * add copy option * fix bottomWidget * fix bottomWidget * remove import * add rbf * add observer for rbf * post-review fixes * add mweb explorer * proper formatting for source address
This commit is contained in:
@@ -60,6 +60,7 @@ import 'package:cake_wallet/new-ui/pages/lightning_username_page.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/receive_page.dart';
|
||||
import 'package:cake_wallet/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/addresses_page/address_label_input.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_label_modal.dart';
|
||||
import 'package:cake_wallet/new-ui/pages/swap_page.dart';
|
||||
import 'package:cake_wallet/order/order.dart';
|
||||
@@ -1419,6 +1420,11 @@ Future<void> setup({
|
||||
}
|
||||
);
|
||||
|
||||
getIt.registerFactoryParam<TransactionDetailsModal, TransactionInfo, void>(
|
||||
(transactionInfo, _) => TransactionDetailsModal(transactionDetailsViewModel: getIt.get<TransactionDetailsViewModel>(
|
||||
param1: [transactionInfo, false]))
|
||||
);
|
||||
|
||||
getIt.registerFactoryParam<TransactionDetailsPage, TransactionInfo, void>(
|
||||
(TransactionInfo transactionInfo, _) => TransactionDetailsPage(
|
||||
transactionDetailsViewModel: getIt.get<TransactionDetailsViewModel>(
|
||||
|
||||
@@ -11,6 +11,8 @@ class ListItemRegularRow extends ListItem {
|
||||
this.onTap,
|
||||
this.trailingIconPath,
|
||||
this.showArrow = true,
|
||||
this.bottomWidget,
|
||||
this.trailingWidget,
|
||||
this.truncateTrailingText = false,
|
||||
this.foregroundColor,
|
||||
this.trailingIconSize
|
||||
@@ -22,6 +24,8 @@ class ListItemRegularRow extends ListItem {
|
||||
final String? trailingIconPath;
|
||||
final VoidCallback? onTap;
|
||||
final bool showArrow;
|
||||
final Widget? bottomWidget;
|
||||
final Widget? trailingWidget;
|
||||
final bool truncateTrailingText;
|
||||
final Color? foregroundColor;
|
||||
final double? trailingIconSize;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/anonpay_history_tile.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_order_tile.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_tile.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/history_trade_tile.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/payjoin_history_tile.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/utils/date_formatter.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
|
||||
@@ -15,7 +17,6 @@ import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
|
||||
import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
@@ -70,8 +71,10 @@ class HistorySection extends StatelessWidget {
|
||||
asset = CryptoCurrency.btcln;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushNamed(Routes.transactionDetails, arguments: transaction),
|
||||
onTap: () {
|
||||
final page = getIt.get<TransactionDetailsModal>(param1: transaction);
|
||||
showModalBottomSheet(isScrollControlled:true,context: context, builder: (context) => page);
|
||||
},
|
||||
child: HistoryTile(
|
||||
title: item.formattedTitle + transactionType,
|
||||
date: DateFormat('HH:mm').format(transaction.date),
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item.dart';
|
||||
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
import 'package:cake_wallet/src/screens/transaction_details/confirmations_list_item.dart';
|
||||
import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart';
|
||||
import 'package:cake_wallet/src/screens/transaction_details/address_list_item.dart';
|
||||
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
|
||||
import 'package:cake_wallet/utils/address_formatter.dart';
|
||||
import 'package:cake_wallet/view_model/transaction_details_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
class TransactionDetailsModal extends StatefulWidget {
|
||||
const TransactionDetailsModal({super.key, required this.transactionDetailsViewModel});
|
||||
|
||||
final TransactionDetailsViewModel transactionDetailsViewModel;
|
||||
|
||||
@override
|
||||
State<TransactionDetailsModal> createState() => _TransactionDetailsModalState();
|
||||
}
|
||||
|
||||
class _TransactionDetailsModalState extends State<TransactionDetailsModal> {
|
||||
final TextEditingController noteController = TextEditingController();
|
||||
final FocusNode noteFocusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
noteController.text = widget.transactionDetailsViewModel.note;
|
||||
|
||||
noteFocusNode.addListener(() {
|
||||
if (!noteFocusNode.hasFocus) {
|
||||
widget.transactionDetailsViewModel.updateNote(noteController.text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: 0.6,
|
||||
minChildSize: 0.25,
|
||||
maxChildSize: 0.9,
|
||||
snap: true,
|
||||
snapSizes: const [0.6, 0.9],
|
||||
builder: (context, controller) => SafeArea(
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: GestureDetector(
|
||||
onTap: FocusScope.of(context).unfocus,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(25))),
|
||||
child: Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).transaction,
|
||||
leadingIcon: Icon(Icons.close),
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
controller: controller,
|
||||
child: Column(
|
||||
children: [
|
||||
Image.asset(
|
||||
widget.transactionDetailsViewModel.transactionAsset.iconPath ??
|
||||
"",
|
||||
width: 64,
|
||||
height: 64),
|
||||
SizedBox(height: 10),
|
||||
Text(
|
||||
widget.transactionDetailsViewModel.formattedTitle +
|
||||
widget.transactionDetailsViewModel.formattedStatus,
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
|
||||
),
|
||||
Text(
|
||||
widget.transactionDetailsViewModel.transactionInfo
|
||||
.amountFormatted(),
|
||||
style: TextStyle(fontSize: 28),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
NewListSections(sections: {
|
||||
"": widget.transactionDetailsViewModel.items
|
||||
.map((item) {
|
||||
if (item.value.isEmpty) return null;
|
||||
|
||||
final shouldBuildBottomWidget =
|
||||
item.value.length > 25;
|
||||
|
||||
return ListItemRegularRow(
|
||||
onTap: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: item.value));
|
||||
},
|
||||
showArrow: false,
|
||||
keyValue:
|
||||
((item.key as ValueKey?)?.value as String?) ??
|
||||
item.title,
|
||||
label: item.title,
|
||||
trailingWidget: shouldBuildBottomWidget
|
||||
? null
|
||||
: _buildTrailingWIdget(item),
|
||||
bottomWidget: shouldBuildBottomWidget
|
||||
? _buildBottomWidget(item)
|
||||
: null);
|
||||
})
|
||||
.whereType<ListItem>()
|
||||
.toList(),
|
||||
}),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(S.of(context).note),
|
||||
TextField(
|
||||
focusNode: noteFocusNode,
|
||||
controller: noteController,
|
||||
decoration: InputDecoration(
|
||||
hintText: S.of(context).add_a_note,
|
||||
border: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
isDense: true),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => NewListSections(sections: {
|
||||
"view tx": [
|
||||
ListItemRegularRow(
|
||||
keyValue: "view tx on",
|
||||
label: widget.transactionDetailsViewModel
|
||||
.explorerDescription,
|
||||
onTap: widget
|
||||
.transactionDetailsViewModel.launchExplorer,
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.primary,
|
||||
trailingIconPath: "assets/new-ui/link_arrow.svg",
|
||||
trailingIconSize: 8)
|
||||
],
|
||||
if (widget.transactionDetailsViewModel.canReplaceByFee)
|
||||
"rbf": [
|
||||
ListItemRegularRow(
|
||||
keyValue: "replace by fee",
|
||||
label: S.of(context).bump_fee,
|
||||
onTap: () {
|
||||
Navigator.of(context)
|
||||
.pushNamed(Routes.bumpFeePage, arguments: [
|
||||
widget.transactionDetailsViewModel
|
||||
.transactionInfo,
|
||||
widget.transactionDetailsViewModel
|
||||
.rawTransaction
|
||||
]);
|
||||
})
|
||||
]
|
||||
}),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: MediaQuery.of(context).viewPadding.bottom)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildTrailingWIdget(TransactionDetailsListItem item) {
|
||||
return switch (item.runtimeType) {
|
||||
ConfirmationsListItem => Row(
|
||||
children: [
|
||||
Text((item as ConfirmationsListItem).current.toString(),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.primary)),
|
||||
if (item.needed > 0)
|
||||
Text("/${item.needed}",
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant))
|
||||
],
|
||||
),
|
||||
_ => Text(
|
||||
item.value,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
Widget _buildBottomWidget(TransactionDetailsListItem item) {
|
||||
return switch (item.runtimeType) {
|
||||
AddressListItem => AddressFormatter.buildSegmentedAddress(
|
||||
address: item.value,
|
||||
evenTextStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: "IBM Plex Mono",
|
||||
color: Theme.of(context).colorScheme.onSurface)),
|
||||
_ => Text(
|
||||
item.value,
|
||||
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart';
|
||||
|
||||
class AddressListItem extends TransactionDetailsListItem {
|
||||
AddressListItem({required super.title, required super.value, super.key});
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart';
|
||||
|
||||
class ConfirmationsListItem extends TransactionDetailsListItem {
|
||||
late final int current;
|
||||
late final int needed;
|
||||
|
||||
ConfirmationsListItem({required super.title, required super.value, super.key}) {
|
||||
final parts = value.split("/");
|
||||
current = int.tryParse(parts.first)??0;
|
||||
needed = int.tryParse(parts.last)??0;
|
||||
}
|
||||
}
|
||||
@@ -7,14 +7,14 @@ class ListItemStyleWrapper extends StatelessWidget {
|
||||
required this.isLastInSection,
|
||||
required this.builder,
|
||||
this.onTap,
|
||||
this.height = 50,
|
||||
this.iconPath,
|
||||
this.height,
|
||||
});
|
||||
|
||||
final String? iconPath;
|
||||
final bool isFirstInSection;
|
||||
final bool isLastInSection;
|
||||
final double height;
|
||||
final double? height;
|
||||
final VoidCallback? onTap;
|
||||
final Widget Function(BuildContext context, TextStyle textStyle, TextStyle labelStyle) builder;
|
||||
|
||||
@@ -59,7 +59,7 @@ class ListItemStyleWrapper extends StatelessWidget {
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
padding: EdgeInsets.symmetric(horizontal: 12, vertical: height == null ? 12 : 0),
|
||||
child: builder(context, textStyle, labelStyle))))),
|
||||
if(iconPath != null && isLastInSection == false) Container(
|
||||
color: theme.colorScheme.surfaceContainer,
|
||||
|
||||
@@ -19,6 +19,8 @@ class ListItemRegularRowWidget extends StatelessWidget {
|
||||
this.truncateTrailingText = false,
|
||||
this.foregroundColor,
|
||||
this.trailingIconSize,
|
||||
this.bottomWidget,
|
||||
this.trailingWidget
|
||||
});
|
||||
|
||||
final String keyValue;
|
||||
@@ -31,6 +33,8 @@ class ListItemRegularRowWidget extends StatelessWidget {
|
||||
final bool isLastInSection;
|
||||
final bool showArrow;
|
||||
final String? trailingIconPath;
|
||||
final Widget? bottomWidget;
|
||||
final Widget? trailingWidget;
|
||||
final bool truncateTrailingText;
|
||||
final Color? foregroundColor;
|
||||
final double? trailingIconSize;
|
||||
@@ -38,74 +42,84 @@ class ListItemRegularRowWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final trailingTextToShow = truncateTrailingText && trailingText != null && trailingText!.length > 20
|
||||
? "${trailingText!.substring(0, 17)}..."
|
||||
: trailingText;
|
||||
final trailingTextToShow =
|
||||
truncateTrailingText && trailingText != null && trailingText!.length > 20
|
||||
? "${trailingText!.substring(0, 17)}..."
|
||||
: trailingText;
|
||||
|
||||
return ListItemStyleWrapper(
|
||||
onTap: onTap,
|
||||
iconPath: iconPath,
|
||||
isFirstInSection: isFirstInSection,
|
||||
isLastInSection: isLastInSection,
|
||||
height: subtitle != null ? 64 : 50,
|
||||
builder: (context, textStyle, labelStyle) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
if(iconPath != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: CakeImageWidget(imageUrl: iconPath!, width: 24,height: 24,)
|
||||
),
|
||||
Flexible(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label, style: foregroundColor == null ? textStyle : textStyle.copyWith(color: foregroundColor)),
|
||||
if (subtitle != null)
|
||||
Text(
|
||||
subtitle!,
|
||||
style: labelStyle.copyWith(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (trailingTextToShow != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Text(
|
||||
trailingTextToShow,
|
||||
style: labelStyle,
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
if (iconPath != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: CakeImageWidget(imageUrl: iconPath!, width: 24,height: 24,)
|
||||
),
|
||||
Flexible(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label,
|
||||
style: foregroundColor == null
|
||||
? textStyle
|
||||
: textStyle.copyWith(color: foregroundColor)),
|
||||
if (subtitle != null)
|
||||
Text(
|
||||
subtitle!,
|
||||
style: labelStyle.copyWith(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if(trailingIconPath != null)
|
||||
CakeImageWidget(imageUrl:
|
||||
trailingIconPath!,
|
||||
height: trailingIconSize ?? 18,
|
||||
width:trailingIconSize ?? 18,
|
||||
colorFilter: ColorFilter.mode(foregroundColor ?? Theme.of(context).colorScheme.onSurfaceVariant,BlendMode.srcIn),
|
||||
)
|
||||
else if(showArrow)
|
||||
CakeImageWidget(imageUrl:
|
||||
"assets/new-ui/arrow_forward.svg",
|
||||
height: 14,
|
||||
color: theme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
if (trailingTextToShow != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Text(
|
||||
trailingTextToShow,
|
||||
style: labelStyle,
|
||||
),
|
||||
),
|
||||
if (trailingWidget != null)
|
||||
trailingWidget!
|
||||
else if (trailingIconPath != null)
|
||||
CakeImageWidget(imageUrl:
|
||||
trailingIconPath!,
|
||||
height: trailingIconSize ?? 18,
|
||||
width:trailingIconSize ?? 18,
|
||||
colorFilter: ColorFilter.mode(foregroundColor ?? Theme.of(context).colorScheme.onSurfaceVariant,BlendMode.srcIn),
|
||||
)
|
||||
else if (showArrow)
|
||||
CakeImageWidget(imageUrl:
|
||||
"assets/new-ui/arrow_forward.svg",
|
||||
height: 14,
|
||||
color: theme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (bottomWidget != null) bottomWidget!
|
||||
],
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ class _ListItemTextFieldWidgetState extends State<ListItemTextFieldWidget> {
|
||||
return ListItemStyleWrapper(
|
||||
isFirstInSection: widget.isFirstInSection,
|
||||
isLastInSection: widget.isLastInSection,
|
||||
height:50,
|
||||
builder: (context, textStyle, labelStyle) {
|
||||
return Row(
|
||||
children: [
|
||||
|
||||
@@ -104,6 +104,8 @@ class NewListSections extends StatelessWidget {
|
||||
truncateTrailingText: item.truncateTrailingText,
|
||||
foregroundColor: item.foregroundColor,
|
||||
trailingIconSize: item.trailingIconSize,
|
||||
trailingWidget: item.trailingWidget,
|
||||
bottomWidget: item.bottomWidget,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user