Skip to content
Snippets Groups Projects
Commit d5558eea authored by vjrj's avatar vjrj
Browse files

Added tutorial intro

parent 61bb2f82
No related branches found
No related tags found
No related merge requests found
Showing
with 510 additions and 137 deletions
......@@ -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!
......
......@@ -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."
}
......@@ -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."
}
......@@ -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;
}
}
......@@ -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
];
}
......@@ -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();
......
......@@ -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(),
......
......@@ -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()
......
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();
......
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()]),
......
......@@ -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(),
);
......
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);
}
}
}
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();
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),
),
)
],
))
]);
}
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;
}
}
......@@ -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)}',
......
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;
}
}
......@@ -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() ||
......
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;
}
}
......@@ -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'))))))
/*
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment