Skip to content
Snippets Groups Projects
main.dart 19.8 KiB
Newer Older
vjrj's avatar
vjrj committed
import 'dart:async';
tylersavery's avatar
tylersavery committed
import 'dart:io';
import 'package:connectivity_wrapper/connectivity_wrapper.dart';
anfeichtinger's avatar
anfeichtinger committed
import 'package:easy_localization/easy_localization.dart';
vjrj's avatar
vjrj committed
import 'package:feedback/feedback.dart';
vjrj's avatar
vjrj committed
import 'package:filesystem_picker/filesystem_picker.dart';
vjrj's avatar
vjrj committed
import 'package:flutter/foundation.dart';
anfeichtinger's avatar
anfeichtinger committed
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
anfeichtinger's avatar
anfeichtinger committed
import 'package:flutter_displaymode/flutter_displaymode.dart';
vjrj's avatar
vjrj committed
import 'package:flutter_dotenv/flutter_dotenv.dart';
vjrj's avatar
vjrj committed
import 'package:introduction_screen/introduction_screen.dart';
vjrj's avatar
vjrj committed
import 'package:l10n_esperanto/l10n_esperanto.dart';
vjrj's avatar
vjrj committed
import 'package:lehttp_overrides/lehttp_overrides.dart';
import 'package:once/once.dart';
import 'package:provider/provider.dart';
vjrj's avatar
vjrj committed
import 'package:pwa_install/pwa_install.dart';
vjrj's avatar
vjrj committed
import 'package:responsive_framework/responsive_wrapper.dart';
vjrj's avatar
vjrj committed
import 'package:responsive_framework/utils/scroll_behavior.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
vjrj's avatar
vjrj committed
import 'package:sentry_logging/sentry_logging.dart';
vjrj's avatar
vjrj committed
import 'package:timeago/timeago.dart' as timeago;
vjrj's avatar
vjrj committed
import 'package:uni_links/uni_links.dart';
import 'package:workmanager/workmanager.dart';
anfeichtinger's avatar
anfeichtinger committed

vjrj's avatar
vjrj committed
import 'app_bloc_observer.dart';
vjrj's avatar
vjrj committed
import 'custom_feedback_localization.dart';
vjrj's avatar
vjrj committed
import 'data/eo_timeago_support.dart';
import 'data/eu_timeago_support.dart';
import 'data/gl_timeago_support.dart';
vjrj's avatar
vjrj committed
import 'data/models/app_cubit.dart';
import 'data/models/app_state.dart';
vjrj's avatar
vjrj committed
import 'data/models/bottom_nav_cubit.dart';
vjrj's avatar
vjrj committed
import 'data/models/contact_cubit.dart';
vjrj's avatar
vjrj committed
import 'data/models/multi_wallet_transaction_cubit.dart';
vjrj's avatar
vjrj committed
import 'data/models/node_list_cubit.dart';
vjrj's avatar
vjrj committed
import 'data/models/node_list_state.dart';
import 'data/models/node_manager.dart';
vjrj's avatar
vjrj committed
import 'data/models/node_type.dart';
import 'data/models/payment_cubit.dart';
vjrj's avatar
vjrj committed
import 'data/models/theme_cubit.dart';
vjrj's avatar
vjrj committed
import 'data/models/transaction_cubit.dart';
vjrj's avatar
vjrj committed
import 'g1/api.dart';
vjrj's avatar
vjrj committed
import 'g1/g1_helper.dart';
import 'shared_prefs_helper.dart';
vjrj's avatar
vjrj committed
import 'ui/contacts_cache.dart';
vjrj's avatar
vjrj committed
import 'ui/logger.dart';
vjrj's avatar
vjrj committed
import 'ui/notification_controller.dart';
vjrj's avatar
vjrj committed
import 'ui/pay_helper.dart';
import 'ui/screens/skeleton_screen.dart';
vjrj's avatar
vjrj committed
import 'ui/ui_helpers.dart';
import 'ui/widgets/connectivity_widget_wrapper_wrapper.dart';
vjrj's avatar
vjrj committed

