Merge branch 'paymentEndingRefactor'

This commit is contained in:
poka 2022-05-24 18:47:04 +02:00
commit 047720e65f
16 changed files with 329 additions and 72 deletions

View File

@ -7,6 +7,9 @@ import 'package:hive/hive.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
// Version of box data
const int dataVersion = 1;
// Files paths // Files paths
Directory? appPath; Directory? appPath;

View File

@ -5,28 +5,32 @@ part 'wallet_data.g.dart';
@HiveType(typeId: 0) @HiveType(typeId: 0)
class WalletData extends HiveObject { class WalletData extends HiveObject {
@HiveField(0) @HiveField(0)
int? chest; int? version;
@HiveField(1) @HiveField(1)
String? address; int? chest;
@HiveField(2) @HiveField(2)
int? number; String? address;
@HiveField(3) @HiveField(3)
String? name; int? number;
@HiveField(4) @HiveField(4)
int? derivation; String? name;
@HiveField(5) @HiveField(5)
String? imageName; int? derivation;
@HiveField(6) @HiveField(6)
String? imageName;
@HiveField(7)
File? imageFile; File? imageFile;
WalletData( WalletData(
{this.chest, {this.version,
this.chest,
this.address, this.address,
this.number, this.number,
this.name, this.name,

View File

@ -17,33 +17,36 @@ class WalletDataAdapter extends TypeAdapter<WalletData> {
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
}; };
return WalletData( return WalletData(
chest: fields[0] as int?, version: fields[0] as int?,
address: fields[1] as String?, chest: fields[1] as int?,
number: fields[2] as int?, address: fields[2] as String?,
name: fields[3] as String?, number: fields[3] as int?,
derivation: fields[4] as int?, name: fields[4] as String?,
imageName: fields[5] as String?, derivation: fields[5] as int?,
imageFile: fields[6] as File?, imageName: fields[6] as String?,
imageFile: fields[7] as File?,
); );
} }
@override @override
void write(BinaryWriter writer, WalletData obj) { void write(BinaryWriter writer, WalletData obj) {
writer writer
..writeByte(7) ..writeByte(8)
..writeByte(0) ..writeByte(0)
..write(obj.chest) ..write(obj.version)
..writeByte(1) ..writeByte(1)
..write(obj.address) ..write(obj.chest)
..writeByte(2) ..writeByte(2)
..write(obj.number) ..write(obj.address)
..writeByte(3) ..writeByte(3)
..write(obj.name) ..write(obj.number)
..writeByte(4) ..writeByte(4)
..write(obj.derivation) ..write(obj.name)
..writeByte(5) ..writeByte(5)
..write(obj.imageName) ..write(obj.derivation)
..writeByte(6) ..writeByte(6)
..write(obj.imageName)
..writeByte(7)
..write(obj.imageFile); ..write(obj.imageFile);
} }

View File

@ -2,6 +2,9 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gecko/globals.dart'; import 'package:gecko/globals.dart';
import 'package:gecko/models/chest_data.dart'; import 'package:gecko/models/chest_data.dart';
import 'package:gecko/models/wallet_data.dart';
import 'package:gecko/providers/substrate_sdk.dart';
import 'package:provider/provider.dart';
class ChestProvider with ChangeNotifier { class ChestProvider with ChangeNotifier {
void rebuildWidget() { void rebuildWidget() {
@ -10,8 +13,9 @@ class ChestProvider with ChangeNotifier {
Future deleteChest(context, ChestData _chest) async { Future deleteChest(context, ChestData _chest) async {
final bool? _answer = await (_confirmDeletingChest(context, _chest.name)); final bool? _answer = await (_confirmDeletingChest(context, _chest.name));
SubstrateSdk _sub = Provider.of<SubstrateSdk>(context, listen: false);
if (_answer!) { if (_answer!) {
await _sub.deleteAccounts(getChestWallets(_chest));
await chestBox.delete(_chest.key); await chestBox.delete(_chest.key);
if (chestBox.isEmpty) { if (chestBox.isEmpty) {
await configBox.put('currentChest', 0); await configBox.put('currentChest', 0);
@ -28,6 +32,17 @@ class ChestProvider with ChangeNotifier {
} }
} }
List<String> getChestWallets(ChestData _chest) {
List<String> toDelete = [];
log.d(_chest.key);
walletBox.toMap().forEach((key, WalletData value) {
if (value.chest == _chest.key) {
toDelete.add(value.address!);
}
});
return toDelete;
}
Future<bool?> _confirmDeletingChest(context, String? _walletName) async { Future<bool?> _confirmDeletingChest(context, String? _walletName) async {
return showDialog<bool>( return showDialog<bool>(
context: context, context: context,

View File

@ -81,6 +81,7 @@ class GenerateWalletsProvider with ChangeNotifier {
int? chestKey = chestBox.keys.last; int? chestKey = chestBox.keys.last;
WalletData myWallet = WalletData( WalletData myWallet = WalletData(
version: dataVersion,
chest: chestKey, chest: chestKey,
address: address, address: address,
number: 0, number: 0,

View File

@ -169,6 +169,7 @@ class MyWalletsProvider with ChangeNotifier {
context, _currentChest.address!, _newDerivationNbr, pinCode); context, _currentChest.address!, _newDerivationNbr, pinCode);
WalletData newWallet = WalletData( WalletData newWallet = WalletData(
version: dataVersion,
chest: _chest, chest: _chest,
address: address, address: address,
number: _newWalletNbr, number: _newWalletNbr,

View File

@ -12,6 +12,7 @@ import 'package:polkawallet_sdk/polkawallet_sdk.dart';
import 'package:polkawallet_sdk/storage/keyring.dart'; import 'package:polkawallet_sdk/storage/keyring.dart';
import 'package:polkawallet_sdk/storage/types/keyPairData.dart'; import 'package:polkawallet_sdk/storage/types/keyPairData.dart';
import 'package:truncate/truncate.dart'; import 'package:truncate/truncate.dart';
// import 'package:web_socket_channel/io.dart';
class SubstrateSdk with ChangeNotifier { class SubstrateSdk with ChangeNotifier {
final int ss58 = 42; final int ss58 = 42;
@ -47,6 +48,28 @@ class SubstrateSdk with ChangeNotifier {
n.endpoint = configBox.get('endpoint'); n.endpoint = configBox.get('endpoint');
n.ss58 = ss58; n.ss58 = ss58;
node.add(n); node.add(n);
int timeout = 7000;
// if (n.endpoint!.startsWith('ws://')) {
// timeout = 5000;
// }
//// Check websocket conenction - only for wss
// final channel = IOWebSocketChannel.connect(
// Uri.parse('wss://192.168.1.72:9944'),
// );
// channel.stream.listen(
// (dynamic message) {
// log.d('message $message');
// },
// onDone: () {
// log.d('ws channel closed');
// },
// onError: (error) {
// log.d('ws error $error');
// },
// );
if (sdk.api.connectedNode?.endpoint != null) { if (sdk.api.connectedNode?.endpoint != null) {
await sdk.api.setting.unsubscribeBestNumber(); await sdk.api.setting.unsubscribeBestNumber();
@ -55,7 +78,7 @@ class SubstrateSdk with ChangeNotifier {
isLoadingEndpoint = true; isLoadingEndpoint = true;
notifyListeners(); notifyListeners();
final res = await sdk.api.connectNode(keyring, node).timeout( final res = await sdk.api.connectNode(keyring, node).timeout(
const Duration(seconds: 7), Duration(milliseconds: timeout),
onTimeout: () => null, onTimeout: () => null,
); );
isLoadingEndpoint = false; isLoadingEndpoint = false;
@ -209,6 +232,13 @@ class SubstrateSdk with ChangeNotifier {
} }
} }
Future<void> deleteAccounts(List<String> address) async {
for (var a in address) {
final account = getKeypair(a);
await sdk.api.keyring.deleteAccount(keyring, account);
}
}
Future<String> generateMnemonic({String lang = appLang}) async { Future<String> generateMnemonic({String lang = appLang}) async {
final gen = await sdk.api.keyring.generateMnemonic(ss58); final gen = await sdk.api.keyring.generateMnemonic(ss58);
generatedMnemonic = gen.mnemonic!; generatedMnemonic = gen.mnemonic!;
@ -271,6 +301,11 @@ class SubstrateSdk with ChangeNotifier {
required String password}) async { required String password}) async {
setCurrentWallet(fromAddress); setCurrentWallet(fromAddress);
log.d(keyring.current.address);
log.d(fromAddress);
log.d(password);
log.d(await checkPassword(fromAddress, password));
final sender = TxSenderData( final sender = TxSenderData(
keyring.current.address, keyring.current.address,
keyring.current.pubKey, keyring.current.pubKey,
@ -295,25 +330,76 @@ class SubstrateSdk with ChangeNotifier {
} }
} }
Future<String> idtyStatus(String address) async {
// var tata = await sdk.webView!
// .evalJavascript('api.query.system.account("$address")');
var idtyIndex = await sdk.webView!
.evalJavascript('api.query.identity.identityIndexOf("$address")');
if (idtyIndex == null) {
return 'noid';
}
final idtyStatus = await sdk.webView!
.evalJavascript('api.query.identity.identities($idtyIndex)');
if (idtyStatus != null) {
final String _status = idtyStatus['status'];
log.d(_status);
return (_status);
} else {
return 'expired';
}
}
Future<String> confirmIdentity(
String address, String name, String password) async {
// Confirm identity
setCurrentWallet(address);
log.d('idty: ' + keyring.current.address!);
final sender = TxSenderData(
keyring.current.address,
keyring.current.pubKey,
);
final txInfo = TxInfoData(
'identity',
'confirmIdentity',
sender,
);
try {
final tata = await sdk.api.tx.signAndSend(
txInfo,
[name],
password,
);
log.d(tata);
return 'confirmed';
} on Exception catch (e) {
log.e(e);
// if (e.toString() == 'Exception: password check failed') {
// throw PasswordException('Bad password');
// }
return e.toString();
}
}
Future<String> derive( Future<String> derive(
BuildContext context, String address, int number, String password) async { BuildContext context, String address, int number, String password) async {
final keypair = getKeypair(address); final keypair = getKeypair(address);
final seedMap = final seedMap =
await keyring.store.getDecryptedSeed(keypair.pubKey, password); await keyring.store.getDecryptedSeed(keypair.pubKey, password);
print(seedMap);
if (seedMap?['type'] != 'mnemonic') return ''; if (seedMap?['type'] != 'mnemonic') return '';
final List seedList = seedMap!['seed'].split('//'); final List seedList = seedMap!['seed'].split('//');
generatedMnemonic = seedList[0]; generatedMnemonic = seedList[0];
int sourceDerivation = -1; // To get derivation number of this account
if (seedList.length > 1) {
sourceDerivation = int.parse(seedList[1]);
}
print(generatedMnemonic);
print(sourceDerivation);
return await importAccount(fromMnemonic: true, derivePath: '//$number'); return await importAccount(
fromMnemonic: true, derivePath: '//$number', password: password);
} }
Future<bool> isMnemonicValid(String mnemonic) async { Future<bool> isMnemonicValid(String mnemonic) async {
@ -369,3 +455,8 @@ String getShortPubkey(String pubkey) {
return pubkeyShort; return pubkeyShort;
} }
class PasswordException implements Exception {
String cause;
PasswordException(this.cause);
}

View File

@ -148,6 +148,124 @@ class WalletOptionsProvider with ChangeNotifier {
} }
} }
Widget idtyStatus(BuildContext context, String address,
{bool isOwner = false}) {
return Consumer<SubstrateSdk>(builder: (context, _sub, _) {
return FutureBuilder(
future: _sub.idtyStatus(address),
initialData: '...',
builder: (context, snapshot) {
switch (snapshot.data.toString()) {
case 'noid':
{
return Column(children: const <Widget>[
Text(
'Aucune identité',
style: TextStyle(fontSize: 18, color: Colors.black),
),
]);
}
case 'Created':
{
return Column(children: <Widget>[
isOwner
? InkWell(
child: const Text(
'Identité créé, cliquez pour la confirmer',
style:
TextStyle(fontSize: 18, color: Colors.black),
),
onTap: () async {
await validateIdentity(context);
},
)
: const Text(
'Identité créé',
style: TextStyle(fontSize: 18, color: Colors.black),
),
]);
}
case 'ConfirmedByOwner':
{
return Column(children: const <Widget>[
Text(
'Identité confirmé',
style: TextStyle(fontSize: 18, color: Colors.black),
),
]);
}
case 'Validated':
{
return Column(children: const <Widget>[
Text(
'Membre validé !',
style: TextStyle(fontSize: 18, color: Colors.black),
),
]);
}
case 'expired':
{
return Column(children: const <Widget>[
Text(
'Identité expiré',
style: TextStyle(fontSize: 18, color: Colors.black),
),
]);
}
}
return SizedBox(
width: 230,
child: Column(children: const <Widget>[
Text(
'Statut inconnu',
style: TextStyle(fontSize: 18, color: Colors.black),
),
]),
);
});
});
}
Future<String?> validateIdentity(BuildContext context) async {
TextEditingController idtyName = TextEditingController();
SubstrateSdk _sub = Provider.of<SubstrateSdk>(context, listen: false);
WalletOptionsProvider _walletOptions =
Provider.of<WalletOptionsProvider>(context, listen: false);
MyWalletsProvider _myWalletProvider =
Provider.of<MyWalletsProvider>(context, listen: false);
return showDialog<String>(
context: context,
barrierDismissible: true, // user must tap button!
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Confirmez votre identité'),
content: SizedBox(
height: 100,
child: Column(children: [
const Text('Nom:'),
TextField(
controller: idtyName,
)
]),
),
actions: <Widget>[
TextButton(
child: const Text("Valider"),
onPressed: () async {
_sub.confirmIdentity(_walletOptions.address.text, idtyName.text,
_myWalletProvider.pinCode);
Navigator.pop(context);
},
),
],
);
},
);
}
void reloadBuild() { void reloadBuild() {
notifyListeners(); notifyListeners();
} }
@ -167,9 +285,9 @@ class WalletOptionsProvider with ChangeNotifier {
} }
} }
Widget balance(BuildContext context, String address, double size) { Map<String, String> balanceCache = {};
String balanceCache = '';
Widget balance(BuildContext context, String address, double size) {
return Column(children: <Widget>[ return Column(children: <Widget>[
Consumer<SubstrateSdk>(builder: (context, _sdk, _) { Consumer<SubstrateSdk>(builder: (context, _sdk, _) {
return FutureBuilder( return FutureBuilder(
@ -177,14 +295,25 @@ Widget balance(BuildContext context, String address, double size) {
builder: (BuildContext context, AsyncSnapshot<num?> _balance) { builder: (BuildContext context, AsyncSnapshot<num?> _balance) {
if (_balance.connectionState != ConnectionState.done || if (_balance.connectionState != ConnectionState.done ||
_balance.hasError) { _balance.hasError) {
return Text(balanceCache, if (balanceCache[address] != null) {
style: TextStyle( return Text(balanceCache[address]!,
fontSize: isTall ? size : size * 0.9, style: TextStyle(
)); fontSize: isTall ? size : size * 0.9,
));
} else {
return SizedBox(
height: 15,
width: 15,
child: CircularProgressIndicator(
color: orangeC,
strokeWidth: 2,
),
);
}
} }
balanceCache = "${_balance.data.toString()} $currencyName"; balanceCache[address] = "${_balance.data.toString()} $currencyName";
return Text( return Text(
balanceCache, balanceCache[address]!,
style: TextStyle( style: TextStyle(
fontSize: isTall ? size : 18, fontSize: isTall ? size : 18,
), ),

View File

@ -58,7 +58,7 @@ class ChooseWalletScreen extends StatelessWidget {
destAddress: _walletViewProvider.outputPubkey.text, destAddress: _walletViewProvider.outputPubkey.text,
amount: amount:
double.parse(_walletViewProvider.payAmount.text), double.parse(_walletViewProvider.payAmount.text),
password: pin); password: pin.toUpperCase());
await paymentsResult(context, resultPay); await paymentsResult(context, resultPay);
}, },
child: const Text( child: const Text(

View File

@ -140,8 +140,9 @@ class ConfirmStoreWallet extends StatelessWidget with ChangeNotifier {
fromMnemonic: true, fromMnemonic: true,
mnemonic: _generateWalletProvider mnemonic: _generateWalletProvider
.generatedMnemonic!, .generatedMnemonic!,
password: password: _generateWalletProvider
_generateWalletProvider.pin.text, .pin.text
.toUpperCase(),
derivePath: '//2'); derivePath: '//2');
await _generateWalletProvider.storeHDWChest( await _generateWalletProvider.storeHDWChest(
address, address,

View File

@ -26,7 +26,7 @@ class GenerateFastChestScreen extends StatelessWidget {
_generateWalletProvider.pin.text = kDebugMode && debugPin _generateWalletProvider.pin.text = kDebugMode && debugPin
? 'AAAAA' ? 'AAAAA'
: _generateWalletProvider.changePinCode(reload: false); : _generateWalletProvider.changePinCode(reload: false).toUpperCase();
return WillPopScope( return WillPopScope(
onWillPop: () { onWillPop: () {

View File

@ -90,6 +90,10 @@ class WalletOptions extends StatelessWidget {
walletName(walletProvider, _walletOptions), walletName(walletProvider, _walletOptions),
SizedBox(height: isTall ? 5 : 0), SizedBox(height: isTall ? 5 : 0),
balance(context, walletProvider.address.text, 20), balance(context, walletProvider.address.text, 20),
SizedBox(height: isTall ? 5 : 0),
_walletOptions.idtyStatus(
context, _walletOptions.address.text,
isOwner: true),
]), ]),
const Spacer(flex: 3), const Spacer(flex: 3),
]), ]),
@ -212,8 +216,8 @@ class WalletOptions extends StatelessWidget {
walletProvider.isEditing walletProvider.isEditing
? 'assets/walletOptions/android-checkmark.png' ? 'assets/walletOptions/android-checkmark.png'
: 'assets/walletOptions/edit.png', : 'assets/walletOptions/edit.png',
width: 20, width: 25,
height: 20), height: 25),
), ),
), ),
), ),

View File

@ -23,7 +23,7 @@ class OnboardingStepThirteen extends StatelessWidget {
_generateWalletProvider.pin.text = kDebugMode && debugPin _generateWalletProvider.pin.text = kDebugMode && debugPin
? 'AAAAA' ? 'AAAAA'
: _generateWalletProvider.changePinCode(reload: false); : _generateWalletProvider.changePinCode(reload: false).toUpperCase();
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(

View File

@ -27,7 +27,6 @@ class WalletViewScreen extends StatelessWidget {
Provider.of<WalletsProfilesProvider>(context, listen: false); Provider.of<WalletsProfilesProvider>(context, listen: false);
CesiumPlusProvider _cesiumPlusProvider = CesiumPlusProvider _cesiumPlusProvider =
Provider.of<CesiumPlusProvider>(context, listen: false); Provider.of<CesiumPlusProvider>(context, listen: false);
_historyProvider.pubkey = pubkey!; _historyProvider.pubkey = pubkey!;
return Scaffold( return Scaffold(
@ -285,21 +284,21 @@ class WalletViewScreen extends StatelessWidget {
primary: orangeC, // background primary: orangeC, // background
onPrimary: Colors.white, // foreground onPrimary: Colors.white, // foreground
), ),
onPressed: _walletViewProvider.payAmount.text != onPressed:
'' _walletViewProvider.payAmount.text != ''
? () { ? () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) { builder: (context) {
return UnlockingWallet( return UnlockingWallet(
wallet: defaultWallet, wallet: defaultWallet,
action: "pay"); action: "pay");
}, },
), ),
); );
} }
: null, : null,
child: const Text( child: const Text(
'Effectuer le virement', 'Effectuer le virement',
style: TextStyle( style: TextStyle(
@ -325,6 +324,9 @@ class WalletViewScreen extends StatelessWidget {
CesiumPlusProvider _cesiumPlusProvider) { CesiumPlusProvider _cesiumPlusProvider) {
const double _avatarSize = 140; const double _avatarSize = 140;
WalletOptionsProvider _walletOptions =
Provider.of<WalletOptionsProvider>(context, listen: false);
return Column(children: <Widget>[ return Column(children: <Widget>[
Container( Container(
height: 10, height: 10,
@ -365,9 +367,11 @@ class WalletViewScreen extends StatelessWidget {
const SizedBox(height: 25), const SizedBox(height: 25),
Consumer<WalletOptionsProvider>( Consumer<WalletOptionsProvider>(
builder: (context, walletProvider, _) { builder: (context, walletProvider, _) {
return balance(context, pubkey!, 20); return balance(context, pubkey!, 22);
}), }),
//// const SizedBox(height: 10),
_walletOptions.idtyStatus(context, pubkey!, isOwner: false),
// if (username == null && // if (username == null &&
// g1WalletsBox.get(pubkey)?.username == null) // g1WalletsBox.get(pubkey)?.username == null)
// Query( // Query(

View File

@ -140,7 +140,7 @@ packages:
name: built_value name: built_value
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "8.3.0" version: "8.3.2"
carousel_slider: carousel_slider:
dependency: "direct main" dependency: "direct main"
description: description:
@ -259,7 +259,7 @@ packages:
name: cross_file name: cross_file
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.3.3" version: "0.3.3+1"
crypto: crypto:
dependency: "direct main" dependency: "direct main"
description: description:
@ -431,7 +431,7 @@ packages:
name: get name: get
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.6.3" version: "4.6.5"
get_storage: get_storage:
dependency: transitive dependency: transitive
description: description:
@ -636,7 +636,7 @@ packages:
name: infinite_scroll_pagination name: infinite_scroll_pagination
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.0" version: "3.2.0"
integration_test: integration_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -999,7 +999,7 @@ packages:
description: description:
path: printing path: printing
ref: HEAD ref: HEAD
resolved-ref: "95bbc1f33b7cb45f7a4b98088b96bc541fefc495" resolved-ref: "96fe6ef3c1ee702a86dd311b3feebdb1f7491cd6"
url: "https://github.com/DavBfr/dart_pdf.git" url: "https://github.com/DavBfr/dart_pdf.git"
source: git source: git
version: "5.9.1" version: "5.9.1"
@ -1016,7 +1016,7 @@ packages:
name: provider name: provider
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.2" version: "6.0.3"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@ -1356,7 +1356,7 @@ packages:
source: hosted source: hosted
version: "1.0.1" version: "1.0.1"
web_socket_channel: web_socket_channel:
dependency: transitive dependency: "direct main"
description: description:
name: web_socket_channel name: web_socket_channel
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"

View File

@ -5,7 +5,7 @@ description: Pay with G1.
# pub.dev using `pub publish`. This is preferred for private packages. # pub.dev using `pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 0.0.5+3 version: 0.0.6+2
environment: environment:
sdk: '>=2.12.0 <3.0.0' sdk: '>=2.12.0 <3.0.0'
@ -80,6 +80,7 @@ dependencies:
url: https://github.com/poka-IT/sdk.git url: https://github.com/poka-IT/sdk.git
ref: fixAndroidActivityVersion ref: fixAndroidActivityVersion
dots_indicator: ^2.1.0 dots_indicator: ^2.1.0
web_socket_channel: ^2.2.0
dev_dependencies: dev_dependencies:
# flutter_launcher_icons: ^0.9.2 # flutter_launcher_icons: ^0.9.2