/* * This file is part of Stack Wallet. * * Copyright (c) 2023 Cypher Stack * All Rights Reserved. * The code is distributed under GPLv3 license, see LICENSE file for details. * Generated by Cypher Stack on 2023-05-26 * */ import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'package:coinlib_flutter/coinlib_flutter.dart'; import 'package:compat/compat.dart' as lib_monero_compat; import 'package:cs_monero/cs_monero.dart' as lib_monero; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:isar/isar.dart'; import 'package:keyboard_dismisser/keyboard_dismisser.dart'; import 'package:path_provider/path_provider.dart'; import 'package:window_size/window_size.dart'; import 'app_config.dart'; import 'db/db_version_migration.dart'; import 'db/hive/db.dart'; import 'db/isar/main_db.dart'; import 'db/special_migrations.dart'; import 'db/sqlite/firo_cache.dart'; import 'models/exchange/change_now/exchange_transaction.dart'; import 'models/exchange/change_now/exchange_transaction_status.dart'; import 'models/exchange/response_objects/trade.dart'; import 'models/isar/models/isar_models.dart'; import 'models/models.dart'; import 'models/node_model.dart'; import 'models/notification_model.dart'; import 'models/trade_wallet_lookup.dart'; import 'pages/campfire_migrate_view.dart'; import 'pages/home_view/home_view.dart'; import 'pages/intro_view.dart'; import 'pages/loading_view.dart'; import 'pages/pinpad_views/create_pin_view.dart'; import 'pages/pinpad_views/lock_screen_view.dart'; import 'pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart'; import 'pages_desktop_specific/password/desktop_login_view.dart'; import 'providers/db/main_db_provider.dart'; import 'providers/desktop/storage_crypto_handler_provider.dart'; import 'providers/global/auto_swb_service_provider.dart'; import 'providers/global/base_currencies_provider.dart'; // import 'package:stackwallet/providers/global/has_authenticated_start_state_provider.dart'; import 'providers/global/trades_service_provider.dart'; import 'providers/providers.dart'; import 'route_generator.dart'; // import 'package:stackwallet/services/buy/buy_data_loading_service.dart'; import 'services/debug_service.dart'; import 'services/exchange/exchange_data_loading_service.dart'; import 'services/locale_service.dart'; import 'services/node_service.dart'; import 'services/notifications_api.dart'; import 'services/notifications_service.dart'; import 'services/tor_service.dart'; import 'services/trade_service.dart'; import 'themes/theme_providers.dart'; import 'themes/theme_service.dart'; import 'utilities/constants.dart'; import 'utilities/enums/backup_frequency_type.dart'; import 'utilities/flutter_secure_storage_interface.dart'; import 'utilities/logger.dart'; import 'utilities/prefs.dart'; import 'utilities/stack_file_system.dart'; import 'utilities/util.dart'; import 'wallets/isar/providers/all_wallets_info_provider.dart'; import 'widgets/crypto_notifications.dart'; final openedFromSWBFileStringStateProvider = StateProvider((ref) => null); // main() is the entry point to the app. It initializes Hive (local database), // runs the MyApp widget and checks for new users, caching the value in the // miscellaneous box for later use void main(List args) async { WidgetsFlutterBinding.ensureInitialized(); if (Util.isDesktop && args.length == 2 && args.first == "-d") { StackFileSystem.setDesktopOverrideDir(args.last); } // no longer requried // // Tell flutter_libmonero how to get access to the application dir // FS.setApplicationRootDirectoryFunction( // StackFileSystem.applicationRootDirectory, // ); // TODO set any other external libs file paths (bad external lib design workaround) final loadCoinlibFuture = loadCoinlib(); GoogleFonts.config.allowRuntimeFetching = false; if (Platform.isIOS) { Util.libraryPath = await getLibraryDirectory(); } Screen? screen; if (Platform.isLinux || (Util.isDesktop && !Platform.isIOS)) { screen = await getCurrentScreen(); Util.screenWidth = screen?.frame.width; } if (Util.isDesktop && !Platform.isIOS) { setWindowTitle(AppConfig.appName); setWindowMinSize(const Size(1220, 100)); setWindowMaxSize(Size.infinite); final screenHeight = screen?.frame.height; if (screenHeight != null) { // starting to height be 3/4 screen height or 900, whichever is smaller final height = min(screenHeight * 0.75, 900); setWindowFrame( Rect.fromLTWH(0, 0, 1220, height), ); } } // FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); if (!(Logging.isArmLinux || Logging.isTestEnv)) { final isar = await Isar.open( [LogSchema], directory: (await StackFileSystem.applicationIsarDirectory()).path, inspector: false, maxSizeMiB: 512, ); await Logging.instance.init(isar); await DebugService.instance.init(isar); // clear out all info logs on startup. No need to await and block unawaited(DebugService.instance.deleteLogsOlderThan()); } // Registering Transaction Model Adapters DB.instance.hive.registerAdapter(TransactionDataAdapter()); DB.instance.hive.registerAdapter(TransactionChunkAdapter()); DB.instance.hive.registerAdapter(TransactionAdapter()); DB.instance.hive.registerAdapter(InputAdapter()); DB.instance.hive.registerAdapter(OutputAdapter()); // Registering Utxo Model Adapters DB.instance.hive.registerAdapter(UtxoDataAdapter()); DB.instance.hive.registerAdapter(UtxoObjectAdapter()); DB.instance.hive.registerAdapter(StatusAdapter()); // Registering Lelantus Model Adapters DB.instance.hive.registerAdapter(LelantusCoinAdapter()); // notification model adapter DB.instance.hive.registerAdapter(NotificationModelAdapter()); // change now trade adapters DB.instance.hive.registerAdapter(ExchangeTransactionAdapter()); DB.instance.hive.registerAdapter(ExchangeTransactionStatusAdapter()); DB.instance.hive.registerAdapter(TradeAdapter()); // reference lookup data adapter DB.instance.hive.registerAdapter(TradeWalletLookupAdapter()); // node model adapter DB.instance.hive.registerAdapter(NodeModelAdapter()); // DB.instance.hive.registerAdapter(NodeAdapter()); if (!DB.instance.hive .isAdapterRegistered(lib_monero_compat.WalletInfoAdapter().typeId)) { DB.instance.hive.registerAdapter(lib_monero_compat.WalletInfoAdapter()); } DB.instance.hive.registerAdapter(lib_monero_compat.WalletTypeAdapter()); // DB.instance.hive.registerAdapter(UnspentCoinsInfoAdapter()); lib_monero.Logging.useLogger = true; DB.instance.hive.init( (await StackFileSystem.applicationHiveDirectory()).path, ); await DB.instance.hive.openBox(DB.boxNameDBInfo); await DB.instance.hive.openBox(DB.boxNamePrefs); await Prefs.instance.init(); if (AppConfig.appName == "Campfire" && !Util.isDesktop && !CampfireMigration.didRun) { await CampfireMigration.init(); } // TODO: // This should be moved to happen during the loading animation instead of // showing a blank screen for 4-10 seconds. // Some refactoring will need to be done here to make sure we don't make any // network calls before starting up tor if (Prefs.instance.useTor) { TorService.sharedInstance.init( torDataDirPath: (await StackFileSystem.applicationTorDirectory()).path, ); await TorService.sharedInstance.start(); } await StackFileSystem.initThemesDir(); await FiroCacheCoordinator.init(); // Desktop migrate handled elsewhere (currently desktop_login_view.dart) if (!Util.isDesktop) { final int dbVersion = DB.instance.get( boxName: DB.boxNameDBInfo, key: "hive_data_version", ) as int? ?? 0; if (dbVersion < Constants.currentDataVersion) { try { await DbVersionMigrator().migrate( dbVersion, secureStore: const SecureStorageWrapper( store: FlutterSecureStorage(), isDesktop: false, ), ); } catch (e, s) { Logging.instance.log( "Cannot migrate mobile database\n$e $s", level: LogLevel.Error, printFullLength: true, ); } } } // SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, // overlays: [SystemUiOverlay.bottom]); unawaited(NotificationApi.init()); await loadCoinlibFuture; await MainDB.instance.initMainDB(); ThemeService.instance.init(MainDB.instance); // check and update or install default themes await ThemeService.instance.checkDefaultThemesOnStartup(); // verify current user preference theme and revert to default // if problems are found to prevent app being unusable if (!(await ThemeService.instance .verifyInstalled(themeId: Prefs.instance.themeId))) { Prefs.instance.themeId = "light"; } // verify current user preference light brightness theme and revert to default // if problems are found to prevent app being unusable if (!(await ThemeService.instance .verifyInstalled(themeId: Prefs.instance.systemBrightnessLightThemeId))) { Prefs.instance.systemBrightnessLightThemeId = "light"; } // verify current user preference dark brightness theme and revert to default // if problems are found to prevent app being unusable if (!(await ThemeService.instance .verifyInstalled(themeId: Prefs.instance.systemBrightnessDarkThemeId))) { Prefs.instance.systemBrightnessDarkThemeId = "dark"; } runApp(const ProviderScope(child: MyApp())); } /// MyApp initialises relevant services with a MultiProvider class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { final localeService = LocaleService(); localeService.loadLocale(); return const KeyboardDismisser( child: MaterialAppWithTheme(), ); } } // Sidenote: MaterialAppWithTheme and InitView are only separated for clarity. No other reason. class MaterialAppWithTheme extends ConsumerStatefulWidget { const MaterialAppWithTheme({ super.key, }); @override ConsumerState createState() => _MaterialAppWithThemeState(); } class _MaterialAppWithThemeState extends ConsumerState with WidgetsBindingObserver { static const platform = MethodChannel("STACK_WALLET_RESTORE"); final GlobalKey navigatorKey = GlobalKey(); // late final Wallets _wallets; // late final Prefs _prefs; late final NotificationsService _notificationsService; late final NodeService _nodeService; late final TradesService _tradesService; late final Completer loadingCompleter; bool didLoad = false; bool didLoadShared = false; bool _desktopHasPassword = false; Future loadShared() async { if (didLoadShared) { return; } didLoadShared = true; await DB.instance.init(); await ref.read(prefsChangeNotifierProvider).init(); final familiarity = ref.read(prefsChangeNotifierProvider).familiarity + 1; ref.read(prefsChangeNotifierProvider).familiarity = familiarity; Constants.exchangeForExperiencedUsers(familiarity); if (Util.isDesktop) { _desktopHasPassword = await ref.read(storageCryptoHandlerProvider).hasPassword(); } } Future load() async { try { if (didLoad) { return; } didLoad = true; if (!Util.isDesktop) { await loadShared(); } ref.read(applicationThemesDirectoryPathProvider.notifier).state = StackFileSystem.themesDir!.path; _notificationsService = ref.read(notificationsProvider); _nodeService = ref.read(nodeServiceChangeNotifierProvider); _tradesService = ref.read(tradesServiceProvider); NotificationApi.prefs = ref.read(prefsChangeNotifierProvider); NotificationApi.notificationsService = _notificationsService; unawaited(ref.read(baseCurrenciesProvider).update()); await _nodeService.updateDefaults(); await _notificationsService.init( nodeService: _nodeService, tradesService: _tradesService, prefs: ref.read(prefsChangeNotifierProvider), ); ref.read(priceAnd24hChangeNotifierProvider).start(true); await ref.read(pWallets).load( ref.read(prefsChangeNotifierProvider), ref.read(mainDBProvider), ); loadingCompleter.complete(); // TODO: this should probably run unawaited. Keep commented out for now as proper community nodes ui hasn't been implemented yet // unawaited(_nodeService.updateCommunityNodes()); if (AppConfig.hasFeature(AppFeature.swap) && ref.read(prefsChangeNotifierProvider).enableExchange) { await ExchangeDataLoadingService.instance.initDB(); // run without awaiting if (ref.read(prefsChangeNotifierProvider).externalCalls && await ref.read(prefsChangeNotifierProvider).isExternalCallsSet()) { if (Constants.enableExchange) { await ExchangeDataLoadingService.instance.setCurrenciesIfEmpty( ref.read(efCurrencyPairProvider), ref.read(efRateTypeProvider), ); unawaited(ExchangeDataLoadingService.instance.loadAll()); } // if (Constants.enableBuy) { // unawaited(BuyDataLoadingService().loadAll(ref)); // } } } if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled) { switch (ref.read(prefsChangeNotifierProvider).backupFrequencyType) { case BackupFrequencyType.everyTenMinutes: ref.read(autoSWBServiceProvider).startPeriodicBackupTimer( duration: const Duration(minutes: 10), ); break; case BackupFrequencyType.everyAppStart: unawaited(ref.read(autoSWBServiceProvider).doBackup()); break; case BackupFrequencyType.afterClosingAWallet: // ignore this case here break; } } // ref // .read(prefsChangeNotifierProvider) // .userID; // Just reading the ref should set it if it's not already set // We shouldn't need to do this, instead only generating an ID when (or if) the userID is looked up when creating a quote } catch (e, s) { Logger.print("$e $s", normalLength: false); } } @override void initState() { String themeId; if (ref.read(prefsChangeNotifierProvider).enableSystemBrightness) { final brightness = WidgetsBinding.instance.window.platformBrightness; switch (brightness) { case Brightness.dark: themeId = ref.read(prefsChangeNotifierProvider).systemBrightnessDarkThemeId; break; case Brightness.light: themeId = ref .read(prefsChangeNotifierProvider) .systemBrightnessLightThemeId; break; } } else { themeId = ref.read(prefsChangeNotifierProvider).themeId; } loadingCompleter = Completer(); WidgetsBinding.instance.addObserver(this); // load locale and prefs ref .read(localeServiceChangeNotifierProvider.notifier) .loadLocale(notify: false); WidgetsBinding.instance.addPostFrameCallback((_) async { //Add themes path to provider ref.read(applicationThemesDirectoryPathProvider.notifier).state = StackFileSystem.themesDir!.path; ref.read(themeProvider.state).state = ref.read(pThemeService).getTheme( themeId: themeId, )!; if (Platform.isAndroid) { // fetch open file if it exists await getOpenFile(); if (ref.read(openedFromSWBFileStringStateProvider.state).state != null) { // waiting for loading to complete before going straight to restore if the app was opened via file await loadingCompleter.future; await goToRestoreSWB( ref.read(openedFromSWBFileStringStateProvider.state).state!, ); ref.read(openedFromSWBFileStringStateProvider.state).state = null; } // ref.read(shouldShowLockscreenOnResumeStateProvider.state).state = false; } }); WidgetsBinding.instance.window.onPlatformBrightnessChanged = () { String themeId; switch (WidgetsBinding.instance.window.platformBrightness) { case Brightness.dark: themeId = ref.read(prefsChangeNotifierProvider).systemBrightnessDarkThemeId; break; case Brightness.light: themeId = ref .read(prefsChangeNotifierProvider) .systemBrightnessLightThemeId; break; } WidgetsBinding.instance.addPostFrameCallback((_) { if (ref.read(prefsChangeNotifierProvider).enableSystemBrightness) { ref.read(themeProvider.state).state = ref.read(pThemeService).getTheme( themeId: themeId, )!; } }); }; super.initState(); } @override dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeLocales(List? locales) { ref.read(localeServiceChangeNotifierProvider).loadLocale(); super.didChangeLocales(locales); } @override void didChangeAppLifecycleState(AppLifecycleState state) async { debugPrint("didChangeAppLifecycleState: ${state.name}"); if (state == AppLifecycleState.resumed) {} switch (state) { case AppLifecycleState.inactive: break; case AppLifecycleState.paused: break; case AppLifecycleState.resumed: if (Platform.isAndroid) { // fetch open file if it exists await getOpenFile(); // go straight to restore if the app was resumed via file if (ref.read(openedFromSWBFileStringStateProvider.state).state != null) { await goToRestoreSWB( ref.read(openedFromSWBFileStringStateProvider.state).state!, ); ref.read(openedFromSWBFileStringStateProvider.state).state = null; } } // if (ref.read(hasAuthenticatedOnStartStateProvider.state).state && // ref.read(shouldShowLockscreenOnResumeStateProvider.state).state) { // final now = DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000; // // if (now - _prefs.lastUnlocked > _prefs.lastUnlockedTimeout) { // ref.read(shouldShowLockscreenOnResumeStateProvider.state).state = // false; // Navigator.of(navigatorKey.currentContext!).push( // MaterialPageRoute( // builder: (_) => LockscreenView( // routeOnSuccess: "", // popOnSuccess: true, // biometricsAuthenticationTitle: "Unlock Stack", // biometricsLocalizedReason: // "Unlock your stack wallet using biometrics", // biometricsCancelButtonString: "Cancel", // onSuccess: () { // ref // .read(shouldShowLockscreenOnResumeStateProvider.state) // .state = true; // }, // ), // ), // ); // } // } break; case AppLifecycleState.detached: break; case AppLifecycleState.hidden: break; } } /// should only be called on android currently Future getOpenFile() async { // update provider with new file content state ref.read(openedFromSWBFileStringStateProvider.state).state = await platform.invokeMethod("getOpenFile"); // call reset to clear cached value await resetOpenPath(); Logging.instance.log( "This is the .swb content from intent: ${ref.read(openedFromSWBFileStringStateProvider.state).state}", level: LogLevel.Info, ); } /// should only be called on android currently Future resetOpenPath() async { await platform.invokeMethod("resetOpenPath"); } Future goToRestoreSWB(String encrypted) async { if (!ref.read(prefsChangeNotifierProvider).hasPin) { await Navigator.of(navigatorKey.currentContext!) .pushNamed(CreatePinView.routeName, arguments: true) .then((value) { if (value is! bool || value == false) { Navigator.of(navigatorKey.currentContext!).pushNamed( RestoreFromEncryptedStringView.routeName, arguments: encrypted, ); } }); } else { unawaited( Navigator.push( navigatorKey.currentContext!, RouteGenerator.getRoute( shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, builder: (_) => LockscreenView( showBackButton: true, routeOnSuccess: RestoreFromEncryptedStringView.routeName, routeOnSuccessArguments: encrypted, biometricsCancelButtonString: "CANCEL", biometricsLocalizedReason: "Authenticate to restore ${AppConfig.appName} backup", biometricsAuthenticationTitle: "Restore ${AppConfig.prefix} backup", ), settings: const RouteSettings(name: "/swbrestorelockscreen"), ), ), ); } } InputBorder _buildOutlineInputBorder(Color color) { return OutlineInputBorder( borderSide: BorderSide( width: 1, color: color, ), borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius), ); } @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); // ref.listen(shouldShowLockscreenOnResumeStateProvider, (previous, next) { // Logging.instance.log("shouldShowLockscreenOnResumeStateProvider set to: $next", // addToDebugMessagesDB: false); // }); final colorScheme = ref.watch(colorProvider.state).state; return MaterialApp( key: GlobalKey(), navigatorKey: navigatorKey, title: AppConfig.appName, onGenerateRoute: RouteGenerator.generateRoute, theme: ThemeData( extensions: [colorScheme], highlightColor: colorScheme.highlight, brightness: colorScheme.brightness, fontFamily: GoogleFonts.inter().fontFamily, unselectedWidgetColor: colorScheme.radioButtonBorderDisabled, // textTheme: GoogleFonts.interTextTheme().copyWith( // button: STextStyles.button(context), // subtitle1: STextStyles.field(context).copyWith( // color: colorScheme.textDark, // ), // ), radioTheme: const RadioThemeData( splashRadius: 0, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), // splashFactory: NoSplash.splashFactory, splashColor: Colors.transparent, buttonTheme: ButtonThemeData( splashColor: colorScheme.splash, ), textButtonTheme: TextButtonThemeData( style: ButtonStyle( // splashFactory: NoSplash.splashFactory, overlayColor: MaterialStateProperty.all(colorScheme.splash), minimumSize: MaterialStateProperty.all(const Size(46, 46)), // textStyle: MaterialStateProperty.all( // STextStyles.button(context)), foregroundColor: MaterialStateProperty.all(colorScheme.buttonTextSecondary), backgroundColor: MaterialStateProperty.all( colorScheme.buttonBackSecondary, ), shape: MaterialStateProperty.all( RoundedRectangleBorder( // 1000 to be relatively sure it keeps its pill shape borderRadius: BorderRadius.circular(1000), ), ), ), ), primaryColor: colorScheme.accentColorDark, primarySwatch: Util.createMaterialColor(colorScheme.accentColorDark), checkboxTheme: CheckboxThemeData( splashRadius: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(Constants.size.checkboxBorderRadius), ), checkColor: MaterialStateColor.resolveWith( (state) { if (state.contains(MaterialState.selected)) { return colorScheme.checkboxIconChecked; } return colorScheme.checkboxBGChecked; }, ), fillColor: MaterialStateColor.resolveWith( (states) { if (states.contains(MaterialState.selected)) { return colorScheme.checkboxBGChecked; } return colorScheme.checkboxBorderEmpty; }, ), ), appBarTheme: AppBarTheme( centerTitle: false, color: colorScheme.background, surfaceTintColor: colorScheme.background, elevation: 0, ), inputDecorationTheme: InputDecorationTheme( focusColor: colorScheme.textFieldDefaultBG, fillColor: colorScheme.textFieldDefaultBG, filled: true, contentPadding: const EdgeInsets.symmetric( vertical: 6, horizontal: 12, ), // labelStyle: STextStyles.fieldLabel(context), // hintStyle: STextStyles.fieldLabel(context), enabledBorder: _buildOutlineInputBorder(colorScheme.textFieldDefaultBG), focusedBorder: _buildOutlineInputBorder(colorScheme.textFieldDefaultBG), errorBorder: _buildOutlineInputBorder(colorScheme.textFieldDefaultBG), disabledBorder: _buildOutlineInputBorder(colorScheme.textFieldDefaultBG), focusedErrorBorder: _buildOutlineInputBorder(colorScheme.textFieldDefaultBG), ), ), home: CryptoNotifications( child: Util.isDesktop ? FutureBuilder( future: loadShared(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { if (_desktopHasPassword) { String? startupWalletId; if (ref .read(prefsChangeNotifierProvider) .gotoWalletOnStartup) { startupWalletId = ref .read(prefsChangeNotifierProvider) .startupWalletId; } return DesktopLoginView( startupWalletId: startupWalletId, load: load, ); } else { return const IntroView(); } } else { return const LoadingView(); } }, ) : FutureBuilder( future: load(), builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done) { // FlutterNativeSplash.remove(); if (ref.read(pAllWalletsInfo).isNotEmpty || ref.read(prefsChangeNotifierProvider).hasPin) { // return HomeView(); String? startupWalletId; if (ref .read(prefsChangeNotifierProvider) .gotoWalletOnStartup) { startupWalletId = ref .read(prefsChangeNotifierProvider) .startupWalletId; } return LockscreenView( isInitialAppLogin: true, routeOnSuccess: HomeView.routeName, routeOnSuccessArguments: startupWalletId, biometricsAuthenticationTitle: "Unlock ${AppConfig.prefix}", biometricsLocalizedReason: "Unlock your ${AppConfig.appName} using biometrics", biometricsCancelButtonString: "Cancel", ); } else { if (AppConfig.appName == "Campfire" && !CampfireMigration.didRun && CampfireMigration.hasOldWallets) { return const CampfireMigrateView(); } else { return const IntroView(); } } } else { // CURRENTLY DISABLED as cannot be animated // technically not needed as FlutterNativeSplash will overlay // anything returned here until the future completes but // FutureBuilder requires you to return something return const LoadingView(); } }, ), ), ); } }