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

Prettify invoice details #1245

Merged
merged 10 commits into from
Sep 13, 2023
36 changes: 36 additions & 0 deletions mobile/lib/common/middle_ellipsised_text.dart
Copy link
Contributor Author

@Restioson Restioson Sep 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This took me quite a while and I am still not quite happy with it, but it's good enough 😅

Intrinsic sizes and dialogues in flutter are weird

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';

class MiddleEllipsisedText extends StatelessWidget {
final String value;
final TextStyle? style;

const MiddleEllipsisedText(this.value, {this.style, super.key});

@override
Widget build(BuildContext context) {
final Size textSize = (TextPainter(
text: TextSpan(text: value),
maxLines: 1,
textScaleFactor: MediaQuery.of(context).textScaleFactor,
textDirection: TextDirection.ltr)
..layout())
.size;

if (textSize.width > MediaQuery.sizeOf(context).width - 100) {
return Row(children: [
Text(value.substring(0, 3), style: style),
Expanded(
child: Text(
value.substring(3, value.length - 3),
softWrap: false,
overflow: TextOverflow.fade,
style: style,
)),
Text('...', overflow: TextOverflow.fade, softWrap: false, style: style),
Text(value.substring(value.length - 3, value.length), style: style),
]);
} else {
return Text(value, style: style);
}
}
}
15 changes: 14 additions & 1 deletion mobile/lib/features/wallet/domain/wallet_history.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,20 @@ import 'package:get_10101/features/wallet/wallet_history_item.dart';
import 'payment_flow.dart';
import 'package:get_10101/bridge_generated/bridge_definitions.dart' as rust;

enum WalletHistoryStatus { pending, expired, confirmed, failed }
enum WalletHistoryStatus {
pending,
expired,
confirmed,
failed;

@override
String toString() => switch (this) {
WalletHistoryStatus.pending => "Pending",
WalletHistoryStatus.expired => "Expired",
WalletHistoryStatus.confirmed => "Confirmed",
WalletHistoryStatus.failed => "Failed",
};
}

