diff --git a/lib/app/routes/routes.dart b/lib/app/routes/routes.dart index acef2593..44c88de1 100644 --- a/lib/app/routes/routes.dart +++ b/lib/app/routes/routes.dart @@ -21,6 +21,7 @@ import 'package:l_breez/routes/refund/refund_page.dart'; import 'package:l_breez/routes/security/lock_screen.dart'; import 'package:l_breez/routes/security/secured_page.dart'; import 'package:l_breez/routes/security/security_page.dart'; +import 'package:l_breez/routes/send_payment/enter_payment_info/enter_payment_info_page.dart'; import 'package:l_breez/routes/splash/splash_page.dart'; import 'package:l_breez/widgets/route.dart'; import 'package:logging/logging.dart'; @@ -116,6 +117,11 @@ Route? onGenerateRoute({ ), settings: settings, ); + case EnterPaymentInfoPage.routeName: + return FadeInRoute( + builder: (_) => const EnterPaymentInfoPage(), + settings: settings, + ); case SendChainSwapPage.routeName: return FadeInRoute( builder: (_) => BlocProvider( diff --git a/lib/routes/home/widgets/bottom_actions_bar/enter_payment_info_dialog.dart b/lib/routes/home/widgets/bottom_actions_bar/enter_payment_info_dialog.dart deleted file mode 100644 index bd3e5ec7..00000000 --- a/lib/routes/home/widgets/bottom_actions_bar/enter_payment_info_dialog.dart +++ /dev/null @@ -1,243 +0,0 @@ -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/qr_scan/qr_scan.dart'; -import 'package:l_breez/theme/theme.dart'; -import 'package:l_breez/widgets/flushbar.dart'; -import 'package:l_breez/widgets/loader.dart'; -import 'package:logging/logging.dart'; - -final _log = Logger("EnterPaymentInfoDialog"); - -class EnterPaymentInfoDialog extends StatefulWidget { - final GlobalKey paymentItemKey; - - const EnterPaymentInfoDialog({super.key, required this.paymentItemKey}); - - @override - State createState() => EnterPaymentInfoDialogState(); -} - -class EnterPaymentInfoDialogState extends State { - final _formKey = GlobalKey(); - final _paymentInfoController = TextEditingController(); - final _paymentInfoFocusNode = FocusNode(); - - String _scannerErrorMessage = ""; - String _validatorErrorMessage = ""; - - ModalRoute? _loaderRoute; - - @override - void initState() { - super.initState(); - _paymentInfoController.addListener(() { - setState(() {}); - }); - } - - @override - Widget build(BuildContext context) { - final texts = context.texts(); - - return AlertDialog( - titlePadding: const EdgeInsets.fromLTRB(24.0, 22.0, 0.0, 16.0), - title: Text(texts.payment_info_dialog_title), - contentPadding: const EdgeInsets.fromLTRB(24.0, 8.0, 24.0, 24.0), - content: _buildPaymentInfoForm(context), - actions: _buildActions(context), - ); - } - - Theme _buildPaymentInfoForm(BuildContext context) { - final themeData = Theme.of(context); - final texts = context.texts(); - - return Theme( - data: themeData.copyWith( - inputDecorationTheme: InputDecorationTheme( - enabledBorder: UnderlineInputBorder( - borderSide: greyBorderSide, - ), - ), - hintColor: themeData.dialogTheme.contentTextStyle!.color!, - primaryColor: themeData.textTheme.labelLarge!.color, - colorScheme: ColorScheme.dark( - primary: themeData.textTheme.labelLarge!.color!, - error: themeData.isLightTheme ? Colors.red : themeData.colorScheme.error, - ), - ), - child: SizedBox( - width: MediaQuery.of(context).size.width, - child: Form( - key: _formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextFormField( - decoration: InputDecoration( - labelText: texts.payment_info_dialog_hint, - suffixIcon: IconButton( - padding: const EdgeInsets.only(top: 21.0), - alignment: Alignment.bottomRight, - icon: Image( - image: const AssetImage("assets/icons/qr_scan.png"), - color: themeData.primaryIconTheme.color, - width: 24.0, - height: 24.0, - ), - tooltip: texts.payment_info_dialog_barcode, - onPressed: () => _scanBarcode(context), - ), - ), - focusNode: _paymentInfoFocusNode, - controller: _paymentInfoController, - style: TextStyle( - color: themeData.primaryTextTheme.headlineMedium!.color, - ), - validator: (value) { - if (_validatorErrorMessage.isNotEmpty) { - return _validatorErrorMessage; - } - return null; - }, - ), - if (_scannerErrorMessage.isNotEmpty) ...[ - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - _scannerErrorMessage, - style: validatorStyle, - ), - ), - ], - Padding( - padding: const EdgeInsets.only(top: 8), - child: Text( - texts.payment_info_dialog_hint_expanded, - style: FieldTextStyle.labelStyle.copyWith( - fontSize: 13.0, - color: themeData.isLightTheme ? BreezColors.grey[500] : BreezColors.white[200], - ), - ), - ), - ], - ), - ), - ), - ); - } - - List _buildActions(BuildContext context) { - final themeData = Theme.of(context); - final texts = context.texts(); - - List actions = [ - SimpleDialogOption( - onPressed: () => Navigator.pop(context), - child: Text( - texts.payment_info_dialog_action_cancel, - style: themeData.primaryTextTheme.labelLarge, - ), - ) - ]; - - if (_paymentInfoController.text.isNotEmpty) { - actions.add( - SimpleDialogOption( - onPressed: (() async { - final inputCubit = context.read(); - final navigator = Navigator.of(context); - _setLoading(true); - - try { - await _validateInput(_paymentInfoController.text); - if (_formKey.currentState!.validate()) { - _setLoading(false); - navigator.pop(); - inputCubit.addIncomingInput(_paymentInfoController.text, InputSource.inputField); - } - } catch (error) { - _setLoading(false); - _log.warning(error.toString(), error); - _setValidatorErrorMessage(texts.payment_info_dialog_error); - } finally { - _setLoading(false); - } - }), - child: Text( - texts.payment_info_dialog_action_approve, - style: themeData.primaryTextTheme.labelLarge, - ), - ), - ); - } - return actions; - } - - Future _scanBarcode(BuildContext context) async { - final texts = context.texts(); - - FocusScope.of(context).requestFocus(FocusNode()); - String? barcode = await Navigator.pushNamed(context, QRScan.routeName); - if (barcode == null) { - return; - } - if (barcode.isEmpty) { - if (!context.mounted) return; - showFlushbar( - context, - message: texts.payment_info_dialog_error_qrcode, - ); - return; - } - setState(() { - _paymentInfoController.text = barcode; - _scannerErrorMessage = ""; - _setValidatorErrorMessage(""); - }); - } - - Future _validateInput(String input) async { - final texts = context.texts(); - try { - _setValidatorErrorMessage(""); - final inputCubit = context.read(); - final inputType = await inputCubit.parseInput(input: input); - _log.info("Parsed input type: '${inputType.runtimeType.toString()}"); - // Can't compare against a list of InputType as runtime type comparison is a bit tricky with binding generated enums - if (!(inputType is InputType_Bolt11 || - inputType is InputType_LnUrlPay || - inputType is InputType_LnUrlWithdraw || - inputType is InputType_LnUrlAuth || - inputType is InputType_LnUrlError)) { - _setValidatorErrorMessage(texts.payment_info_dialog_error_unsupported_input); - } - } catch (e) { - rethrow; - } - } - - _setValidatorErrorMessage(String errorMessage) { - setState(() { - _validatorErrorMessage = errorMessage; - }); - _formKey.currentState?.validate(); - } - - void _setLoading(bool visible) { - if (visible && _loaderRoute == null) { - _loaderRoute = createLoaderRoute(context); - Navigator.of(context).push(_loaderRoute!); - return; - } - - if (!visible && (_loaderRoute != null && _loaderRoute!.isActive)) { - _loaderRoute!.navigator?.removeRoute(_loaderRoute!); - _loaderRoute = null; - } - } -} diff --git a/lib/routes/home/widgets/bottom_actions_bar/send_options_bottom_sheet.dart b/lib/routes/home/widgets/bottom_actions_bar/send_options_bottom_sheet.dart index 8cf79d6a..b543a25f 100644 --- a/lib/routes/home/widgets/bottom_actions_bar/send_options_bottom_sheet.dart +++ b/lib/routes/home/widgets/bottom_actions_bar/send_options_bottom_sheet.dart @@ -4,7 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:l_breez/cubit/account/account_cubit.dart'; import 'package:l_breez/routes/chainswap/send/send_chainswap_page.dart'; import 'package:l_breez/routes/home/widgets/bottom_actions_bar/bottom_action_item_image.dart'; -import 'package:l_breez/routes/home/widgets/bottom_actions_bar/enter_payment_info_dialog.dart'; +import 'package:l_breez/routes/send_payment/enter_payment_info/enter_payment_info_page.dart'; import 'package:l_breez/theme/theme.dart'; class SendOptionsBottomSheet extends StatefulWidget { @@ -38,7 +38,11 @@ class _SendOptionsBottomSheetState extends State { texts.bottom_action_bar_paste_invoice, style: bottomSheetTextStyle, ), - onTap: () => _showEnterPaymentInfoDialog(context, widget.firstPaymentItemKey), + onTap: () { + final navigatorState = Navigator.of(context); + navigatorState.pop(); + navigatorState.pushNamed(EnterPaymentInfoPage.routeName); + }, ), Divider( height: 0.0, @@ -66,19 +70,4 @@ class _SendOptionsBottomSheetState extends State { }, ); } - - Future _showEnterPaymentInfoDialog( - BuildContext context, - GlobalKey> firstPaymentItemKey, - ) async { - Navigator.of(context).pop(); - await showDialog( - useRootNavigator: false, - context: context, - barrierDismissible: false, - builder: (_) => EnterPaymentInfoDialog( - paymentItemKey: firstPaymentItemKey, - ), - ); - } } diff --git a/lib/routes/send_payment/enter_payment_info/enter_payment_info_dialog.dart b/lib/routes/send_payment/enter_payment_info/enter_payment_info_dialog.dart new file mode 100644 index 00000000..d75c980a --- /dev/null +++ b/lib/routes/send_payment/enter_payment_info/enter_payment_info_dialog.dart @@ -0,0 +1,185 @@ +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/qr_scan/qr_scan.dart'; +import 'package:l_breez/theme/theme.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'; +import 'package:logging/logging.dart'; + +final _log = Logger("EnterPaymentInfoPage"); + +class EnterPaymentInfoPage extends StatefulWidget { + static const routeName = "/enter_payment_info"; + const EnterPaymentInfoPage({super.key}); + + @override + State createState() => _EnterPaymentInfoPageState(); +} + +class _EnterPaymentInfoPageState extends State { + final _formKey = GlobalKey(); + final _paymentInfoController = TextEditingController(); + + String errorMessage = ""; + ModalRoute? _loaderRoute; + + @override + Widget build(BuildContext context) { + final texts = context.texts(); + final themeData = Theme.of(context); + + return Scaffold( + appBar: AppBar( + leading: const back_button.BackButton(), + title: Text(texts.payment_info_dialog_title), + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: SingleChildScrollView( + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + controller: _paymentInfoController, + decoration: InputDecoration( + labelText: texts.payment_info_dialog_hint, + suffixIcon: IconButton( + padding: const EdgeInsets.only(top: 21.0), + alignment: Alignment.bottomRight, + icon: Image( + image: const AssetImage("assets/icons/qr_scan.png"), + color: themeData.primaryIconTheme.color, + width: 24.0, + height: 24.0, + ), + tooltip: texts.payment_info_dialog_barcode, + onPressed: () => _scanBarcode(), + ), + ), + style: TextStyle(color: themeData.primaryTextTheme.headlineMedium!.color), + validator: (value) => errorMessage.isNotEmpty ? errorMessage : null, + onFieldSubmitted: (input) async { + if (input.isNotEmpty) { + setState(() { + _paymentInfoController.text = input; + }); + await _validateInput(); + } + }, + ), + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + texts.payment_info_dialog_hint_expanded, + style: FieldTextStyle.labelStyle.copyWith( + fontSize: 13.0, + color: themeData.isLightTheme ? BreezColors.grey[500] : BreezColors.white[200], + ), + ), + ), + ], + ), + ), + ), + ), + bottomNavigationBar: SingleButtonBottomBar( + text: _paymentInfoController.text.isNotEmpty && errorMessage.isEmpty + ? texts.payment_info_dialog_action_approve + : texts.payment_info_dialog_action_cancel, + onPressed: _paymentInfoController.text.isNotEmpty && errorMessage.isEmpty + ? _onApprovePressed + : () => Navigator.pop(context), + ), + ); + } + + void _scanBarcode() { + final texts = context.texts(); + + Focus.maybeOf(context)?.unfocus(); + Navigator.pushNamed(context, QRScan.routeName).then((barcode) async { + if (barcode == null || barcode.isEmpty) { + if (context.mounted) showFlushbar(context, message: texts.payment_info_dialog_error_qrcode); + return; + } + setState(() { + _paymentInfoController.text = barcode; + }); + await _validateInput(); + }); + } + + Future _validateInput() async { + final texts = context.texts(); + final inputCubit = context.read(); + var errMsg = ""; + setState(() { + errorMessage = errMsg; + }); + try { + final inputType = await inputCubit.parseInput(input: _paymentInfoController.text); + if (!(inputType is InputType_Bolt11 || + inputType is InputType_LnUrlPay || + inputType is InputType_LnUrlWithdraw)) { + errMsg = texts.payment_info_dialog_error_unsupported_input; + } + if (inputType is InputType_Bolt11 && inputType.invoice.amountMsat == BigInt.zero) { + errMsg = texts.payment_request_zero_amount_not_supported; + } + if (inputType is InputType_BitcoinAddress) { + errMsg = "Please use \"Send to BTC Address\" option from main menu."; + } + } catch (error) { + var errStr = error.toString(); + errMsg = errStr.contains("Unrecognized") ? texts.payment_info_dialog_error_unsupported_input : errStr; + } finally { + setState(() { + errorMessage = errMsg; + }); + } + + _formKey.currentState?.validate(); + } + + Future _onApprovePressed() async { + final inputCubit = context.read(); + _setLoading(true); + + try { + await _validateInput(); + if (_formKey.currentState!.validate()) { + if (mounted) { + Navigator.of(context).pop(); + } + inputCubit.addIncomingInput(_paymentInfoController.text, InputSource.inputField); + } + } catch (error) { + _log.warning(error.toString(), error); + if (mounted) { + setState(() { + errorMessage = context.texts().payment_info_dialog_error; + }); + } + } finally { + _setLoading(false); + } + } + + void _setLoading(bool visible) { + if (visible && _loaderRoute == null) { + _loaderRoute = createLoaderRoute(context); + Navigator.of(context).push(_loaderRoute!); + } else if (!visible && _loaderRoute?.isActive == true) { + _loaderRoute?.navigator?.removeRoute(_loaderRoute!); + _loaderRoute = null; + } + } +}