diff --git a/README.md b/README.md index ef204830dc4f91438e57799b5a1da4cee7c37c02..641b7499e40ee32024b29c9c3e0c5d5f8661d069 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,7 @@ Thanks! - undraw intro images: https://undraw.co/license - Chipcard https://commons.wikimedia.org/wiki/File:Chipcard.svg under the Creative Commons Attribution-Share Alike 3.0 Unported license. +- [POS svg from wikimedia](https://commons.wikimedia.org/wiki/File:Card_Terminal_POS_Flat_Icon_Vector.svg) CC-BY-SA 4.0 Thanks! diff --git a/assets/translations/en.json b/assets/translations/en.json index 1e8c5e8bfcc19a354b24c14966b4af42983c5df5..ffb8b0742c3c7fa615fc72dac8ef74bd82810355 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -141,5 +141,36 @@ "error_importing_wallet": "Error importing wallet", "no_nodes_found": "We couldn't communicate with any node. Please try again later.", "fetch_tx_error": "Something went wrong while fetching your transactions.", - "retry": "RETRY" + "retry": "RETRY", + "creditCardKey_title": "Wallet Created!", + "creditCardKey_desc": "Congratulations! You have successfully created your Äž1 wallet. Please note that everything is stored on this device, so make sure not to delete this application to avoid losing your wallet. Soon we will show you how to make a backup of your wallet, in case your device has any issues and you need to access your wallet.", + "creditCardKey_web_title": "Wallet Created!", + "creditCardKey_web_desc": "Congratulations! You have successfully created your Äž1 wallet. It is important to note that this wallet is stored only in your browser. Therefore, if you close the browser and open it again, make sure that the same wallet appears. If not, you may be using an unsupported browser. Remember that we also have an app. Soon we will show you how to make a backup of your wallet, in case your device has any issues and you need to access your wallet.", + "creditCardPubKey_title": "Wallet Public Address", + "creditCardPubKey_desc": "On this screen you will find the abbreviated public address of your wallet. You can copy it by tapping on it to share it with others.", + "paySearchUserKey_title": "Making Payments with Äž1", + "paySearchUserKey_desc": "From this section you can make payments in Äž1. You can search for users on the network or scan QR codes to make your payments.", + "payAmountKey_title": "Enter Payment Amount", + "payAmountKey_desc": "In this field you should indicate the amount you want to pay in Äž1.", + "paySentKey_title": "Sending Äž1", + "paySentKey_desc": "Once you have indicated the payment amount, you only need to press the 'Send' button to make the payment. It is important to note that this wallet does not have a password, so it operates like a pocket wallet for quick transactions with small amounts.", + "receiveMainKey_title": "Receiving Äž1", + "receiveMainKey_desc": "This works like the card machines in stores for customers to make payments, but here we operate with QR codes. Here you can generate QR codes so that other people can scan them and make payments to you.", + "receiveQrKey_desc": "On this screen you will find your own QR code that you can share with others to receive payments in Äž1.", + "receiveAmountKey_title": "QR with Amounts", + "receiveAmountKey_desc": "If you want to sell a product or service, you can generate a QR code with your address and the amount to be charged in Äž1. Please note that these QR codes only work between Äž1nkgo wallets for now.", + "receiveSumKey_title": "Quick Total", + "receiveSumKey_desc": "You can also generate a QR code with the total amount of a purchase by adding up the prices of the items you are selling.", + "contactsMainKey_title": "Contacts", + "contactsMainKey_desc": "In this section you can save your most frequent contacts and scan QR codes from other people.", + "txMainKey_title": "Transactions", + "txMainKey_desc": "Here you can see the history of your transactions. If your wallet is empty, to start using Äž1, you can offer your services on markets or web platforms such as Girala, Gchange, among others. If you already have Äž1, you can transfer them to this Äž1nkgo wallet and start using it.", + "txBalanceKey_title": "Balance", + "txBalanceKey_desc": "On this screen you can see the current balance of your Äž1nkgo wallet.", + "txRefreshKey_title": "Refresh", + "txRefreshKey_desc": "If you are waiting for a payment, you can press this button to refresh the screen. However, this wallet will also do it periodically for you and send you notifications of new payments.", + "infoMainKey_title": "More Information About the Wallet", + "infoMainKey_desc": "Here you will find more information about your virtual wallet.", + "exportMainKey_title": "Exporting the Wallet", + "exportMainKey_desc": "It is important that you make a backup of your wallet as soon as possible and keep it safe, so that you can import it into another browser or the app, or restore your wallet in case you lose your device. To do this, press this 'Export' button, which will allow you to download a file with all the information from your wallet. This way, you can import your wallet into another browser or device and have access to your funds at all times." } diff --git a/assets/translations/es.json b/assets/translations/es.json index 1e0d9d59c0fc06a14b86c977600d6475d190b8d6..1a06801a6ff98b4be62fa67c32f979c7704b8eee 100644 --- a/assets/translations/es.json +++ b/assets/translations/es.json @@ -141,5 +141,36 @@ "error_importing_wallet": "Error importando monedero", "no_nodes_found": "No hemos podido comunicarnos con ningún nodo. Por favor, inténtalo de nuevo más tarde.", "fetch_tx_error": "Algo ha ido mal al obtener tus transacciones", - "retry": "REINTENTAR" + "retry": "REINTENTAR", + "creditCardKey_title": "¡Monedero creado!", + "creditCardKey_desc": "¡Felicidades! Has creado tu monedero Äž1 con éxito. Ten en cuenta que todo se almacena en este dispositivo, asà que asegúrate de no borrar esta aplicación para no perder tu monedero. En breve te enseñaremos cómo hacer un backup del monedero, por si tu dispositivo sufre algún problema, puedas acceder a tu monedero sin problemas.", + "creditCardKey_web_title": "¡Monedero creado!", + "creditCardKey_web_desc": "¡Felicidades! Has creado tu monedero Äž1 con éxito. Es importante que sepas que este monedero está almacenado sólo en tu navegador. Por eso, si cierras el navegador y lo abres de nuevo, asegúrate de que te sale el mismo monedero. Si no es asÃ, puede que estés usando un navegador no soportado. Recuerda que también tenemos una app. En breve te enseñaremos cómo hacer un backup del monedero, por si tu dispositivo sufre algún problema, puedas acceder a tu monedero sin problemas.", + "creditCardPubKey_title": "Dirección Pública del Monedero", + "creditCardPubKey_desc": "En esta pantalla encontrarás la dirección pública abreviada de tu monedero. Puedes copiarla pulsando sobre ella para compartirla con otras personas.", + "paySearchUserKey_title": "Pagos con Junas", + "paySearchUserKey_desc": "Desde esta sección puedes hacer pagos en Junas. Puedes buscar usuarios/as en la red o escanear códigos QR para hacer tus pagos.", + "payAmountKey_title": "Indicar cantidad a pagar", + "payAmountKey_desc": "En este campo deberás indicar la cantidad que deseas pagar en Junas.", + "paySentKey_title": "Una vez indicada la cantidad, solo necesitas presionar el botón 'Enviar' para realizar el pago. Es importante tener en cuenta que este monedero no tiene contraseña, asà que es como un monedero de bolsillo para operar rápidamente con pequeñas cantidades.", + "paySentKey_desc": "Recibiendo Junas", + "receiveMainKey_title": "Si deseas recibir Junas, esta sección es para ti.", + "receiveMainKey_desc": "Esto es como los datafonos de las tiendas para cobrar a clientes, pero aquà funcionamos con QRs. Aquà podrás generar códigos QR para que otras personas puedan escanearlos y realizarte pagos.", + "receiveQrKey_desc": "En esta pantalla encontrarás tu propio código QR que podrás compartir con otras personas para recibir pagos en Junas.", + "receiveAmountKey_title": "QR con cantidades", + "receiveAmountKey_desc": "Si quieres vender algún producto o servicio, puedes generar un código QR con tu dirección y la cantidad a cobrar en Junas. Ten en cuenta que estos QRs por ahora solo funcionan entre monederos Äž1nkgo.", + "receiveSumKey_title": "Cuenta rápida", + "receiveSumKey_desc": "También puedes generar un código QR con el total de una compra, sumando el precio de los artÃculos que estás vendiendo.", + "contactsMainKey_title": "Contactos", + "contactsMainKey_desc": "En esta sección puedes guardar tus contactos más frecuentes y escanear códigos QR de otras personas.", + "txMainKey_title": "Transacciones", + "txMainKey_desc": "Aquà podrás ver el historial de tus transacciones. Si tu monedero está vacÃo, para empezar a usar Junas, puedes ofrecer tus servicios en mercados o plataformas webs como Girala, Gchange, entre otros. Si ya tienes Junas, puedes transferirlos a este monedero Äž1nkgo y empezar a usarlo.", + "txBalanceKey_title": "Balance", + "txBalanceKey_desc": "En esta pantalla podrás ver el balance actual de tu monedero Äž1nkgo.", + "txRefreshKey_title": "Actualización", + "txRefreshKey_desc": "Si estás esperando un pago, puedes presionar este botón para refrescar la pantalla. Sin embargo, este monedero también lo hará periódicamente por ti y te enviará notificaciones de nuevos pagos.", + "infoMainKey_title": "Más información del Monedero", + "infoMainKey_desc": "Aquà encontrarás más información acerca de tu monedero virtual.", + "exportMainKey_title": "Exportación del Monedero", + "exportMainKey_desc": "Es importante que realices un backup de tu monedero cuanto antes y ponerlo a buen recaudo, para poder importarlo en otro navegador o en la App, o en cualquier caso restaurar tu monedero en caso de perder tu dispositivo. Para hacerlo, presiona este botón de 'Exportar', que te permitirá descargar un archivo con toda la información de tu monedero. De esta forma, podrás importar tu monedero en otro navegador o dispositivo y tener acceso a tus fondos en todo momento." } diff --git a/lib/data/models/app_cubit.dart b/lib/data/models/app_cubit.dart index 16c435c911bb9c8f04a064c0be73e4006c9f289e..163513af3c3b9b70cc8f327ebabc87ea849629f1 100644 --- a/lib/data/models/app_cubit.dart +++ b/lib/data/models/app_cubit.dart @@ -4,7 +4,7 @@ import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'app_state.dart'; class AppCubit extends HydratedCubit<AppState> { - AppCubit() : super(const AppState()); + AppCubit() : super(AppState()); @override String get storagePrefix => kIsWeb ? 'AppCubit' : super.storagePrefix; @@ -42,4 +42,13 @@ class AppCubit extends HydratedCubit<AppState> { void setExpertMode(bool value) { emit(state.copyWith(expertMode: value)); } + + void onFinishTutorial(String tutorialId) { + state.tutorials[tutorialId] = true; + emit(state.copyWith(tutorials: state.tutorials)); + } + + bool wasTutorialShown(String tutorialId) { + return state.tutorials[tutorialId] ?? false; + } } diff --git a/lib/data/models/app_state.dart b/lib/data/models/app_state.dart index ce04851e883a76c0eebc7cb5795b10fedc303af1..5742b78e97b5a84735962f96accf8dde7f722cf3 100644 --- a/lib/data/models/app_state.dart +++ b/lib/data/models/app_state.dart @@ -7,12 +7,13 @@ part 'app_state.g.dart'; @JsonSerializable() class AppState extends Equatable implements IsJsonSerializable<AppState> { - const AppState({ - this.introViewed = false, - this.warningViewed = false, - this.warningBrowserViewed = false, - this.expertMode = false, - }); + AppState( + {this.introViewed = false, + this.warningViewed = false, + this.warningBrowserViewed = false, + this.expertMode = false, + Map<String, bool>? tutorials}) + : tutorials = tutorials ?? <String, bool>{}; factory AppState.fromJson(Map<String, dynamic> json) => _$AppStateFromJson(json); @@ -21,19 +22,20 @@ class AppState extends Equatable implements IsJsonSerializable<AppState> { final bool warningViewed; final bool warningBrowserViewed; final bool expertMode; - - AppState copyWith({ - bool? introViewed, - bool? warningViewed, - bool? warningBrowserViewed, - bool? expertMode, - String? locale, - }) { + final Map<String, bool> tutorials; + + AppState copyWith( + {bool? introViewed, + bool? warningViewed, + bool? warningBrowserViewed, + bool? expertMode, + Map<String, bool>? tutorials}) { return AppState( introViewed: introViewed ?? this.introViewed, warningViewed: warningViewed ?? this.warningViewed, warningBrowserViewed: warningBrowserViewed ?? this.warningBrowserViewed, - expertMode: expertMode ?? this.expertMode); + expertMode: expertMode ?? this.expertMode, + tutorials: tutorials ?? this.tutorials); } @override @@ -43,6 +45,11 @@ class AppState extends Equatable implements IsJsonSerializable<AppState> { Map<String, dynamic> toJson() => _$AppStateToJson(this); @override - List<Object?> get props => - <Object>[introViewed, warningViewed, expertMode, warningBrowserViewed]; + List<Object?> get props => <Object>[ + introViewed, + warningViewed, + expertMode, + warningBrowserViewed, + tutorials + ]; } diff --git a/lib/main.dart b/lib/main.dart index 8d8ab92bfc77dc9af17cce93ddab93e792fd9de7..087942fd99d5f0ed1fe770dfcb358854c1b5326b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,6 +12,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:introduction_screen/introduction_screen.dart'; +import 'package:lehttp_overrides/lehttp_overrides.dart'; import 'package:once/once.dart'; import 'package:path_provider/path_provider.dart'; import 'package:pwa_install/pwa_install.dart'; @@ -44,6 +45,11 @@ import 'ui/ui_helpers.dart'; void main() async { await NotificationController.initializeLocalNotifications(); + // To resolve Let's Encrypt SSL certificate problems with Android 7.1.1 and below + if (Platform.isAndroid) { + HttpOverrides.global = LEHttpOverrides(); + } + /// Initialize packages WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); diff --git a/lib/ui/screens/fifth_screen.dart b/lib/ui/screens/fifth_screen.dart index 38f5a8cd3da482abf60becbb888345cabd872903..6c6de02068eefbf24971cf8c7bde82bc3f39ece6 100644 --- a/lib/ui/screens/fifth_screen.dart +++ b/lib/ui/screens/fifth_screen.dart @@ -4,30 +4,51 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pwa_install/pwa_install.dart'; import 'package:share_plus/share_plus.dart'; +import '../../cubit/bottom_nav_cubit.dart'; import '../../cubit/theme_cubit.dart'; import '../../data/models/app_cubit.dart'; import '../../data/models/app_state.dart'; import '../../shared_prefs.dart'; import '../notification_controller.dart'; +import '../tutorial.dart'; +import '../tutorial_keys.dart'; import '../ui_helpers.dart'; import '../widgets/bottom_widget.dart'; import '../widgets/card_drawer.dart'; import '../widgets/faq.dart'; import '../widgets/fifth_screen/export_dialog.dart'; +import '../widgets/fifth_screen/fifth_tutorial.dart'; import '../widgets/fifth_screen/grid_item.dart'; import '../widgets/fifth_screen/import_dialog.dart'; import '../widgets/fifth_screen/link_card.dart'; import '../widgets/fifth_screen/node_info.dart'; import '../widgets/fifth_screen/text_divider.dart'; -class FifthScreen extends StatelessWidget { +class FifthScreen extends StatefulWidget { const FifthScreen({super.key}); + @override + State<FifthScreen> createState() => _FifthScreenState(); +} + +class _FifthScreenState extends State<FifthScreen> { + late Tutorial tutorial; + + @override + void initState() { + tutorial = FifthTutorial(context); + if (context.read<BottomNavCubit>().state == 4) { + Future<void>.delayed(Duration.zero, () => tutorial.showTutorial()); + } + super.initState(); + } + @override Widget build(BuildContext context) { return BlocBuilder<AppCubit, AppState>( builder: (BuildContext context, AppState state) => Scaffold( appBar: AppBar( + key: infoMainKey, title: Text(tr('bottom_nav_fifth')), actions: <Widget>[ IconButton( @@ -102,8 +123,6 @@ class FifthScreen extends StatelessWidget { // Add more DropdownMenuItem for more languages ], ), - const TextDivider(text: 'faq_title'), - const FAQ(), const TextDivider(text: 'key_tools_title'), const SizedBox(height: 20), GridView.count( @@ -148,6 +167,7 @@ class FifthScreen extends StatelessWidget { }, ), GridItem( + key: exportMainKey, title: 'export_key', icon: Icons.download, onTap: () { @@ -170,6 +190,8 @@ class FifthScreen extends StatelessWidget { ); }), ]), + const TextDivider(text: 'faq_title'), + const FAQ(), if (state.expertMode) const TextDivider(text: 'technical_info_title'), if (state.expertMode) const NodeInfoCard(), diff --git a/lib/ui/screens/first_screen.dart b/lib/ui/screens/first_screen.dart index 451a01a19135f1b9a358de5317e05fae8f483c0b..ce94f0c395b37e0c8c715a77e7bd59a6cbf4535b 100644 --- a/lib/ui/screens/first_screen.dart +++ b/lib/ui/screens/first_screen.dart @@ -4,13 +4,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:web_browser_detect/web_browser_detect.dart'; +import '../../cubit/bottom_nav_cubit.dart'; import '../../data/models/app_cubit.dart'; import '../../data/models/app_state.dart'; import '../../data/models/payment_cubit.dart'; import '../../data/models/payment_state.dart'; +import '../tutorial.dart'; +import '../tutorial_keys.dart'; import '../widgets/bottom_widget.dart'; import '../widgets/card_drawer.dart'; import '../widgets/first_screen/credit_card.dart'; +import '../widgets/first_screen/first_tutorial.dart'; import '../widgets/first_screen/pay_contact_search_button.dart'; import '../widgets/first_screen/pay_form.dart'; @@ -22,6 +26,17 @@ class FirstScreen extends StatefulWidget { } class _FirstScreenState extends State<FirstScreen> { + late Tutorial tutorial; + + @override + void initState() { + tutorial = FirstTutorial(context); + if (context.read<BottomNavCubit>().state == 0) { + Future<void>.delayed(Duration.zero, () => tutorial.showTutorial()); + } + super.initState(); + } + @override Widget build(BuildContext context) => BlocBuilder<AppCubit, AppState>( builder: (BuildContext context, AppState state) { @@ -40,7 +55,6 @@ class _FirstScreenState extends State<FirstScreen> { ), ); } - // FIXME if (kIsWeb) { final Browser? browser = Browser.detectOrNull(); if (!state.warningBrowserViewed) { @@ -77,7 +91,7 @@ class _FirstScreenState extends State<FirstScreen> { //controller: _controller, // shrinkWrap: true, children: <Widget>[ - CreditCard(), + CreditCard(key: creditCardKey), const SizedBox(height: 8), Padding( padding: @@ -90,7 +104,7 @@ class _FirstScreenState extends State<FirstScreen> { ), ), const SizedBox(height: 10), - const PayContactSearchButton(), + PayContactSearchButton(key: paySearchUserKey), const SizedBox(height: 10), const PayForm(), const BottomWidget() diff --git a/lib/ui/screens/fourth_screen.dart b/lib/ui/screens/fourth_screen.dart index 7a67c4b058b03b03ac5609b4f0fd4fb77d319c68..3df8abeb586c0c15e1a39246aa323ab0a4581a77 100644 --- a/lib/ui/screens/fourth_screen.dart +++ b/lib/ui/screens/fourth_screen.dart @@ -1,10 +1,30 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../cubit/bottom_nav_cubit.dart'; +import '../tutorial.dart'; +import '../widgets/fourth_screen/fourth_tutorial.dart'; import '../widgets/fourth_screen/transaction_page.dart'; -class FourthScreen extends StatelessWidget { +class FourthScreen extends StatefulWidget { const FourthScreen({super.key}); + @override + State<FourthScreen> createState() => _FourthScreenState(); +} + +class _FourthScreenState extends State<FourthScreen> { + late Tutorial tutorial; + + @override + void initState() { + tutorial = FourthTutorial(context); + if (context.read<BottomNavCubit>().state == 3) { + Future<void>.delayed(Duration.zero, () => tutorial.showTutorial()); + } + super.initState(); + } + @override Widget build(BuildContext context) { return const TransactionsAndBalanceWidget(); diff --git a/lib/ui/screens/second_screen.dart b/lib/ui/screens/second_screen.dart index c250db5b2eb57a5547fc4520476e5daefa1cea62..a17dc0a3a8d7f9238952f2be0a54b275d5191764 100644 --- a/lib/ui/screens/second_screen.dart +++ b/lib/ui/screens/second_screen.dart @@ -1,16 +1,37 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../cubit/bottom_nav_cubit.dart'; +import '../tutorial.dart'; +import '../tutorial_keys.dart'; import '../widgets/card_drawer.dart'; import '../widgets/second_screen/card_terminal.dart'; +import '../widgets/second_screen/second_tutorial.dart'; -class SecondScreen extends StatelessWidget { +class SecondScreen extends StatefulWidget { const SecondScreen({super.key}); + @override + State<SecondScreen> createState() => _SecondScreenState(); +} + +class _SecondScreenState extends State<SecondScreen> { + late Tutorial tutorial; + + @override + void initState() { + tutorial = SecondTutorial(context); + if (context.read<BottomNavCubit>().state == 1) { + Future<void>.delayed(Duration.zero, () => tutorial.showTutorial()); + } + super.initState(); + } + @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text(tr('receive_g1'))), + appBar: AppBar(key: receiveMainKey, title: Text(tr('receive_g1'))), drawer: const CardDrawer(), body: Column(children: const <Widget>[SizedBox(height: 2), CardTerminal()]), diff --git a/lib/ui/screens/third_screen.dart b/lib/ui/screens/third_screen.dart index 7f8c0bd6cfb2b685675472e5a3ed79dffe688592..5d84d968eac5bd58d07cf32bb47205dce18e8ca4 100644 --- a/lib/ui/screens/third_screen.dart +++ b/lib/ui/screens/third_screen.dart @@ -2,13 +2,17 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../cubit/bottom_nav_cubit.dart'; import '../../data/models/contact.dart'; import '../../data/models/contact_cubit.dart'; import '../../g1/g1_helper.dart'; import '../contacts_cache.dart'; import '../qr_manager.dart'; +import '../tutorial.dart'; +import '../tutorial_keys.dart'; import '../widgets/card_drawer.dart'; import '../widgets/third_screen/contacts_page.dart'; +import '../widgets/third_screen/third_tutorial.dart'; class ThirdScreen extends StatefulWidget { const ThirdScreen({super.key}); @@ -18,47 +22,62 @@ class ThirdScreen extends StatefulWidget { } class _ThirdScreenState extends State<ThirdScreen> { + late Tutorial tutorial; + + @override + void initState() { + tutorial = ThirdTutorial(context); + if (context.read<BottomNavCubit>().state == 2) { + Future<void>.delayed(Duration.zero, () => tutorial.showTutorial()); + } + super.initState(); + } + @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: Text(tr('bottom_nav_trd')), actions: <Widget>[ - IconButton( - icon: const Icon(Icons.qr_code), - onPressed: () async { - final String? pubKey = await QrManager.qrScan(context); - if (pubKey != null && validateKey(pubKey)) { - final Contact contact = - await ContactsCache().getContact(pubKey); - if (!mounted) { - return; - } - if (!context.read<ContactsCubit>().isContact(pubKey)) { - context.read<ContactsCubit>().addContact(contact); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(tr('contact_added')), - ), - ); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(tr('contact_already_exists')), - ), - ); - } - } else { - if (!mounted) { - return; - } - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(tr('wrong_public_key')), - ), - ); - } - }), - const SizedBox(width: 5), - ]), + appBar: AppBar( + key: contactsMainKey, + title: Text(tr('bottom_nav_trd')), + actions: <Widget>[ + IconButton( + key: contactsQrKey, + icon: const Icon(Icons.qr_code), + onPressed: () async { + final String? pubKey = await QrManager.qrScan(context); + if (pubKey != null && validateKey(pubKey)) { + final Contact contact = + await ContactsCache().getContact(pubKey); + if (!mounted) { + return; + } + if (!context.read<ContactsCubit>().isContact(pubKey)) { + context.read<ContactsCubit>().addContact(contact); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(tr('contact_added')), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(tr('contact_already_exists')), + ), + ); + } + } else { + if (!mounted) { + return; + } + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(tr('wrong_public_key')), + ), + ); + } + }), + const SizedBox(width: 5), + ]), drawer: const CardDrawer(), body: const ContactsPage(), ); diff --git a/lib/ui/tutorial.dart b/lib/ui/tutorial.dart new file mode 100644 index 0000000000000000000000000000000000000000..71803b4c293a42ec6f2c6e4dd9b9146ba366327d --- /dev/null +++ b/lib/ui/tutorial.dart @@ -0,0 +1,43 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; + +import '../data/models/app_cubit.dart'; + +abstract class Tutorial { + Tutorial({ + required this.tutorialId, + required this.context, + }) { + _tutorial = TutorialCoachMark( + targets: createTargets(), + // colorShadow: Colors.red, + textSkip: tr('skip').toUpperCase(), + // paddingFocus: 10, + // opacityShadow: 0.8, + onFinish: () { + context.read<AppCubit>().onFinishTutorial(tutorialId); + }, + onClickTarget: (TargetFocus target) {}, + onClickTargetWithTapPosition: + (TargetFocus target, TapDownDetails tapDetails) {}, + onClickOverlay: (TargetFocus target) {}, + onSkip: () { + context.read<AppCubit>().onFinishTutorial(tutorialId); + }, + ); + } + + late TutorialCoachMark _tutorial; + final BuildContext context; + final String tutorialId; + + List<TargetFocus> createTargets(); + + void showTutorial() { + if (!context.read<AppCubit>().wasTutorialShown(tutorialId)) { + _tutorial.show(context: context); + } + } +} diff --git a/lib/ui/tutorial_keys.dart b/lib/ui/tutorial_keys.dart new file mode 100644 index 0000000000000000000000000000000000000000..e5b5c7227ba96a65f91d4a61ac1e1174812b24c9 --- /dev/null +++ b/lib/ui/tutorial_keys.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +// first screen +final GlobalKey creditCardKey = GlobalKey(); +final GlobalKey creditCardPubKey = GlobalKey(); +final GlobalKey paySearchUserKey = GlobalKey(); +final GlobalKey payAmountKey = GlobalKey(); +final GlobalKey paySentKey = GlobalKey(); + +// second screen +final GlobalKey receiveMainKey = GlobalKey(); +final GlobalKey receiveQrKey = GlobalKey(); +final GlobalKey receiveAmountKey = GlobalKey(); +final GlobalKey receiveSumKey = GlobalKey(); + +// third screen +final GlobalKey contactsMainKey = GlobalKey(); +final GlobalKey contactsQrKey = GlobalKey(); + +// fourth screen +final GlobalKey txMainKey = GlobalKey(); +final GlobalKey txRefreshKey = GlobalKey(); +final GlobalKey txBalanceKey = GlobalKey(); + +// fifth screen +final GlobalKey infoMainKey = GlobalKey(); +final GlobalKey exportMainKey = GlobalKey(); diff --git a/lib/ui/tutorial_target.dart b/lib/ui/tutorial_target.dart new file mode 100644 index 0000000000000000000000000000000000000000..2db7d48bb506f321bcf54ef75866c441870b7161 --- /dev/null +++ b/lib/ui/tutorial_target.dart @@ -0,0 +1,39 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; + +class TutorialTarget extends TargetFocus { + TutorialTarget({ + required String super.identify, + required GlobalKey super.keyTarget, + super.shape = ShapeLightFocus.Circle, + super.enableOverlayTab = true, + super.enableTargetTab = true, + bool? title = true, + ContentAlign align = ContentAlign.bottom, + }) : super(contents: <TargetContent>[ + TargetContent( + align: align, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: <Widget>[ + if (title!) + Text( + tr('${identify}_title'), + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.white, + fontSize: 20.0), + ), + Padding( + padding: const EdgeInsets.only(top: 10.0), + child: Text( + tr('${identify}_desc'), + style: const TextStyle(color: Colors.white), + ), + ) + ], + )) + ]); +} diff --git a/lib/ui/widgets/fifth_screen/fifth_tutorial.dart b/lib/ui/widgets/fifth_screen/fifth_tutorial.dart new file mode 100644 index 0000000000000000000000000000000000000000..d42d3d3c726f498fb077821105816cdc162a375f --- /dev/null +++ b/lib/ui/widgets/fifth_screen/fifth_tutorial.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; + +import '../../tutorial.dart'; +import '../../tutorial_keys.dart'; +import '../../tutorial_target.dart'; + +class FifthTutorial extends Tutorial { + FifthTutorial(BuildContext context) + : super(tutorialId: 'fifth_screen', context: context); + + @override + List<TargetFocus> createTargets() { + final List<TargetFocus> targets = <TargetFocus>[]; + targets.add(TutorialTarget( + identify: 'infoMainKey', + keyTarget: infoMainKey, + shape: ShapeLightFocus.RRect, + )); + targets.add(TutorialTarget( + identify: 'exportMainKey', + keyTarget: exportMainKey, + align: ContentAlign.top, + shape: ShapeLightFocus.RRect)); + return targets; + } +} diff --git a/lib/ui/widgets/first_screen/credit_card.dart b/lib/ui/widgets/first_screen/credit_card.dart index 01bf1e74eba8080ea25dd46b2f95a9c285c8efa6..cfe3b10365704cf06eeaf57474a63fbe8b102751 100644 --- a/lib/ui/widgets/first_screen/credit_card.dart +++ b/lib/ui/widgets/first_screen/credit_card.dart @@ -5,6 +5,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:qr_flutter/qr_flutter.dart'; import '../../../shared_prefs.dart'; +import '../../tutorial_keys.dart'; import '../../ui_helpers.dart'; import 'card_text_style.dart'; @@ -95,6 +96,7 @@ class CreditCard extends StatelessWidget { GestureDetector( onTap: () => copyPublicKeyToClipboard(context), child: FittedBox( + key: creditCardPubKey, fit: BoxFit.scaleDown, child: Text( '${pubKey.substring(0, 4)} ${pubKey.substring(4, 8)}', diff --git a/lib/ui/widgets/first_screen/first_tutorial.dart b/lib/ui/widgets/first_screen/first_tutorial.dart new file mode 100644 index 0000000000000000000000000000000000000000..e1d1b9e8c6c8594775227548e0ae3ca857ef11b1 --- /dev/null +++ b/lib/ui/widgets/first_screen/first_tutorial.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; + +import '../../tutorial.dart'; +import '../../tutorial_keys.dart'; +import '../../tutorial_target.dart'; + +class FirstTutorial extends Tutorial { + FirstTutorial(BuildContext context) + : super(tutorialId: 'first_screen', context: context); + + @override + List<TargetFocus> createTargets() { + final List<TargetFocus> targets = <TargetFocus>[]; + targets.add(TutorialTarget( + identify: 'creditCardKey', + keyTarget: creditCardKey, + shape: ShapeLightFocus.RRect, + )); + targets.add(TutorialTarget( + identify: 'creditCardPubKey', + keyTarget: creditCardPubKey, + shape: ShapeLightFocus.RRect, + align: ContentAlign.right)); + targets.add(TutorialTarget( + identify: 'paySearchUserKey', + keyTarget: paySearchUserKey, + align: ContentAlign.top, + shape: ShapeLightFocus.RRect)); + targets.add(TutorialTarget( + identify: 'payAmountKey', + keyTarget: payAmountKey, + align: ContentAlign.top, + shape: ShapeLightFocus.RRect)); + /* targets.add(TutorialTarget( + identify: 'paySentKey', + keyTarget: paySentKey, + align: ContentAlign.top, + shape: ShapeLightFocus.RRect));*/ + return targets; + } +} diff --git a/lib/ui/widgets/first_screen/pay_form.dart b/lib/ui/widgets/first_screen/pay_form.dart index 8a45b3e4b60dc98aa54914e2ff12d6d76e75bbb5..4e1f46b1228e75f3c0bc9c0810d2a5d5fab649b7 100644 --- a/lib/ui/widgets/first_screen/pay_form.dart +++ b/lib/ui/widgets/first_screen/pay_form.dart @@ -12,6 +12,7 @@ import '../../../data/models/payment_state.dart'; import '../../../data/models/transaction_cubit.dart'; import '../../../g1/api.dart'; import '../../logger.dart'; +import '../../tutorial_keys.dart'; import '../../ui_helpers.dart'; import 'g1_textfield.dart'; @@ -63,7 +64,7 @@ class _PayFormState extends State<PayForm> { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ - const G1PayAmountField(), + G1PayAmountField(key: payAmountKey), const SizedBox(height: 10.0), TextFormField( inputFormatters: <TextInputFormatter>[ @@ -96,6 +97,7 @@ class _PayFormState extends State<PayForm> { child: _buildBtn(Text(tr('offline'))), ), child: ElevatedButton( + key: paySentKey, onPressed: (!state.canBeSent() || state.amount == null || !_commentValidate() || diff --git a/lib/ui/widgets/fourth_screen/fourth_tutorial.dart b/lib/ui/widgets/fourth_screen/fourth_tutorial.dart new file mode 100644 index 0000000000000000000000000000000000000000..7faa07a113b55baac60a82e926fd3c0dc6ba64f9 --- /dev/null +++ b/lib/ui/widgets/fourth_screen/fourth_tutorial.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; + +import '../../tutorial.dart'; +import '../../tutorial_keys.dart'; +import '../../tutorial_target.dart'; + +class FourthTutorial extends Tutorial { + FourthTutorial(BuildContext context) + : super(tutorialId: 'fourth_screen', context: context); + + @override + List<TargetFocus> createTargets() { + final List<TargetFocus> targets = <TargetFocus>[]; + targets.add(TutorialTarget( + identify: 'txMainKey', + keyTarget: txMainKey, + shape: ShapeLightFocus.RRect, + )); + targets + .add(TutorialTarget(identify: 'txRefreshKey', keyTarget: txRefreshKey)); + targets + .add(TutorialTarget(identify: 'txBalanceKey', keyTarget: txBalanceKey)); + return targets; + } +} diff --git a/lib/ui/widgets/fourth_screen/transaction_page.dart b/lib/ui/widgets/fourth_screen/transaction_page.dart index a746ebc614be3293a325084683acc66a265ac536..05fce95029a9d4e65e030ccd489c737fa1bcc1b2 100644 --- a/lib/ui/widgets/fourth_screen/transaction_page.dart +++ b/lib/ui/widgets/fourth_screen/transaction_page.dart @@ -12,6 +12,7 @@ import '../../../data/models/transaction_balance_state.dart'; import '../../../data/models/transaction_cubit.dart'; import '../../../shared_prefs.dart'; import '../../logger.dart'; +import '../../tutorial_keys.dart'; import '../../ui_helpers.dart'; import 'transaction_chart.dart'; import 'transaction_item.dart'; @@ -35,7 +36,7 @@ class _TransactionsAndBalanceWidgetState static const int _pageSize = 20; final PagingController<String?, Transaction> _pagingController = - PagingController<String?, Transaction>(firstPageKey: null); + PagingController<String?, Transaction>(firstPageKey: null); @override void initState() { @@ -44,10 +45,10 @@ class _TransactionsAndBalanceWidgetState nodeListCubit = context.read<NodeListCubit>(); _pagingController.addPageRequestListener((String? cursor) { EasyThrottle.throttle('my-throttler-$cursor', const Duration(seconds: 1), - () => _fetchPage(cursor), + () => _fetchPage(cursor), onAfter: () {} // <-- Optional callback, called after the duration has passed - ); + ); }); _pagingController.addStatusListener((PagingStatus status) { if (status == PagingStatus.subsequentPageError) { @@ -56,6 +57,7 @@ class _TransactionsAndBalanceWidgetState content: Text(tr('fetch_tx_error')), action: SnackBarAction( label: tr('retry'), + textColor: Theme.of(context).primaryColor, onPressed: () => _pagingController.retryLastFailedRequest(), ), ), @@ -70,7 +72,8 @@ class _TransactionsAndBalanceWidgetState try { final List<Transaction> newItems = await transCubit.fetchTransactions( nodeListCubit, - cursor: cursor, pageSize: _pageSize); + cursor: cursor, + pageSize: _pageSize); final bool isLastPage = newItems.length < _pageSize; if (isLastPage) { @@ -100,33 +103,29 @@ class _TransactionsAndBalanceWidgetState final double balance = transBalanceState.balance; return BackdropScaffold( appBar: BackdropAppBar( - backgroundColor: Theme - .of(context) - .colorScheme - .inversePrimary, + backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(tr('balance')), actions: <Widget>[ IconButton( + key: txRefreshKey, icon: const Icon(Icons.refresh), - onPressed: () => - EasyThrottle.throttle( - 'my-throttler-refresh', - const Duration(seconds: 1), - () => _pagingController.refresh(), - onAfter: - () {} // <-- Optional callback, called after the duration has passed + onPressed: () => EasyThrottle.throttle( + 'my-throttler-refresh', + const Duration(seconds: 1), + () => _pagingController.refresh(), + onAfter: + () {} // <-- Optional callback, called after the duration has passed )), // const BackdropToggleButton(), LayoutBuilder( builder: (BuildContext lContext, - BoxConstraints constraints) => + BoxConstraints constraints) => IconButton( - // icon: const Icon(Icons.account_balance_wallet), + key: txBalanceKey, + // icon: const Icon(Icons.account_balance_wallet), icon: const Icon(Icons.savings), onPressed: () { - if (Backdrop - .of(lContext) - .isBackLayerConcealed) { + if (Backdrop.of(lContext).isBackLayerConcealed) { Backdrop.of(lContext).revealBackLayer(); } else { Backdrop.of(lContext).concealBackLayer(); @@ -137,64 +136,50 @@ class _TransactionsAndBalanceWidgetState ), backLayer: Center( child: Container( - decoration: BoxDecoration( - color: Theme - .of(context) - .colorScheme - .inversePrimary, - border: Border.all( - color: Theme - .of(context) - .colorScheme - .inversePrimary, - width: 3), - /* borderRadius: const BorderRadius.only( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.inversePrimary, + border: Border.all( + color: Theme.of(context).colorScheme.inversePrimary, + width: 3), + /* borderRadius: const BorderRadius.only( topLeft: Radius.circular(8), topRight: Radius.circular(8), ), */ + ), + child: Scrollbar( + child: ListView( + // controller: scrollController, + children: <Widget>[ + Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: Center( + child: Text( + formatKAmount(context, balance), + style: TextStyle( + fontSize: 36.0, + color: + balance == 0 ? Colors.lightBlue : Colors.lightBlue, + fontWeight: FontWeight.bold), + )), ), - child: Scrollbar( - child: ListView( - // controller: scrollController, - children: <Widget>[ - Padding( - padding: const EdgeInsets.symmetric(vertical: 10.0), - child: Center( - child: Text( - formatKAmount(context, balance), - style: TextStyle( - fontSize: 36.0, - color: - balance == 0 ? Colors.lightBlue : Colors - .lightBlue, - fontWeight: FontWeight.bold), - )), - ), - if (!kReleaseMode) TransactionChart( - transactions: transactions) - ], - )), - )), + if (!kReleaseMode) TransactionChart(transactions: transactions) + ], + )), + )), subHeader: BackdropSubHeader( + key: txMainKey, title: Text(tr('transactions')), divider: Divider( - color: Theme - .of(context) - .colorScheme - .surfaceVariant, + color: Theme.of(context).colorScheme.surfaceVariant, height: 0, ), ), frontLayer: RefreshIndicator( color: Colors.white, - backgroundColor: Theme - .of(context) - .colorScheme - .primary, + backgroundColor: Theme.of(context).colorScheme.primary, strokeWidth: 4.0, - onRefresh: () => - Future<void>.sync( - () => _pagingController.refresh(), + onRefresh: () => Future<void>.sync( + () => _pagingController.refresh(), ), child: CustomScrollView( shrinkWrap: true, @@ -216,11 +201,10 @@ class _TransactionsAndBalanceWidgetState transaction: tx, ); }, - noItemsFoundIndicatorBuilder: (_) => - Padding( - padding: + noItemsFoundIndicatorBuilder: (_) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), - child: + child: Center(child: Text(tr('no_transactions')))))) /* diff --git a/lib/ui/widgets/second_screen/card_terminal.dart b/lib/ui/widgets/second_screen/card_terminal.dart index 55867639cc0a896dd4dedb11b6203ab6352506d2..774194f5379effd19395f9b03627595bc4cd2488 100644 --- a/lib/ui/widgets/second_screen/card_terminal.dart +++ b/lib/ui/widgets/second_screen/card_terminal.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:vibration/vibration.dart'; +import '../../tutorial_keys.dart'; import '../../ui_helpers.dart'; import 'card_terminal_screen.dart'; import 'rubber_button.dart'; @@ -69,6 +70,7 @@ class _CardTerminalState extends State<CardTerminal> { Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), child: GridView.count( + key: receiveAmountKey, physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, crossAxisCount: 3, @@ -127,6 +129,7 @@ class _CardTerminalState extends State<CardTerminal> { }); } else return RubberButton( + key: index == 11 ? receiveSumKey : null, label: _numbers[index], onPressed: () { vibrateIfPossible(); diff --git a/lib/ui/widgets/second_screen/card_terminal_screen.dart b/lib/ui/widgets/second_screen/card_terminal_screen.dart index 9f4715bfe0fe45b46ba5d285706c4a9feff21e74..5223faeda125ffec73063ffca02c9ee4f3c01956 100644 --- a/lib/ui/widgets/second_screen/card_terminal_screen.dart +++ b/lib/ui/widgets/second_screen/card_terminal_screen.dart @@ -5,6 +5,7 @@ import 'package:qr_flutter/qr_flutter.dart'; import '../../../g1/g1_helper.dart'; import '../../../shared_prefs.dart'; +import '../../tutorial_keys.dart'; import '../../ui_helpers.dart'; import 'card_terminal_status.dart'; @@ -16,6 +17,7 @@ class CardTerminalScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Card( + key: receiveQrKey, elevation: 8, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), child: Container( diff --git a/lib/ui/widgets/second_screen/second_tutorial.dart b/lib/ui/widgets/second_screen/second_tutorial.dart new file mode 100644 index 0000000000000000000000000000000000000000..f6dc5ca387d442beb09b4f66c670dabfff76f813 --- /dev/null +++ b/lib/ui/widgets/second_screen/second_tutorial.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; + +import '../../tutorial.dart'; +import '../../tutorial_keys.dart'; +import '../../tutorial_target.dart'; + +class SecondTutorial extends Tutorial { + SecondTutorial(BuildContext context) + : super(tutorialId: 'second_screen', context: context); + + @override + List<TargetFocus> createTargets() { + final List<TargetFocus> targets = <TargetFocus>[]; + targets.add(TutorialTarget( + identify: 'receiveMainKey', + keyTarget: receiveMainKey, + shape: ShapeLightFocus.RRect, + )); + targets.add(TutorialTarget( + identify: 'receiveQrKey', + keyTarget: receiveQrKey, + title: false, + shape: ShapeLightFocus.RRect)); + targets.add(TutorialTarget( + identify: 'receiveAmountKey', + keyTarget: receiveAmountKey, + align: ContentAlign.top)); + targets.add(TutorialTarget( + identify: 'receiveSumKey', + keyTarget: receiveSumKey, + align: ContentAlign.left)); + return targets; + } +} diff --git a/lib/ui/widgets/third_screen/third_tutorial.dart b/lib/ui/widgets/third_screen/third_tutorial.dart new file mode 100644 index 0000000000000000000000000000000000000000..bb89c1f3cf2f610729b1f3cff737aab3987b698b --- /dev/null +++ b/lib/ui/widgets/third_screen/third_tutorial.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:tutorial_coach_mark/tutorial_coach_mark.dart'; + +import '../../tutorial.dart'; +import '../../tutorial_keys.dart'; +import '../../tutorial_target.dart'; + +class ThirdTutorial extends Tutorial { + ThirdTutorial(BuildContext context) + : super(tutorialId: 'third_screen', context: context); + + @override + List<TargetFocus> createTargets() { + final List<TargetFocus> targets = <TargetFocus>[]; + targets.add(TutorialTarget( + identify: 'contactsMainKey', + keyTarget: contactsMainKey, + shape: ShapeLightFocus.RRect)); + targets.add( + TutorialTarget(identify: 'contactsQrKey', keyTarget: contactsQrKey)); + return targets; + } +} diff --git a/pubspec.lock b/pubspec.lock index e8149bb6c136344070130c3699aff64909eb80c9..8074ff669bc971df459dec23e84e1f921a2f3946 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -329,6 +329,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.8" + dio: + dependency: transitive + description: + name: dio + sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" + url: "https://pub.dev" + source: hosted + version: "4.0.6" dots_indicator: dependency: transitive description: @@ -812,6 +820,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.4" + lehttp_overrides: + dependency: "direct main" + description: + name: lehttp_overrides + sha256: "9c54ada96cda6ff848aafa1608bf95c35377a0628e4a9a30d9af1500cfc4f04b" + url: "https://pub.dev" + source: hosted + version: "1.0.2" lints: dependency: transitive description: @@ -1369,6 +1385,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + tutorial_coach_mark: + dependency: "direct main" + description: + name: tutorial_coach_mark + sha256: "669d4bd517d22e14671f3ddee0e7442d976ab71f94bb6eec6ad057ca3944fd73" + url: "https://pub.dev" + source: hosted + version: "1.2.8" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index fc49686731cf62506e5df8331e1c9aa3822a3d82..f8fa99a18c9f6f48c11ac466c2e9bee7422a8fc6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -78,6 +78,8 @@ dependencies: tuple: ^2.0.1 infinite_scroll_pagination: ^3.2.0 easy_debounce: ^2.0.3 + tutorial_coach_mark: ^1.2.8 + lehttp_overrides: ^1.0.2 dev_dependencies: flutter_test: @@ -124,6 +126,7 @@ flutter: - assets/tx.json - assets/gva-tx.json - assets/img/animated-bell.gif + - assets/img/pos.svg fonts: - family: Nunito