abstract class WalletHistoryItemData {
final PaymentFlow flow;
Expand Down
206 changes: 168 additions & 38 deletions mobile/lib/features/wallet/wallet_history_item.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/svg.dart';
import 'package:get_10101/bridge_generated/bridge_definitions.dart' as bridge;
import 'package:get_10101/common/amount_text.dart';
import 'package:get_10101/common/domain/model.dart';
import 'package:get_10101/common/middle_ellipsised_text.dart';
import 'package:get_10101/common/snack_bar.dart';
import 'package:get_10101/features/wallet/domain/payment_flow.dart';
import 'package:get_10101/features/wallet/domain/wallet_history.dart';
import 'package:get_10101/features/wallet/wallet_theme.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:timeago/timeago.dart' as timeago;
import 'package:url_launcher/url_launcher.dart';

abstract class WalletHistoryItem extends StatelessWidget {
abstract final WalletHistoryItemData data;
Expand Down Expand Up @@ -54,7 +62,7 @@ abstract class WalletHistoryItem extends StatelessWidget {
return Card(
child: ListTile(
onTap: () async {
await showDialog(context: context, builder: (ctx) => showItemDetails(title, ctx));
await showItemDetails(title, context);
},
leading: Stack(children: [
Container(
Expand Down Expand Up @@ -108,45 +116,120 @@ abstract class WalletHistoryItem extends StatelessWidget {
);
}

Widget showItemDetails(String title, BuildContext context) {
int directionMultiplier = switch (data.flow) {
PaymentFlow.inbound => 1,
PaymentFlow.outbound => -1,
Future<void> showItemDetails(String title, BuildContext context) {
final (directionMultiplier, verb) = switch ((data.flow, data.status)) {
(PaymentFlow.inbound, WalletHistoryStatus.failed) => (1, "failed to receive"),
(PaymentFlow.inbound, WalletHistoryStatus.expired) => (1, "failed to receive"),
(PaymentFlow.inbound, WalletHistoryStatus.pending) => (1, "are receiving"),
(PaymentFlow.inbound, WalletHistoryStatus.confirmed) => (1, "received"),
(PaymentFlow.outbound, WalletHistoryStatus.failed) => (-1, "tried to send"),
(PaymentFlow.outbound, WalletHistoryStatus.expired) => (-1, "tried to send"),
(PaymentFlow.outbound, WalletHistoryStatus.confirmed) => (-1, "sent"),
(PaymentFlow.outbound, WalletHistoryStatus.pending) => (-1, "are sending"),
};

return AlertDialog(
title: Text(title),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
HistoryDetail(
label: "Amount", value: formatSats(Amount(data.amount.sats * directionMultiplier))),
HistoryDetail(label: "Date and time", value: dateFormat.format(data.timestamp)),
...getDetails(),
],
),
);
int sats = data.amount.sats * directionMultiplier;

WalletTheme theme = Theme.of(context).extension<WalletTheme>()!;
HSLColor hsl = HSLColor.fromColor(theme.lightning);
Color lightningColor = hsl.withLightness(hsl.lightness - 0.15).toColor();

// TODO(stable): when we have stablesats send & receive, we can
// set the right icon here
SvgPicture icon = switch (isOnChain()) {
true => SvgPicture.asset("assets/Bitcoin_logo.svg"),
false => SvgPicture.asset("assets/Lightning_logo.svg",
colorFilter: ColorFilter.mode(lightningColor, BlendMode.srcIn)),
};

List<Widget> details = [
Visibility(
visible: data.status != WalletHistoryStatus.confirmed,
child: HistoryDetail(
label: "Status",
value: data.status.toString(),
)),
HistoryDetail(
label: "When",
displayWidget:
Text(timeago.format(data.timestamp), style: HistoryDetail.defaultValueStyle),
value: dateFormat.format(data.timestamp)),
...getDetails(),
];

return showModalBottomSheet<void>(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
clipBehavior: Clip.antiAlias,
isScrollControlled: true,
useRootNavigator: true,
context: context,
builder: (BuildContext context) => SafeArea(
child: Padding(
padding: EdgeInsets.fromLTRB(
16,
16,
16,
MediaQuery.of(context).viewInsets.bottom + 16,
),
child: Column(mainAxisSize: MainAxisSize.min, children: [
SizedBox(width: 50, height: 50, child: icon),
const SizedBox(height: 10),
Text("You $verb"),
AmountText(
amount: Amount(sats),
textStyle: const TextStyle(fontSize: 25, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
...details
.take(details.length - 1)
.where((child) => child is! Visibility || child.visible)
.expand((child) => [child, const Divider()]),
details.last,
]),
)));
}
}

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

const HistoryDetail({super.key, required this.label, required this.value});
static const TextStyle defaultValueStyle = TextStyle(fontSize: 16);
static const TextStyle codeStyle = TextStyle(fontSize: 16, fontFamily: "Courier");
final TextStyle? valueStyle;

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

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
),
Flexible(child: Text(value)),
]),
);
return Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Text(label, style: defaultValueStyle.copyWith(fontWeight: FontWeight.bold)),
),
Expanded(
child: Row(children: [
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: displayWidget ??
MiddleEllipsisedText(value, style: valueStyle ?? 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))
]),
)
]);
}
}

Expand All @@ -159,6 +242,37 @@ IconData iconForFlow(PaymentFlow flow) {
}
}

class TransactionIdText extends StatelessWidget {
final String txId;

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

@override
Widget build(BuildContext context) {
final bridge.Config config = context.read<bridge.Config>();

List<String> network = switch (config.network) {
"signet" => ["signet"],
"testnet" => ["testnet"],
_ => [],
};

return Row(
children: [
Expanded(child: MiddleEllipsisedText(txId, style: HistoryDetail.codeStyle)),
IconButton(
padding: EdgeInsets.zero,
onPressed: () => launchUrl(Uri(
scheme: 'https',
host: 'mempool.space',
pathSegments: [...network, 'tx', txId],
)),
icon: const Icon(Icons.open_in_new, size: 18))
],
);
}
}