const String fetchWalletsTransactionsTask =
    'org.comunes.ginkgo.fetchWalletsTransactionsTask';

anfeichtinger's avatar
anfeichtinger committed
void main() async {
vjrj's avatar
vjrj committed
  await NotificationController.initializeLocalNotifications();
vjrj's avatar
vjrj committed

vjrj's avatar
vjrj committed
  // To resolve Let's Encrypt SSL certificate problems with Android 7.1.1 and below
vjrj's avatar
vjrj committed
  if (!kIsWeb && Platform.isAndroid) {
vjrj's avatar
vjrj committed
    HttpOverrides.global = LEHttpOverrides();
  }

anfeichtinger's avatar
anfeichtinger committed
  /// Initialize packages
  WidgetsFlutterBinding.ensureInitialized();
  await EasyLocalization.ensureInitialized();
vjrj's avatar
vjrj committed

  if (!kIsWeb && Platform.isAndroid) {
tylersavery's avatar
tylersavery committed
    await FlutterDisplayMode.setHighRefreshRate();
vjrj's avatar
vjrj committed
  }

vjrj's avatar
vjrj committed
  // .env
vjrj's avatar
vjrj committed
  await dotenv.load(
vjrj's avatar
vjrj committed
      fileName: kReleaseMode
          ? 'assets/env.production.txt'
          : 'assets/.env.development');
vjrj's avatar
vjrj committed

vjrj's avatar
vjrj committed
  final SharedPreferencesHelper shared = SharedPreferencesHelper();
  await shared.init();
  if (shared.cesiumCards.isEmpty) {
    await shared.getWallet();
  }
vjrj's avatar
vjrj committed
  assert(shared.getPubKey() != null);

vjrj's avatar
vjrj committed

vjrj's avatar
vjrj committed
  PWAInstall().setup(installCallback: () {
    logger('APP INSTALLED!');
  });

vjrj's avatar
vjrj committed
  Bloc.observer = AppBlocObserver();

vjrj's avatar
vjrj committed
  timeago.setLocaleMessages('eo', EoMessages());
  timeago.setLocaleMessages('eo_short', EoShortMessages());
  timeago.setLocaleMessages('eu', EuMessages());
  timeago.setLocaleMessages('eu_short', EuShortMessages());
  timeago.setLocaleMessages('de', timeago.DeMessages());
  timeago.setLocaleMessages('de_short', timeago.DeShortMessages());
  timeago.setLocaleMessages('fr', timeago.FrMessages());
  timeago.setLocaleMessages('fr_short', timeago.FrShortMessages());
  timeago.setLocaleMessages('ca', timeago.CaMessages());
  timeago.setLocaleMessages('ca_short', timeago.CaShortMessages());
  timeago.setLocaleMessages('nl', timeago.NlMessages());
  timeago.setLocaleMessages('nl_short', timeago.NlShortMessages());
  timeago.setLocaleMessages('it', timeago.ItMessages());
  timeago.setLocaleMessages('it_short', timeago.ItShortMessages());
  timeago.setLocaleMessages('pt', timeago.PtBrMessages());
  timeago.setLocaleMessages('pt_short', timeago.PtBrShortMessages());
  timeago.setLocaleMessages('gl', GlMessages());
  timeago.setLocaleMessages('gl_short', GlShortMessages());
vjrj's avatar
vjrj committed

  void appRunner() => runApp(ChangeNotifierProvider<SharedPreferencesHelper>(
        create: (BuildContext context) => SharedPreferencesHelper(),
        child: EasyLocalization(
vjrj's avatar
vjrj committed
          path: 'assets/translations',
vjrj's avatar
vjrj committed
          supportedLocales: const <Locale>[
vjrj's avatar
vjrj committed
            // Asturian is not supported in flutter
            // More info: https://docs.flutter.dev/development/accessibility-and-localization/internationalization#adding-support-for-a-new-language
            // Meantime we use this workaround:
            // https://github.com/aissat/easy_localization/issues/220#issuecomment-846035493
            Locale('es', 'AST'),
vjrj's avatar
vjrj committed
            Locale('ca'),
            Locale('de'),
vjrj's avatar
vjrj committed
            Locale('en'),
vjrj's avatar
vjrj committed
            Locale('es'),
            Locale('eu'),
vjrj's avatar
vjrj committed
            Locale('fr'),
vjrj's avatar
vjrj committed
            Locale('gl'),
            Locale('it'),
vjrj's avatar
vjrj committed
            Locale('nl'),
vjrj's avatar
vjrj committed
            Locale('pt'),
vjrj's avatar
vjrj committed
          ],
          fallbackLocale: const Locale('en'),
          useFallbackTranslations: true,
          child: MultiBlocProvider(providers: <BlocProvider<dynamic>>[
            BlocProvider<BottomNavCubit>(
                create: (BuildContext context) => BottomNavCubit()),
            BlocProvider<AppCubit>(
                create: (BuildContext context) => AppCubit()),
            BlocProvider<PaymentCubit>(
                create: (BuildContext context) => PaymentCubit()),
            BlocProvider<NodeListCubit>(
                create: (BuildContext context) => NodeListCubit()),
            BlocProvider<ContactsCubit>(
                create: (BuildContext context) => ContactsCubit()),
            // TODO(vjrj): Remove when clean the state of this
vjrj's avatar
vjrj committed
            BlocProvider<TransactionCubitRemove>(
                create: (BuildContext context) => TransactionCubitRemove()),
            BlocProvider<MultiWalletTransactionCubit>(
                create: (BuildContext context) =>
                    MultiWalletTransactionCubit()),
            BlocProvider<ThemeCubit>(
                create: (BuildContext context) => ThemeCubit()),
vjrj's avatar
vjrj committed
            // Add other BlocProviders here if needed
          ], child: const GinkgoApp()),
        ),
vjrj's avatar
vjrj committed

  if (kReleaseMode) {
    // Only use sentry in production
    await SentryFlutter.init((
      SentryFlutterOptions options,
    ) {
vjrj's avatar
vjrj committed
      options.tracesSampleRate = 1.0;
      options.reportPackages = false;
      // options.addInAppInclude('sentry_flutter_example');
      options.considerInAppFramesByDefault = false;
      // options.attachThreads = true;
      // options.enableWindowMetricBreadcrumbs = true;
      options.addIntegration(LoggingIntegration());
      options.sendDefaultPii = true;
      options.reportSilentFlutterErrors = true;
      // options.attachScreenshot = true;
      // options.screenshotQuality = SentryScreenshotQuality.low;
      // This fails:
      // options.attachViewHierarchy = true;
      // We can enable Sentry debug logging during development. This is likely
      // going to log too much for your app, but can be useful when figuring out
      // configuration issues, e.g. finding out why your events are not uploaded.
      options.debug = false;

      options.maxRequestBodySize = MaxRequestBodySize.always;
      options.maxResponseBodySize = MaxResponseBodySize.always;

      // options.release = version;
      // options.environment = 'production';
      // options.beforeSend = (SentryEvent event, {dynamic hint}) {
      //  return event;
      //};
vjrj's avatar
vjrj committed

vjrj's avatar
vjrj committed
      options.dsn = "${dotenv.env['SENTRY_DSN']}";
      // Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring.
      // We recommend adjusting this value in production.
      // options.tracesSampleRate = 1.0;
    }, appRunner: appRunner);
  } else {
    appRunner();
  }
anfeichtinger's avatar
anfeichtinger committed
}

@pragma(
    'vm:entry-point') // Mandatory if the App is obfuscated or using Flutter 3.1+
void workManagerCallbackDispatcher() {
  Workmanager()
      .executeTask((String task, Map<String, dynamic>? inputData) async {
    try {
      loggerDev(
          '---------- Start fetchTransactionsTask Workmanager background task');
      switch (task) {
        case fetchWalletsTransactionsTask:
          await NotificationController.initializeLocalNotifications();
vjrj's avatar
vjrj committed
          fetchTransactionsFromBackground();
          break;
        case Workmanager.iOSBackgroundTask:
          break;
      }
      loggerDev(
          '---------- End fetchTransactionsTask Workmanager background task');
    } catch (err, stacktrace) {
      logger(err.toString());
      await Sentry.captureException(err, stackTrace: stacktrace);
    }
    return Future<bool>.value(true);
  });
}

vjrj's avatar
vjrj committed
class AppIntro extends StatefulWidget {
  const AppIntro({super.key});

  @override
  State<AppIntro> createState() => _AppIntro();
}

class _AppIntro extends State<AppIntro> {
  final GlobalKey<IntroductionScreenState> introKey =
      GlobalKey<IntroductionScreenState>();
vjrj's avatar
vjrj committed

vjrj's avatar
vjrj committed
  void _onIntroEnd(BuildContext context, AppCubit cubit) {
    cubit.introViewed();
vjrj's avatar
vjrj committed
    Navigator.of(context).pushReplacement(
vjrj's avatar
vjrj committed
      MaterialPageRoute<void>(
          builder: (BuildContext _) => const SkeletonScreen()),
vjrj's avatar
vjrj committed
    );
  }

  @override
  Widget build(BuildContext context) {
vjrj's avatar
vjrj committed
    return BlocBuilder<AppCubit, AppState>(
vjrj's avatar
vjrj committed
        builder: (BuildContext buildContext, AppState state) {
      final AppCubit cubit = context.read<AppCubit>();
      return IntroductionScreen(
        key: introKey,
        pages: <PageViewModel>[
          for (int i = 1; i <= 5; i++)
            createPageViewModel('intro_${i}_title', 'intro_${i}_description',
                'assets/img/undraw_intro_$i.png', context),
        ],
        onDone: () => _onIntroEnd(buildContext, cubit),
        showSkipButton: true,
        skipOrBackFlex: 0,
        onSkip: () => _onIntroEnd(buildContext, cubit),
        nextFlex: 0,
        skip: Text(tr('skip')),
        next: const Icon(Icons.arrow_forward),
        done: Text(tr('start'),
            style: const TextStyle(fontWeight: FontWeight.w600)),
        dotsDecorator: const DotsDecorator(
          size: Size(10.0, 10.0),
          color: Color(0xFFBDBDBD),
          activeColor: Colors.blueAccent,
          activeSize: Size(22.0, 10.0),
          activeShape: RoundedRectangleBorder(
            borderRadius: BorderRadius.all(Radius.circular(25.0)),
          ),
        ),
      );
    });
PageViewModel createPageViewModel(
    String title, String body, String imageAsset, BuildContext context) {
  final ColorScheme colorScheme = Theme.of(context).colorScheme;
vjrj's avatar
vjrj committed
  final TextStyle titleStyle = TextStyle(
    color: colorScheme.primary,
    fontWeight: FontWeight.bold,
    fontSize: 24.0,
  );
  final TextStyle bodyStyle = TextStyle(
    color: colorScheme.onSurface,
    fontSize: 18.0,
  );

vjrj's avatar
vjrj committed
  return PageViewModel(
    title: tr(title),
    body: tr(body),
    image: Image.asset(imageAsset),
vjrj's avatar
vjrj committed
    decoration: PageDecoration(
      titleTextStyle: titleStyle,
      bodyTextStyle: bodyStyle,
      pageColor: colorScheme.background,
vjrj's avatar
vjrj committed
class GinkgoApp extends StatefulWidget {
  const GinkgoApp({super.key});
vjrj's avatar
vjrj committed
  // The navigator key is necessary to navigate using static methods
  static final GlobalKey<NavigatorState> navigatorKey =
      GlobalKey<NavigatorState>();
vjrj's avatar
vjrj committed

vjrj's avatar
vjrj committed
  @override
vjrj's avatar
vjrj committed
  State<GinkgoApp> createState() => _GinkgoAppState();
vjrj's avatar
vjrj committed
class _GinkgoAppState extends State<GinkgoApp> {
  Future<void> _loadNodes() async {
vjrj's avatar
vjrj committed
    _printNodeStatus();
vjrj's avatar
vjrj committed
    for (final NodeType nodeType in NodeType.values) {
      await fetchNodes(nodeType, false);
    }
vjrj's avatar
vjrj committed
    _printNodeStatus(prefix: 'Continuing');
  }

  void _printNodeStatus({String prefix = 'Starting'}) {
    final int nDuniterNodes = NodeManager().nodeList(NodeType.duniter).length;
    final int nCesiumPlusNodes =
        NodeManager().nodeList(NodeType.cesiumPlus).length;
    final int nGvaNodes = NodeManager().nodeList(NodeType.gva).length;
    logger(
vjrj's avatar
vjrj committed
        '$prefix with $nDuniterNodes duniter nodes, $nCesiumPlusNodes c+ nodes, and $nGvaNodes gva nodes');
vjrj's avatar
vjrj committed
    if (!kReleaseMode) {
      logger('${NodeManager().nodeList(NodeType.cesiumPlus)}');
    }
    if (!kReleaseMode) {
      logger('${NodeManager().nodeList(NodeType.gva)}');
    }
vjrj's avatar
vjrj committed
  @override
  void initState() {
    super.initState();
vjrj's avatar
vjrj committed
    NodeManager().loadFromCubit(context.read<NodeListCubit>());
vjrj's avatar
vjrj committed
    // Only after at least the action method is set, the notification events are delivered
    NotificationController.startListeningNotificationEvents();
vjrj's avatar
vjrj committed
    // Wipe Old Transactions Cubit
    context.read<TransactionCubitRemove>().close();
    Once.runHourly('load_nodes', callback: () async {
      final bool isConnected =
          await ConnectivityWidgetWrapperWrapper.isConnected;
      if (isConnected) {
        logger('Load nodes via once');
        _loadNodes();
      }
    }, fallback: () {
      _printNodeStatus(prefix: 'After once hourly having');
    });
    Once.runDaily('clear_errors', callback: () {
      logger('clearErrors via once');
      NodeManager().cleanErrorStats();
    });
vjrj's avatar
vjrj committed
    Once.runDaily('clear_cache', callback: () {
      logger('clear cache via once');
      ContactsCache().clear();
      ContactsCache().addContacts(context.read<ContactsCubit>().state.contacts);
vjrj's avatar
vjrj committed
    });
    Once.runOnce('resize_avatars', callback: () {
      logger('resize avatar via once');
      context.read<ContactsCubit>().resizeAvatars();
    });

    initGetItAll();

    fetchTxsCronTask = Cron()
        .schedule(Schedule.parse(kReleaseMode ? '*/10 * * * *' : '*/5 * * * *'),
            () async {
      logger('---------- fetchTransactions via cron');
      // Disabled to check the back development
vjrj's avatar
vjrj committed
      // if (!inDevelopment) {
      fetchTransactions(context);
      // }
    });

    if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
      Workmanager().initialize(
          workManagerCallbackDispatcher); // The top level function, aka callbackDispatcher
      /* , isInDebugMode:
              true // If enabled it will post a notification whenever the task is running. Handy for debugging tasks
          ); */
      Workmanager().registerPeriodicTask(
        fetchWalletsTransactionsTask,
        fetchWalletsTransactionsTask,
        frequency: const Duration(minutes: 15),
      );
    }
  @override
  void dispose() {
    ContactsCache().dispose();
vjrj's avatar
vjrj committed
    _disposeDeepLinkListener();
    fetchTxsCronTask.cancel();
    Workmanager().cancelAll();
    super.dispose();
  }

vjrj's avatar
vjrj committed
  late StreamSubscription<dynamic>? _sub;

  Future<void> _initDeepLinkListener() async {
    _sub = linkStream.listen((String? link) async {
      if (!mounted) {
        return;
      }
      if (link != null) {
        logger('got link: $link');
        if (parseScannedUri(link) != null) {
vjrj's avatar
vjrj committed
          await onKeyScanned(context, link);
          if (!mounted) {
            return;
          }
          context.read<BottomNavCubit>().updateIndex(0);
vjrj's avatar
vjrj committed
        }
      }
    }, onError: (Object err) {
      if (!mounted) {
        return;
      }
      logger('got err: $err');
    });
  }

  void _disposeDeepLinkListener() {
    if (_sub != null) {
      _sub!.cancel();
      _sub = null;
    }
  }

anfeichtinger's avatar
anfeichtinger committed
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<NodeListCubit, NodeListState>(
        builder: (BuildContext nodeContext, NodeListState state) {
      return ConnectivityAppWrapper(
          app: FilesystemPickerDefaultOptions(
              fileTileSelectMode: FileTileSelectMode.wholeTile,
              theme: FilesystemPickerTheme(
                topBar: FilesystemPickerTopBarThemeData(
                  backgroundColor: Theme.of(context).colorScheme.primary,
                ),
              ),
              child: MaterialApp(
                /// Localization is not available for the title.
                title: 'Ğ1nkgo',
                theme: ThemeData(
                    useMaterial3: true, colorScheme: lightColorScheme),
                darkTheme:
                    ThemeData(useMaterial3: true, colorScheme: darkColorScheme),

                navigatorKey: GinkgoApp.navigatorKey,
                scaffoldMessengerKey: globalMessengerKey,

                /// Theme stuff
                themeMode: context.watch<ThemeCubit>().state.themeMode,

                /// Localization stuff
vjrj's avatar
vjrj committed
                localizationsDelegates: context.localizationDelegates
                  ..addAll(<LocalizationsDelegate<dynamic>>[
                    MaterialLocalizationsEo.delegate,
                    CupertinoLocalizationsEo.delegate
                  ]),
                supportedLocales: context.supportedLocales,
                locale: context.locale,
                debugShowCheckedModeBanner: false,
                home: context.read<AppCubit>().isIntroViewed
                    ? BetterFeedback(
                        localizationsDelegates: context.localizationDelegates
                          ..add(CustomFeedbackLocalizationsDelegate()),
                        child: const SkeletonScreen())
                    : const AppIntro(),
                builder: (BuildContext buildContext, Widget? widget) {
                  NotificationController.locale = context.locale;
                  return ResponsiveWrapper.builder(
                    BouncingScrollWrapper.builder(
                        context,
                        ConnectivityWidgetWrapperWrapper(
                          //message: tr('offline'),
                          //height: 18,

                          offlineWidget: /* Container(
vjrj's avatar
vjrj committed
                                color: Colors.transparent,
                                child: Center(
                                  child: */
                              Column(
                            mainAxisSize: MainAxisSize.min,
                            children: <Widget>[
                              const Icon(
                                Icons.cloud_off,
                                size: 48,
                                color: Colors.grey,
                              ),
                              const SizedBox(height: 6),
                              Container(
                                  padding: const EdgeInsets.all(5.0),
                                  decoration: const BoxDecoration(
vjrj's avatar
vjrj committed
                                    color: Colors.grey,
                                    borderRadius:
vjrj's avatar
vjrj committed
                                        BorderRadius.all(Radius.circular(10.0)),
                                  ),
                                  child: Text(
                                    tr('offline'),
                                    style: const TextStyle(
                                      color: Colors.white,
                                      decoration: TextDecoration.none,
                                      fontSize: 14,
                                    ),
                                  )),
                              const SizedBox(height: 110),
                            ],
                          ),

                          child: widget!,
                        )),
                    maxWidth: 480,
                    minWidth: 480,
                    // defaultScale: true,
                    breakpoints: <ResponsiveBreakpoint>[
                      const ResponsiveBreakpoint.resize(200, name: MOBILE),
                      const ResponsiveBreakpoint.resize(480, name: TABLET),
                      const ResponsiveBreakpoint.resize(1000, name: DESKTOP),
                    ],
                    background: Container(color: const Color(0xFFF5F5F5)),
                  );
                },
              )));
    });
anfeichtinger's avatar
anfeichtinger committed
  }
}