diff --git a/assets/translations/en.json b/assets/translations/en.json index de3c231..dab93e9 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -220,5 +220,10 @@ "addedToContacts": "Added to contacts", "certificationsOf": "Certifications of {}", "explainDraggableWallet": "Drag and drop a tile onto another to make a transaction, by holding down the tile.", - "skip": "Skip" + "skip": "Skip", + "fees": "fees: {} {}", + "feesExplanation": "To ensure network security, an execution fee is charged on each transaction.", + "feesExplanationDetails": "These fees are transferred to the common treasury account.", + "gotit": "Got it", + "moreInfo": "More information" } \ No newline at end of file diff --git a/assets/translations/es.json b/assets/translations/es.json index e6dfc13..89a7f77 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -221,5 +221,10 @@ "addedToContacts": "Added to contacts", "certificationsOf": "Certifications of {}", "explainDraggableWallet": "Drag and drop a tile onto another to make a transaction, by holding down the tile.", - "skip": "Skip" + "skip": "Skip", + "fees": "fees: {} {}", + "feesExplanation": "To ensure network security, an execution fee is charged on each transaction.", + "feesExplanationDetails": "These fees are transferred to the common treasury account.", + "gotit": "Got it", + "moreInfo": "More information" } \ No newline at end of file diff --git a/assets/translations/fr.json b/assets/translations/fr.json index 1121420..17463bd 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -220,5 +220,10 @@ "addedToContacts": "Ajouté au contacts", "certificationsOf": "Certifications de {}", "explainDraggableWallet": "Faites glisser et déposez une tuile sur une autre pour effectuer une transaction, avec un appui long.", - "skip": "Passer" + "skip": "Passer", + "fees": "frais: {} {}", + "feesExplanation": "Pour garantir la sécurité du réseau, des frais d'exécution sont prélevés sur chaque transaction.", + "feesExplanationDetails": "Ces frais sont transférés vers le compte de trésorerie commune.", + "gotit": "J'ai compris", + "moreInfo": "Plus d'info" } \ No newline at end of file diff --git a/lib/screens/myWallets/wallets_home.dart b/lib/screens/myWallets/wallets_home.dart index 65d3465..3c1a327 100644 --- a/lib/screens/myWallets/wallets_home.dart +++ b/lib/screens/myWallets/wallets_home.dart @@ -15,13 +15,12 @@ import 'package:gecko/screens/myWallets/chest_options.dart'; import 'package:gecko/screens/myWallets/import_g1_v1.dart'; import 'package:gecko/screens/myWallets/unlocking_wallet.dart'; import 'package:gecko/screens/myWallets/wallet_options.dart'; -import 'package:gecko/screens/wallet_view.dart'; -// import 'package:gecko/screens/myWallets/choose_chest.dart'; import 'package:gecko/widgets/balance.dart'; import 'package:gecko/widgets/bottom_app_bar.dart'; import 'package:gecko/widgets/commons/offline_info.dart'; import 'package:gecko/widgets/commons/smooth_transition.dart'; import 'package:gecko/widgets/name_by_address.dart'; +import 'package:gecko/widgets/payment_popup.dart'; import 'package:provider/provider.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; diff --git a/lib/screens/wallet_view.dart b/lib/screens/wallet_view.dart index d0ac785..50fffe0 100644 --- a/lib/screens/wallet_view.dart +++ b/lib/screens/wallet_view.dart @@ -11,18 +11,16 @@ import 'package:gecko/providers/duniter_indexer.dart'; import 'package:gecko/providers/substrate_sdk.dart'; import 'package:gecko/providers/my_wallets.dart'; import 'package:gecko/models/wallet_data.dart'; -import 'package:gecko/providers/wallet_options.dart'; import 'package:gecko/providers/wallets_profiles.dart'; import 'package:gecko/screens/activity.dart'; import 'package:gecko/widgets/commons/common_elements.dart'; -import 'package:gecko/screens/myWallets/choose_wallet.dart'; import 'package:gecko/screens/myWallets/unlocking_wallet.dart'; import 'package:gecko/screens/qrcode_fullscreen.dart'; import 'package:gecko/screens/transaction_in_progress.dart'; -import 'package:gecko/widgets/balance.dart'; import 'package:gecko/widgets/bottom_app_bar.dart'; import 'package:gecko/widgets/header_profile.dart'; import 'package:gecko/widgets/page_route_no_transition.dart'; +import 'package:gecko/widgets/payment_popup.dart'; import 'package:provider/provider.dart'; import 'package:qr_flutter/qr_flutter.dart'; @@ -405,302 +403,3 @@ class WalletViewScreen extends StatelessWidget { ]); } } - -void paymentPopup(BuildContext context, String toAddress, String username) { - final walletViewProvider = - Provider.of(context, listen: false); - - final myWalletProvider = - Provider.of(context, listen: false); - - const double shapeSize = 20; - final defaultWallet = myWalletProvider.getDefaultWallet(); - log.d(defaultWallet.address); - - bool canValidate = false; - - Future executeTransfert() async { - String? pin; - if (myWalletProvider.pinCode == '') { - pin = await Navigator.push( - context, - MaterialPageRoute( - builder: (homeContext) { - return UnlockingWallet(wallet: defaultWallet); - }, - ), - ); - } - log.d(pin); - if (pin != null || myWalletProvider.pinCode != '') { - // Payment workflow ! - final sub = Provider.of(context, listen: false); - final acc = sub.getCurrentWallet(); - log.d( - "fromAddress: ${acc.address!},destAddress: $toAddress, amount: ${double.parse(walletViewProvider.payAmount.text)}, password: $pin"); - sub.pay( - fromAddress: acc.address!, - destAddress: toAddress, - amount: double.parse(walletViewProvider.payAmount.text), - password: pin ?? myWalletProvider.pinCode); - Navigator.push( - context, - MaterialPageRoute(builder: (context) { - return const TransactionInProgress(); - }), - ); - } - } - - showModalBottomSheet( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topRight: Radius.circular(shapeSize), - topLeft: Radius.circular(shapeSize), - ), - ), - isScrollControlled: true, - context: context, - builder: (BuildContext context) { - final sub = Provider.of(homeContext, listen: false); - final walletOptions = - Provider.of(context, listen: false); - - double fees = 0; - return StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - if (walletViewProvider.payAmount.text != '' && - (double.parse(walletViewProvider.payAmount.text) + - 2 / balanceRatio) <= - (walletOptions.balanceCache[defaultWallet.address] ?? 0) && - toAddress != defaultWallet.address) { - if ((walletOptions.balanceCache[toAddress] == 0 || - walletOptions.balanceCache[toAddress] == null) && - double.parse(walletViewProvider.payAmount.text) < - 5 / balanceRatio) { - canValidate = false; - } else { - canValidate = true; - } - } else { - canValidate = false; - } - final bool isUdUnit = configBox.get('isUdUnit') ?? false; - return Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom), - child: Container( - height: 420, - decoration: const ShapeDecoration( - color: Color(0xffffeed1), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topRight: Radius.circular(shapeSize), - topLeft: Radius.circular(shapeSize), - ), - ), - ), - child: Padding( - padding: const EdgeInsets.only( - top: 24, bottom: 0, left: 24, right: 24), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'executeATransfer'.tr(), - style: const TextStyle( - fontSize: 26, fontWeight: FontWeight.w700), - ), - IconButton( - iconSize: 40, - icon: const Icon(Icons.cancel_outlined), - onPressed: () { - Navigator.pop(context); - }, - ), - ]), - const SizedBox(height: 20), - Text( - 'from'.tr(), - style: TextStyle( - fontSize: 19, - fontWeight: FontWeight.w500, - color: Colors.grey[600]), - ), - const SizedBox(height: 10), - Consumer(builder: (context, sub, _) { - return InkWell( - key: keyChangeChest, - onTap: () async { - String? pin; - if (myWalletProvider.pinCode == '') { - pin = await Navigator.push( - context, - MaterialPageRoute( - builder: (homeContext) { - return UnlockingWallet( - wallet: defaultWallet); - }, - ), - ); - } - if (pin != null || myWalletProvider.pinCode != '') { - Navigator.push( - context, - MaterialPageRoute(builder: (context) { - return ChooseWalletScreen( - pin: pin ?? myWalletProvider.pinCode); - }), - ); - } - }, - child: Container( - width: double.infinity, - decoration: BoxDecoration( - border: Border.all( - color: Colors.blueAccent.shade200, width: 2), - borderRadius: - const BorderRadius.all(Radius.circular(10.0)), - ), - padding: const EdgeInsets.all(10), - child: Row(children: [ - Text(g1WalletsBox - .get(defaultWallet.address) - ?.username ?? - defaultWallet.name!), - const Spacer(), - Balance(address: defaultWallet.address, size: 20), - ]), - ), - ); - }), - const SizedBox(height: 12), - Row( - children: [ - Text( - 'to'.tr(), - style: TextStyle( - fontSize: 19, - fontWeight: FontWeight.w500, - color: Colors.grey[600]), - ), - const SizedBox(width: 10), - Column( - children: [ - const SizedBox(height: 2), - Text( - username == '' - ? getShortPubkey(toAddress) - : username, - style: const TextStyle( - fontSize: 21, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - Text( - 'amount'.tr(), - style: TextStyle( - fontSize: 19, - fontWeight: FontWeight.w500, - color: Colors.grey[600]), - ), - const Spacer(), - Text( - 'frais: $fees $currencyName', - style: const TextStyle( - color: orangeC, - fontSize: 17, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(width: 10), - ], - ), - const SizedBox(height: 10), - TextField( - textInputAction: TextInputAction.done, - onEditingComplete: () async => - canValidate ? await executeTransfert() : null, - key: keyAmountField, - controller: walletViewProvider.payAmount, - autofocus: true, - maxLines: 1, - textAlign: TextAlign.center, - keyboardType: const TextInputType.numberWithOptions( - decimal: true), - onChanged: (_) async { - fees = await sub.txFees( - defaultWallet.address, - toAddress, - double.parse( - walletViewProvider.payAmount.text == '' - ? '0' - : walletViewProvider.payAmount.text)); - log.d(fees); - setState(() {}); - }, - inputFormatters: [ - FilteringTextInputFormatter.deny(',', - replacementString: '.'), - FilteringTextInputFormatter.allow( - RegExp(r'(^\d+\.?\d{0,2})')), - ], - decoration: InputDecoration( - hintText: '0.00', - suffix: Text(isUdUnit - ? 'ud'.tr(args: ['']) - : currencyName), // udUnitDisplay(40), - filled: true, - fillColor: Colors.transparent, - focusedBorder: OutlineInputBorder( - borderSide: - BorderSide(color: Colors.grey[500]!, width: 2), - borderRadius: BorderRadius.circular(8), - ), - contentPadding: const EdgeInsets.all(20), - ), - style: const TextStyle( - fontSize: 35, - color: Colors.black, - fontWeight: FontWeight.w600, - ), - ), - const Spacer(), - SizedBox( - width: double.infinity, - height: 60, - child: ElevatedButton( - key: keyConfirmPayment, - style: ElevatedButton.styleFrom( - foregroundColor: Colors.white, elevation: 4, - backgroundColor: orangeC, // foreground - ), - onPressed: canValidate - ? () async => await executeTransfert() - : null, - child: Text( - 'executeTheTransfer'.tr(), - style: const TextStyle( - fontSize: 20, fontWeight: FontWeight.w600), - ), - ), - ), - const Spacer(), - ]), - ), - ), - ); - }); - }).then((value) => walletViewProvider.payAmount.text = ''); -} diff --git a/lib/widgets/commons/common_elements.dart b/lib/widgets/commons/common_elements.dart index 5944f43..93f42fb 100644 --- a/lib/widgets/commons/common_elements.dart +++ b/lib/widgets/commons/common_elements.dart @@ -155,11 +155,14 @@ Future infoPopup(BuildContext context, String title) async { children: [ TextButton( key: keyInfoPopup, - child: const Text( - "D'accord", - style: TextStyle( - fontSize: 21, - color: Color(0xffD80000), + child: Padding( + padding: const EdgeInsets.all(8), + child: Text( + "gotit".tr(), + style: const TextStyle( + fontSize: 21, + color: Color(0xffD80000), + ), ), ), onPressed: () { diff --git a/lib/widgets/payment_popup.dart b/lib/widgets/payment_popup.dart new file mode 100644 index 0000000..eeddcfc --- /dev/null +++ b/lib/widgets/payment_popup.dart @@ -0,0 +1,407 @@ +// ignore_for_file: use_build_context_synchronously + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:gecko/globals.dart'; +import 'package:gecko/models/widgets_keys.dart'; +import 'package:gecko/providers/my_wallets.dart'; +import 'package:gecko/providers/substrate_sdk.dart'; +import 'package:gecko/providers/wallet_options.dart'; +import 'package:gecko/providers/wallets_profiles.dart'; +import 'package:gecko/screens/myWallets/choose_wallet.dart'; +import 'package:gecko/screens/myWallets/unlocking_wallet.dart'; +import 'package:gecko/screens/transaction_in_progress.dart'; +import 'package:gecko/widgets/balance.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; + +void paymentPopup(BuildContext context, String toAddress, String username) { + final walletViewProvider = + Provider.of(context, listen: false); + final myWalletProvider = + Provider.of(context, listen: false); + + double fees = 0; + const double shapeSize = 20; + final defaultWallet = myWalletProvider.getDefaultWallet(); + bool canValidate = false; + + Future executeTransfert() async { + String? pin; + if (myWalletProvider.pinCode == '') { + pin = await Navigator.push( + context, + MaterialPageRoute( + builder: (homeContext) { + return UnlockingWallet(wallet: defaultWallet); + }, + ), + ); + } + log.d(pin); + if (pin != null || myWalletProvider.pinCode != '') { + // Payment workflow ! + final sub = Provider.of(context, listen: false); + final acc = sub.getCurrentWallet(); + log.d( + "fromAddress: ${acc.address!},destAddress: $toAddress, amount: ${double.parse(walletViewProvider.payAmount.text)}, password: $pin"); + sub.pay( + fromAddress: acc.address!, + destAddress: toAddress, + amount: double.parse(walletViewProvider.payAmount.text), + password: pin ?? myWalletProvider.pinCode); + Navigator.push( + context, + MaterialPageRoute(builder: (context) { + return const TransactionInProgress(); + }), + ); + } + } + + showModalBottomSheet( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(shapeSize), + topLeft: Radius.circular(shapeSize), + ), + ), + isScrollControlled: true, + context: context, + builder: (BuildContext context) { + final sub = Provider.of(homeContext, listen: false); + final walletOptions = + Provider.of(context, listen: false); + + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + if (walletViewProvider.payAmount.text != '' && + (double.parse(walletViewProvider.payAmount.text) + + 2 / balanceRatio) <= + (walletOptions.balanceCache[defaultWallet.address] ?? 0) && + toAddress != defaultWallet.address) { + if ((walletOptions.balanceCache[toAddress] == 0 || + walletOptions.balanceCache[toAddress] == null) && + double.parse(walletViewProvider.payAmount.text) < + 5 / balanceRatio) { + canValidate = false; + } else { + canValidate = true; + } + } else { + canValidate = false; + } + final bool isUdUnit = configBox.get('isUdUnit') ?? false; + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom), + child: Container( + height: 420, + decoration: const ShapeDecoration( + color: Color(0xffffeed1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(shapeSize), + topLeft: Radius.circular(shapeSize), + ), + ), + ), + child: Padding( + padding: const EdgeInsets.only( + top: 24, bottom: 0, left: 24, right: 24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'executeATransfer'.tr(), + style: const TextStyle( + fontSize: 26, fontWeight: FontWeight.w700), + ), + IconButton( + iconSize: 40, + icon: const Icon(Icons.cancel_outlined), + onPressed: () { + Navigator.pop(context); + }, + ), + ]), + const SizedBox(height: 20), + Text( + 'from'.tr(), + style: TextStyle( + fontSize: 19, + fontWeight: FontWeight.w500, + color: Colors.grey[600]), + ), + const SizedBox(height: 10), + Consumer(builder: (context, sub, _) { + return InkWell( + key: keyChangeChest, + onTap: () async { + String? pin; + if (myWalletProvider.pinCode == '') { + pin = await Navigator.push( + context, + MaterialPageRoute( + builder: (homeContext) { + return UnlockingWallet( + wallet: defaultWallet); + }, + ), + ); + } + if (pin != null || myWalletProvider.pinCode != '') { + Navigator.push( + context, + MaterialPageRoute(builder: (context) { + return ChooseWalletScreen( + pin: pin ?? myWalletProvider.pinCode); + }), + ); + } + }, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + border: Border.all( + color: Colors.blueAccent.shade200, width: 2), + borderRadius: + const BorderRadius.all(Radius.circular(10.0)), + ), + padding: const EdgeInsets.all(10), + child: Row(children: [ + Text(g1WalletsBox + .get(defaultWallet.address) + ?.username ?? + defaultWallet.name!), + const Spacer(), + Balance(address: defaultWallet.address, size: 20), + ]), + ), + ); + }), + const SizedBox(height: 12), + Row( + children: [ + Text( + 'to'.tr(), + style: TextStyle( + fontSize: 19, + fontWeight: FontWeight.w500, + color: Colors.grey[600]), + ), + const SizedBox(width: 10), + Column( + children: [ + const SizedBox(height: 2), + Text( + username == '' + ? getShortPubkey(toAddress) + : username, + style: const TextStyle( + fontSize: 21, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Text( + 'amount'.tr(), + style: TextStyle( + fontSize: 19, + fontWeight: FontWeight.w500, + color: Colors.grey[600]), + ), + const Spacer(), + InkWell( + onTap: () => infoFeesPopup(context), + child: Row( + children: [ + const Icon(Icons.info_outlined, color: orangeC), + const SizedBox(width: 5), + Text( + 'fees'.tr( + args: [fees.toString(), currencyName]), + style: const TextStyle( + color: orangeC, + fontSize: 17, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + const SizedBox(width: 10), + ], + ), + const SizedBox(height: 10), + TextField( + textInputAction: TextInputAction.done, + onEditingComplete: () async => + canValidate ? await executeTransfert() : null, + key: keyAmountField, + controller: walletViewProvider.payAmount, + autofocus: true, + maxLines: 1, + textAlign: TextAlign.center, + keyboardType: const TextInputType.numberWithOptions( + decimal: true), + onChanged: (_) async { + fees = await sub.txFees( + defaultWallet.address, + toAddress, + double.parse( + walletViewProvider.payAmount.text == '' + ? '0' + : walletViewProvider.payAmount.text)); + log.d(fees); + setState(() {}); + }, + inputFormatters: [ + FilteringTextInputFormatter.deny(',', + replacementString: '.'), + FilteringTextInputFormatter.allow( + RegExp(r'(^\d+\.?\d{0,2})')), + ], + decoration: InputDecoration( + hintText: '0.00', + suffix: Text(isUdUnit + ? 'ud'.tr(args: ['']) + : currencyName), // udUnitDisplay(40), + filled: true, + fillColor: Colors.transparent, + focusedBorder: OutlineInputBorder( + borderSide: + BorderSide(color: Colors.grey[500]!, width: 2), + borderRadius: BorderRadius.circular(8), + ), + contentPadding: const EdgeInsets.all(20), + ), + style: const TextStyle( + fontSize: 35, + color: Colors.black, + fontWeight: FontWeight.w600, + ), + ), + const Spacer(), + SizedBox( + width: double.infinity, + height: 60, + child: ElevatedButton( + key: keyConfirmPayment, + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, elevation: 4, + backgroundColor: orangeC, // foreground + ), + onPressed: canValidate + ? () async => await executeTransfert() + : null, + child: Text( + 'executeTheTransfer'.tr(), + style: const TextStyle( + fontSize: 20, fontWeight: FontWeight.w600), + ), + ), + ), + const Spacer(), + ]), + ), + ), + ); + }); + }).then((value) => walletViewProvider.payAmount.text = ''); +} + +Future infoFeesPopup(BuildContext context) async { + return showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: backgroundColor, + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.info_outlined, color: orangeC, size: 40), + const SizedBox(height: 20), + Text( + 'feesExplanation'.tr(), + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 30), + Text( + 'feesExplanationDetails'.tr(), + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w300), + ), + const SizedBox(height: 5), + InkWell( + onTap: () async => await _launchUrl('https://duniter.org'), + child: Container( + padding: const EdgeInsets.only( + bottom: 2, + ), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.blueAccent, + width: 1, + ))), + child: Text( + 'moreInfo'.tr(), + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w300, + color: Colors.blueAccent, + // decoration: TextDecoration.underline, + ), + ), + ), + ), + ], + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + key: keyInfoPopup, + child: Padding( + padding: const EdgeInsets.all(8), + child: Text( + 'gotit'.tr(), + style: const TextStyle( + fontSize: 21, + color: Color(0xffD80000), + ), + ), + ), + onPressed: () { + Navigator.pop(context, true); + }, + ), + ], + ) + ], + ); + }, + ); +} + +Future _launchUrl(String url) async { + if (!await launchUrl(Uri.parse(url))) { + throw Exception('Could not launch $url'); + } +} diff --git a/pubspec.lock b/pubspec.lock index c29b20d..f7c1218 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1532,7 +1532,7 @@ packages: source: hosted version: "0.2.0" url_launcher: - dependency: transitive + dependency: "direct main" description: name: url_launcher sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 diff --git a/pubspec.yaml b/pubspec.yaml index 5a987af..53e952b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -66,6 +66,7 @@ dependencies: riverpod: ^2.1.1 tutorial_coach_mark: ^1.2.8 confetti: ^0.7.0 + url_launcher: ^6.1.11 dev_dependencies: # flutter_launcher_icons: ^0.9.2