class LightningPaymentHistoryItem extends WalletHistoryItem {
@override
final LightningPaymentData data;
Expand All @@ -168,7 +282,7 @@ class LightningPaymentHistoryItem extends WalletHistoryItem {
List<Widget> getDetails() {
return [
Visibility(
visible: data.feeMsats != null,
visible: data.feeMsats != null && data.flow == PaymentFlow.outbound,
child: HistoryDetail(label: "Fee", value: "${(data.feeMsats ?? 0) / 1000} sats"),
),
Visibility(
Expand All @@ -177,15 +291,22 @@ class LightningPaymentHistoryItem extends WalletHistoryItem {
label: "Expiry time",
value: WalletHistoryItem.dateFormat.format(data.expiry ?? DateTime.utc(0))),
),
HistoryDetail(label: "Invoice description", value: data.description),
Visibility(
visible: data.invoice != null,
child: HistoryDetail(label: "Invoice", value: data.invoice ?? ''),
child: HistoryDetail(
label: "Lightning invoice",
value: data.invoice ?? '',
valueStyle: HistoryDetail.codeStyle),
),
HistoryDetail(label: "Payment hash", value: data.paymentHash),
HistoryDetail(label: "Invoice description", value: data.description),
HistoryDetail(
label: "Payment hash", value: data.paymentHash, valueStyle: HistoryDetail.codeStyle),
Visibility(
visible: data.preimage != null,
child: HistoryDetail(label: "Payment preimage", value: data.preimage ?? ''),
child: HistoryDetail(
label: "Payment preimage",
value: data.preimage ?? '',
valueStyle: HistoryDetail.codeStyle),
),
];
}
Expand Down Expand Up @@ -213,7 +334,9 @@ class TradeHistoryItem extends WalletHistoryItem {

@override
List<Widget> getDetails() {
return [HistoryDetail(label: "Order", value: data.orderId)];
return [
HistoryDetail(label: "Order", value: data.orderId, valueStyle: HistoryDetail.codeStyle)
];
}

@override
Expand Down Expand Up @@ -245,8 +368,9 @@ class OrderMatchingFeeHistoryItem extends WalletHistoryItem {
@override
List<Widget> getDetails() {
return [
HistoryDetail(label: "Order", value: data.orderId),
HistoryDetail(label: "Payment hash", value: data.paymentHash)
HistoryDetail(label: "Order", value: data.orderId, valueStyle: HistoryDetail.codeStyle),
HistoryDetail(
label: "Payment hash", value: data.paymentHash, valueStyle: HistoryDetail.codeStyle)
];
}

Expand All @@ -273,7 +397,12 @@ class JitChannelOpenFeeHistoryItem extends WalletHistoryItem {

@override
List<Widget> getDetails() {
return [HistoryDetail(label: "Funding transaction ID", value: data.txid)];
return [
HistoryDetail(
label: "Funding transaction ID",
displayWidget: TransactionIdText(data.txid),
value: data.txid)
];
}

@override
Expand All @@ -300,7 +429,8 @@ class OnChainPaymentHistoryItem extends WalletHistoryItem {
@override
List<Widget> getDetails() {
final details = [
HistoryDetail(label: "Transaction ID", value: data.txid),
HistoryDetail(
label: "Transaction ID", value: data.txid, displayWidget: TransactionIdText(data.txid)),
HistoryDetail(label: "Confirmations", value: data.confirmations.toString()),
Visibility(
visible: data.fee != null,
Expand Down
2 changes: 2 additions & 0 deletions mobile/macos/Flutter/GeneratedPluginRegistrant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import package_info_plus
import path_provider_foundation
import share_plus
import shared_preferences_foundation
import url_launcher_macos

func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
Expand All @@ -21,4 +22,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}
Loading