From d5558eea80b33db09486e1298cedb59265701f1a Mon Sep 17 00:00:00 2001 From: vjrj <vjrj@comunes.org> Date: Mon, 24 Apr 2023 19:56:10 +0200 Subject: [PATCH] Added tutorial intro --- README.md | 1 + assets/translations/en.json | 33 ++++- assets/translations/es.json | 33 ++++- lib/data/models/app_cubit.dart | 11 +- lib/data/models/app_state.dart | 41 +++--- lib/main.dart | 6 + lib/ui/screens/fifth_screen.dart | 28 +++- lib/ui/screens/first_screen.dart | 20 ++- lib/ui/screens/fourth_screen.dart | 22 +++- lib/ui/screens/second_screen.dart | 25 +++- lib/ui/screens/third_screen.dart | 95 ++++++++------ lib/ui/tutorial.dart | 43 ++++++ lib/ui/tutorial_keys.dart | 27 ++++ lib/ui/tutorial_target.dart | 39 ++++++ .../widgets/fifth_screen/fifth_tutorial.dart | 27 ++++ lib/ui/widgets/first_screen/credit_card.dart | 2 + .../widgets/first_screen/first_tutorial.dart | 42 ++++++ lib/ui/widgets/first_screen/pay_form.dart | 4 +- .../fourth_screen/fourth_tutorial.dart | 26 ++++ .../fourth_screen/transaction_page.dart | 122 ++++++++---------- .../widgets/second_screen/card_terminal.dart | 3 + .../second_screen/card_terminal_screen.dart | 2 + .../second_screen/second_tutorial.dart | 35 +++++ .../widgets/third_screen/third_tutorial.dart | 23 ++++ pubspec.lock | 24 ++++ pubspec.yaml | 3 + 26 files changed, 600 insertions(+), 137 deletions(-) create mode 100644 lib/ui/tutorial.dart create mode 100644 lib/ui/tutorial_keys.dart create mode 100644 lib/ui/tutorial_target.dart create mode 100644 lib/ui/widgets/fifth_screen/fifth_tutorial.dart create mode 100644 lib/ui/widgets/first_screen/first_tutorial.dart create mode 100644 lib/ui/widgets/fourth_screen/fourth_tutorial.dart create mode 100644 lib/ui/widgets/second_screen/second_tutorial.dart create mode 100644 lib/ui/widgets/third_screen/third_tutorial.dart diff --git a/README.md b/README.md index ef204830..641b7499 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 1e8c5e8b..ffb8b074 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 1e0d9d59..1a06801a 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 16c435c9..163513af 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 ce04851e..5742b78e 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 8d8ab92b..087942fd 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 38f5a8cd..6c6de020 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 451a01a1..ce94f0c3 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 7a67c4b0..3df8abeb 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 c250db5b..a17dc0a3 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 7f8c0bd6..5d84d968 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 00000000..71803b4c --- /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 00000000..e5b5c722 --- /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 00000000..2db7d48b --- /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 00000000..d42d3d3c --- /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 01bf1e74..cfe3b103 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 00000000..e1d1b9e8 --- /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 8a45b3e4..4e1f46b1 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 00000000..7faa07a1 --- /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 a746ebc6..05fce950 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 55867639..774194f5 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 9f4715bf..5223faed 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 00000000..f6dc5ca3 --- /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 00000000..bb89c1f3 --- /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 e8149bb6..8074ff66 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 fc496867..f8fa99a1 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 -- GitLab