From 8a9f499e1bbb94e084abe891bf6d982e0ce73340 Mon Sep 17 00:00:00 2001 From: Erdem Yerebasmaz Date: Wed, 6 Nov 2024 20:58:36 +0300 Subject: [PATCH] LNURL-Payment & outgoing LN Invoices UI Rework (#214) * Convert processing payment dialog into a fullscreen Match system navigation bar color to background color * Handle all LNURL Payment requests on LnUrlPaymentPage * Remove LNURLPaymentDialog, LNURLPaymentInfo * Return PrepareLnUrlPayResponse from LnUrlPaymentPage * Check if metadata text is available before rendering LNURLMetadataText Display LNURLMetadataImage at top * Move comment section to bottom * Fetch fees on clicking min & max limits * Change payment limits error message Remove trailing zeros from formatted amounts * Add Fee information * Show prepareLnurlPay errors on text form field * Add payee information for fixed amount invoices * Correct form paddings * Calculate fees each time amount form field gets submitted * Add Amount information for fixed amount invoices * Add missing imports * Remove unnecessary value reset * Display Close button if there's an error validating LNURL-Payment Show error * Show errors in a warning box * Do not display payee information widget if fees are not fetched for fixed amount invoices * Display fee as "?" if no valid amount is selected in accepted range * Create LnUrlPaymentPage widgets * Make LnUrlPaymentPage form scrollable * Show errors as validation error messages * Increase validation error max lines to 3 of AmountFormField * Merge payment validation functions Merge validatorErrorMessage & _errorMessage to errorMessage Prepare LnUrlPayment only if there are no error messages * Display error message if fetching lightning payment limits fails * Display error messages below payment amount for fixed amount invoices * Correct the displayed payment amount * Don't validate manually on pasting & submitting * Fix light theme issues on LNURL Payment pages * Handle LNURL success actions * Add custom handler for currency converter dialog Only call handlers if amount str is not empty * Convert SuccessAction dialog into fullscreen dialog Display scrollbar by default on the message Use the same message widget on ShareablePaymentRow * Hide fees if there's an error * Create a Fullscreen UI for outgoing Lightning payments closes #210 * Remove 'PaymentRequestDialog' and it's components * Remove 'Invoice' helper class * Make LnUrlMetadaText widget scrollable * Fix the zero-amount invoice dismissal * TODO: Handle SendPaymentResponse results * Show fiat value on long pressing invoice amount * unfocus amount field if min or max limits are pressed * Show payment amount at all times on LNInvoicePaymentPage * fix: Validate the correct amount for non-fixed amount lnurl payments * Add a confirmation page for non-fixed amount LNURL payments --- lib/app/routes/routes.dart | 22 + lib/cubit/input/input_cubit.dart | 12 +- lib/cubit/input/input_state.dart | 21 +- .../input_handler/src/input_handler.dart | 67 ++- lib/models/invoice.dart | 58 -- .../ln_invoice/ln_invoice_payment_page.dart | 249 +++++++++ .../lnurl/payment/lnurl_payment_dialog.dart | 165 ------ .../lnurl/payment/lnurl_payment_handler.dart | 43 +- .../lnurl/payment/lnurl_payment_info.dart | 9 - .../lnurl/payment/lnurl_payment_page.dart | 495 +++++++++++------- .../success_action/success_action_dialog.dart | 100 ++-- .../payment/widgets/lnurl_payment_amount.dart | 47 ++ .../widgets/lnurl_payment_comment.dart | 35 ++ .../widgets/lnurl_payment_description.dart | 34 ++ .../payment/widgets/lnurl_payment_fee.dart | 80 +++ .../payment/widgets/lnurl_payment_header.dart | 132 +++++ .../payment/widgets/lnurl_payment_limits.dart | 71 +++ lib/routes/lnurl/payment/widgets/widgets.dart | 6 + lib/routes/lnurl/widgets/lnurl_metadata.dart | 25 +- .../amount_form_field/amount_form_field.dart | 2 + .../payment_confirmation_dialog.dart | 160 ------ .../payment_failed_report_dialog.dart | 106 ---- .../payment_request_dialog.dart | 147 ------ .../payment_request_info_dialog.dart | 394 -------------- .../processing_payment_dialog.dart | 25 +- .../processing_payment_animated_content.dart | 0 .../processing_payment_content.dart | 2 +- .../processing_payment_title.dart | 0 lib/widgets/shareable_payment_row.dart | 15 +- 29 files changed, 1144 insertions(+), 1378 deletions(-) delete mode 100644 lib/models/invoice.dart create mode 100644 lib/routes/ln_invoice/ln_invoice_payment_page.dart delete mode 100644 lib/routes/lnurl/payment/lnurl_payment_dialog.dart delete mode 100644 lib/routes/lnurl/payment/lnurl_payment_info.dart create mode 100644 lib/routes/lnurl/payment/widgets/lnurl_payment_amount.dart create mode 100644 lib/routes/lnurl/payment/widgets/lnurl_payment_comment.dart create mode 100644 lib/routes/lnurl/payment/widgets/lnurl_payment_description.dart create mode 100644 lib/routes/lnurl/payment/widgets/lnurl_payment_fee.dart create mode 100644 lib/routes/lnurl/payment/widgets/lnurl_payment_header.dart create mode 100644 lib/routes/lnurl/payment/widgets/lnurl_payment_limits.dart create mode 100644 lib/routes/lnurl/payment/widgets/widgets.dart delete mode 100644 lib/widgets/payment_dialogs/payment_confirmation_dialog.dart delete mode 100644 lib/widgets/payment_dialogs/payment_failed_report_dialog.dart delete mode 100644 lib/widgets/payment_dialogs/payment_request_dialog.dart delete mode 100644 lib/widgets/payment_dialogs/payment_request_info_dialog.dart rename lib/widgets/{payment_dialogs => }/processing_payment/processing_payment_animated_content.dart (100%) rename lib/widgets/{payment_dialogs => }/processing_payment/processing_payment_content.dart (95%) rename lib/widgets/{payment_dialogs => }/processing_payment/processing_payment_title.dart (100%) diff --git a/lib/app/routes/routes.dart b/lib/app/routes/routes.dart index 1353ad26..acef2593 100644 --- a/lib/app/routes/routes.dart +++ b/lib/app/routes/routes.dart @@ -9,6 +9,8 @@ import 'package:l_breez/routes/home/home_page.dart'; import 'package:l_breez/routes/initial_walkthrough/initial_walkthrough.dart'; import 'package:l_breez/routes/initial_walkthrough/mnemonics/enter_mnemonics_page.dart'; import 'package:l_breez/routes/initial_walkthrough/mnemonics/mnemonics_confirmation_page.dart'; +import 'package:l_breez/routes/ln_invoice/ln_invoice_payment_page.dart'; +import 'package:l_breez/routes/lnurl/payment/lnurl_payment_page.dart'; import 'package:l_breez/routes/qr_scan/qr_scan.dart'; import 'package:l_breez/routes/receive_payment/lightning/receive_lightning_page.dart'; import 'package:l_breez/routes/receive_payment/ln_address/receive_lightning_address_page.dart'; @@ -124,6 +126,26 @@ Route? onGenerateRoute({ ), settings: settings, ); + case LNInvoicePaymentPage.routeName: + return FadeInRoute( + builder: (_) => BlocProvider( + create: (BuildContext context) => PaymentLimitsCubit(ServiceInjector().liquidSDK), + child: LNInvoicePaymentPage( + lnInvoice: settings.arguments as LNInvoice, + ), + ), + settings: settings, + ); + case LnUrlPaymentPage.routeName: + return FadeInRoute( + builder: (_) => BlocProvider( + create: (BuildContext context) => PaymentLimitsCubit(ServiceInjector().liquidSDK), + child: LnUrlPaymentPage( + requestData: settings.arguments as LnUrlPayRequestData, + ), + ), + settings: settings, + ); case FiatCurrencySettings.routeName: return FadeInRoute( builder: (_) => const FiatCurrencySettings(), diff --git a/lib/cubit/input/input_cubit.dart b/lib/cubit/input/input_cubit.dart index ae39d710..27cdc859 100644 --- a/lib/cubit/input/input_cubit.dart +++ b/lib/cubit/input/input_cubit.dart @@ -6,7 +6,6 @@ import 'package:device_client/device_client.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_breez_liquid/flutter_breez_liquid.dart'; import 'package:l_breez/cubit/cubit.dart'; -import 'package:l_breez/models/invoice.dart'; import 'package:lightning_links/lightning_links.dart'; import 'package:logging/logging.dart'; import 'package:rxdart/rxdart.dart'; @@ -86,16 +85,7 @@ class InputCubit extends Cubit { Future handlePaymentRequest(InputType_Bolt11 inputData, InputSource source) async { _log.info("handlePaymentRequest: $inputData source: $source"); - final LNInvoice lnInvoice = inputData.invoice; - - final invoice = Invoice( - bolt11: lnInvoice.bolt11, - paymentHash: lnInvoice.paymentHash, - description: lnInvoice.description ?? "", - amountMsat: lnInvoice.amountMsat ?? BigInt.zero, - expiry: lnInvoice.expiry, - ); - return InputState.invoice(invoice, source); + return InputState.invoice(inputData.invoice, source); } Future _handleParsedInput(InputType parsedInput, InputSource source) async { diff --git a/lib/cubit/input/input_state.dart b/lib/cubit/input/input_state.dart index 4db262be..dfbaea8a 100644 --- a/lib/cubit/input/input_state.dart +++ b/lib/cubit/input/input_state.dart @@ -1,7 +1,6 @@ import 'package:flutter_breez_liquid/flutter_breez_liquid.dart'; import 'package:l_breez/cubit/model/src/input/input_printer.dart'; import 'package:l_breez/cubit/model/src/input/input_source.dart'; -import 'package:l_breez/models/invoice.dart'; class InputState { const InputState._(); @@ -11,9 +10,9 @@ class InputState { const factory InputState.loading() = LoadingInputState; const factory InputState.invoice( - Invoice invoice, + LNInvoice invoice, InputSource source, - ) = InvoiceInputState; + ) = LnInvoiceInputState; const factory InputState.lnUrlPay( LnUrlPayRequestData data, @@ -83,30 +82,30 @@ class LoadingInputState extends InputState { int get hashCode => 0; } -class InvoiceInputState extends InputState { - const InvoiceInputState( - this.invoice, +class LnInvoiceInputState extends InputState { + const LnInvoiceInputState( + this.lnInvoice, this.source, ) : super._(); - final Invoice invoice; + final LNInvoice lnInvoice; final InputSource source; @override String toString() { - return 'InvoiceInputState{invoice: $invoice, source: $source}'; + return 'InvoiceInputState{lnInvoice: $lnInvoice, source: $source}'; } @override bool operator ==(Object other) => identical(this, other) || - other is InvoiceInputState && + other is LnInvoiceInputState && runtimeType == other.runtimeType && - invoice == other.invoice && + lnInvoice == other.lnInvoice && source == other.source; @override - int get hashCode => invoice.hashCode ^ source.hashCode; + int get hashCode => lnInvoice.hashCode ^ source.hashCode; } class LnUrlPayInputState extends InputState { diff --git a/lib/handlers/input_handler/src/input_handler.dart b/lib/handlers/input_handler/src/input_handler.dart index 572140a9..2f076260 100644 --- a/lib/handlers/input_handler/src/input_handler.dart +++ b/lib/handlers/input_handler/src/input_handler.dart @@ -3,17 +3,20 @@ import 'dart:async'; import 'package:breez_translations/breez_translations_locales.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_breez_liquid/flutter_breez_liquid.dart'; import 'package:l_breez/cubit/cubit.dart'; import 'package:l_breez/handlers/handler/handler.dart'; -import 'package:l_breez/models/invoice.dart'; import 'package:l_breez/routes/chainswap/send/send_chainswap_page.dart'; +import 'package:l_breez/routes/ln_invoice/ln_invoice_payment_page.dart'; import 'package:l_breez/routes/lnurl/auth/lnurl_auth_handler.dart'; +import 'package:l_breez/routes/lnurl/lnurl_invoice_delegate.dart'; import 'package:l_breez/routes/lnurl/payment/lnurl_payment_handler.dart'; +import 'package:l_breez/routes/lnurl/widgets/lnurl_page_result.dart'; import 'package:l_breez/routes/lnurl/withdraw/lnurl_withdraw_handler.dart'; import 'package:l_breez/utils/exceptions.dart'; import 'package:l_breez/widgets/flushbar.dart'; import 'package:l_breez/widgets/loader.dart'; -import 'package:l_breez/widgets/payment_dialogs/payment_request_dialog.dart'; +import 'package:l_breez/widgets/payment_dialogs/processing_payment_dialog.dart'; import 'package:logging/logging.dart'; final _log = Logger("InputHandler"); @@ -61,7 +64,10 @@ class InputHandler extends Handler { _handlingRequest = isLoading; _setLoading(isLoading); - handleInputData(inputState).whenComplete(() => _handlingRequest = false).onError((error, _) { + handleInputData(inputState) + .then((result) => handleResult(result)) + .whenComplete(() => _handlingRequest = false) + .onError((error, _) { _log.severe("Input state error", error); _handlingRequest = false; _setLoading(false); @@ -84,8 +90,8 @@ class InputHandler extends Handler { return; } - if (inputState is InvoiceInputState) { - return handleInvoice(context, inputState.invoice); + if (inputState is LnInvoiceInputState) { + return handleLnInvoice(context, inputState.lnInvoice); } else if (inputState is LnUrlPayInputState) { return handlePayRequest(context, firstPaymentItemKey, inputState.data); } else if (inputState is LnUrlWithdrawInputState) { @@ -101,19 +107,37 @@ class InputHandler extends Handler { } } - Future handleInvoice(BuildContext context, Invoice invoice) async { - _log.info("handle invoice $invoice"); + Future handleLnInvoice(BuildContext context, LNInvoice lnInvoice) async { + _log.info("handle LnInvoice $lnInvoice"); + final navigator = Navigator.of(context); + PrepareSendResponse? prepareResponse = await navigator.pushNamed( + LNInvoicePaymentPage.routeName, + arguments: lnInvoice, + ); + if (prepareResponse == null || !context.mounted) { + return Future.value(); + } + + // Show Processing Payment Dialog return await showDialog( useRootNavigator: false, context: context, barrierDismissible: false, - builder: (_) => PaymentRequestDialog( - invoice, - firstPaymentItemKey, + builder: (_) => ProcessingPaymentDialog( + isLnUrlPayment: true, + firstPaymentItemKey: firstPaymentItemKey, + paymentFunc: () async { + final paymentsCubit = context.read(); + return await paymentsCubit.sendPayment(prepareResponse); + }, ), - ).then((message) { - if (message != null && context.mounted) { - showFlushbar(context, message: message); + ).then((result) { + if (result is String && context.mounted) { + showFlushbar(context, message: result); + } + // TODO: Handle SendPaymentResponse results, return a SendPaymentResult to be handled by handleResult() + if (result is SendPaymentResponse) { + _log.info("SendPaymentResponse result - payment status: ${result.payment.status}"); } }); } @@ -121,7 +145,22 @@ class InputHandler extends Handler { Future handleBitcoinAddress(BuildContext context, BitcoinAddressInputState inputState) async { _log.fine("handle bitcoin address $inputState"); if (inputState.source == InputSource.qrcodeReader) { - return await Navigator.of(context).pushNamed(SendChainSwapPage.routeName, arguments: inputState.data); + return await Navigator.of(context).pushNamed( + SendChainSwapPage.routeName, + arguments: inputState.data, + ); + } + } + + void handleResult(result) { + _log.info("Input state handled: $result"); + if (result is LNURLPageResult && result.protocol != null) { + final context = contextProvider?.getBuildContext(); + if (context != null) { + handleLNURLPageResult(context, result); + } else { + _log.info("Skipping handling of result: $result because context is null"); + } } } diff --git a/lib/models/invoice.dart b/lib/models/invoice.dart deleted file mode 100644 index 0cd8fc55..00000000 --- a/lib/models/invoice.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:l_breez/utils/extensions/breez_pos_message_extractor.dart'; - -class Invoice { - final String bolt11; - final String paymentHash; - final String description; - final BigInt amountMsat; - final BigInt expiry; - final int lspFee; - - const Invoice({ - required this.bolt11, - required this.paymentHash, - this.description = "", - required this.amountMsat, - required this.expiry, - this.lspFee = 0, - }); - - String get payeeName => ""; - - String get payerName => ""; - - String get payerImageURL => ""; - - String get payeeImageURL => ""; - - String extractDescription() { - return extractPosMessage(description) ?? description; - } - - @override - String toString() { - return 'Invoice{bolt11: $bolt11, paymentHash: $paymentHash, description: $description, ' - 'amountMsat: $amountMsat, expiry: $expiry, lspFee: $lspFee}'; - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is Invoice && - runtimeType == other.runtimeType && - bolt11 == other.bolt11 && - paymentHash == other.paymentHash && - description == other.description && - amountMsat == other.amountMsat && - expiry == other.expiry && - lspFee == other.lspFee; - - @override - int get hashCode => - bolt11.hashCode ^ - paymentHash.hashCode ^ - description.hashCode ^ - amountMsat.hashCode ^ - expiry.hashCode ^ - lspFee.hashCode; -} diff --git a/lib/routes/ln_invoice/ln_invoice_payment_page.dart b/lib/routes/ln_invoice/ln_invoice_payment_page.dart new file mode 100644 index 00000000..44075963 --- /dev/null +++ b/lib/routes/ln_invoice/ln_invoice_payment_page.dart @@ -0,0 +1,249 @@ +import 'package:breez_translations/breez_translations_locales.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_breez_liquid/flutter_breez_liquid.dart'; +import 'package:l_breez/cubit/cubit.dart'; +import 'package:l_breez/routes/lnurl/payment/widgets/widgets.dart'; +import 'package:l_breez/utils/exceptions.dart'; +import 'package:l_breez/utils/payment_validator.dart'; +import 'package:l_breez/widgets/back_button.dart' as back_button; +import 'package:l_breez/widgets/flushbar.dart'; +import 'package:l_breez/widgets/loader.dart'; +import 'package:l_breez/widgets/single_button_bottom_bar.dart'; + +class LNInvoicePaymentPage extends StatefulWidget { + final LNInvoice lnInvoice; + + static const routeName = "/ln_invoice_payment"; + static const paymentMethod = PaymentMethod.lightning; + + const LNInvoicePaymentPage({super.key, required this.lnInvoice}); + + @override + State createState() => LNInvoicePaymentPageState(); +} + +class LNInvoicePaymentPageState extends State { + final _scaffoldKey = GlobalKey(); + + bool _loading = true; + bool _isCalculatingFees = false; + String errorMessage = ""; + LightningPaymentLimitsResponse? _lightningLimits; + + int? amountSat; + PrepareSendResponse? _prepareResponse; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + final amountMsat = widget.lnInvoice.amountMsat; + if ((amountMsat == null || amountMsat == BigInt.zero) && context.mounted) { + final texts = context.texts(); + Navigator.pop(context); + showFlushbar(context, message: texts.payment_request_zero_amount_not_supported); + } + + setState(() { + amountSat = amountMsat!.toInt() ~/ 1000; + }); + await _fetchLightningLimits(); + }); + } + + Future _fetchLightningLimits() async { + final paymentLimitsCubit = context.read(); + try { + final response = await paymentLimitsCubit.fetchLightningLimits(); + setState(() { + _lightningLimits = response; + }); + await _handleLightningPaymentLimitsResponse(); + } catch (error) { + setState(() { + errorMessage = error.toString(); + }); + } finally { + setState(() { + _loading = false; + }); + } + } + + Future _handleLightningPaymentLimitsResponse() async { + final errorMessage = validatePayment( + amountSat: amountSat!, + throwError: true, + ); + if (errorMessage == null) { + await _prepareSendPayment(amountSat!); + } + } + + Future _prepareSendPayment(int amountSat) async { + final texts = context.texts(); + final paymentsCubit = context.read(); + try { + setState(() { + _isCalculatingFees = true; + _prepareResponse = null; + errorMessage = ""; + }); + + final response = await paymentsCubit.prepareSendPayment( + destination: widget.lnInvoice.bolt11, + amountSat: BigInt.from(amountSat), + ); + setState(() { + _prepareResponse = response; + }); + } catch (error) { + setState(() { + _prepareResponse = null; + errorMessage = extractExceptionMessage(error, texts); + _loading = false; + }); + rethrow; + } finally { + setState(() { + _isCalculatingFees = false; + }); + } + } + + @override + Widget build(BuildContext context) { + final texts = context.texts(); + final themeData = Theme.of(context); + + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + leading: const back_button.BackButton(), + title: Text(texts.spontaneous_payment_send_payment_title), + ), + body: BlocBuilder( + builder: (context, currencyState) { + if (_loading) { + return Center( + child: Loader( + color: themeData.primaryColor.withOpacity(0.5), + ), + ); + } + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0), + child: Center( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: LnUrlPaymentHeader( + payeeName: "", + totalAmount: amountSat! + (_prepareResponse?.feesSat.toInt() ?? 0), + errorMessage: errorMessage, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: LnUrlPaymentAmount(amountSat: amountSat!), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: LnUrlPaymentFee( + isCalculatingFees: _isCalculatingFees, + feesSat: errorMessage.isEmpty ? _prepareResponse?.feesSat.toInt() : null, + ), + ), + if (widget.lnInvoice.description != null && widget.lnInvoice.description!.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: LnUrlPaymentDescription( + metadataText: widget.lnInvoice.description!, + ), + ), + ], + ], + ), + ), + ), + ); + }, + ), + bottomNavigationBar: errorMessage.isNotEmpty + ? SingleButtonBottomBar( + stickToBottom: true, + text: texts.qr_code_dialog_action_close, + onPressed: () { + Navigator.of(context).pop(); + }, + ) + : _prepareResponse != null + ? SingleButtonBottomBar( + stickToBottom: true, + text: texts.lnurl_payment_page_action_pay, + onPressed: () async { + Navigator.pop(context, _prepareResponse); + }, + ) + : const SizedBox.shrink(), + ); + } + + String? validatePayment({ + required int amountSat, + bool throwError = false, + }) { + final texts = context.texts(); + final currencyCubit = context.read(); + final currencyState = currencyCubit.state; + + String? message; + if (_lightningLimits == null) { + message = "Failed to retrieve network payment limits. Please try again later."; + } + final effectiveMinSat = _lightningLimits!.send.minSat.toInt(); + final effectiveMaxSat = _lightningLimits!.send.maxSat.toInt(); + if (amountSat > effectiveMaxSat) { + final networkLimit = "(${currencyState.bitcoinCurrency.format( + effectiveMaxSat, + includeDisplayName: true, + )})"; + message = throwError + ? texts.valid_payment_error_exceeds_the_limit(networkLimit) + : texts.lnurl_payment_page_error_exceeds_limit(effectiveMaxSat); + } else if (amountSat < effectiveMinSat) { + final effMinSendableFormatted = currencyState.bitcoinCurrency.format(effectiveMinSat); + message = throwError + ? "${texts.invoice_payment_validator_error_payment_below_invoice_limit(effMinSendableFormatted)}." + : texts.lnurl_payment_page_error_below_limit(effectiveMinSat); + } else { + message = PaymentValidator( + validatePayment: _validateLnUrlPayment, + currency: currencyState.bitcoinCurrency, + texts: context.texts(), + ).validateOutgoing(amountSat); + } + setState(() { + errorMessage = message ?? ""; + }); + if (message != null && throwError) { + throw message; + } + return message; + } + + void _validateLnUrlPayment(int amount, bool outgoing) { + final accountCubit = context.read(); + final accountState = accountCubit.state; + final balance = accountState.balance; + final lnUrlCubit = context.read(); + return lnUrlCubit.validateLnUrlPayment(BigInt.from(amount), outgoing, _lightningLimits!, balance); + } +} diff --git a/lib/routes/lnurl/payment/lnurl_payment_dialog.dart b/lib/routes/lnurl/payment/lnurl_payment_dialog.dart deleted file mode 100644 index 82491b72..00000000 --- a/lib/routes/lnurl/payment/lnurl_payment_dialog.dart +++ /dev/null @@ -1,165 +0,0 @@ -import 'dart:convert'; - -import 'package:breez_translations/breez_translations_locales.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_breez_liquid/flutter_breez_liquid.dart'; -import 'package:l_breez/cubit/cubit.dart'; -import 'package:l_breez/models/currency.dart'; -import 'package:l_breez/routes/lnurl/payment/lnurl_payment_info.dart'; -import 'package:l_breez/utils/fiat_conversion.dart'; -import 'package:logging/logging.dart'; - -final _log = Logger("LNURLPaymentDialog"); - -class LNURLPaymentDialog extends StatefulWidget { - final LnUrlPayRequestData data; - - const LNURLPaymentDialog({super.key, required this.data}); - - @override - State createState() => LNURLPaymentDialogState(); -} - -class LNURLPaymentDialogState extends State { - bool _showFiatCurrency = false; - - @override - void initState() { - super.initState(); - final paymentLimitsCubit = context.read(); - final paymentLimitsState = paymentLimitsCubit.state; - final minSat = paymentLimitsState.lightningPaymentLimits?.send.minSat.toInt(); - if (minSat != null && widget.data.maxSendable.toInt() ~/ 1000 < minSat) { - throw Exception("Payment is below network limit of $minSat sats."); - } - } - - @override - Widget build(BuildContext context) { - final themeData = Theme.of(context); - final texts = context.texts(); - final currencyCubit = context.read(); - final currencyState = currencyCubit.state; - final metadataMap = { - for (var v in json.decode(widget.data.metadataStr)) v[0] as String: v[1], - }; - final description = metadataMap['text/long-desc'] ?? metadataMap['text/plain']; - FiatConversion? fiatConversion; - if (currencyState.fiatEnabled) { - fiatConversion = FiatConversion( - currencyState.fiatCurrency!, - currencyState.fiatExchangeRate!, - ); - } - - return AlertDialog( - title: Text( - Uri.parse(widget.data.callback).host, - style: themeData.primaryTextTheme.headlineMedium!.copyWith(fontSize: 16), - textAlign: TextAlign.center, - ), - content: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - texts.payment_request_dialog_requesting, - style: themeData.primaryTextTheme.displaySmall!.copyWith(fontSize: 16), - textAlign: TextAlign.center, - ), - GestureDetector( - behavior: HitTestBehavior.translucent, - onLongPressStart: (_) { - setState(() { - _showFiatCurrency = true; - }); - }, - onLongPressEnd: (_) { - setState(() { - _showFiatCurrency = false; - }); - }, - child: ConstrainedBox( - constraints: const BoxConstraints( - minWidth: double.infinity, - ), - child: Text( - _showFiatCurrency && fiatConversion != null - ? fiatConversion.format(widget.data.maxSendable.toInt() ~/ 1000) - : BitcoinCurrency.fromTickerSymbol(currencyState.bitcoinTicker) - .format(widget.data.maxSendable.toInt() ~/ 1000), - style: themeData.primaryTextTheme.headlineSmall, - textAlign: TextAlign.center, - ), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 8.0, left: 16.0, right: 16.0), - child: Container( - constraints: const BoxConstraints( - maxHeight: 200, - minWidth: double.infinity, - ), - child: Scrollbar( - child: SingleChildScrollView( - child: Text( - description, - style: themeData.primaryTextTheme.displaySmall!.copyWith( - fontSize: 16, - ), - textAlign: description.length > 40 && !description.contains("\n") - ? TextAlign.start - : TextAlign.center, - ), - ), - ), - ), - ), - ], - ), - actions: [ - TextButton( - style: ButtonStyle( - overlayColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.pressed)) { - return Colors.transparent; - } - // Defer to the widget's default. - return themeData.textTheme.labelLarge!.color!; - }), - ), - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text( - texts.payment_request_dialog_action_cancel, - style: themeData.primaryTextTheme.labelLarge, - ), - ), - TextButton( - style: ButtonStyle( - overlayColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.pressed)) { - return Colors.transparent; - } - // Defer to the widget's default. - return themeData.textTheme.labelLarge!.color!; - }), - ), - onPressed: () { - final amount = widget.data.maxSendable.toInt() ~/ 1000; - _log.info("LNURL payment of $amount sats where " - "min is ${widget.data.minSendable.toInt() * 1000} sats " - "and max is ${widget.data.maxSendable.toInt() * 1000} sats."); - Navigator.pop(context, LNURLPaymentInfo(amount: amount)); - }, - child: Text( - texts.spontaneous_payment_action_pay, - style: themeData.primaryTextTheme.labelLarge, - ), - ), - ], - ); - } -} diff --git a/lib/routes/lnurl/payment/lnurl_payment_handler.dart b/lib/routes/lnurl/payment/lnurl_payment_handler.dart index b9ca34b9..08484911 100644 --- a/lib/routes/lnurl/payment/lnurl_payment_handler.dart +++ b/lib/routes/lnurl/payment/lnurl_payment_handler.dart @@ -2,15 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_breez_liquid/flutter_breez_liquid.dart'; import 'package:l_breez/cubit/cubit.dart'; -import 'package:l_breez/routes/lnurl/payment/lnurl_payment_dialog.dart'; -import 'package:l_breez/routes/lnurl/payment/lnurl_payment_info.dart'; import 'package:l_breez/routes/lnurl/payment/lnurl_payment_page.dart'; import 'package:l_breez/routes/lnurl/payment/success_action/success_action_dialog.dart'; import 'package:l_breez/routes/lnurl/widgets/lnurl_page_result.dart'; import 'package:l_breez/widgets/payment_dialogs/processing_payment_dialog.dart'; -import 'package:l_breez/widgets/route.dart'; import 'package:logging/logging.dart'; -import 'package:service_injector/service_injector.dart'; final _log = Logger("HandleLNURLPayRequest"); @@ -19,33 +15,15 @@ Future handlePayRequest( GlobalKey firstPaymentItemKey, LnUrlPayRequestData data, ) async { - LNURLPaymentInfo? paymentInfo; - bool fixedAmount = data.minSendable == data.maxSendable; - final paymentLimitsCubit = PaymentLimitsCubit(ServiceInjector().liquidSDK); - if (fixedAmount && !(data.commentAllowed > 0)) { - // Show dialog if payment is of fixed amount with no payer comment allowed - paymentInfo = await showDialog( - useRootNavigator: false, - context: context, - barrierDismissible: false, - builder: (_) => BlocProvider( - create: (BuildContext context) => paymentLimitsCubit, - child: LNURLPaymentDialog(data: data), - ), - ); - } else { - paymentInfo = await Navigator.of(context).push( - FadeInRoute( - builder: (_) => BlocProvider( - create: (BuildContext context) => paymentLimitsCubit, - child: LnUrlPaymentPage(requestData: data), - ), - ), - ); - } - if (paymentInfo == null || !context.mounted) { + final navigator = Navigator.of(context); + PrepareLnUrlPayResponse? prepareResponse = await navigator.pushNamed( + LnUrlPaymentPage.routeName, + arguments: data, + ); + if (prepareResponse == null || !context.mounted) { return Future.value(); } + // Show Processing Payment Dialog return await showDialog( useRootNavigator: false, @@ -56,13 +34,6 @@ Future handlePayRequest( firstPaymentItemKey: firstPaymentItemKey, paymentFunc: () async { final lnurlCubit = context.read(); - final amountMsat = BigInt.from(paymentInfo!.amount * 1000); - final prepareReq = PrepareLnUrlPayRequest( - data: data, - amountMsat: amountMsat, - comment: paymentInfo.comment, - ); - final prepareResponse = await lnurlCubit.prepareLnurlPay(req: prepareReq); final req = LnUrlPayRequest(prepareResponse: prepareResponse); return await lnurlCubit.lnurlPay(req: req); }, diff --git a/lib/routes/lnurl/payment/lnurl_payment_info.dart b/lib/routes/lnurl/payment/lnurl_payment_info.dart deleted file mode 100644 index f5be9669..00000000 --- a/lib/routes/lnurl/payment/lnurl_payment_info.dart +++ /dev/null @@ -1,9 +0,0 @@ -class LNURLPaymentInfo { - final int amount; - final String? comment; - - const LNURLPaymentInfo({ - required this.amount, - this.comment, - }); -} diff --git a/lib/routes/lnurl/payment/lnurl_payment_page.dart b/lib/routes/lnurl/payment/lnurl_payment_page.dart index acecd015..ecfe33db 100644 --- a/lib/routes/lnurl/payment/lnurl_payment_page.dart +++ b/lib/routes/lnurl/payment/lnurl_payment_page.dart @@ -2,27 +2,31 @@ import 'dart:convert'; import 'dart:math'; import 'package:breez_translations/breez_translations_locales.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_breez_liquid/flutter_breez_liquid.dart'; import 'package:l_breez/cubit/cubit.dart'; -import 'package:l_breez/routes/lnurl/payment/lnurl_payment_info.dart'; +import 'package:l_breez/routes/lnurl/payment/widgets/widgets.dart'; import 'package:l_breez/routes/lnurl/widgets/lnurl_metadata.dart'; import 'package:l_breez/theme/theme.dart'; -import 'package:l_breez/utils/always_disabled_focus_node.dart'; +import 'package:l_breez/utils/exceptions.dart'; import 'package:l_breez/utils/payment_validator.dart'; import 'package:l_breez/widgets/amount_form_field/amount_form_field.dart'; import 'package:l_breez/widgets/back_button.dart' as back_button; import 'package:l_breez/widgets/keyboard_done_action.dart'; import 'package:l_breez/widgets/loader.dart'; +import 'package:l_breez/widgets/route.dart'; import 'package:l_breez/widgets/single_button_bottom_bar.dart'; +import 'package:service_injector/service_injector.dart'; class LnUrlPaymentPage extends StatefulWidget { final LnUrlPayRequestData requestData; + final String? comment; - const LnUrlPaymentPage({super.key, required this.requestData}); + static const routeName = "/lnurl_payment"; + static const paymentMethod = PaymentMethod.lightning; + + const LnUrlPaymentPage({super.key, required this.requestData, this.comment}); @override State createState() => LnUrlPaymentPageState(); @@ -38,9 +42,12 @@ class LnUrlPaymentPageState extends State { bool _isFixedAmount = false; bool _loading = true; - String? _errorMessage; + bool _isCalculatingFees = false; + String errorMessage = ""; LightningPaymentLimitsResponse? _lightningLimits; + PrepareLnUrlPayResponse? _prepareResponse; + @override void initState() { super.initState(); @@ -56,73 +63,90 @@ class LnUrlPaymentPageState extends State { final paymentLimitsCubit = context.read(); try { final response = await paymentLimitsCubit.fetchLightningLimits(); - _handleLightningPaymentLimitsResponse(response); + setState(() { + _lightningLimits = response; + }); + await _handleLightningPaymentLimitsResponse(); } catch (error) { setState(() { - _errorMessage = error.toString(); + errorMessage = error.toString(); + }); + } finally { + setState(() { _loading = false; }); } } - void _handleLightningPaymentLimitsResponse(LightningPaymentLimitsResponse response) { + Future _handleLightningPaymentLimitsResponse() async { final minSendableSat = widget.requestData.minSendable.toInt() ~/ 1000; final maxSendableSat = widget.requestData.maxSendable.toInt() ~/ 1000; - final effectiveMinSat = max(response.send.minSat.toInt(), minSendableSat); - final effectiveMaxSat = min(response.send.maxSat.toInt(), maxSendableSat); - - _validateEffectiveLimits(effectiveMinSat: effectiveMinSat, effectiveMaxSat: effectiveMaxSat); - - _updateFormFields(effectiveMaxSat: effectiveMaxSat); - - setState(() { - _lightningLimits = response; - _loading = false; - }); - } - - void _validateEffectiveLimits({ - required int effectiveMinSat, - required int effectiveMaxSat, - }) { - if (effectiveMaxSat < effectiveMinSat) { - final texts = context.texts(); - final currencyCubit = context.read(); - final currencyState = currencyCubit.state; - - final isFixedAmountWithinLimits = _isFixedAmount && (effectiveMinSat == effectiveMaxSat); - if (!isFixedAmountWithinLimits) { - final effMinWithdrawableFormatted = currencyState.bitcoinCurrency.format(effectiveMinSat); - final effMaxWithdrawableFormatted = currencyState.bitcoinCurrency.format(effectiveMaxSat); - throw Exception( - "Payment amount is outside the allowed limits, which range from $effMinWithdrawableFormatted to $effMaxWithdrawableFormatted", - ); - } - - final networkLimit = currencyState.bitcoinCurrency.format( - effectiveMinSat, - includeDisplayName: true, - ); - throw Exception(texts.invoice_payment_validator_error_payment_below_invoice_limit(networkLimit)); + final effectiveMinSat = min( + max(_lightningLimits!.send.minSat.toInt(), minSendableSat), + _lightningLimits!.send.maxSat.toInt(), + ); + final effectiveMaxSat = min(_lightningLimits!.send.maxSat.toInt(), maxSendableSat); + final errorMessage = validatePayment( + amountSat: _isFixedAmount ? minSendableSat : effectiveMinSat, + effectiveMinSat: effectiveMinSat, + effectiveMaxSat: effectiveMaxSat, + throwError: true, + ); + if (errorMessage == null) { + await _updateFormFields(amountSat: effectiveMaxSat); } } - void _updateFormFields({ - required int effectiveMaxSat, - }) { + Future _updateFormFields({ + required int amountSat, + }) async { if (_isFixedAmount) { final currencyCubit = context.read(); final currencyState = currencyCubit.state; _amountController.text = currencyState.bitcoinCurrency.format( - effectiveMaxSat, + amountSat, includeDisplayName: false, ); + _descriptionController.text = widget.comment ?? ""; + await _prepareLnUrlPayment(amountSat); } else if (_amountFocusNode.canRequestFocus) { _amountFocusNode.requestFocus(); } } + Future _prepareLnUrlPayment(int amountSat) async { + final texts = context.texts(); + final lnUrlCubit = context.read(); + try { + setState(() { + _isCalculatingFees = true; + _prepareResponse = null; + errorMessage = ""; + }); + final req = PrepareLnUrlPayRequest( + data: widget.requestData, + amountMsat: BigInt.from(amountSat * 1000), + ); + final response = await lnUrlCubit.prepareLnurlPay(req: req); + setState(() { + _prepareResponse = response; + }); + } catch (error) { + setState(() { + _prepareResponse = null; + errorMessage = extractExceptionMessage(error, texts); + _loading = false; + }); + rethrow; + } finally { + setState(() { + _isCalculatingFees = false; + }); + _formKey.currentState?.validate(); + } + } + @override void dispose() { _doneAction.dispose(); @@ -132,6 +156,7 @@ class LnUrlPaymentPageState extends State { @override Widget build(BuildContext context) { final texts = context.texts(); + final themeData = Theme.of(context); return Scaffold( key: _scaffoldKey, @@ -139,189 +164,267 @@ class LnUrlPaymentPageState extends State { leading: const back_button.BackButton(), title: Text(texts.lnurl_fetch_invoice_pay_to_payee(widget.requestData.domain)), ), - body: BlocBuilder(builder: (context, currencyState) { - if (_loading) { - final themeData = Theme.of(context); - - return Center( - child: Loader( - color: themeData.primaryColor.withOpacity(0.5), - ), - ); - } - - if (_errorMessage != null) { - return Center( - child: Padding( - padding: const EdgeInsets.all(32.0), - child: Text( - _errorMessage!, - textAlign: TextAlign.center, + body: BlocBuilder( + builder: (context, currencyState) { + if (_loading) { + return Center( + child: Loader( + color: themeData.primaryColor.withOpacity(0.5), ), - ), + ); + } + + final metadataMap = { + for (var v in json.decode(widget.requestData.metadataStr)) v[0] as String: v[1], + }; + String? base64String = metadataMap['image/png;base64'] ?? metadataMap['image/jpeg;base64']; + String payeeName = metadataMap["text/identifier"] ?? widget.requestData.domain; + String? metadataText = metadataMap['text/long-desc'] ?? metadataMap['text/plain']; + + final minSendableSat = widget.requestData.minSendable.toInt() ~/ 1000; + final maxSendableSat = widget.requestData.maxSendable.toInt() ~/ 1000; + final effectiveMinSat = min( + max(_lightningLimits!.send.minSat.toInt(), minSendableSat), + _lightningLimits!.send.maxSat.toInt(), ); - } + final effectiveMaxSat = min(_lightningLimits!.send.maxSat.toInt(), maxSendableSat); - final metadataMap = { - for (var v in json.decode(widget.requestData.metadataStr)) v[0] as String: v[1], - }; - String? base64String = metadataMap['image/png;base64'] ?? metadataMap['image/jpeg;base64']; - - final minSendableSat = widget.requestData.minSendable.toInt() ~/ 1000; - final maxSendableSat = widget.requestData.maxSendable.toInt() ~/ 1000; - final effectiveMinSat = max(_lightningLimits!.send.minSat.toInt(), minSendableSat); - final effectiveMaxSat = min(_lightningLimits!.send.maxSat.toInt(), maxSendableSat); - final effMinSendableFormatted = currencyState.bitcoinCurrency.format(effectiveMinSat); - final effMaxSendableFormatted = currencyState.bitcoinCurrency.format(effectiveMaxSat); - - return Form( - key: _formKey, - child: Padding( - padding: const EdgeInsets.fromLTRB(16.0, 24.0, 16.0, 0.0), - child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.requestData.commentAllowed > 0) ...[ - TextFormField( - controller: _descriptionController, - keyboardType: TextInputType.multiline, - textInputAction: TextInputAction.done, - maxLines: null, - maxLength: widget.requestData.commentAllowed.toInt(), - maxLengthEnforcement: MaxLengthEnforcement.enforced, - decoration: InputDecoration( - labelText: texts.lnurl_payment_page_comment, - ), - style: FieldTextStyle.textStyle, - ) - ], - AmountFormField( - context: context, - texts: texts, - bitcoinCurrency: currencyState.bitcoinCurrency, - focusNode: _isFixedAmount ? AlwaysDisabledFocusNode() : _amountFocusNode, - autofocus: !_isFixedAmount, - readOnly: _isFixedAmount, - controller: _amountController, - validatorFn: (amount) => validatePayment( - amount: amount, - effectiveMinSat: effectiveMinSat, - effectiveMaxSat: effectiveMaxSat, - ), - style: FieldTextStyle.textStyle, - ), - if (!_isFixedAmount) ...[ - Padding( - padding: const EdgeInsets.only(top: 8), - child: RichText( - text: TextSpan( - style: FieldTextStyle.labelStyle, - children: [ - TextSpan( - text: texts.lnurl_fetch_invoice_min( - effMinSendableFormatted, - ), - recognizer: TapGestureRecognizer() - ..onTap = () => _pasteAmount(currencyState, effectiveMinSat), - ), - TextSpan( - text: texts.lnurl_fetch_invoice_and( - effMaxSendableFormatted, - ), - recognizer: TapGestureRecognizer() - ..onTap = () => _pasteAmount(currencyState, effectiveMaxSat), - ) - ], + return Form( + key: _formKey, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (base64String != null && base64String.isNotEmpty) ...[ + Padding( + padding: EdgeInsets.zero, + child: Center(child: LNURLMetadataImage(base64String: base64String)), ), - ), - ), - ], - Container( - width: MediaQuery.of(context).size.width, - height: 48, - padding: const EdgeInsets.only(top: 16.0), - child: LNURLMetadataText(metadataMap: metadataMap), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.fromLTRB(0, 10, 0, 22), - child: Center( - child: LNURLMetadataImage( - base64String: base64String, + ], + if (_isFixedAmount) ...[ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: LnUrlPaymentHeader( + payeeName: payeeName, + totalAmount: maxSendableSat + (_prepareResponse?.feesSat.toInt() ?? 0), + errorMessage: errorMessage, + ), + ), + ], + if (!_isFixedAmount) ...[ + AmountFormField( + context: context, + texts: texts, + bitcoinCurrency: currencyState.bitcoinCurrency, + focusNode: _amountFocusNode, + autofocus: true, + controller: _amountController, + validatorFn: (amountSat) => validatePayment( + amountSat: amountSat, + effectiveMinSat: effectiveMinSat, + effectiveMaxSat: effectiveMaxSat, + ), + returnFN: (amountStr) async { + if (amountStr.isNotEmpty) { + final amountSat = currencyState.bitcoinCurrency.parse(amountStr); + setState(() { + _amountController.text = currencyState.bitcoinCurrency.format( + amountSat, + includeDisplayName: false, + ); + }); + _formKey.currentState?.validate(); + } + }, + onFieldSubmitted: (amountStr) async { + if (amountStr.isNotEmpty) { + _formKey.currentState?.validate(); + } + }, + style: FieldTextStyle.textStyle, + errorMaxLines: 3, + ), + ], + if (!_isFixedAmount) ...[ + Padding( + padding: const EdgeInsets.only(top: 8), + child: LnUrlPaymentLimits( + limitsResponse: _lightningLimits, + minSendableSat: minSendableSat, + maxSendableSat: maxSendableSat, + onTap: (amountSat) async { + _amountFocusNode.unfocus(); + setState(() { + _amountController.text = currencyState.bitcoinCurrency.format( + amountSat, + includeDisplayName: false, + ); + }); + _formKey.currentState?.validate(); + }, + ), + ), + ], + if (_prepareResponse != null && _isFixedAmount) ...[ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: LnUrlPaymentAmount(amountSat: maxSendableSat), ), - ), - ), + ], + if (_isFixedAmount) ...[ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: LnUrlPaymentFee( + isCalculatingFees: _isCalculatingFees, + feesSat: errorMessage.isEmpty ? _prepareResponse?.feesSat.toInt() : null, + ), + ), + ], + if (metadataText != null && metadataText.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: LnUrlPaymentDescription( + metadataText: metadataText, + ), + ), + ], + if (widget.requestData.commentAllowed > 0) ...[ + LnUrlPaymentComment( + descriptionController: _descriptionController, + maxCommentLength: widget.requestData.commentAllowed.toInt(), + ) + ], + ], ), - ], + ), ), - ), - ); - }), - bottomNavigationBar: _loading - ? null - : _errorMessage != null + ); + }, + ), + bottomNavigationBar: errorMessage.isNotEmpty + ? SingleButtonBottomBar( + stickToBottom: true, + text: texts.qr_code_dialog_action_close, + onPressed: () { + Navigator.of(context).pop(); + }, + ) + : !_isFixedAmount ? SingleButtonBottomBar( - stickToBottom: true, - text: texts.qr_code_dialog_action_close, - onPressed: () { - Navigator.of(context).pop(); - }, - ) - : SingleButtonBottomBar( stickToBottom: true, text: texts.lnurl_fetch_invoice_action_continue, onPressed: () async { - if (_formKey.currentState!.validate()) { + if (_formKey.currentState?.validate() ?? false) { final currencyCubit = context.read(); - final amount = currencyCubit.state.bitcoinCurrency.parse(_amountController.text); - final comment = _descriptionController.text; - // TODO: Instead of popping LNURLPaymentInfo to show Processing Payment Dialog. Call LNURL pay and consequently payment success animation. - Navigator.pop(context, LNURLPaymentInfo(amount: amount, comment: comment)); + final currencyState = currencyCubit.state; + final amountSat = currencyState.bitcoinCurrency.parse(_amountController.text); + final amountMsat = BigInt.from(amountSat * 1000); + final requestData = widget.requestData.copyWith( + minSendable: amountMsat, + maxSendable: amountMsat, + ); + PrepareLnUrlPayResponse? prepareResponse = + await Navigator.of(context).push( + FadeInRoute( + builder: (_) => BlocProvider( + create: (BuildContext context) => PaymentLimitsCubit(ServiceInjector().liquidSDK), + child: LnUrlPaymentPage( + requestData: requestData, + comment: _descriptionController.text, + ), + ), + ), + ); + if (prepareResponse == null || !context.mounted) { + return Future.value(); + } + Navigator.pop(context, prepareResponse); } }, - ), + ) + : _prepareResponse != null + ? SingleButtonBottomBar( + stickToBottom: true, + text: texts.lnurl_payment_page_action_pay, + onPressed: () async { + Navigator.pop(context, _prepareResponse); + }, + ) + : const SizedBox.shrink(), ); } String? validatePayment({ - required int amount, + required int amountSat, required int effectiveMinSat, required int effectiveMaxSat, + bool throwError = false, }) { final texts = context.texts(); final currencyCubit = context.read(); final currencyState = currencyCubit.state; - if (amount > effectiveMaxSat) { - return texts.lnurl_payment_page_error_exceeds_limit(effectiveMaxSat); + String? message; + if (_lightningLimits == null) { + message = "Failed to retrieve network payment limits. Please try again later."; } - if (amount < effectiveMinSat) { - return texts.lnurl_payment_page_error_below_limit(effectiveMinSat); + if (amountSat > effectiveMaxSat) { + final networkLimit = "(${currencyState.bitcoinCurrency.format( + effectiveMaxSat, + includeDisplayName: true, + )})"; + message = throwError + ? texts.valid_payment_error_exceeds_the_limit(networkLimit) + : texts.lnurl_payment_page_error_exceeds_limit(effectiveMaxSat); + } else if (amountSat < effectiveMinSat) { + final effMinSendableFormatted = currencyState.bitcoinCurrency.format(effectiveMinSat); + message = throwError + ? "${texts.invoice_payment_validator_error_payment_below_invoice_limit(effMinSendableFormatted)}." + : texts.lnurl_payment_page_error_below_limit(effectiveMinSat); + } else { + message = PaymentValidator( + validatePayment: _validateLnUrlPayment, + currency: currencyState.bitcoinCurrency, + texts: context.texts(), + ).validateOutgoing(amountSat); } - - return PaymentValidator( - validatePayment: _validatePayment, - currency: currencyState.bitcoinCurrency, - texts: context.texts(), - ).validateOutgoing(amount); + setState(() { + errorMessage = message ?? ""; + }); + if (message != null && throwError) { + throw message; + } + return message; } - void _validatePayment(int amount, bool outgoing) { + void _validateLnUrlPayment(int amount, bool outgoing) { final accountCubit = context.read(); final accountState = accountCubit.state; final balance = accountState.balance; final lnUrlCubit = context.read(); return lnUrlCubit.validateLnUrlPayment(BigInt.from(amount), outgoing, _lightningLimits!, balance); } +} - void _pasteAmount(CurrencyState currencyState, int amountSat) { - setState(() { - _amountController.text = currencyState.bitcoinCurrency.format( - amountSat, - includeDisplayName: false, - ); - }); +extension LnUrlPayRequestDataCopyWith on LnUrlPayRequestData { + LnUrlPayRequestData copyWith({ + BigInt? minSendable, + BigInt? maxSendable, + }) { + return LnUrlPayRequestData( + callback: callback, + minSendable: minSendable ?? this.minSendable, + maxSendable: maxSendable ?? this.maxSendable, + metadataStr: metadataStr, + commentAllowed: commentAllowed, + domain: domain, + allowsNostr: allowsNostr, + nostrPubkey: nostrPubkey, + lnAddress: lnAddress, + ); } } diff --git a/lib/routes/lnurl/payment/success_action/success_action_dialog.dart b/lib/routes/lnurl/payment/success_action/success_action_dialog.dart index e825f7e4..d2cea1e0 100644 --- a/lib/routes/lnurl/payment/success_action/success_action_dialog.dart +++ b/lib/routes/lnurl/payment/success_action/success_action_dialog.dart @@ -1,7 +1,10 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:breez_translations/breez_translations_locales.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:l_breez/theme/theme.dart'; import 'package:l_breez/widgets/shareable_payment_row.dart'; +import 'package:l_breez/widgets/single_button_bottom_bar.dart'; class SuccessActionDialog extends StatefulWidget { final String message; @@ -19,56 +22,59 @@ class SuccessActionDialogState extends State { final themeData = Theme.of(context); final texts = context.texts(); - return AlertDialog( - title: Text(texts.ln_url_success_action_title), - content: SizedBox( - width: MediaQuery.of(context).size.width, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.url == null) Message(widget.message), - if (widget.url != null) ...[ - ShareablePaymentRow( - title: widget.message, - sharedValue: widget.url!, - isURL: true, - isExpanded: true, - titleTextStyle: themeData.primaryTextTheme.displaySmall!.copyWith(fontSize: 16), - childrenTextStyle: themeData.primaryTextTheme.displaySmall!.copyWith( - fontSize: 12, - height: 1.5, - color: Colors.blue, + return AnnotatedRegion( + value: Theme.of(context).appBarTheme.systemOverlayStyle!.copyWith( + systemNavigationBarColor: Theme.of(context).colorScheme.surface, + ), + child: Dialog.fullscreen( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Expanded(child: SizedBox.expand()), + Text( + texts.ln_url_success_action_title, + style: themeData.dialogTheme.titleTextStyle, + ), + Column( + children: [ + widget.url == null + ? Message(widget.message) + : ShareablePaymentRow( + title: widget.message, + titleWidget: Message(widget.message), + sharedValue: widget.url!, + isURL: true, + isExpanded: true, + titleTextStyle: themeData.primaryTextTheme.displaySmall!.copyWith(fontSize: 16), + childrenTextStyle: themeData.primaryTextTheme.displaySmall!.copyWith( + fontSize: 12, + height: 1.5, + color: Colors.blue, + ), + iconPadding: EdgeInsets.zero, + tilePadding: EdgeInsets.zero, + childrenPadding: EdgeInsets.zero, + ), + ], + ), + const Expanded(child: SizedBox.expand()), + Theme( + data: breezDarkTheme, + child: SingleButtonBottomBar( + text: texts.lnurl_withdraw_dialog_action_close, + stickToBottom: false, + onPressed: () { + Navigator.of(context).pop(); + }, ), - iconPadding: EdgeInsets.zero, - tilePadding: EdgeInsets.zero, - childrenPadding: EdgeInsets.zero, ), ], - ], - ), - ), - contentPadding: const EdgeInsets.only(top: 16.0, left: 32.0, right: 32.0), - actions: [ - TextButton( - style: ButtonStyle( - overlayColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.pressed)) { - return Colors.transparent; - } - // Defer to the widget's default. - return themeData.textTheme.labelLarge!.color!; - }), - ), - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text( - texts.lnurl_withdraw_dialog_action_close, - style: themeData.primaryTextTheme.labelLarge, ), ), - ], + ), ); } } @@ -82,13 +88,15 @@ class Message extends StatelessWidget { Widget build(BuildContext context) { final themeData = Theme.of(context); return Padding( - padding: const EdgeInsets.only(top: 8.0, bottom: 8.0), + padding: const EdgeInsets.only(top: 24.0, bottom: 8.0), child: Container( constraints: const BoxConstraints( maxHeight: 200, minWidth: double.infinity, ), child: Scrollbar( + radius: const Radius.circular(16.0), + thumbVisibility: true, child: SingleChildScrollView( child: AutoSizeText( message, diff --git a/lib/routes/lnurl/payment/widgets/lnurl_payment_amount.dart b/lib/routes/lnurl/payment/widgets/lnurl_payment_amount.dart new file mode 100644 index 00000000..355bb046 --- /dev/null +++ b/lib/routes/lnurl/payment/widgets/lnurl_payment_amount.dart @@ -0,0 +1,47 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:breez_translations/breez_translations_locales.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:l_breez/cubit/cubit.dart'; + +class LnUrlPaymentAmount extends StatelessWidget { + final int amountSat; + + const LnUrlPaymentAmount({super.key, required this.amountSat}); + + @override + Widget build(BuildContext context) { + final currencyCubit = context.read(); + final currencyState = currencyCubit.state; + + final texts = context.texts(); + final themeData = Theme.of(context); + + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: AutoSizeText( + texts.send_on_chain_amount, + style: themeData.primaryTextTheme.headlineMedium?.copyWith(color: Colors.white), + textAlign: TextAlign.left, + maxLines: 1, + ), + ), + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + reverse: true, + child: AutoSizeText( + currencyState.bitcoinCurrency.format(amountSat), + style: TextStyle(color: themeData.colorScheme.error), + textAlign: TextAlign.right, + maxLines: 1, + ), + ), + ), + ], + ); + } +} diff --git a/lib/routes/lnurl/payment/widgets/lnurl_payment_comment.dart b/lib/routes/lnurl/payment/widgets/lnurl_payment_comment.dart new file mode 100644 index 00000000..61bfa77e --- /dev/null +++ b/lib/routes/lnurl/payment/widgets/lnurl_payment_comment.dart @@ -0,0 +1,35 @@ +import 'package:breez_translations/breez_translations_locales.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:l_breez/theme/src/theme.dart'; + +class LnUrlPaymentComment extends StatelessWidget { + final int maxCommentLength; + final TextEditingController descriptionController; + + const LnUrlPaymentComment({ + super.key, + required this.descriptionController, + required this.maxCommentLength, + }); + + @override + Widget build(BuildContext context) { + final texts = context.texts(); + final themeData = Theme.of(context); + + return TextFormField( + controller: descriptionController, + keyboardType: TextInputType.multiline, + textInputAction: TextInputAction.done, + maxLines: null, + maxLength: maxCommentLength, + maxLengthEnforcement: MaxLengthEnforcement.enforced, + decoration: InputDecoration( + labelText: "${texts.lnurl_payment_page_comment}:", + labelStyle: themeData.primaryTextTheme.headlineMedium?.copyWith(color: Colors.white), + ), + style: themeData.paymentItemSubtitleTextStyle.copyWith(color: Colors.white70), + ); + } +} diff --git a/lib/routes/lnurl/payment/widgets/lnurl_payment_description.dart b/lib/routes/lnurl/payment/widgets/lnurl_payment_description.dart new file mode 100644 index 00000000..74dcbf8c --- /dev/null +++ b/lib/routes/lnurl/payment/widgets/lnurl_payment_description.dart @@ -0,0 +1,34 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:breez_translations/breez_translations_locales.dart'; +import 'package:flutter/material.dart'; +import 'package:l_breez/routes/lnurl/widgets/lnurl_metadata.dart'; + +class LnUrlPaymentDescription extends StatelessWidget { + final String metadataText; + + const LnUrlPaymentDescription({super.key, required this.metadataText}); + + @override + Widget build(BuildContext context) { + final texts = context.texts(); + final themeData = Theme.of(context); + + return Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + AutoSizeText( + texts.utils_print_pdf_header_description, + style: themeData.primaryTextTheme.headlineMedium?.copyWith(color: Colors.white), + textAlign: TextAlign.left, + maxLines: 1, + ), + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: LNURLMetadataText(metadataText: metadataText), + ), + ], + ); + } +} diff --git a/lib/routes/lnurl/payment/widgets/lnurl_payment_fee.dart b/lib/routes/lnurl/payment/widgets/lnurl_payment_fee.dart new file mode 100644 index 00000000..66805a34 --- /dev/null +++ b/lib/routes/lnurl/payment/widgets/lnurl_payment_fee.dart @@ -0,0 +1,80 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:breez_translations/breez_translations_locales.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:l_breez/cubit/cubit.dart'; + +class LnUrlPaymentFee extends StatelessWidget { + final bool isCalculatingFees; + final int? feesSat; + + const LnUrlPaymentFee({ + super.key, + required this.isCalculatingFees, + required this.feesSat, + }); + + @override + Widget build(BuildContext context) { + final currencyCubit = context.read(); + final currencyState = currencyCubit.state; + + final texts = context.texts(); + final themeData = Theme.of(context); + + return Row( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: AutoSizeText( + "${texts.csv_exporter_fee}:", + style: themeData.primaryTextTheme.headlineMedium?.copyWith(color: Colors.white), + textAlign: TextAlign.left, + maxLines: 1, + ), + ), + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + reverse: true, + child: (isCalculatingFees) + ? Center( + child: SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator( + strokeWidth: 2.0, + color: themeData.primaryColor.withOpacity(0.5), + ), + ), + ) + : (feesSat != null) + ? AutoSizeText( + texts.payment_details_dialog_amount_positive( + currencyState.bitcoinCurrency.format( + feesSat!, + ), + ), + style: TextStyle( + color: themeData.colorScheme.error.withOpacity(0.4), + ), + textAlign: TextAlign.right, + maxLines: 1, + ) + : AutoSizeText( + texts.payment_details_dialog_amount_positive( + "? ${currencyState.bitcoinCurrency.displayName}", + ), + style: TextStyle( + color: themeData.colorScheme.error.withOpacity(0.4), + ), + textAlign: TextAlign.right, + maxLines: 1, + ), + ), + ), + ], + ); + } +} diff --git a/lib/routes/lnurl/payment/widgets/lnurl_payment_header.dart b/lib/routes/lnurl/payment/widgets/lnurl_payment_header.dart new file mode 100644 index 00000000..f39c4837 --- /dev/null +++ b/lib/routes/lnurl/payment/widgets/lnurl_payment_header.dart @@ -0,0 +1,132 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:breez_translations/breez_translations_locales.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:l_breez/cubit/cubit.dart'; +import 'package:l_breez/theme/src/theme.dart'; +import 'package:l_breez/utils/fiat_conversion.dart'; + +class LnUrlPaymentHeader extends StatefulWidget { + final String payeeName; + final int totalAmount; + final String errorMessage; + + const LnUrlPaymentHeader({ + super.key, + required this.payeeName, + required this.totalAmount, + required this.errorMessage, + }); + + @override + State createState() => _LnUrlPaymentHeaderState(); +} + +class _LnUrlPaymentHeaderState extends State { + bool _showFiatCurrency = false; + + @override + Widget build(BuildContext context) { + final currencyCubit = context.read(); + final currencyState = currencyCubit.state; + + final texts = context.texts(); + final themeData = Theme.of(context); + + FiatConversion? fiatConversion; + if (currencyState.fiatEnabled) { + fiatConversion = FiatConversion(currencyState.fiatCurrency!, currencyState.fiatExchangeRate!); + } + + return Center( + child: Column( + children: [ + Text( + widget.payeeName, + style: Theme.of(context) + .primaryTextTheme + .headlineMedium! + .copyWith(fontSize: 16, color: Colors.white), + textAlign: TextAlign.center, + ), + Text( + widget.payeeName.isEmpty + ? texts.payment_request_dialog_requested + : texts.payment_request_dialog_requesting, + style: themeData.primaryTextTheme.displaySmall!.copyWith(fontSize: 16, color: Colors.white), + textAlign: TextAlign.center, + ), + GestureDetector( + behavior: HitTestBehavior.translucent, + onLongPressStart: (_) { + setState(() { + _showFiatCurrency = true; + }); + }, + onLongPressEnd: (_) { + setState(() { + _showFiatCurrency = false; + }); + }, + child: ConstrainedBox( + constraints: const BoxConstraints( + minWidth: double.infinity, + ), + child: _showFiatCurrency && fiatConversion != null + ? Text( + fiatConversion.format(widget.totalAmount), + style: balanceAmountTextStyle.copyWith( + color: themeData.colorScheme.onSurface, + ), + textAlign: TextAlign.center, + ) + : RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: balanceAmountTextStyle.copyWith( + color: themeData.colorScheme.onSurface, + ), + text: currencyState.bitcoinCurrency.format( + widget.totalAmount, + removeTrailingZeros: true, + includeDisplayName: false, + ), + children: [ + TextSpan( + text: " ${currencyState.bitcoinCurrency.displayName}", + style: balanceCurrencyTextStyle.copyWith( + color: themeData.colorScheme.onSurface, + ), + ), + ], + ), + ), + ), + ), + /* + if (fiatConversion != null) ...[ + AutoSizeText( + "≈ ${fiatConversion.format(widget.totalAmount)}", + style: balanceFiatConversionTextStyle.copyWith( + color: themeData.colorScheme.onSurface.withOpacity(0.7), + ), + textAlign: TextAlign.center, + ), + ], + */ + if (widget.errorMessage.isNotEmpty) ...[ + AutoSizeText( + widget.errorMessage, + maxLines: 3, + textAlign: TextAlign.center, + style: themeData.primaryTextTheme.displaySmall?.copyWith( + fontSize: 14.3, + color: themeData.colorScheme.error, + ), + ), + ] + ], + ), + ); + } +} diff --git a/lib/routes/lnurl/payment/widgets/lnurl_payment_limits.dart b/lib/routes/lnurl/payment/widgets/lnurl_payment_limits.dart new file mode 100644 index 00000000..7d915c14 --- /dev/null +++ b/lib/routes/lnurl/payment/widgets/lnurl_payment_limits.dart @@ -0,0 +1,71 @@ +import 'dart:math'; + +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:breez_translations/breez_translations_locales.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_breez_liquid/flutter_breez_liquid.dart'; +import 'package:l_breez/cubit/cubit.dart'; +import 'package:l_breez/theme/src/theme.dart'; + +class LnUrlPaymentLimits extends StatelessWidget { + final LightningPaymentLimitsResponse? limitsResponse; + final int minSendableSat; + final int maxSendableSat; + final Future Function(dynamic amount) onTap; + + const LnUrlPaymentLimits({ + super.key, + required this.limitsResponse, + required this.minSendableSat, + required this.maxSendableSat, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final currencyCubit = context.read(); + final currencyState = currencyCubit.state; + + final texts = context.texts(); + final themeData = Theme.of(context); + + if (limitsResponse == null) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: AutoSizeText( + "Failed to fetch payment limits.", + maxLines: 3, + textAlign: TextAlign.left, + style: FieldTextStyle.labelStyle.copyWith( + color: themeData.isLightTheme ? Colors.red : themeData.colorScheme.error, + ), + ), + ); + } + final effectiveMinSat = min( + max(limitsResponse!.send.minSat.toInt(), minSendableSat), + limitsResponse!.send.maxSat.toInt(), + ); + final effectiveMaxSat = min(limitsResponse!.send.maxSat.toInt(), maxSendableSat); + final effMinSendableFormatted = currencyState.bitcoinCurrency.format(effectiveMinSat); + final effMaxSendableFormatted = currencyState.bitcoinCurrency.format(effectiveMaxSat); + + return RichText( + text: TextSpan( + style: FieldTextStyle.labelStyle, + children: [ + TextSpan( + text: texts.lnurl_fetch_invoice_min(effMinSendableFormatted), + recognizer: TapGestureRecognizer()..onTap = () => onTap(effectiveMinSat), + ), + TextSpan( + text: texts.lnurl_fetch_invoice_and(effMaxSendableFormatted), + recognizer: TapGestureRecognizer()..onTap = () => onTap(effectiveMaxSat), + ) + ], + ), + ); + } +} diff --git a/lib/routes/lnurl/payment/widgets/widgets.dart b/lib/routes/lnurl/payment/widgets/widgets.dart new file mode 100644 index 00000000..6060ae8d --- /dev/null +++ b/lib/routes/lnurl/payment/widgets/widgets.dart @@ -0,0 +1,6 @@ +export 'lnurl_payment_amount.dart'; +export 'lnurl_payment_comment.dart'; +export 'lnurl_payment_description.dart'; +export 'lnurl_payment_fee.dart'; +export 'lnurl_payment_header.dart'; +export 'lnurl_payment_limits.dart'; diff --git a/lib/routes/lnurl/widgets/lnurl_metadata.dart b/lib/routes/lnurl/widgets/lnurl_metadata.dart index 4592859f..ba429179 100644 --- a/lib/routes/lnurl/widgets/lnurl_metadata.dart +++ b/lib/routes/lnurl/widgets/lnurl_metadata.dart @@ -6,17 +6,28 @@ import 'package:l_breez/theme/theme.dart'; import 'package:l_breez/utils/min_font_size.dart'; class LNURLMetadataText extends StatelessWidget { - const LNURLMetadataText({super.key, required this.metadataMap}); + const LNURLMetadataText({super.key, required this.metadataText}); - final Map metadataMap; + final String metadataText; @override Widget build(BuildContext context) { - return AutoSizeText( - metadataMap['text/long-desc'] ?? metadataMap['text/plain'], - style: FieldTextStyle.textStyle, - maxLines: 1, - minFontSize: MinFontSize(context).minFontSize, + return Container( + constraints: const BoxConstraints( + maxHeight: 200, + minWidth: double.infinity, + ), + child: Scrollbar( + radius: const Radius.circular(16.0), + thumbVisibility: true, + child: SingleChildScrollView( + child: AutoSizeText( + metadataText, + style: Theme.of(context).paymentItemSubtitleTextStyle.copyWith(color: Colors.white70), + minFontSize: MinFontSize(context).minFontSize, + ), + ), + ), ); } } diff --git a/lib/widgets/amount_form_field/amount_form_field.dart b/lib/widgets/amount_form_field/amount_form_field.dart index 2fd9cc38..686aeb3e 100644 --- a/lib/widgets/amount_form_field/amount_form_field.dart +++ b/lib/widgets/amount_form_field/amount_form_field.dart @@ -38,6 +38,7 @@ class AmountFormField extends TextFormField { super.onChanged, bool? readOnly, bool? autofocus, + int? errorMaxLines, }) : super( keyboardType: TextInputType.numberWithOptions( decimal: bitcoinCurrency != BitcoinCurrency.sat, @@ -47,6 +48,7 @@ class AmountFormField extends TextFormField { labelText: texts.amount_form_denomination( bitcoinCurrency.displayName, ), + errorMaxLines: errorMaxLines, suffixIcon: (readOnly ?? false) ? null : IconButton( diff --git a/lib/widgets/payment_dialogs/payment_confirmation_dialog.dart b/lib/widgets/payment_dialogs/payment_confirmation_dialog.dart deleted file mode 100644 index 5a12f783..00000000 --- a/lib/widgets/payment_dialogs/payment_confirmation_dialog.dart +++ /dev/null @@ -1,160 +0,0 @@ -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:breez_translations/breez_translations_locales.dart'; -import 'package:flutter/material.dart'; - -class PaymentConfirmationDialog extends StatelessWidget { - final String bolt11; - final int _amountToPay; - final String _amountToPayStr; - final Function() _onCancel; - final Function(String bolt11, int amount) _onPaymentApproved; - final double minHeight; - - const PaymentConfirmationDialog( - this.bolt11, - this._amountToPay, - this._amountToPayStr, - this._onCancel, - this._onPaymentApproved, - this.minHeight, { - super.key, - }); - - @override - Widget build(BuildContext context) { - return Dialog( - child: Container( - constraints: BoxConstraints(minHeight: minHeight), - width: MediaQuery.of(context).size.width, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.min, - children: _buildConfirmationDialog(context), - ), - ), - ); - } - - List _buildConfirmationDialog(BuildContext context) { - return [ - _buildTitle(context), - _buildContent(context), - _buildActions(context), - ]; - } - - Container _buildTitle(BuildContext context) { - final themeData = Theme.of(context); - final texts = context.texts(); - - return Container( - height: 64.0, - padding: const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 8.0), - child: Text( - texts.payment_confirmation_dialog_title, - style: themeData.dialogTheme.titleTextStyle, - textAlign: TextAlign.center, - ), - ); - } - - Widget _buildContent(BuildContext context) { - final themeData = Theme.of(context); - final queryData = MediaQuery.of(context); - final texts = context.texts(); - - return Padding( - padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 0.0), - child: SizedBox( - width: queryData.size.width, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - texts.payment_confirmation_dialog_confirmation, - style: themeData.dialogTheme.contentTextStyle, - textAlign: TextAlign.center, - ), - AutoSizeText.rich( - TextSpan( - children: [ - TextSpan( - text: _amountToPayStr, - style: themeData.dialogTheme.contentTextStyle!.copyWith( - fontSize: 20.0, - fontWeight: FontWeight.bold, - ), - ), - TextSpan( - text: texts.payment_confirmation_dialog_confirmation_end, - ) - ], - ), - maxLines: 2, - textAlign: TextAlign.center, - style: themeData.dialogTheme.contentTextStyle, - ), - ], - ), - ), - ); - } - - Widget _buildActions(BuildContext context) { - final themeData = Theme.of(context); - final texts = context.texts(); - - List children = [ - TextButton( - style: ButtonStyle( - overlayColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.pressed)) { - return Colors.transparent; - } - // Defer to the widget's default. - return themeData.textTheme.labelLarge!.color!; - }), - ), - child: Text( - texts.payment_confirmation_dialog_action_no, - style: themeData.primaryTextTheme.labelLarge, - ), - onPressed: () => _onCancel(), - ), - TextButton( - style: ButtonStyle( - overlayColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.pressed)) { - return Colors.transparent; - } - // Defer to the widget's default. - return themeData.textTheme.labelLarge!.color!; - }), - ), - child: Text( - texts.payment_confirmation_dialog_action_yes, - style: themeData.primaryTextTheme.labelLarge, - ), - onPressed: () { - _onPaymentApproved( - bolt11, - _amountToPay, - ); - }, - ), - ]; - - return SizedBox( - height: 64.0, - child: Padding( - padding: const EdgeInsets.only(bottom: 16.0, right: 8.0), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.end, - children: children, - ), - ), - ); - } -} diff --git a/lib/widgets/payment_dialogs/payment_failed_report_dialog.dart b/lib/widgets/payment_dialogs/payment_failed_report_dialog.dart deleted file mode 100644 index fa05b6d3..00000000 --- a/lib/widgets/payment_dialogs/payment_failed_report_dialog.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'package:breez_translations/breez_translations_locales.dart'; -import 'package:flutter/material.dart'; - -// TODO: Liquid - This file is unused - Re-use after payment report API is implemented on Liquid SDK -class PaymentFailedReportDialog extends StatefulWidget { - const PaymentFailedReportDialog({super.key}); - - @override - PaymentFailedReportDialogState createState() { - return PaymentFailedReportDialogState(); - } -} - -class PaymentFailedReportDialogState extends State { - bool _doNotAskAgain = false; - - @override - Widget build(BuildContext context) { - final themeData = Theme.of(context); - final texts = context.texts(); - - return Theme( - data: themeData.copyWith( - unselectedWidgetColor: themeData.canvasColor, - ), - child: AlertDialog( - titlePadding: const EdgeInsets.fromLTRB(24.0, 22.0, 0.0, 16.0), - title: Text( - texts.payment_failed_report_dialog_title, - style: themeData.dialogTheme.titleTextStyle, - ), - contentPadding: const EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 24.0), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.only(left: 15.0, right: 12.0), - child: Text( - texts.payment_failed_report_dialog_message, - style: themeData.primaryTextTheme.displaySmall?.copyWith(fontSize: 16), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Row( - children: [ - Theme( - data: themeData.copyWith( - unselectedWidgetColor: themeData.textTheme.labelLarge?.color, - ), - child: Checkbox( - activeColor: themeData.canvasColor, - value: _doNotAskAgain, - onChanged: (doNotAskAgain) { - setState(() { - _doNotAskAgain = doNotAskAgain ?? false; - }); - }, - ), - ), - Text( - texts.payment_failed_report_dialog_do_not_ask_again, - style: themeData.primaryTextTheme.displaySmall?.copyWith(fontSize: 16), - ), - ], - ), - ), - ], - ), - actions: [ - SimpleDialogOption( - onPressed: () { - Navigator.pop(context, PaymentFailedReportDialogResult(false, _doNotAskAgain)); - }, - child: Text( - texts.payment_failed_report_dialog_action_no, - style: themeData.primaryTextTheme.labelLarge, - ), - ), - SimpleDialogOption( - onPressed: (() async { - Navigator.pop(context, PaymentFailedReportDialogResult(true, _doNotAskAgain)); - }), - child: Text( - texts.payment_failed_report_dialog_action_yes, - style: themeData.primaryTextTheme.labelLarge, - ), - ), - ], - ), - ); - } -} - -class PaymentFailedReportDialogResult { - final bool report; - final bool doNotAskAgain; - - const PaymentFailedReportDialogResult(this.report, this.doNotAskAgain); - - @override - String toString() { - return 'PaymentFailedReportDialogResult{report: $report, doNotAskAgain: $doNotAskAgain}'; - } -} diff --git a/lib/widgets/payment_dialogs/payment_request_dialog.dart b/lib/widgets/payment_dialogs/payment_request_dialog.dart deleted file mode 100644 index a2398de9..00000000 --- a/lib/widgets/payment_dialogs/payment_request_dialog.dart +++ /dev/null @@ -1,147 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:l_breez/cubit/cubit.dart'; -import 'package:l_breez/models/invoice.dart'; -import 'package:l_breez/widgets/payment_dialogs/payment_confirmation_dialog.dart'; -import 'package:l_breez/widgets/payment_dialogs/payment_request_info_dialog.dart'; -import 'package:l_breez/widgets/payment_dialogs/processing_payment_dialog.dart'; -import 'package:service_injector/service_injector.dart'; - -enum PaymentRequestState { - paymentRequest, - waitingForConfirmation, - processingPayment, - userCancelled, - paymentCompleted -} - -class PaymentRequestDialog extends StatefulWidget { - final Invoice invoice; - final GlobalKey firstPaymentItemKey; - - const PaymentRequestDialog( - this.invoice, - this.firstPaymentItemKey, { - super.key, - }); - - @override - State createState() { - return PaymentRequestDialogState(); - } -} - -class PaymentRequestDialogState extends State { - late PaymentsCubit paymentsCubit; - PaymentRequestState? _state; - String? _amountToPayStr; - int? _amountToPay; - - ModalRoute? _currentRoute; - - @override - void initState() { - super.initState(); - paymentsCubit = context.read(); - _state = PaymentRequestState.paymentRequest; - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _currentRoute ??= ModalRoute.of(context); - } - - @override - Widget build(BuildContext context) { - return PopScope( - canPop: false, - onPopInvoked: (bool didPop) async { - if (_state == PaymentRequestState.processingPayment) { - return; - } else { - final NavigatorState navigator = Navigator.of(context); - paymentsCubit.cancelPayment(widget.invoice.bolt11); - if (_currentRoute != null && _currentRoute!.isActive) { - navigator.removeRoute(_currentRoute!); - } - return; - } - }, - child: showPaymentRequestDialog(), - ); - } - - Widget showPaymentRequestDialog() { - const double minHeight = 220; - - if (_state == PaymentRequestState.processingPayment) { - return ProcessingPaymentDialog( - firstPaymentItemKey: widget.firstPaymentItemKey, - minHeight: minHeight, - paymentFunc: () async { - try { - final prepareSendResponse = await paymentsCubit.prepareSendPayment( - destination: widget.invoice.bolt11, - amountSat: BigInt.from(_amountToPay!), - ); - return await paymentsCubit.sendPayment(prepareSendResponse); - } catch (e) { - rethrow; - } - }, - onStateChange: (state) => _onStateChange(state), - ); - } else if (_state == PaymentRequestState.waitingForConfirmation) { - return PaymentConfirmationDialog( - widget.invoice.bolt11, - _amountToPay!, - _amountToPayStr!, - () => _onStateChange(PaymentRequestState.userCancelled), - (bolt11, amount) => setState(() { - _amountToPay = amount + widget.invoice.lspFee; - _onStateChange(PaymentRequestState.processingPayment); - }), - minHeight, - ); - } else { - return BlocProvider( - create: (BuildContext context) => PaymentLimitsCubit(ServiceInjector().liquidSDK), - child: PaymentRequestInfoDialog( - widget.invoice, - (message) => _onStateChange(PaymentRequestState.userCancelled, message: message), - () => _onStateChange(PaymentRequestState.waitingForConfirmation), - (bolt11, amount) { - _amountToPay = amount + widget.invoice.lspFee; - _onStateChange(PaymentRequestState.processingPayment); - }, - (map) => _setAmountToPay(map), - minHeight, - ), - ); - } - } - - void _onStateChange( - PaymentRequestState state, { - String? message, - }) { - if (state == PaymentRequestState.paymentCompleted) { - Navigator.of(context).pop(); - return; - } - if (state == PaymentRequestState.userCancelled) { - Navigator.of(context).pop(message); - paymentsCubit.cancelPayment(widget.invoice.bolt11); - return; - } - setState(() { - _state = state; - }); - } - - void _setAmountToPay(Map map) { - _amountToPay = map["_amountToPay"] + widget.invoice.lspFee; - _amountToPayStr = map["_amountToPayStr"]; - } -} diff --git a/lib/widgets/payment_dialogs/payment_request_info_dialog.dart b/lib/widgets/payment_dialogs/payment_request_info_dialog.dart deleted file mode 100644 index 7a19adbd..00000000 --- a/lib/widgets/payment_dialogs/payment_request_info_dialog.dart +++ /dev/null @@ -1,394 +0,0 @@ -import 'package:auto_size_text/auto_size_text.dart'; -import 'package:breez_translations/breez_translations_locales.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_breez_liquid/flutter_breez_liquid.dart'; -import 'package:l_breez/cubit/cubit.dart'; -import 'package:l_breez/models/currency.dart'; -import 'package:l_breez/models/invoice.dart'; -import 'package:l_breez/theme/theme.dart'; -import 'package:l_breez/utils/fiat_conversion.dart'; -import 'package:l_breez/utils/payment_validator.dart'; -import 'package:l_breez/widgets/amount_form_field/amount_form_field.dart'; -import 'package:l_breez/widgets/breez_avatar.dart'; -import 'package:l_breez/widgets/keyboard_done_action.dart'; -import 'package:l_breez/widgets/loader.dart'; - -class PaymentRequestInfoDialog extends StatefulWidget { - final Invoice invoice; - final Function(String? message) _onCancel; - final Function() _onWaitingConfirmation; - final Function(String bot11, int amount) _onPaymentApproved; - final Function(Map map) _setAmountToPay; - final double minHeight; - - const PaymentRequestInfoDialog( - this.invoice, - this._onCancel, - this._onWaitingConfirmation, - this._onPaymentApproved, - this._setAmountToPay, - this.minHeight, { - super.key, - }); - - @override - State createState() { - return PaymentRequestInfoDialogState(); - } -} - -class PaymentRequestInfoDialogState extends State { - final _dialogKey = GlobalKey(); - final _formKey = GlobalKey(); - final _invoiceAmountController = TextEditingController(); - final _amountFocusNode = FocusNode(); - final _amountToPayMap = {}; - - KeyboardDoneAction? _doneAction; - bool _showFiatCurrency = false; - - late LightningPaymentLimitsResponse _lightningLimits; - - @override - void initState() { - super.initState(); - if (widget.invoice.amountMsat == BigInt.zero) { - final texts = context.texts(); - widget._onCancel(texts.payment_request_zero_amount_not_supported); - } - _invoiceAmountController.addListener(() { - setState(() {}); - }); - _doneAction = KeyboardDoneAction(focusNodes: [_amountFocusNode]); - } - - @override - void dispose() { - _doneAction?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - List paymentRequestDialog = []; - _addIfNotNull(paymentRequestDialog, _buildPaymentRequestTitle()); - _addIfNotNull(paymentRequestDialog, _buildPaymentRequestContent()); - return Dialog( - child: Container( - constraints: BoxConstraints(minHeight: widget.minHeight), - key: _dialogKey, - width: MediaQuery.of(context).size.width, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - mainAxisSize: MainAxisSize.min, - children: paymentRequestDialog, - ), - ), - ); - } - - Widget? _buildPaymentRequestTitle() { - return widget.invoice.payeeImageURL.isEmpty - ? null - : Padding( - padding: const EdgeInsets.only(top: 48, bottom: 8), - child: BreezAvatar( - widget.invoice.payeeImageURL, - radius: 32.0, - ), - ); - } - - Widget _buildPaymentRequestContent() { - return BlocBuilder( - builder: (c, currencyState) { - return BlocBuilder( - builder: (context, account) { - final texts = context.texts(); - - return BlocBuilder( - builder: (BuildContext context, PaymentLimitsState snapshot) { - if (snapshot.hasError) { - return Center( - child: Padding( - padding: const EdgeInsets.fromLTRB(32, 0, 32, 0), - child: Text( - texts.reverse_swap_upstream_generic_error_message(snapshot.errorMessage), - textAlign: TextAlign.center, - ), - ), - ); - } - if (snapshot.lightningPaymentLimits == null) { - final themeData = Theme.of(context); - - return Center( - child: Loader( - color: themeData.primaryColor.withOpacity(0.5), - ), - ); - } - - _lightningLimits = snapshot.lightningPaymentLimits!; - - List children = []; - _addIfNotNull(children, _buildPayeeNameWidget()); - _addIfNotNull(children, _buildRequestPayTextWidget()); - _addIfNotNull(children, _buildAmountWidget(account, currencyState)); - _addIfNotNull(children, _buildDescriptionWidget()); - _addIfNotNull(children, _buildErrorMessage(currencyState)); - _addIfNotNull(children, _buildActions(currencyState, account)); - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Column(children: children), - ); - }, - ); - }, - ); - }, - ); - } - - void _addIfNotNull(List widgets, Widget? w) { - if (w != null) { - widgets.add(w); - } - } - - Widget? _buildPayeeNameWidget() { - return widget.invoice.payeeName.isEmpty - ? null - : Text( - widget.invoice.payeeName, - style: Theme.of(context).primaryTextTheme.headlineMedium!.copyWith(fontSize: 16), - textAlign: TextAlign.center, - ); - } - - Widget _buildRequestPayTextWidget() { - final themeData = Theme.of(context); - final texts = context.texts(); - final payeeName = widget.invoice.payeeName; - - return Text( - payeeName.isEmpty ? texts.payment_request_dialog_requested : texts.payment_request_dialog_requesting, - style: themeData.primaryTextTheme.displaySmall!.copyWith(fontSize: 16), - textAlign: TextAlign.center, - ); - } - - Widget _buildAmountWidget(AccountState account, CurrencyState currencyState) { - final themeData = Theme.of(context); - final texts = context.texts(); - - if (widget.invoice.amountMsat == BigInt.zero) { - return Theme( - data: themeData.copyWith( - inputDecorationTheme: InputDecorationTheme( - enabledBorder: UnderlineInputBorder( - borderSide: greyBorderSide, - ), - ), - hintColor: themeData.dialogTheme.contentTextStyle!.color, - colorScheme: ColorScheme.dark( - primary: themeData.textTheme.labelLarge!.color!, - error: themeData.isLightTheme ? Colors.red : themeData.colorScheme.error, - ), - primaryColor: themeData.textTheme.labelLarge!.color!, - ), - child: Form( - autovalidateMode: AutovalidateMode.always, - key: _formKey, - child: Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16.0), - child: SizedBox( - height: 80.0, - child: AmountFormField( - context: context, - texts: texts, - bitcoinCurrency: BitcoinCurrency.fromTickerSymbol(currencyState.bitcoinTicker), - iconColor: themeData.primaryIconTheme.color, - focusNode: _amountFocusNode, - autofocus: true, - controller: _invoiceAmountController, - validatorFn: PaymentValidator( - validatePayment: _validatePayment, - currency: currencyState.bitcoinCurrency, - texts: context.texts(), - ).validateOutgoing, - style: themeData.dialogTheme.contentTextStyle!.copyWith(height: 1.0), - ), - ), - ), - ), - ); - } - - FiatConversion? fiatConversion; - if (currencyState.fiatEnabled) { - fiatConversion = FiatConversion(currencyState.fiatCurrency!, currencyState.fiatExchangeRate!); - } - final totalAmount = (widget.invoice.amountMsat.toInt() ~/ 1000) + widget.invoice.lspFee; - - return GestureDetector( - behavior: HitTestBehavior.translucent, - onLongPressStart: (_) { - setState(() { - _showFiatCurrency = true; - }); - }, - onLongPressEnd: (_) { - setState(() { - _showFiatCurrency = false; - }); - }, - child: ConstrainedBox( - constraints: const BoxConstraints( - minWidth: double.infinity, - ), - child: Text( - _showFiatCurrency && fiatConversion != null - ? fiatConversion.format(totalAmount) - : BitcoinCurrency.fromTickerSymbol(currencyState.bitcoinTicker).format(totalAmount), - style: themeData.primaryTextTheme.headlineSmall, - textAlign: TextAlign.center, - ), - ), - ); - } - - Widget? _buildDescriptionWidget() { - final themeData = Theme.of(context); - final description = widget.invoice.extractDescription(); - - return description.isEmpty - ? null - : Padding( - padding: const EdgeInsets.only(top: 8.0, left: 16.0, right: 16.0), - child: Container( - constraints: const BoxConstraints( - maxHeight: 200, - minWidth: double.infinity, - ), - child: Scrollbar( - child: SingleChildScrollView( - child: AutoSizeText( - description, - style: themeData.primaryTextTheme.displaySmall!.copyWith(fontSize: 16), - textAlign: description.length > 40 && !description.contains("\n") - ? TextAlign.start - : TextAlign.center, - ), - ), - ), - ), - ); - } - - Widget? _buildErrorMessage(CurrencyState currencyState) { - final validationError = PaymentValidator( - validatePayment: _validatePayment, - currency: currencyState.bitcoinCurrency, - texts: context.texts(), - ).validateOutgoing( - amountToPay(currencyState), - ); - if (widget.invoice.amountMsat == BigInt.zero || validationError == null || validationError.isEmpty) { - return null; - } - - final themeData = Theme.of(context); - - return Padding( - padding: const EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0), - child: AutoSizeText( - validationError, - maxLines: 3, - textAlign: TextAlign.center, - style: themeData.primaryTextTheme.displaySmall!.copyWith( - fontSize: 16, - color: themeData.isLightTheme ? Colors.red : themeData.colorScheme.error, - ), - ), - ); - } - - Widget _buildActions(CurrencyState currency, AccountState accState) { - final themeData = Theme.of(context); - final texts = context.texts(); - - List actions = [ - SimpleDialogOption( - onPressed: () => widget._onCancel(null), - child: Text( - texts.payment_request_dialog_action_cancel, - style: themeData.primaryTextTheme.labelLarge, - ), - ) - ]; - - int toPaySat = amountToPay(currency); - if (toPaySat >= _lightningLimits.send.minSat.toInt() && toPaySat <= accState.balance) { - actions.add( - SimpleDialogOption( - onPressed: (() async { - if (widget.invoice.amountMsat > BigInt.zero || _formKey.currentState!.validate()) { - if (widget.invoice.amountMsat == BigInt.zero) { - _amountToPayMap["_amountToPay"] = toPaySat; - _amountToPayMap["_amountToPayStr"] = - BitcoinCurrency.fromTickerSymbol(currency.bitcoinTicker).format(amountToPay(currency)); - widget._setAmountToPay(_amountToPayMap); - widget._onWaitingConfirmation(); - } else { - widget._onPaymentApproved( - widget.invoice.bolt11, - amountToPay(currency), - ); - } - } - }), - child: Text( - texts.payment_request_dialog_action_approve, - style: themeData.primaryTextTheme.labelLarge, - ), - ), - ); - } - - return Theme( - data: themeData.copyWith( - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - ), - child: Padding( - padding: const EdgeInsets.only(top: 24.0), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.end, - children: actions, - ), - ), - ); - } - - int amountToPay(CurrencyState acc) { - int amount = widget.invoice.amountMsat.toInt() ~/ 1000; - if (amount == 0) { - try { - amount = BitcoinCurrency.fromTickerSymbol(acc.bitcoinTicker).parse(_invoiceAmountController.text); - } catch (_) {} - } - return amount + widget.invoice.lspFee; - } - - void _validatePayment(int amount, bool outgoing) { - final accountCubit = context.read(); - final accountState = accountCubit.state; - final balance = accountState.balance; - final lnUrlCubit = context.read(); - return lnUrlCubit.validateLnUrlPayment(BigInt.from(amount), outgoing, _lightningLimits, balance); - } -} diff --git a/lib/widgets/payment_dialogs/processing_payment_dialog.dart b/lib/widgets/payment_dialogs/processing_payment_dialog.dart index ed1dd482..6bef2aab 100644 --- a/lib/widgets/payment_dialogs/processing_payment_dialog.dart +++ b/lib/widgets/payment_dialogs/processing_payment_dialog.dart @@ -2,13 +2,13 @@ import 'dart:async'; import 'package:breez_translations/breez_translations_locales.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_breez_liquid/flutter_breez_liquid.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge.dart'; import 'package:l_breez/utils/exceptions.dart'; import 'package:l_breez/widgets/flushbar.dart'; -import 'package:l_breez/widgets/payment_dialogs/payment_request_dialog.dart'; -import 'package:l_breez/widgets/payment_dialogs/processing_payment/processing_payment_animated_content.dart'; -import 'package:l_breez/widgets/payment_dialogs/processing_payment/processing_payment_content.dart'; +import 'package:l_breez/widgets/processing_payment/processing_payment_animated_content.dart'; +import 'package:l_breez/widgets/processing_payment/processing_payment_content.dart'; const _kPaymentListItemHeight = 72.0; @@ -18,7 +18,6 @@ class ProcessingPaymentDialog extends StatefulWidget { final bool popOnCompletion; final bool isLnUrlPayment; final Future Function() paymentFunc; - final Function(PaymentRequestState state)? onStateChange; const ProcessingPaymentDialog({ this.firstPaymentItemKey, @@ -26,7 +25,6 @@ class ProcessingPaymentDialog extends StatefulWidget { this.popOnCompletion = false, this.isLnUrlPayment = false, required this.paymentFunc, - this.onStateChange, super.key, }); @@ -64,7 +62,6 @@ class ProcessingPaymentDialogState extends State if (widget.popOnCompletion) { Navigator.of(context).removeRoute(_currentRoute!); } - widget.onStateChange?.call(PaymentRequestState.paymentCompleted); } }); } @@ -102,7 +99,6 @@ class ProcessingPaymentDialogState extends State if (widget.popOnCompletion) { navigator.removeRoute(_currentRoute!); } - widget.onStateChange?.call(PaymentRequestState.paymentCompleted); if (widget.isLnUrlPayment) { navigator.pop(err); } @@ -168,10 +164,17 @@ class ProcessingPaymentDialogState extends State transitionAnimation: transitionAnimation!, child: const ProcessingPaymentContent(), ) - : Dialog( - child: Container( - constraints: BoxConstraints(minHeight: widget.minHeight), - child: ProcessingPaymentContent(dialogKey: _dialogKey), + : AnnotatedRegion( + value: Theme.of(context).appBarTheme.systemOverlayStyle!.copyWith( + systemNavigationBarColor: Theme.of(context).colorScheme.surface, + ), + child: Dialog.fullscreen( + child: Container( + constraints: BoxConstraints(minHeight: widget.minHeight), + child: Center( + child: ProcessingPaymentContent(dialogKey: _dialogKey), + ), + ), ), ); } diff --git a/lib/widgets/payment_dialogs/processing_payment/processing_payment_animated_content.dart b/lib/widgets/processing_payment/processing_payment_animated_content.dart similarity index 100% rename from lib/widgets/payment_dialogs/processing_payment/processing_payment_animated_content.dart rename to lib/widgets/processing_payment/processing_payment_animated_content.dart diff --git a/lib/widgets/payment_dialogs/processing_payment/processing_payment_content.dart b/lib/widgets/processing_payment/processing_payment_content.dart similarity index 95% rename from lib/widgets/payment_dialogs/processing_payment/processing_payment_content.dart rename to lib/widgets/processing_payment/processing_payment_content.dart index dbb1afed..9bd1ef2a 100644 --- a/lib/widgets/payment_dialogs/processing_payment/processing_payment_content.dart +++ b/lib/widgets/processing_payment/processing_payment_content.dart @@ -2,7 +2,7 @@ import 'package:breez_translations/breez_translations_locales.dart'; import 'package:flutter/material.dart'; import 'package:l_breez/theme/theme.dart'; import 'package:l_breez/widgets/loading_animated_text.dart'; -import 'package:l_breez/widgets/payment_dialogs/processing_payment/processing_payment_title.dart'; +import 'package:l_breez/widgets/processing_payment/processing_payment_title.dart'; class ProcessingPaymentContent extends StatelessWidget { final GlobalKey? dialogKey; diff --git a/lib/widgets/payment_dialogs/processing_payment/processing_payment_title.dart b/lib/widgets/processing_payment/processing_payment_title.dart similarity index 100% rename from lib/widgets/payment_dialogs/processing_payment/processing_payment_title.dart rename to lib/widgets/processing_payment/processing_payment_title.dart diff --git a/lib/widgets/shareable_payment_row.dart b/lib/widgets/shareable_payment_row.dart index 8e389428..fabb5fd9 100644 --- a/lib/widgets/shareable_payment_row.dart +++ b/lib/widgets/shareable_payment_row.dart @@ -8,6 +8,7 @@ import 'package:share_plus/share_plus.dart'; class ShareablePaymentRow extends StatelessWidget { final String title; + final Widget? titleWidget; final String sharedValue; final String? urlValue; final bool isURL; @@ -23,6 +24,7 @@ class ShareablePaymentRow extends StatelessWidget { const ShareablePaymentRow({ super.key, required this.title, + this.titleWidget, required this.sharedValue, this.urlValue, this.isURL = false, @@ -51,12 +53,13 @@ class ShareablePaymentRow extends StatelessWidget { collapsedIconColor: color, initiallyExpanded: isExpanded, tilePadding: tilePadding, - title: AutoSizeText( - title, - style: titleTextStyle ?? themeData.primaryTextTheme.headlineMedium, - maxLines: 2, - group: labelAutoSizeGroup, - ), + title: titleWidget ?? + AutoSizeText( + title, + style: titleTextStyle ?? themeData.primaryTextTheme.headlineMedium, + maxLines: 2, + group: labelAutoSizeGroup, + ), children: [ Row( mainAxisSize: MainAxisSize.max,