Skip to content
Snippets Groups Projects
main.dart 12.7 KiB
Newer Older
tylersavery's avatar
tylersavery committed
import 'dart:io';
vjrj's avatar
vjrj committed
import 'package:connectivity_wrapper/connectivity_wrapper.dart';
import 'package:cron/cron.dart';
anfeichtinger's avatar
anfeichtinger committed
import 'package:easy_localization/easy_localization.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';
import 'package:hive_flutter/hive_flutter.dart';
anfeichtinger's avatar
anfeichtinger committed
import 'package:hydrated_bloc/hydrated_bloc.dart';
vjrj's avatar
vjrj committed
import 'package:introduction_screen/introduction_screen.dart';
import 'package:once/once.dart';
anfeichtinger's avatar
anfeichtinger committed
import 'package:path_provider/path_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';
anfeichtinger's avatar
anfeichtinger committed

vjrj's avatar
vjrj committed
import 'app_bloc_observer.dart';
vjrj's avatar
vjrj committed
import 'cubit/bottom_nav_cubit.dart';
import 'cubit/theme_cubit.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/contact_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/transaction_cubit.dart';
vjrj's avatar
vjrj committed
import 'g1/api.dart';
vjrj's avatar
vjrj committed
import 'shared_prefs.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';
import 'ui/screens/skeleton_screen.dart';
vjrj's avatar
vjrj committed
import 'ui/ui_helpers.dart';
vjrj's avatar
vjrj committed

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

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(
      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();
  await shared.getWallet();
  assert(shared.getPubKey() != null);

vjrj's avatar
vjrj committed
  await Hive.initFlutter();

vjrj's avatar
vjrj committed
  if (kIsWeb) {
    HydratedBloc.storage = await HydratedStorage.build(
        storageDirectory: HydratedStorage.webStorageDirectory);
  } else {
    final Directory tmpDir = await getTemporaryDirectory();
    Hive.init(tmpDir.toString());
vjrj's avatar
vjrj committed
    HydratedBloc.storage =
        await HydratedStorage.build(storageDirectory: tmpDir);
tylersavery's avatar
tylersavery committed
  }
vjrj's avatar
vjrj committed

vjrj's avatar
vjrj committed
  // Reset hive during developing
  if (!kReleaseMode) {
    // Once.clearAll();
vjrj's avatar
vjrj committed
    // await HydratedBloc.storage.clear();
vjrj's avatar
vjrj committed
  }
vjrj's avatar
vjrj committed

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

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

  void appRunner() => runApp(
vjrj's avatar
vjrj committed
        EasyLocalization(
          path: 'assets/translations',
          supportedLocales: const <Locale>[
vjrj's avatar
vjrj committed
            Locale('ast'),
vjrj's avatar
vjrj committed
            Locale('ca'),
            Locale('de'),
vjrj's avatar
vjrj committed
            Locale('en'),
            Locale('es'),
            Locale('fr'),
vjrj's avatar
vjrj committed
            Locale('gl'),
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()),
            BlocProvider<TransactionsCubit>(
                create: (BuildContext context) => TransactionsCubit()),
            BlocProvider<ThemeCubit>(
                create: (BuildContext context) => ThemeCubit()),
vjrj's avatar
vjrj committed
            // Add other BlocProviders here if needed
          ], child: const GinkgoApp()),
        ),
      );

  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
}

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(
vjrj's avatar
vjrj committed
    String title, String body, String imageAsset, BuildContext context) {
  final ColorScheme colorScheme = Theme.of(context).colorScheme;
  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
  @override
vjrj's avatar
vjrj committed
  State<GinkgoApp> createState() => _GinkgoAppState();
vjrj's avatar
vjrj committed
class _GinkgoAppState extends State<GinkgoApp> {
vjrj's avatar
vjrj committed
  Future<void> _loadNodes() async {
    _printNodeStatus();
    await fetchDuniterNodes();
vjrj's avatar
vjrj committed
    await fetchCesiumPlusNodes();
    await fetchGvaNodes();
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;
vjrj's avatar
vjrj committed
    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();
    ContactsCache().init();
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();
    final Cron cron = Cron();
    cron.schedule(Schedule.parse(kReleaseMode ? '*/10 * * * *' : '*/2 * * * *'),
        () async {
      logger('---------- fetchTransactions via cron');
      fetchTransactions(context);
    });
    Once.runHourly('load_nodes', callback: () {
      logger('Load nodes via once');
      _loadNodes();
    }, fallback: () {
      _printNodeStatus(prefix: 'After once hourly having');
    });
    Once.runDaily('clear_errors', callback: () {
      logger('clearErrors via once');
      NodeManager().cleanErrorStats();
    });
  @override
  void dispose() {
    ContactsCache().dispose();
    super.dispose();
  }

anfeichtinger's avatar
anfeichtinger committed
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<NodeListCubit, NodeListState>(
        builder: (BuildContext nodeContext, NodeListState state) {
      return ConnectivityAppWrapper(
vjrj's avatar
vjrj committed
          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),

vjrj's avatar
vjrj committed
                navigatorKey: GinkgoApp.navigatorKey,

vjrj's avatar
vjrj committed
                /// Theme stuff
                themeMode: context.watch<ThemeCubit>().state.themeMode,
vjrj's avatar
vjrj committed

                /// Localization stuff
                localizationsDelegates: context.localizationDelegates,
                supportedLocales: context.supportedLocales,
                locale: context.locale,
                debugShowCheckedModeBanner: false,
                home: context.read<AppCubit>().isIntroViewed
                    ? const SkeletonScreen()
                    : const AppIntro(),
                builder: (BuildContext buildContext, Widget? widget) {
vjrj's avatar
vjrj committed
                  NotificationController.locale = context.locale;
vjrj's avatar
vjrj committed
                  return ResponsiveWrapper.builder(
                    BouncingScrollWrapper.builder(
                        context,
                        ConnectivityWidgetWrapper(
                          message: tr('offline'),
                          height: 20,
                          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
  }
}