From 9e18303198d853848c7638d6839dc249cc0e2697 Mon Sep 17 00:00:00 2001 From: Erdem Yerebasmaz Date: Fri, 12 Jul 2024 18:40:49 +0300 Subject: [PATCH] Integrate Send/Receive Chain Swap (#46, #47) * Update dependencies to latest * Create ChainSwap Bloc * Apply BigInt/int changes on payment errors * Integrate Receive Chain Swap * Integrate Send Chain Swap --- ios/Podfile.lock | 57 ++--- lib/bloc/account/payment_error.dart | 8 +- lib/bloc/chainswap/chainswap_bloc.dart | 63 +++++ lib/bloc/chainswap/chainswap_state.dart | 5 + lib/main.dart | 4 + .../receive/chainswap_qr_dialog.dart | 189 +++++++++++++++ .../receive/receive_chainswap_page.dart | 225 ++++++++++++++++++ .../send/fee/fee_breakdown/fee_breakdown.dart | 33 +++ .../boltz_service_fee.dart | 37 +++ .../recipient_amount.dart | 49 ++++ .../fee_breakdown_widgets/sender_amount.dart | 50 ++++ .../transaction_fee.dart | 37 +++ lib/routes/chainswap/send/fee/fee_option.dart | 7 + .../chainswap/send/send_chainswap_button.dart | 56 +++++ .../send_chainswap_confirmation_page.dart | 125 ++++++++++ .../chainswap/send/send_chainswap_form.dart | 121 ++++++++++ .../send/send_chainswap_form_page.dart | 123 ++++++++++ .../chainswap/send/send_chainswap_page.dart | 87 +++++++ .../chainswap/send/validator_holder.dart | 11 + .../bitcoin_address_text_form_field.dart | 85 +++++++ .../send/widgets/chainswap_available_btc.dart | 42 ++++ ...withdraw_funds_amount_text_form_field.dart | 43 ++++ .../chainswap/send/withdraw_funds_model.dart | 21 ++ .../widgets/compact_qr_image.dart | 5 +- .../create_invoice/widgets/invoice_qr.dart | 4 +- .../receive_options_bottom_sheet.dart | 15 ++ .../send_options_bottom_sheet.dart | 15 ++ lib/user_app.dart | 15 ++ lib/utils/payment_validator.dart | 8 +- pubspec.lock | 52 ++-- pubspec.yaml | 6 +- 31 files changed, 1530 insertions(+), 68 deletions(-) create mode 100644 lib/bloc/chainswap/chainswap_bloc.dart create mode 100644 lib/bloc/chainswap/chainswap_state.dart create mode 100644 lib/routes/chainswap/receive/chainswap_qr_dialog.dart create mode 100644 lib/routes/chainswap/receive/receive_chainswap_page.dart create mode 100644 lib/routes/chainswap/send/fee/fee_breakdown/fee_breakdown.dart create mode 100644 lib/routes/chainswap/send/fee/fee_breakdown/widgets/fee_breakdown_widgets/boltz_service_fee.dart create mode 100644 lib/routes/chainswap/send/fee/fee_breakdown/widgets/fee_breakdown_widgets/recipient_amount.dart create mode 100644 lib/routes/chainswap/send/fee/fee_breakdown/widgets/fee_breakdown_widgets/sender_amount.dart create mode 100644 lib/routes/chainswap/send/fee/fee_breakdown/widgets/fee_breakdown_widgets/transaction_fee.dart create mode 100644 lib/routes/chainswap/send/fee/fee_option.dart create mode 100644 lib/routes/chainswap/send/send_chainswap_button.dart create mode 100644 lib/routes/chainswap/send/send_chainswap_confirmation_page.dart create mode 100644 lib/routes/chainswap/send/send_chainswap_form.dart create mode 100644 lib/routes/chainswap/send/send_chainswap_form_page.dart create mode 100644 lib/routes/chainswap/send/send_chainswap_page.dart create mode 100644 lib/routes/chainswap/send/validator_holder.dart create mode 100644 lib/routes/chainswap/send/widgets/bitcoin_address_text_form_field.dart create mode 100644 lib/routes/chainswap/send/widgets/chainswap_available_btc.dart create mode 100644 lib/routes/chainswap/send/widgets/withdraw_funds_amount_text_form_field.dart create mode 100644 lib/routes/chainswap/send/withdraw_funds_model.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6f6e1890..26268206 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -8,39 +8,39 @@ PODS: - FlutterMacOS - device_info_plus (0.0.1): - Flutter - - Firebase/CoreOnly (10.27.0): - - FirebaseCore (= 10.27.0) - - Firebase/DynamicLinks (10.27.0): + - Firebase/CoreOnly (10.28.0): + - FirebaseCore (= 10.28.0) + - Firebase/DynamicLinks (10.28.0): - Firebase/CoreOnly - - FirebaseDynamicLinks (~> 10.27.0) - - Firebase/Messaging (10.27.0): + - FirebaseDynamicLinks (~> 10.28.0) + - Firebase/Messaging (10.28.0): - Firebase/CoreOnly - - FirebaseMessaging (~> 10.27.0) - - firebase_core (3.1.1): - - Firebase/CoreOnly (= 10.27.0) + - FirebaseMessaging (~> 10.28.0) + - firebase_core (3.2.0): + - Firebase/CoreOnly (= 10.28.0) - Flutter - - firebase_dynamic_links (6.0.2): - - Firebase/DynamicLinks (= 10.27.0) + - firebase_dynamic_links (6.0.3): + - Firebase/DynamicLinks (= 10.28.0) - firebase_core - Flutter - - firebase_messaging (15.0.2): - - Firebase/Messaging (= 10.27.0) + - firebase_messaging (15.0.3): + - Firebase/Messaging (= 10.28.0) - firebase_core - Flutter - - FirebaseCore (10.27.0): + - FirebaseCore (10.28.0): - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.12) - GoogleUtilities/Logger (~> 7.12) - - FirebaseCoreInternal (10.28.0): + - FirebaseCoreInternal (10.29.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseDynamicLinks (10.27.0): + - FirebaseDynamicLinks (10.28.0): - FirebaseCore (~> 10.0) - - FirebaseInstallations (10.28.0): + - FirebaseInstallations (10.29.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) - PromisesObjC (~> 2.1) - - FirebaseMessaging (10.27.0): + - FirebaseMessaging (10.28.0): - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - GoogleDataTransport (~> 9.3) @@ -50,7 +50,8 @@ PODS: - GoogleUtilities/UserDefaults (~> 7.8) - nanopb (< 2.30911.0, >= 2.30908.0) - Flutter (1.0.0) - - flutter_breez_liquid (0.1.0) + - flutter_breez_liquid (0.1.0): + - Flutter - flutter_fgbg (0.0.1): - Flutter - flutter_inappwebview_ios (0.0.1): @@ -289,17 +290,17 @@ SPEC CHECKSUMS: clipboard_watcher: 86fb70421aca6f4944e0591a8292605da7784666 connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d - Firebase: 26b040b20866a55f55eb3611b9fcf3ae64816b86 - firebase_core: f8d0424c45e0f1e596811085fc12c638d628457c - firebase_dynamic_links: 8a142e279cd3d683bbb8d501d0ff5521d72a8616 - firebase_messaging: 8b29edaf5adfd3b52b5bfa5af8128c44164670c6 - FirebaseCore: a2b95ae4ce7c83ceecfbbbe3b6f1cddc7415a808 - FirebaseCoreInternal: 58d07f1362fddeb0feb6a857d1d1d1c5e558e698 - FirebaseDynamicLinks: 087bf18ffabe8b6abe0c80301fd16ba2225ce373 - FirebaseInstallations: 60c1d3bc1beef809fd1ad1189a8057a040c59f2e - FirebaseMessaging: 585984d0a1df120617eb10b44cad8968b859815e + Firebase: 5121c624121af81cbc81df3bda414b3c28c4f3c3 + firebase_core: a9d0180d5285527884d07a41eb4a9ec9ed12cdb6 + firebase_dynamic_links: ef37ec989592ed84f0bbcf2d2f938d4407bee1c2 + firebase_messaging: ccc82a143a74de75f440a4e413dbbb37ec3fddbc + FirebaseCore: 857dc1c6dd1255675047404d8466f7dfaac5d779 + FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934 + FirebaseDynamicLinks: f6a65ece086df7d8366400b8bb99e50dd2659ad4 + FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd + FirebaseMessaging: 087a7c7cadef7b9239f005bc4db823894844f323 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_breez_liquid: b61267976647a385b0446e628fe69421153513f7 + flutter_breez_liquid: 707cf69d42f3efbe37836a6fc0d1e7ea7d400e4b flutter_fgbg: 31c0d1140a131daea2d342121808f6aa0dcd879d flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 diff --git a/lib/bloc/account/payment_error.dart b/lib/bloc/account/payment_error.dart index 349ef036..844fd386 100644 --- a/lib/bloc/account/payment_error.dart +++ b/lib/bloc/account/payment_error.dart @@ -1,5 +1,5 @@ class PaymentExceededLimitError implements Exception { - final int limitSat; + final BigInt limitSat; const PaymentExceededLimitError( this.limitSat, @@ -7,7 +7,7 @@ class PaymentExceededLimitError implements Exception { } class PaymentBelowLimitError implements Exception { - final int limitSat; + final BigInt limitSat; const PaymentBelowLimitError( this.limitSat, @@ -35,7 +35,7 @@ class PaymentBelowSetupFeesError implements Exception { } class PaymentExceedLiquidityError implements Exception { - final int limitSat; + final BigInt limitSat; const PaymentExceedLiquidityError( this.limitSat, @@ -43,7 +43,7 @@ class PaymentExceedLiquidityError implements Exception { } class PaymentExcededLiqudityChannelCreationNotPossibleError implements Exception { - final int limitSat; + final BigInt limitSat; const PaymentExcededLiqudityChannelCreationNotPossibleError( this.limitSat, diff --git a/lib/bloc/chainswap/chainswap_bloc.dart b/lib/bloc/chainswap/chainswap_bloc.dart new file mode 100644 index 00000000..dedbdac0 --- /dev/null +++ b/lib/bloc/chainswap/chainswap_bloc.dart @@ -0,0 +1,63 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_breez_liquid/flutter_breez_liquid.dart'; +import 'package:l_breez/bloc/account/breez_sdk_liquid.dart'; +import 'package:l_breez/bloc/account/payment_error.dart'; +import 'package:l_breez/bloc/chainswap/chainswap_state.dart'; + +class ChainSwapBloc extends Cubit { + final BreezSDKLiquid _liquidSdk; + + ChainSwapBloc(this._liquidSdk) : super(ChainSwapState.initial()); + + Future fetchOnchainLimits() async { + return await _liquidSdk.instance!.fetchOnchainLimits(); + } + + Future preparePayOnchain({ + required PreparePayOnchainRequest req, + }) async { + return await _liquidSdk.instance!.preparePayOnchain(req: req); + } + + Future payOnchain({ + required PayOnchainRequest req, + }) async { + return await _liquidSdk.instance!.payOnchain(req: req); + } + + Future prepareReceiveOnchain({ + required PrepareReceiveOnchainRequest req, + }) async { + return await _liquidSdk.instance!.prepareReceiveOnchain(req: req); + } + + Future receiveOnchain({ + required PrepareReceiveOnchainResponse req, + }) async { + return await _liquidSdk.instance!.receiveOnchain(req: req); + } + + Future refund({ + required RefundRequest req, + }) async { + return await _liquidSdk.instance!.refund(req: req); + } + + Future rescanOnchainSwaps() async { + return await _liquidSdk.instance!.rescanOnchainSwaps(); + } + + void validateSwap( + BigInt amount, + bool outgoing, + OnchainPaymentLimitsResponse onchainLimits, + ) { + var limits = outgoing ? onchainLimits.send : onchainLimits.receive; + if (amount > limits.maxSat) { + throw PaymentExceededLimitError(limits.maxSat); + } + if (amount < limits.minSat) { + throw PaymentBelowLimitError(limits.minSat); + } + } +} diff --git a/lib/bloc/chainswap/chainswap_state.dart b/lib/bloc/chainswap/chainswap_state.dart new file mode 100644 index 00000000..9adbf0f1 --- /dev/null +++ b/lib/bloc/chainswap/chainswap_state.dart @@ -0,0 +1,5 @@ +class ChainSwapState { + ChainSwapState(); + + ChainSwapState.initial() : this(); +} diff --git a/lib/main.dart b/lib/main.dart index d5e3e7c1..234c5967 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,6 +12,7 @@ import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:l_breez/bloc/account/account_bloc.dart'; import 'package:l_breez/bloc/account/credentials_manager.dart'; import 'package:l_breez/bloc/backup/backup_bloc.dart'; +import 'package:l_breez/bloc/chainswap/chainswap_bloc.dart'; import 'package:l_breez/bloc/currency/currency_bloc.dart'; import 'package:l_breez/bloc/input/input_bloc.dart'; import 'package:l_breez/bloc/lnurl/lnurl_bloc.dart'; @@ -90,6 +91,9 @@ void main() async { BlocProvider( create: (BuildContext context) => LnUrlBloc(injector.liquidSDK), ), + BlocProvider( + create: (BuildContext context) => ChainSwapBloc(injector.liquidSDK), + ), ], child: UserApp(), ), diff --git a/lib/routes/chainswap/receive/chainswap_qr_dialog.dart b/lib/routes/chainswap/receive/chainswap_qr_dialog.dart new file mode 100644 index 00000000..7e7181d4 --- /dev/null +++ b/lib/routes/chainswap/receive/chainswap_qr_dialog.dart @@ -0,0 +1,189 @@ +// ignore_for_file: use_key_in_widget_constructors + +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/bloc/input/input_bloc.dart'; +import 'package:l_breez/bloc/input/input_state.dart'; +import 'package:l_breez/routes/create_invoice/widgets/expiry_and_fee_message.dart'; +import 'package:l_breez/routes/create_invoice/widgets/invoice_qr.dart'; +import 'package:l_breez/routes/create_invoice/widgets/loading_or_error.dart'; +import 'package:l_breez/services/injector.dart'; +import 'package:l_breez/utils/exceptions.dart'; +import 'package:l_breez/widgets/flushbar.dart'; +import 'package:logging/logging.dart'; +import 'package:share_plus/share_plus.dart'; + +final _log = Logger("QrCodeDialog"); + +class ChainSwapQrDialog extends StatefulWidget { + final PrepareReceiveOnchainResponse prepareReceiveOnchainResponse; + final ReceiveOnchainResponse? receiveOnchainResponse; + final Object? error; + final Function(dynamic result) _onFinish; + + const ChainSwapQrDialog( + this.prepareReceiveOnchainResponse, + this.receiveOnchainResponse, + this.error, + this._onFinish, + ); + + @override + State createState() { + return ChainSwapQrDialogState(); + } +} + +class ChainSwapQrDialogState extends State with SingleTickerProviderStateMixin { + Animation? _opacityAnimation; + ModalRoute? _currentRoute; + AnimationController? _controller; + + @override + void initState() { + _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 1000)); + _opacityAnimation = Tween(begin: 0.0, end: 1.0) + .animate(CurvedAnimation(parent: _controller!, curve: Curves.ease)); + _controller!.value = 1.0; + _controller!.addStatusListener((status) async { + if (status == AnimationStatus.dismissed && mounted) { + onFinish(true); + } + }); + super.initState(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _currentRoute ??= ModalRoute.of(context); + } + + @override + void didUpdateWidget(covariant ChainSwapQrDialog oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.receiveOnchainResponse?.address != oldWidget.receiveOnchainResponse?.address) { + context.read().trackPayment(widget.receiveOnchainResponse!.bip21).then((value) { + Timer(const Duration(milliseconds: 1000), () { + if (mounted) { + _controller!.reverse(); + } + }); + }).catchError((e) { + _log.warning("Failed to track payment", e); + showFlushbar(context, message: extractExceptionMessage(e, context.texts())); + onFinish(false); + }); + } + } + + @override + Widget build(BuildContext context) { + final texts = context.texts(); + final themeData = Theme.of(context); + final error = widget.error; + + return BlocBuilder( + builder: (context, inputState) { + return FadeTransition( + opacity: _opacityAnimation!, + child: SimpleDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(texts.withdraw_funds_btc_address), + Row( + children: [ + Tooltip( + message: texts.qr_code_dialog_share, + child: IconButton( + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + padding: const EdgeInsets.only(top: 8.0, bottom: 8.0, right: 2.0, left: 14.0), + icon: const Icon(IconData(0xe917, fontFamily: 'icomoon')), + color: themeData.primaryTextTheme.labelLarge!.color!, + onPressed: () { + Share.share(widget.receiveOnchainResponse!.bip21); + }, + ), + ), + Tooltip( + message: texts.qr_code_dialog_copy, + child: IconButton( + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + padding: const EdgeInsets.only(top: 8.0, bottom: 8.0, right: 14.0, left: 2.0), + icon: const Icon(IconData(0xe90b, fontFamily: 'icomoon')), + color: themeData.primaryTextTheme.labelLarge!.color!, + onPressed: () { + ServiceInjector().device.setClipboardText(widget.receiveOnchainResponse!.bip21); + showFlushbar( + context, + message: texts.qr_code_dialog_copied, + duration: const Duration(seconds: 3), + ); + }, + ), + ) + ], + ) + ], + ), + titlePadding: const EdgeInsets.fromLTRB(20.0, 22.0, 0.0, 8.0), + contentPadding: const EdgeInsets.only(left: 0.0, right: 0.0, bottom: 20.0), + children: [ + AnimatedCrossFade( + firstChild: LoadingOrError( + error: error, + displayErrorMessage: error != null + ? extractExceptionMessage(error, texts) + : texts.qr_code_dialog_warning_message_error, + ), + secondChild: widget.receiveOnchainResponse == null + ? const SizedBox() + : Column( + children: [ + InvoiceQR(bolt11: widget.receiveOnchainResponse!.bip21, bip21: true), + const Padding(padding: EdgeInsets.only(top: 16.0)), + SizedBox( + width: MediaQuery.of(context).size.width, + child: ExpiryAndFeeMessage( + feesSat: widget.prepareReceiveOnchainResponse.feesSat.toInt(), + ), + ), + const Padding(padding: EdgeInsets.only(top: 16.0)), + ], + ), + duration: const Duration(seconds: 1), + crossFadeState: widget.receiveOnchainResponse == null + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + ), + TextButton( + onPressed: (() { + onFinish(false); + }), + child: Text( + texts.qr_code_dialog_action_close, + style: themeData.primaryTextTheme.labelLarge, + ), + ), + ], + ), + ); + }, + ); + } + + void onFinish(dynamic result) { + _log.info("onFinish $result, mounted: $mounted, _currentRoute: ${_currentRoute?.isCurrent}"); + if (mounted && _currentRoute != null && _currentRoute!.isCurrent) { + Navigator.removeRoute(context, _currentRoute!); + } + widget._onFinish(result); + } +} diff --git a/lib/routes/chainswap/receive/receive_chainswap_page.dart b/lib/routes/chainswap/receive/receive_chainswap_page.dart new file mode 100644 index 00000000..47a98450 --- /dev/null +++ b/lib/routes/chainswap/receive/receive_chainswap_page.dart @@ -0,0 +1,225 @@ +import 'package:breez_translations/breez_translations_locales.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/bloc/chainswap/chainswap_bloc.dart'; +import 'package:l_breez/bloc/currency/currency_bloc.dart'; +import 'package:l_breez/bloc/currency/currency_state.dart'; +import 'package:l_breez/routes/chainswap/receive/chainswap_qr_dialog.dart'; +import 'package:l_breez/routes/create_invoice/widgets/successful_payment.dart'; +import 'package:l_breez/theme/theme_provider.dart' as theme; +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/flushbar.dart'; +import 'package:l_breez/widgets/keyboard_done_action.dart'; +import 'package:l_breez/widgets/loader.dart'; +import 'package:l_breez/widgets/single_button_bottom_bar.dart'; +import 'package:l_breez/widgets/transparent_page_route.dart'; + +class ReceiveChainSwapPage extends StatefulWidget { + const ReceiveChainSwapPage({super.key}); + + @override + State createState() => _ReceiveChainSwapPageState(); +} + +class _ReceiveChainSwapPageState extends State { + final _formKey = GlobalKey(); + final _scaffoldKey = GlobalKey(); + + final _descriptionController = TextEditingController(); + final _amountController = TextEditingController(); + final _amountFocusNode = FocusNode(); + var _doneAction = KeyboardDoneAction(); + + Future? _onchainPaymentLimitsFuture; + late OnchainPaymentLimitsResponse _onchainPaymentLimits; + @override + void initState() { + super.initState(); + _doneAction = KeyboardDoneAction(focusNodes: [_amountFocusNode]); + _fetchOnchainLimits(); + } + + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + _fetchOnchainLimits(); + } + } + + Future _fetchOnchainLimits() async { + final chainSwapBloc = context.read(); + setState(() { + _onchainPaymentLimitsFuture = chainSwapBloc.fetchOnchainLimits(); + }); + } + + @override + void dispose() { + _doneAction.dispose(); + super.dispose(); + } + + @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.bottom_action_bar_receive_btc_address), + ), + body: FutureBuilder( + future: _onchainPaymentLimitsFuture, + builder: (BuildContext context, AsyncSnapshot 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( + extractExceptionMessage(snapshot.error!, texts), + ), + textAlign: TextAlign.center, + ), + ), + ); + } + if (snapshot.connectionState != ConnectionState.done && !snapshot.hasData) { + return Center( + child: Loader( + color: themeData.primaryColor.withOpacity(0.5), + ), + ); + } + + _onchainPaymentLimits = snapshot.data!; + + return Form( + key: _formKey, + child: Padding( + padding: const EdgeInsets.fromLTRB(16.0, 0, 16.0, 40.0), + child: Scrollbar( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + controller: _descriptionController, + keyboardType: TextInputType.multiline, + textInputAction: TextInputAction.done, + maxLines: null, + maxLength: 90, + maxLengthEnforcement: MaxLengthEnforcement.enforced, + decoration: InputDecoration( + labelText: texts.invoice_description_label, + ), + style: theme.FieldTextStyle.textStyle, + ), + BlocBuilder( + builder: (context, currencyState) { + return AmountFormField( + context: context, + texts: texts, + bitcoinCurrency: currencyState.bitcoinCurrency, + focusNode: _amountFocusNode, + controller: _amountController, + validatorFn: (v) => validatePayment(v), + style: theme.FieldTextStyle.textStyle, + ); + }, + ), + ], + ), + ), + ), + ), + ); + }, + ), + bottomNavigationBar: SingleButtonBottomBar( + stickToBottom: true, + text: texts.invoice_action_create, + onPressed: () { + if (_formKey.currentState?.validate() ?? false) { + _createSwap(); + } + }, + ), + ); + } + + Future _createSwap() async { + final navigator = Navigator.of(context); + final currentRoute = ModalRoute.of(navigator.context)!; + final chainSwapBloc = context.read(); + final currencyBloc = context.read(); + + final amountMsat = currencyBloc.state.bitcoinCurrency.parse(_amountController.text); + final prepareReceiveOnchainRequest = + PrepareReceiveOnchainRequest(payerAmountSat: BigInt.from(amountMsat)); + final prepareReceiveOnchainResponse = + await chainSwapBloc.prepareReceiveOnchain(req: prepareReceiveOnchainRequest); + final receiveOnchainResponse = chainSwapBloc.receiveOnchain(req: prepareReceiveOnchainResponse); + + navigator.pop(); + Widget dialog = FutureBuilder( + future: receiveOnchainResponse, + builder: (BuildContext context, AsyncSnapshot snapshot) { + return ChainSwapQrDialog( + prepareReceiveOnchainResponse, + snapshot.data, + snapshot.error, + (result) { + onPaymentFinished(result, currentRoute, navigator); + }, + ); + }, + ); + + return showDialog( + useRootNavigator: false, + // ignore: use_build_context_synchronously + context: context, + barrierDismissible: false, + builder: (_) => dialog, + ); + } + + void onPaymentFinished( + dynamic result, + ModalRoute currentRoute, + NavigatorState navigator, + ) { + if (result == true) { + if (currentRoute.isCurrent) { + navigator.push( + TransparentPageRoute((ctx) => const SuccessfulPaymentRoute()), + ); + } + } else { + if (result is String) { + showFlushbar(context, title: "", message: result); + } + } + } + + String? validatePayment(int amount) { + return PaymentValidator( + validatePayment: _validateSwap, + currency: context.read().state.bitcoinCurrency, + texts: context.texts(), + ).validateIncoming(amount); + } + + void _validateSwap(int amount, bool outgoing) { + final chainSwapBloc = context.read(); + return chainSwapBloc.validateSwap(BigInt.from(amount), outgoing, _onchainPaymentLimits); + } +} diff --git a/lib/routes/chainswap/send/fee/fee_breakdown/fee_breakdown.dart b/lib/routes/chainswap/send/fee/fee_breakdown/fee_breakdown.dart new file mode 100644 index 00000000..5c020969 --- /dev/null +++ b/lib/routes/chainswap/send/fee/fee_breakdown/fee_breakdown.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_breez_liquid/flutter_breez_liquid.dart'; +import 'package:l_breez/routes/chainswap/send/fee/fee_breakdown/widgets/fee_breakdown_widgets/recipient_amount.dart'; +import 'package:l_breez/routes/chainswap/send/fee/fee_breakdown/widgets/fee_breakdown_widgets/sender_amount.dart'; +import 'package:l_breez/routes/chainswap/send/fee/fee_breakdown/widgets/fee_breakdown_widgets/transaction_fee.dart'; + +class FeeBreakdown extends StatelessWidget { + final PreparePayOnchainResponse feeOption; + + const FeeBreakdown({required this.feeOption, super.key}); + + @override + Widget build(BuildContext context) { + final themeData = Theme.of(context); + + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(5.0)), + border: Border.all( + color: themeData.colorScheme.onSurface.withOpacity(0.4), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SenderAmount(amountSat: (feeOption.receiverAmountSat + feeOption.feesSat).toInt()), + TransactionFee(txFeeSat: feeOption.feesSat.toInt()), + RecipientAmount(amountSat: feeOption.receiverAmountSat.toInt()) + ], + ), + ); + } +} diff --git a/lib/routes/chainswap/send/fee/fee_breakdown/widgets/fee_breakdown_widgets/boltz_service_fee.dart b/lib/routes/chainswap/send/fee/fee_breakdown/widgets/fee_breakdown_widgets/boltz_service_fee.dart new file mode 100644 index 00000000..686150d2 --- /dev/null +++ b/lib/routes/chainswap/send/fee/fee_breakdown/widgets/fee_breakdown_widgets/boltz_service_fee.dart @@ -0,0 +1,37 @@ +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/models/currency.dart'; +import 'package:l_breez/utils/min_font_size.dart'; + +class BoltzServiceFee extends StatelessWidget { + final int boltzServiceFee; + + const BoltzServiceFee({required this.boltzServiceFee, super.key}); + + @override + Widget build(BuildContext context) { + final texts = context.texts(); + final themeData = Theme.of(context); + final minFont = MinFontSize(context); + + return ListTile( + title: AutoSizeText( + texts.reverse_swap_confirmation_boltz_fee, + style: TextStyle(color: Colors.white.withOpacity(0.4)), + maxLines: 1, + minFontSize: minFont.minFontSize, + stepGranularity: 0.1, + ), + trailing: AutoSizeText( + texts.reverse_swap_confirmation_boltz_fee_value( + BitcoinCurrency.SAT.format(boltzServiceFee), + ), + style: TextStyle(color: themeData.colorScheme.error.withOpacity(0.4)), + maxLines: 1, + minFontSize: minFont.minFontSize, + stepGranularity: 0.1, + ), + ); + } +} diff --git a/lib/routes/chainswap/send/fee/fee_breakdown/widgets/fee_breakdown_widgets/recipient_amount.dart b/lib/routes/chainswap/send/fee/fee_breakdown/widgets/fee_breakdown_widgets/recipient_amount.dart new file mode 100644 index 00000000..2568ff15 --- /dev/null +++ b/lib/routes/chainswap/send/fee/fee_breakdown/widgets/fee_breakdown_widgets/recipient_amount.dart @@ -0,0 +1,49 @@ +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/bloc/currency/currency_bloc.dart'; +import 'package:l_breez/bloc/currency/currency_state.dart'; +import 'package:l_breez/models/currency.dart'; +import 'package:l_breez/utils/min_font_size.dart'; + +class RecipientAmount extends StatelessWidget { + final int amountSat; + + const RecipientAmount({required this.amountSat, super.key}); + + @override + Widget build(BuildContext context) { + final texts = context.texts(); + final themeData = Theme.of(context); + final minFont = MinFontSize(context); + + return ListTile( + title: AutoSizeText( + texts.sweep_all_coins_label_receive, + style: const TextStyle(color: Colors.white), + maxLines: 1, + minFontSize: minFont.minFontSize, + stepGranularity: 0.1, + ), + trailing: BlocBuilder(builder: (context, currency) { + final fiatConversion = currency.fiatConversion(); + + return AutoSizeText( + fiatConversion == null + ? texts.sweep_all_coins_amount_no_fiat( + BitcoinCurrency.SAT.format(amountSat), + ) + : texts.sweep_all_coins_amount_with_fiat( + BitcoinCurrency.SAT.format(amountSat), + fiatConversion.format(amountSat), + ), + style: TextStyle(color: themeData.colorScheme.error), + maxLines: 1, + minFontSize: minFont.minFontSize, + stepGranularity: 0.1, + ); + }), + ); + } +} diff --git a/lib/routes/chainswap/send/fee/fee_breakdown/widgets/fee_breakdown_widgets/sender_amount.dart b/lib/routes/chainswap/send/fee/fee_breakdown/widgets/fee_breakdown_widgets/sender_amount.dart new file mode 100644 index 00000000..b9d17ca8 --- /dev/null +++ b/lib/routes/chainswap/send/fee/fee_breakdown/widgets/fee_breakdown_widgets/sender_amount.dart @@ -0,0 +1,50 @@ +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/bloc/currency/currency_bloc.dart'; +import 'package:l_breez/bloc/currency/currency_state.dart'; +import 'package:l_breez/models/currency.dart'; +import 'package:l_breez/utils/min_font_size.dart'; + +class SenderAmount extends StatelessWidget { + final int amountSat; + + const SenderAmount({required this.amountSat, super.key}); + + @override + Widget build(BuildContext context) { + final texts = context.texts(); + final themeData = Theme.of(context); + final minFont = MinFontSize(context); + + return ListTile( + title: AutoSizeText( + texts.sweep_all_coins_label_send, + maxLines: 1, + minFontSize: minFont.minFontSize, + stepGranularity: 0.1, + ), + trailing: BlocBuilder( + builder: (context, currency) { + final fiatConversion = currency.fiatConversion(); + + return AutoSizeText( + fiatConversion == null + ? texts.sweep_all_coins_amount_no_fiat( + BitcoinCurrency.SAT.format(amountSat), + ) + : texts.sweep_all_coins_amount_with_fiat( + BitcoinCurrency.SAT.format(amountSat), + fiatConversion.format(amountSat), + ), + style: TextStyle(color: themeData.colorScheme.error), + maxLines: 1, + minFontSize: minFont.minFontSize, + stepGranularity: 0.1, + ); + }, + ), + ); + } +} diff --git a/lib/routes/chainswap/send/fee/fee_breakdown/widgets/fee_breakdown_widgets/transaction_fee.dart b/lib/routes/chainswap/send/fee/fee_breakdown/widgets/fee_breakdown_widgets/transaction_fee.dart new file mode 100644 index 00000000..71afac2f --- /dev/null +++ b/lib/routes/chainswap/send/fee/fee_breakdown/widgets/fee_breakdown_widgets/transaction_fee.dart @@ -0,0 +1,37 @@ +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/models/currency.dart'; +import 'package:l_breez/utils/min_font_size.dart'; + +class TransactionFee extends StatelessWidget { + final int txFeeSat; + + const TransactionFee({required this.txFeeSat, super.key}); + + @override + Widget build(BuildContext context) { + final texts = context.texts(); + final themeData = Theme.of(context); + final minFont = MinFontSize(context); + + return ListTile( + title: AutoSizeText( + texts.sweep_all_coins_label_transaction_fee, + style: TextStyle(color: Colors.white.withOpacity(0.4)), + maxLines: 1, + minFontSize: minFont.minFontSize, + stepGranularity: 0.1, + ), + trailing: AutoSizeText( + texts.sweep_all_coins_fee( + BitcoinCurrency.SAT.format(txFeeSat), + ), + style: TextStyle(color: themeData.colorScheme.error.withOpacity(0.4)), + maxLines: 1, + minFontSize: minFont.minFontSize, + stepGranularity: 0.1, + ), + ); + } +} diff --git a/lib/routes/chainswap/send/fee/fee_option.dart b/lib/routes/chainswap/send/fee/fee_option.dart new file mode 100644 index 00000000..3d24f38f --- /dev/null +++ b/lib/routes/chainswap/send/fee/fee_option.dart @@ -0,0 +1,7 @@ +import 'package:flutter_breez_liquid/flutter_breez_liquid.dart'; + +extension PreparePayOnchainResponseAffordable on PreparePayOnchainResponse { + bool isAffordable({required int balance}) { + return balance >= (receiverAmountSat + feesSat).toInt(); + } +} diff --git a/lib/routes/chainswap/send/send_chainswap_button.dart b/lib/routes/chainswap/send/send_chainswap_button.dart new file mode 100644 index 00000000..8f765851 --- /dev/null +++ b/lib/routes/chainswap/send/send_chainswap_button.dart @@ -0,0 +1,56 @@ +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/bloc/chainswap/chainswap_bloc.dart'; +import 'package:l_breez/utils/exceptions.dart'; +import 'package:l_breez/widgets/error_dialog.dart'; +import 'package:l_breez/widgets/loader.dart'; +import 'package:l_breez/widgets/single_button_bottom_bar.dart'; + +class SendChainSwapButton extends StatelessWidget { + final String recipientAddress; + final PreparePayOnchainResponse preparePayOnchainResponse; + + const SendChainSwapButton({ + super.key, + required this.recipientAddress, + required this.preparePayOnchainResponse, + }); + + @override + Widget build(BuildContext context) { + final texts = context.texts(); + + return SingleButtonBottomBar( + text: texts.sweep_all_coins_action_confirm, + onPressed: () => _payOnchain(context), + ); + } + + Future _payOnchain(BuildContext context) async { + final texts = context.texts(); + final themeData = Theme.of(context); + final chainSwapBloc = context.read(); + + final navigator = Navigator.of(context); + var loaderRoute = createLoaderRoute(context); + navigator.push(loaderRoute); + try { + final req = PayOnchainRequest(address: recipientAddress, prepareRes: preparePayOnchainResponse); + await chainSwapBloc.payOnchain(req: req); + navigator.pushNamedAndRemoveUntil("/", (Route route) => false); + } catch (e) { + navigator.pop(loaderRoute); + if (!context.mounted) return; + promptError( + context, + null, + Text( + extractExceptionMessage(e, texts), + style: themeData.dialogTheme.contentTextStyle, + ), + ); + } + } +} diff --git a/lib/routes/chainswap/send/send_chainswap_confirmation_page.dart b/lib/routes/chainswap/send/send_chainswap_confirmation_page.dart new file mode 100644 index 00000000..5305cf96 --- /dev/null +++ b/lib/routes/chainswap/send/send_chainswap_confirmation_page.dart @@ -0,0 +1,125 @@ +import 'package:breez_liquid/breez_liquid.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/bloc/account/account_bloc.dart'; +import 'package:l_breez/bloc/chainswap/chainswap_bloc.dart'; +import 'package:l_breez/routes/chainswap/send/fee/fee_breakdown/fee_breakdown.dart'; +import 'package:l_breez/routes/chainswap/send/fee/fee_option.dart'; +import 'package:l_breez/routes/chainswap/send/send_chainswap_button.dart'; +import 'package:l_breez/widgets/loader.dart'; + +class SendChainSwapConfirmationPage extends StatefulWidget { + final int amountSat; + final String onchainRecipientAddress; + final bool isMaxValue; + + const SendChainSwapConfirmationPage({ + super.key, + required this.amountSat, + required this.onchainRecipientAddress, + required this.isMaxValue, + }); + + @override + State createState() => _SendChainSwapConfirmationPageState(); +} + +class _SendChainSwapConfirmationPageState extends State { + bool isAffordable = false; + PreparePayOnchainResponse? feeOption; + + late Future _preparePayOnchainResponseFuture; + + @override + void initState() { + super.initState(); + _preparePayOnchainResponse(); + } + + @override + Widget build(BuildContext context) { + final texts = context.texts(); + + return Scaffold( + appBar: AppBar( + title: Text(texts.csv_exporter_fee), + ), + body: FutureBuilder( + future: _preparePayOnchainResponseFuture, + builder: (context, snapshot) { + if (snapshot.error != null) { + return _ErrorMessage( + message: (snapshot.error is PaymentError_InsufficientFunds) + ? texts.reverse_swap_confirmation_error_funds_fee + : texts.reverse_swap_confirmation_error_fetch_fee, + ); + } + if (snapshot.connectionState != ConnectionState.done) { + return const Center(child: Loader()); + } + + if (isAffordable) { + return Padding( + padding: const EdgeInsets.fromLTRB(16.0, 24.0, 16.0, 40.0), + child: FeeBreakdown(feeOption: snapshot.data!), + ); + } else { + return _ErrorMessage(message: texts.reverse_swap_confirmation_error_funds_fee); + } + }, + ), + bottomNavigationBar: (isAffordable) + ? SafeArea( + child: SendChainSwapButton( + recipientAddress: widget.onchainRecipientAddress, + preparePayOnchainResponse: feeOption!, + ), + ) + : null, + ); + } + + void _preparePayOnchainResponse() { + final chainSwapBloc = context.read(); + final preparePayOnchainRequest = PreparePayOnchainRequest( + receiverAmountSat: BigInt.from(widget.amountSat), + ); + _preparePayOnchainResponseFuture = chainSwapBloc.preparePayOnchain( + req: preparePayOnchainRequest, + ); + _preparePayOnchainResponseFuture.then((feeOption) { + final account = context.read().state; + setState(() { + this.feeOption = feeOption; + isAffordable = feeOption.isAffordable(balance: account.balance); + }); + }, onError: (error, stackTrace) { + setState(() { + isAffordable = false; + feeOption = null; + }); + }); + } +} + +class _ErrorMessage extends StatelessWidget { + final String message; + + const _ErrorMessage({ + required this.message, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 40.0), + child: Text( + message, + textAlign: TextAlign.center, + ), + ), + ); + } +} diff --git a/lib/routes/chainswap/send/send_chainswap_form.dart b/lib/routes/chainswap/send/send_chainswap_form.dart new file mode 100644 index 00000000..3ee00870 --- /dev/null +++ b/lib/routes/chainswap/send/send_chainswap_form.dart @@ -0,0 +1,121 @@ +import 'package:breez_translations/breez_translations_locales.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_breez_liquid/flutter_breez_liquid.dart'; +import 'package:l_breez/models/currency.dart'; +import 'package:l_breez/routes/chainswap/send/validator_holder.dart'; +import 'package:l_breez/routes/chainswap/send/widgets/bitcoin_address_text_form_field.dart'; +import 'package:l_breez/routes/chainswap/send/widgets/withdraw_funds_amount_text_form_field.dart'; +import 'package:l_breez/routes/chainswap/send/withdraw_funds_model.dart'; +import 'package:l_breez/widgets/amount_form_field/sat_amount_form_field_formatter.dart'; +import 'package:logging/logging.dart'; + +final _log = Logger("SendChainSwapForm"); + +class SendChainSwapForm extends StatefulWidget { + final GlobalKey formKey; + final TextEditingController amountController; + final TextEditingController addressController; + final bool withdrawMaxValue; + final ValueChanged onChanged; + final BitcoinAddressData? btcAddressData; + final BitcoinCurrency bitcoinCurrency; + final OnchainPaymentLimitsResponse paymentLimits; + + const SendChainSwapForm({ + super.key, + required this.formKey, + required this.amountController, + required this.addressController, + required this.onChanged, + required this.withdrawMaxValue, + this.btcAddressData, + required this.bitcoinCurrency, + required this.paymentLimits, + }); + + @override + State createState() => _SendChainSwapFormState(); +} + +class _SendChainSwapFormState extends State { + final _validatorHolder = ValidatorHolder(); + + @override + void initState() { + super.initState(); + if (widget.btcAddressData != null) { + _fillBtcAddressData(widget.btcAddressData!); + } + } + + void _fillBtcAddressData(BitcoinAddressData addressData) { + _log.info("Filling BTC Address data for ${addressData.address}"); + widget.addressController.text = addressData.address; + if (addressData.amountSat != null) { + _setAmount(addressData.amountSat!.toInt()); + } + } + + void _setAmount(int amountSats) { + setState(() { + widget.amountController.text = widget.bitcoinCurrency + .format(amountSats, includeDisplayName: false, userInput: true) + .formatBySatAmountFormFieldFormatter(); + }); + } + + @override + Widget build(BuildContext context) { + final texts = context.texts(); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Form( + key: widget.formKey, + child: Column( + children: [ + BitcoinAddressTextFormField( + context: context, + controller: widget.addressController, + validatorHolder: _validatorHolder, + ), + WithdrawFundsAmountTextFormField( + context: context, + bitcoinCurrency: widget.bitcoinCurrency, + controller: widget.amountController, + withdrawMaxValue: widget.withdrawMaxValue, + balance: widget.paymentLimits.send.maxSat, + policy: WithdrawFundsPolicy( + WithdrawKind.withdraw_funds, + widget.paymentLimits.send.minSat, + widget.paymentLimits.send.maxSat, + ), + ), + ListTile( + contentPadding: EdgeInsets.zero, + title: Text( + texts.withdraw_funds_use_all_funds, + style: const TextStyle(color: Colors.white), + maxLines: 1, + ), + trailing: Switch( + value: widget.withdrawMaxValue, + activeColor: Colors.white, + onChanged: (bool value) async { + setState(() { + widget.onChanged(value); + if (widget.withdrawMaxValue) { + _setAmount(widget.paymentLimits.send.maxSat.toInt()); + } else { + widget.amountController.text = ""; + } + }); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/routes/chainswap/send/send_chainswap_form_page.dart b/lib/routes/chainswap/send/send_chainswap_form_page.dart new file mode 100644 index 00000000..8d0f4fff --- /dev/null +++ b/lib/routes/chainswap/send/send_chainswap_form_page.dart @@ -0,0 +1,123 @@ +import 'package:breez_translations/breez_translations_locales.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_breez_liquid/flutter_breez_liquid.dart'; +import 'package:l_breez/models/currency.dart'; +import 'package:l_breez/routes/chainswap/send/send_chainswap_confirmation_page.dart'; +import 'package:l_breez/routes/chainswap/send/send_chainswap_form.dart'; +import 'package:l_breez/routes/chainswap/send/widgets/chainswap_available_btc.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/route.dart'; +import 'package:l_breez/widgets/single_button_bottom_bar.dart'; +import 'package:logging/logging.dart'; + +final _log = Logger("SendChainSwapFormPage"); + +class SendChainSwapFormPage extends StatefulWidget { + final BitcoinAddressData? btcAddressData; + final BitcoinCurrency bitcoinCurrency; + final OnchainPaymentLimitsResponse paymentLimits; + + const SendChainSwapFormPage({ + super.key, + this.btcAddressData, + required this.bitcoinCurrency, + required this.paymentLimits, + }); + + @override + State createState() => _SendChainSwapFormPageState(); +} + +class _SendChainSwapFormPageState extends State { + final _formKey = GlobalKey(); + final _amountController = TextEditingController(); + final _addressController = TextEditingController(); + bool _withdrawMaxValue = false; + + @override + Widget build(BuildContext context) { + final texts = context.texts(); + + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + SendChainSwapForm( + formKey: _formKey, + amountController: _amountController, + addressController: _addressController, + withdrawMaxValue: _withdrawMaxValue, + btcAddressData: widget.btcAddressData, + bitcoinCurrency: widget.bitcoinCurrency, + paymentLimits: widget.paymentLimits, + onChanged: (bool value) { + setState(() { + _withdrawMaxValue = value; + }); + }, + ), + const WithdrawFundsAvailableBtc(), + Expanded(child: Container()), + SingleButtonBottomBar( + text: texts.withdraw_funds_action_next, + onPressed: _prepareSendChainSwap, + ), + ], + ), + ), + ); + } + + int _getAmount() { + int amount = 0; + try { + amount = widget.bitcoinCurrency.parse(_amountController.text); + } catch (e) { + _log.warning("Failed to parse the input amount", e); + } + return amount; + } + + void _prepareSendChainSwap() async { + final texts = context.texts(); + final navigator = Navigator.of(context); + if (_formKey.currentState?.validate() ?? false) { + var loaderRoute = createLoaderRoute(context); + navigator.push(loaderRoute); + try { + int amount = _getAmount(); + if (loaderRoute.isActive) { + navigator.removeRoute(loaderRoute); + } + navigator.push( + FadeInRoute( + builder: (_) => SendChainSwapConfirmationPage( + amountSat: amount, + onchainRecipientAddress: _addressController.text, + isMaxValue: _withdrawMaxValue, + ), + ), + ); + } catch (error) { + if (loaderRoute.isActive) { + navigator.removeRoute(loaderRoute); + } + _log.severe("Received error: $error"); + if (!context.mounted) return; + showFlushbar( + context, + message: texts.reverse_swap_upstream_generic_error_message( + extractExceptionMessage(error, texts), + ), + ); + } finally { + if (loaderRoute.isActive) { + navigator.removeRoute(loaderRoute); + } + } + } + } +} diff --git a/lib/routes/chainswap/send/send_chainswap_page.dart b/lib/routes/chainswap/send/send_chainswap_page.dart new file mode 100644 index 00000000..3eab762a --- /dev/null +++ b/lib/routes/chainswap/send/send_chainswap_page.dart @@ -0,0 +1,87 @@ +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/bloc/chainswap/chainswap_bloc.dart'; +import 'package:l_breez/bloc/currency/currency_bloc.dart'; +import 'package:l_breez/routes/chainswap/send/send_chainswap_form_page.dart'; +import 'package:l_breez/utils/exceptions.dart'; +import 'package:l_breez/widgets/back_button.dart' as back_button; +import 'package:l_breez/widgets/loader.dart'; + +class SendChainSwapPage extends StatefulWidget { + final BitcoinAddressData? btcAddressData; + const SendChainSwapPage({super.key, required this.btcAddressData}); + + @override + State createState() => _SendChainSwapPageState(); +} + +class _SendChainSwapPageState extends State { + final _scaffoldKey = GlobalKey(); + + Future? _onchainPaymentLimitsFuture; + @override + void initState() { + super.initState(); + _fetchOnchainLimits(); + } + + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + _fetchOnchainLimits(); + } + } + + Future _fetchOnchainLimits() async { + final chainSwapBloc = context.read(); + setState(() { + _onchainPaymentLimitsFuture = chainSwapBloc.fetchOnchainLimits(); + }); + } + + @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.bottom_action_bar_send_btc_address), + ), + body: FutureBuilder( + future: _onchainPaymentLimitsFuture, + builder: (BuildContext context, AsyncSnapshot 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( + extractExceptionMessage(snapshot.error!, texts), + ), + textAlign: TextAlign.center, + ), + ), + ); + } + if (snapshot.connectionState != ConnectionState.done && !snapshot.hasData) { + return Center( + child: Loader( + color: themeData.primaryColor.withOpacity(0.5), + ), + ); + } + + return SendChainSwapFormPage( + bitcoinCurrency: context.read().state.bitcoinCurrency, + btcAddressData: widget.btcAddressData, + paymentLimits: snapshot.data!, + ); + }, + ), + ); + } +} diff --git a/lib/routes/chainswap/send/validator_holder.dart b/lib/routes/chainswap/send/validator_holder.dart new file mode 100644 index 00000000..5258cc6d --- /dev/null +++ b/lib/routes/chainswap/send/validator_holder.dart @@ -0,0 +1,11 @@ +import 'package:synchronized/synchronized.dart'; + +class ValidatorHolder { + final lock = Lock(); + var valid = false; + + @override + String toString() { + return 'ValidatorHolder{valid: $valid, inLock: ${lock.inLock}, locked ${lock.locked}, hash: $hashCode}'; + } +} diff --git a/lib/routes/chainswap/send/widgets/bitcoin_address_text_form_field.dart b/lib/routes/chainswap/send/widgets/bitcoin_address_text_form_field.dart new file mode 100644 index 00000000..b173c33d --- /dev/null +++ b/lib/routes/chainswap/send/widgets/bitcoin_address_text_form_field.dart @@ -0,0 +1,85 @@ +import 'package:breez_translations/breez_translations_locales.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_breez_liquid/flutter_breez_liquid.dart'; +import 'package:l_breez/models/bitcoin_address_info.dart'; +import 'package:l_breez/routes/chainswap/send/validator_holder.dart'; +import 'package:l_breez/theme/theme_provider.dart' as theme; +import 'package:l_breez/widgets/flushbar.dart'; +import 'package:logging/logging.dart'; + +final _log = Logger("BitcoinAddressTextFormField"); + +class BitcoinAddressTextFormField extends TextFormField { + BitcoinAddressTextFormField({ + super.key, + required BuildContext context, + required TextEditingController super.controller, + required ValidatorHolder validatorHolder, + }) : super( + decoration: InputDecoration( + labelText: context.texts().withdraw_funds_btc_address, + suffixIcon: IconButton( + alignment: Alignment.bottomRight, + icon: Image( + image: const AssetImage("src/icon/qr_scan.png"), + color: theme.BreezColors.white[500], + fit: BoxFit.contain, + width: 24.0, + height: 24.0, + ), + tooltip: context.texts().withdraw_funds_scan_barcode, + onPressed: () async { + Navigator.pushNamed(context, "/qr_scan").then( + (barcode) { + _log.info("Scanned string: '$barcode'"); + final address = BitcoinAddressInfo.fromScannedString(barcode).address; + _log.info("BitcoinAddressInfoFromScannedString: '$address'"); + if (address == null) return; + if (address.isEmpty) { + showFlushbar( + context, + message: context.texts().withdraw_funds_error_qr_code_not_detected, + ); + return; + } + controller.text = address; + _onAddressChanged(context, validatorHolder, address); + }, + ); + }, + ), + ), + style: theme.FieldTextStyle.textStyle, + onChanged: (address) => _onAddressChanged(context, validatorHolder, address), + validator: (address) { + _log.info("validator called for $address, $validatorHolder"); + if (validatorHolder.valid) { + return null; + } else { + return context.texts().withdraw_funds_error_invalid_address; + } + }, + ); + + static void _onAddressChanged( + BuildContext context, + ValidatorHolder holder, + String address, + ) async { + _log.info("Address changed $address"); + await holder.lock.synchronized(() async { + _log.info("Calling validator for $address"); + holder.valid = await isValidBitcoinAddress(address); + _log.info("Address $address validation result $holder"); + }); + } + + static Future isValidBitcoinAddress(String address) async { + try { + final inputType = await parse(input: address); + return inputType is InputType_BitcoinAddress; + } catch (e) { + return false; + } + } +} diff --git a/lib/routes/chainswap/send/widgets/chainswap_available_btc.dart b/lib/routes/chainswap/send/widgets/chainswap_available_btc.dart new file mode 100644 index 00000000..862461d1 --- /dev/null +++ b/lib/routes/chainswap/send/widgets/chainswap_available_btc.dart @@ -0,0 +1,42 @@ +import 'package:breez_translations/breez_translations_locales.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:l_breez/bloc/account/account_bloc.dart'; +import 'package:l_breez/bloc/account/account_state.dart'; +import 'package:l_breez/bloc/currency/currency_bloc.dart'; +import 'package:l_breez/bloc/currency/currency_state.dart'; +import 'package:l_breez/theme/theme_provider.dart' as theme; + +class WithdrawFundsAvailableBtc extends StatelessWidget { + const WithdrawFundsAvailableBtc({super.key}); + + @override + Widget build(BuildContext context) { + final texts = context.texts(); + + return Padding( + padding: const EdgeInsets.only(top: 36.0), + child: BlocBuilder(builder: (context, account) { + return Row( + children: [ + Text( + texts.withdraw_funds_balance, + style: theme.textStyle, + ), + Padding( + padding: const EdgeInsets.only(left: 3.0), + child: BlocBuilder( + builder: (context, currencyState) { + return Text( + currencyState.bitcoinCurrency.format(account.balance), + style: theme.textStyle, + ); + }, + ), + ), + ], + ); + }), + ); + } +} diff --git a/lib/routes/chainswap/send/widgets/withdraw_funds_amount_text_form_field.dart b/lib/routes/chainswap/send/widgets/withdraw_funds_amount_text_form_field.dart new file mode 100644 index 00000000..d7b6bb56 --- /dev/null +++ b/lib/routes/chainswap/send/widgets/withdraw_funds_amount_text_form_field.dart @@ -0,0 +1,43 @@ +import 'package:breez_translations/breez_translations_locales.dart'; +import 'package:flutter/material.dart'; +import 'package:l_breez/bloc/account/payment_error.dart'; +import 'package:l_breez/routes/chainswap/send/withdraw_funds_model.dart'; +import 'package:l_breez/utils/payment_validator.dart'; +import 'package:l_breez/widgets/amount_form_field/amount_form_field.dart'; +import 'package:logging/logging.dart'; + +final _log = Logger("WithdrawFundsAmountTextFormField"); + +class WithdrawFundsAmountTextFormField extends AmountFormField { + WithdrawFundsAmountTextFormField({ + super.key, + required super.bitcoinCurrency, + required super.context, + required TextEditingController super.controller, + required bool withdrawMaxValue, + required WithdrawFundsPolicy policy, + required BigInt balance, + }) : super( + texts: context.texts(), + readOnly: policy.withdrawKind == WithdrawKind.unexpected_funds || withdrawMaxValue, + validatorFn: (amount) { + _log.info("Validator called for $amount"); + return PaymentValidator( + currency: bitcoinCurrency, + texts: context.texts(), + validatePayment: (amount, outgoing) { + _log.info("Validating $amount $policy"); + if (amount < policy.minValue.toInt()) { + throw PaymentBelowLimitError(policy.minValue); + } + if (amount > policy.maxValue.toInt()) { + throw PaymentExceededLimitError(policy.maxValue); + } + if (amount > balance.toInt()) { + throw const InsufficientLocalBalanceError(); + } + }, + ).validateOutgoing(amount); + }, + ); +} diff --git a/lib/routes/chainswap/send/withdraw_funds_model.dart b/lib/routes/chainswap/send/withdraw_funds_model.dart new file mode 100644 index 00000000..2f9573a1 --- /dev/null +++ b/lib/routes/chainswap/send/withdraw_funds_model.dart @@ -0,0 +1,21 @@ +enum WithdrawKind { + withdraw_funds, + unexpected_funds, +} + +class WithdrawFundsPolicy { + final WithdrawKind withdrawKind; + final BigInt minValue; + final BigInt maxValue; + + const WithdrawFundsPolicy( + this.withdrawKind, + this.minValue, + this.maxValue, + ); + + @override + String toString() { + return 'WithdrawFundsPolicy{withdrawKind: $withdrawKind, minValue: $minValue, maxValue: $maxValue}'; + } +} diff --git a/lib/routes/create_invoice/widgets/compact_qr_image.dart b/lib/routes/create_invoice/widgets/compact_qr_image.dart index 67194a57..aa5521f2 100644 --- a/lib/routes/create_invoice/widgets/compact_qr_image.dart +++ b/lib/routes/create_invoice/widgets/compact_qr_image.dart @@ -47,8 +47,9 @@ var _versionsToMaxCharacters = [ class CompactQRImage extends StatelessWidget { final String data; final double? size; + final bool bip21; - const CompactQRImage({super.key, required this.data, this.size}); + const CompactQRImage({super.key, required this.data, this.size, this.bip21 = false}); @override Widget build(BuildContext context) { @@ -61,7 +62,7 @@ class CompactQRImage extends StatelessWidget { we will not change the case of the parameters because BIP21 parameters are case sensitive. Ref. https://bitcoinops.org/en/bech32-sending-support/#creating-more-efficient-qr-codes-with-bech32-addresses */ - data: data.toUpperCase(), + data: bip21 ? data : data.toUpperCase(), size: size, ); } diff --git a/lib/routes/create_invoice/widgets/invoice_qr.dart b/lib/routes/create_invoice/widgets/invoice_qr.dart index e6692cb0..d2f013e5 100644 --- a/lib/routes/create_invoice/widgets/invoice_qr.dart +++ b/lib/routes/create_invoice/widgets/invoice_qr.dart @@ -3,10 +3,12 @@ import 'package:l_breez/routes/create_invoice/widgets/compact_qr_image.dart'; class InvoiceQR extends StatelessWidget { final String bolt11; + final bool bip21; const InvoiceQR({ super.key, required this.bolt11, + this.bip21 = false, }); @override @@ -18,7 +20,7 @@ class InvoiceQR extends StatelessWidget { child: SizedBox( width: 230.0, height: 230.0, - child: CompactQRImage(data: bolt11), + child: CompactQRImage(data: bolt11, bip21: bip21), ), ), ); diff --git a/lib/routes/home/widgets/bottom_actions_bar/receive_options_bottom_sheet.dart b/lib/routes/home/widgets/bottom_actions_bar/receive_options_bottom_sheet.dart index fd1fa9f9..1f9ba8c2 100644 --- a/lib/routes/home/widgets/bottom_actions_bar/receive_options_bottom_sheet.dart +++ b/lib/routes/home/widgets/bottom_actions_bar/receive_options_bottom_sheet.dart @@ -34,6 +34,21 @@ class ReceiveOptionsBottomSheet extends StatelessWidget { navigatorState.pushNamed("/create_invoice"); }, ), + const SizedBox(height: 8.0), + ListTile( + leading: const BottomActionItemImage( + iconAssetPath: "src/icon/bitcoin.png", + ), + title: Text( + texts.bottom_action_bar_receive_btc_address, + style: theme.bottomSheetTextStyle, + ), + onTap: () { + final navigatorState = Navigator.of(context); + navigatorState.pop(); + navigatorState.pushNamed("/receive_chainswap"); + }, + ), ], ); } 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 5e61fac3..ba9c0554 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 @@ -35,6 +35,21 @@ class _SendOptionsBottomSheetState extends State { ), onTap: () => _showEnterPaymentInfoDialog(context, widget.firstPaymentItemKey), ), + const SizedBox(height: 8.0), + ListTile( + leading: const BottomActionItemImage( + iconAssetPath: "src/icon/bitcoin.png", + ), + title: Text( + texts.bottom_action_bar_send_btc_address, + style: theme.bottomSheetTextStyle, + ), + onTap: () { + final navigatorState = Navigator.of(context); + navigatorState.pop(); + navigatorState.pushNamed("/send_chainswap"); + }, + ), ], ); } diff --git a/lib/user_app.dart b/lib/user_app.dart index 37aed1c2..3cb2ec90 100644 --- a/lib/user_app.dart +++ b/lib/user_app.dart @@ -2,6 +2,7 @@ import 'package:breez_translations/breez_translations_locales.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/bloc/account/account_bloc.dart'; import 'package:l_breez/bloc/account/account_state.dart'; import 'package:l_breez/bloc/ext/block_builder_extensions.dart'; @@ -9,6 +10,8 @@ import 'package:l_breez/bloc/security/security_bloc.dart'; import 'package:l_breez/bloc/security/security_state.dart'; import 'package:l_breez/bloc/user_profile/user_profile_bloc.dart'; import 'package:l_breez/bloc/user_profile/user_profile_state.dart'; +import 'package:l_breez/routes/chainswap/receive/receive_chainswap_page.dart'; +import 'package:l_breez/routes/chainswap/send/send_chainswap_page.dart'; import 'package:l_breez/routes/create_invoice/create_invoice_page.dart'; import 'package:l_breez/routes/dev/developers_view.dart'; import 'package:l_breez/routes/fiat_currencies/fiat_currency_settings.dart'; @@ -139,6 +142,18 @@ class UserApp extends StatelessWidget { builder: (_) => const CreateInvoicePage(), settings: settings, ); + case '/receive_chainswap': + return FadeInRoute( + builder: (_) => const ReceiveChainSwapPage(), + settings: settings, + ); + case '/send_chainswap': + return FadeInRoute( + builder: (_) => SendChainSwapPage( + btcAddressData: settings.arguments as BitcoinAddressData?, + ), + settings: settings, + ); case '/fiat_currency': return FadeInRoute( builder: (_) => const FiatCurrencySettings(), diff --git a/lib/utils/payment_validator.dart b/lib/utils/payment_validator.dart index 7cc793e2..929bcb63 100644 --- a/lib/utils/payment_validator.dart +++ b/lib/utils/payment_validator.dart @@ -32,12 +32,12 @@ class PaymentValidator { } on PaymentExceededLimitError catch (e) { _log.info("Got PaymentExceededLimitError", e); return texts.invoice_payment_validator_error_payment_exceeded_limit( - currency.format(e.limitSat), + currency.format(e.limitSat.toInt()), ); } on PaymentBelowLimitError catch (e) { _log.info("Got PaymentBelowLimitError", e); return texts.invoice_payment_validator_error_payment_below_invoice_limit( - currency.format(e.limitSat), + currency.format(e.limitSat.toInt()), ); } on PaymentBelowReserveError catch (e) { _log.info("Got PaymentBelowReserveError", e); @@ -45,7 +45,7 @@ class PaymentValidator { currency.format(e.reserveAmount), ); } on PaymentExceedLiquidityError catch (e) { - return "Insufficient inbound liquidity (${currency.format(e.limitSat)})"; + return "Insufficient inbound liquidity (${currency.format(e.limitSat.toInt())})"; } on InsufficientLocalBalanceError { return texts.invoice_payment_validator_error_insufficient_local_balance; } on PaymentBelowSetupFeesError catch (e) { @@ -54,7 +54,7 @@ class PaymentValidator { currency.format(e.setupFees), ); } on PaymentExcededLiqudityChannelCreationNotPossibleError catch (e) { - return texts.lnurl_fetch_invoice_error_max(currency.format(e.limitSat)); + return texts.lnurl_fetch_invoice_error_max(currency.format(e.limitSat.toInt())); } on NoChannelCreationZeroLiqudityError { return texts.lsp_error_cannot_open_channel; } catch (e) { diff --git a/pubspec.lock b/pubspec.lock index 219f80aa..fe666bf4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: a315d1c444402c3fa468de626d33a1c666041c87e9e195e8fb355b7084aefcc1 + sha256: b46f62516902afb04befa4b30eb6a12ac1f58ca8cb25fb9d632407259555dd3d url: "https://pub.dev" source: hosted - version: "1.3.38" + version: "1.3.39" analyzer: dependency: transitive description: @@ -438,10 +438,10 @@ packages: dependency: "direct main" description: name: firebase_core - sha256: "1e06b0538ab3108a61d895ee16951670b491c4a94fce8f2d30e5de7a5eca4b28" + sha256: "5159984ce9b70727473eb388394650677c02c925aaa6c9439905e1f30966a4d5" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.0" firebase_core_platform_interface: dependency: transitive description: @@ -454,50 +454,50 @@ packages: dependency: transitive description: name: firebase_core_web - sha256: "6643fe3dbd021e6ccfb751f7882b39df355708afbdeb4130fc50f9305a9d1a3d" + sha256: "23509cb3cddfb3c910c143279ac3f07f06d3120f7d835e4a5d4b42558e978712" url: "https://pub.dev" source: hosted - version: "2.17.2" + version: "2.17.3" firebase_dynamic_links: dependency: "direct main" description: name: firebase_dynamic_links - sha256: facba6ef5cd74a3c0d41a75a205a9a99df8e0cc5dcc6363e266c70059fdd3c23 + sha256: beed25d57e0240728952c0ff540dde214b11b323ca09cb1ae5f71f30e5efc3b3 url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.0.3" firebase_dynamic_links_platform_interface: dependency: transitive description: name: firebase_dynamic_links_platform_interface - sha256: da1253adb4de5b31458bb6d1525f106205fc131895d3f457fc34a2405fe49b90 + sha256: bb949552fcf65e8ed810b69327eb0dcb174b7d69e2b71f8856d27af50c89ddc7 url: "https://pub.dev" source: hosted - version: "0.2.6+38" + version: "0.2.6+39" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: a1eb38242e072118650139f8485a78d8f12e6d9b6ae563808ca0fa406bdebaad + sha256: "156c4292aa63a6a7d508c68ded984cb38730d2823c3265e573cb1e94983e2025" url: "https://pub.dev" source: hosted - version: "15.0.2" + version: "15.0.3" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: "98faf00cbe125bba136787e1678e7bf213f5e694e8f2615b94ad3d4bdcb0bdc2" + sha256: "10408c5ca242b7fc632dd5eab4caf8fdf18ebe88db6052980fa71a18d88bd200" url: "https://pub.dev" source: hosted - version: "4.5.40" + version: "4.5.41" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: a38e9ccdd5dc4d7dc9eef0097b6a5a3c24842772035e1be103dc1b81d8d09f7c + sha256: c7a756e3750679407948de665735e69a368cb902940466e5d68a00ea7aba1aaa url: "https://pub.dev" source: hosted - version: "3.8.10" + version: "3.8.11" fixnum: dependency: transitive description: @@ -893,10 +893,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: "4161e1f843d8480d2e9025ee22411778c3c9eb7e40076dcf2da23d8242b7b51c" + sha256: ff39a10ab4f48f4ac70776d0494a97bf073cd2570892cd46bc8a5cac162c25db url: "https://pub.dev" source: hosted - version: "0.8.12+3" + version: "0.8.12+4" image_picker_for_web: dependency: transitive description: @@ -1226,10 +1226,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" petitparser: dependency: transitive description: @@ -1242,10 +1242,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: "direct main" description: @@ -1735,10 +1735,10 @@ packages: dependency: transitive description: name: uuid - sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" + sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.4.2" vector_graphics: dependency: transitive description: @@ -1775,10 +1775,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.4" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c970ba5e..9247d52d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,9 +41,9 @@ dependencies: email_validator: ^3.0.0 extended_image: ^8.2.1 ffi: ^2.1.2 - firebase_core: ^3.1.1 - firebase_dynamic_links: ^6.0.2 - firebase_messaging: ^15.0.2 + firebase_core: ^3.2.0 + firebase_dynamic_links: ^6.0.3 + firebase_messaging: ^15.0.3 flutter_bloc: ^8.1.6 flutter_fgbg: git: