Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web): show transaction details in a dialog #2177

Merged
merged 6 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions webapp/frontend/devtools_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
extensions:
5 changes: 5 additions & 0 deletions webapp/frontend/lib/common/truncate_text.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
String truncateWithEllipsis(int cutoff, String text) {
return (text.length <= cutoff)
? text
: '${text.substring(0, (cutoff / 2).floor())}...${text.substring(text.length - (cutoff / 2).ceil(), text.length)}';
}
17 changes: 16 additions & 1 deletion webapp/frontend/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,24 @@ import 'package:get_10101/trade/quote_service.dart';
import 'package:get_10101/settings/settings_service.dart';
import 'package:get_10101/wallet/wallet_change_notifier.dart';
import 'package:get_10101/wallet/wallet_service.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl_browser.dart';
import 'package:provider/provider.dart';

import 'common/color.dart';
import 'common/theme.dart';

void main() {
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
buildLogger(false);
logger.i("Logger initialized");

// Get the system's default locale
String defaultLocale = await findSystemLocale();

// Initialize the date format data for the system's default locale
await initializeDateFormatting(defaultLocale, null);

const walletService = WalletService();
const channelService = ChannelService();

Expand Down Expand Up @@ -63,6 +72,12 @@ class _TenTenOneAppState extends State<TenTenOneApp> {
return MaterialApp.router(
title: "10101",
scaffoldMessengerKey: scaffoldMessengerKey,
supportedLocales: const [
Locale('en', 'US'),
Locale('es', 'ES'),
Locale('fr', 'FR'),
Locale('de', 'DE'),
],
theme: ThemeData(
primarySwatch: swatch,
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
Expand Down
7 changes: 1 addition & 6 deletions webapp/frontend/lib/settings/channel_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get_10101/common/channel_state_label.dart';
import 'package:get_10101/common/snack_bar.dart';
import 'package:get_10101/common/truncate_text.dart';
import 'package:get_10101/settings/channel_change_notifier.dart';
import 'package:get_10101/settings/channel_service.dart';
import 'package:get_10101/settings/dlc_channel.dart';
Expand Down Expand Up @@ -286,12 +287,6 @@ class _ChannelDetailWidgetState extends State<ChannelDetailWidget> {
}
}

String truncateWithEllipsis(int cutoff, String text) {
return (text.length <= cutoff)
? text
: '${text.substring(0, (cutoff / 2).floor())}...${text.substring(text.length - (cutoff / 2).ceil(), text.length)}';
}

Uri buildUri(String txId) {
// TODO: support different networks
return Uri(
Expand Down
79 changes: 35 additions & 44 deletions webapp/frontend/lib/wallet/history_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,50 +21,41 @@ class _HistoryScreenState extends State<HistoryScreen> {

final history = walletChangeNotifier.getHistory();

return Container(
padding: const EdgeInsets.only(top: 25),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: ElevatedButton(
onPressed: refreshing
? null
: () async {
setState(() {
refreshing = true;
});
await service.sync();
setState(() {
refreshing = false;
});
},
child:
refreshing ? const CircularProgressIndicator() : const Text("Refresh")),
),
],
return Column(
children: [
Expanded(
child: SingleChildScrollView(
child: Column(
children: history == null
? [
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(),
)
]
: history.map((item) => OnChainPaymentHistoryItem(data: item)).toList(),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
children: history == null
? [
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(),
)
]
: history.map((item) => OnChainPaymentHistoryItem(data: item)).toList(),
),
),
],
),
],
));
),
),
Padding(
padding: const EdgeInsets.all(15.0),
child: ElevatedButton(
onPressed: refreshing
? null
: () async {
setState(() {
refreshing = true;
});
await service.sync();
await walletChangeNotifier.refresh();
setState(() {
refreshing = false;
});
},
child: refreshing ? const CircularProgressIndicator() : const Text("Refresh")),
),
],
);
}
}
20 changes: 18 additions & 2 deletions webapp/frontend/lib/wallet/onchain_payment_history_item.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get_10101/common/payment.dart';
import 'package:get_10101/wallet/walle_history_detail_dialog.dart';
import 'package:intl/intl.dart';
import 'package:timeago/timeago.dart' as timeago;

Expand All @@ -10,6 +11,8 @@ class OnChainPaymentHistoryItem extends StatelessWidget {

@override
Widget build(BuildContext context) {
final formattedDate = DateFormat.yMd().add_jm().format(data.timestamp);

final statusIcon = switch (data.confirmations) {
>= 3 => const Icon(Icons.check_circle, color: Colors.green, size: 18),
_ => const Icon(Icons.pending, size: 18)
Expand All @@ -29,7 +32,11 @@ class OnChainPaymentHistoryItem extends StatelessWidget {
elevation: 0,
child: ListTile(
onTap: () async {
// todo
showDialog(
context: context,
builder: (context) {
return WalletHistoryDetailDialog(data: data);
});
},
leading: Stack(children: [
Container(
Expand All @@ -53,7 +60,9 @@ class OnChainPaymentHistoryItem extends StatelessWidget {
textWidthBasis: TextWidthBasis.longestLine,
text: TextSpan(style: DefaultTextStyle.of(context).style, children: <TextSpan>[
TextSpan(
text: timeago.format(data.timestamp),
text: wasMoreThanHalfAnHourAgo(data.timestamp)
? formattedDate
: timeago.format(data.timestamp),
style: const TextStyle(color: Colors.grey)),
])),
trailing: Padding(
Expand Down Expand Up @@ -90,3 +99,10 @@ class OnChainPaymentHistoryItem extends StatelessWidget {
);
}
}

bool wasMoreThanHalfAnHourAgo(DateTime timestamp) {
DateTime now = DateTime.now();
DateTime oneHourAgo = now.subtract(const Duration(minutes: 30));

return timestamp.isBefore(oneHourAgo);
}
7 changes: 4 additions & 3 deletions webapp/frontend/lib/wallet/receive_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get_10101/common/color.dart';
import 'package:get_10101/common/snack_bar.dart';
import 'package:get_10101/common/truncate_text.dart';
import 'package:get_10101/wallet/wallet_change_notifier.dart';
import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart';
Expand Down Expand Up @@ -41,9 +42,9 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
children: [
address == null
? const SizedBox.square(
dimension: 350, child: Center(child: CircularProgressIndicator()))
dimension: 200, child: Center(child: CircularProgressIndicator()))
: SizedBox.square(
dimension: 350,
dimension: 200,
child: QrImageView(
data: address!,
eyeStyle: const QrEyeStyle(
Expand Down Expand Up @@ -71,7 +72,7 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
child: address == null
? const Center(child: CircularProgressIndicator())
: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Text(address!),
Text(truncateWithEllipsis(30, address!)),
GestureDetector(
child: const Icon(Icons.copy, size: 20),
onTap: () async {
Expand Down
144 changes: 144 additions & 0 deletions webapp/frontend/lib/wallet/walle_history_detail_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/svg.dart';
import 'package:get_10101/common/amount_text.dart';
import 'package:get_10101/common/model.dart';
import 'package:get_10101/common/payment.dart';
import 'package:get_10101/common/snack_bar.dart';
import 'package:get_10101/common/truncate_text.dart';
import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.dart';

class WalletHistoryDetailDialog extends StatelessWidget {
final OnChainPayment data;

const WalletHistoryDetailDialog({
Key? key,
required this.data,
}) : super(key: key);

@override
Widget build(BuildContext context) {
final formattedDate = DateFormat.yMd().add_jm().format(data.timestamp);
final (directionMultiplier, verb) = switch ((data.flow, data.confirmations)) {
(PaymentFlow.inbound, 0) => (1, "are receiving"),
(PaymentFlow.inbound, _) => (1, "received"),
(PaymentFlow.outbound, 0) => (-1, "are sending"),
(PaymentFlow.outbound, _) => (-1, "sent"),
};

return AlertDialog(
content: SizedBox(
width: 440,
child: Padding(
padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Column(
children: [
SizedBox(
width: 50, height: 50, child: SvgPicture.asset("assets/Bitcoin_logo.svg")),
Text("You $verb"),
AmountText(
amount: Amount(data.amount.sats * directionMultiplier),
textStyle: const TextStyle(fontSize: 25, fontWeight: FontWeight.bold)),
],
),
HistoryDetail(
label: "When",
value: formattedDate,
truncate: false,
),
HistoryDetail(
label: "Transaction Id",
value: data.txid,
displayWidget: TransactionIdText(data.txid)),
HistoryDetail(label: "Confirmations", value: data.confirmations.toString()),
HistoryDetail(
label: "Fee",
value: data.fee.toString(),
truncate: false,
),
],
),
),
),
actions: [
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Close'),
),
],
);
}
}

class HistoryDetail extends StatelessWidget {
final String label;
final String value;
final Widget? displayWidget;
final bool truncate;

static const TextStyle defaultValueStyle = TextStyle(fontSize: 16);

const HistoryDetail(
{super.key,
required this.label,
required this.value,
this.displayWidget,
this.truncate = true});

@override
Widget build(BuildContext context) {
return Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Text(label, style: defaultValueStyle.copyWith(fontWeight: FontWeight.bold)),
Expanded(
child: Row(children: [
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: displayWidget ??
Text(truncate ? truncateWithEllipsis(10, value) : value,
style: defaultValueStyle))),
IconButton(
padding: EdgeInsets.zero,
onPressed: () {
Clipboard.setData(ClipboardData(text: value)).then((_) {
showSnackBar(ScaffoldMessenger.of(context), '$label copied to clipboard');
});
},
icon: const Icon(Icons.copy, size: 18))
]),
)
]);
}
}

class TransactionIdText extends StatelessWidget {
final String txId;

const TransactionIdText(this.txId, {super.key});

@override
Widget build(BuildContext context) {
Uri uri = Uri(
scheme: 'https',
host: 'mempool.space',
pathSegments: ['tx', txId],
);

return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(truncateWithEllipsis(10, txId)),
IconButton(
padding: EdgeInsets.zero,
onPressed: () => launchUrl(uri, mode: LaunchMode.externalApplication),
icon: const Icon(Icons.open_in_new, size: 18))
],
);
}
}
6 changes: 3 additions & 3 deletions webapp/frontend/lib/wallet/wallet_change_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ class WalletChangeNotifier extends ChangeNotifier {
List<OnChainPayment>? _history;

WalletChangeNotifier(this.service) {
_refresh();
refresh();
Timer.periodic(const Duration(seconds: 30), (timer) async {
_refresh();
await refresh();
});
}

void _refresh() async {
Future<void> refresh() async {
try {
final data =
await Future.wait<dynamic>([service.getBalance(), service.getOnChainPaymentHistory()]);
Expand Down
Loading
Loading