import 'dart:async'; import 'dart:io'; import 'package:cw_core/node.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_libmonero/monero/monero.dart'; import 'package:flutter_libmonero/wownero/wownero.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:isar/isar.dart'; import 'package:keyboard_dismisser/keyboard_dismisser.dart'; import 'package:path_provider/path_provider.dart'; import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart'; import 'package:stackwallet/models/exchange/change_now/exchange_transaction_status.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; import 'package:stackwallet/models/isar/models/log.dart'; import 'package:stackwallet/models/models.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/notification_model.dart'; import 'package:stackwallet/models/trade_wallet_lookup.dart'; import 'package:stackwallet/pages/exchange_view/exchange_view.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/intro_view.dart'; import 'package:stackwallet/pages/loading_view.dart'; import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart'; import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/restore_from_encrypted_string_view.dart'; import 'package:stackwallet/pages_desktop_specific/home/desktop_home_view.dart'; import 'package:stackwallet/providers/exchange/available_currencies_state_provider.dart'; import 'package:stackwallet/providers/exchange/available_floating_rate_pairs_state_provider.dart'; import 'package:stackwallet/providers/exchange/changenow_initial_load_status.dart'; import 'package:stackwallet/providers/exchange/estimate_rate_exchange_form_provider.dart'; import 'package:stackwallet/providers/exchange/fixed_rate_exchange_form_provider.dart'; import 'package:stackwallet/providers/exchange/fixed_rate_market_pairs_provider.dart'; import 'package:stackwallet/providers/global/auto_swb_service_provider.dart'; import 'package:stackwallet/providers/global/base_currencies_provider.dart'; // import 'package:stackwallet/providers/global/has_authenticated_start_state_provider.dart'; import 'package:stackwallet/providers/global/trades_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/color_theme_provider.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/debug_service.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_api.dart'; import 'package:stackwallet/services/locale_service.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/notifications_api.dart'; import 'package:stackwallet/services/notifications_service.dart'; import 'package:stackwallet/services/trade_service.dart'; import 'package:stackwallet/services/wallets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/db_version_migration.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/dark_colors.dart'; import 'package:stackwallet/utilities/theme/light_colors.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:window_size/window_size.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() async { WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); GoogleFonts.config.allowRuntimeFetching = false; if (Util.isDesktop) { setWindowTitle('Stack Wallet'); setWindowMinSize(const Size(1200, 900)); setWindowMaxSize(Size.infinite); } Directory appDirectory = (await getApplicationDocumentsDirectory()); if (Platform.isIOS) { appDirectory = (await getLibraryDirectory()); } // FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); if (!(Logging.isArmLinux || Logging.isTestEnv)) { final isar = await Isar.open( [LogSchema], directory: appDirectory.path, inspector: false, ); 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.purgeInfoLogs()); } // Registering Transaction Model Adapters Hive.registerAdapter(TransactionDataAdapter()); Hive.registerAdapter(TransactionChunkAdapter()); Hive.registerAdapter(TransactionAdapter()); Hive.registerAdapter(InputAdapter()); Hive.registerAdapter(OutputAdapter()); // Registering Utxo Model Adapters Hive.registerAdapter(UtxoDataAdapter()); Hive.registerAdapter(UtxoObjectAdapter()); Hive.registerAdapter(StatusAdapter()); // Registering Lelantus Model Adapters Hive.registerAdapter(LelantusCoinAdapter()); // notification model adapter Hive.registerAdapter(NotificationModelAdapter()); // change now trade adapters Hive.registerAdapter(ExchangeTransactionAdapter()); Hive.registerAdapter(ExchangeTransactionStatusAdapter()); // reference lookup data adapter Hive.registerAdapter(TradeWalletLookupAdapter()); // node model adapter Hive.registerAdapter(NodeModelAdapter()); Hive.registerAdapter(NodeAdapter()); if (!Hive.isAdapterRegistered(WalletInfoAdapter().typeId)) { Hive.registerAdapter(WalletInfoAdapter()); } Hive.registerAdapter(WalletTypeAdapter()); Hive.registerAdapter(UnspentCoinsInfoAdapter()); await Hive.initFlutter(appDirectory.path); await Hive.openBox(DB.boxNameDBInfo); int dbVersion = DB.instance.get( boxName: DB.boxNameDBInfo, key: "hive_data_version") as int? ?? 0; if (dbVersion < Constants.currentHiveDbVersion) { await DbVersionMigrator().migrate(dbVersion); } monero.onStartup(); wownero.onStartup(); await Hive.openBox(DB.boxNameTheme); // SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, // overlays: [SystemUiOverlay.bottom]); await NotificationApi.init(); runApp(const ProviderScope(child: MyApp())); } /// MyApp initialises relevant services with a MultiProvider class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: 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({ Key? key, }) : super(key: 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; Future load() async { if (didLoad) { return; } didLoad = true; await DB.instance.init(); _notificationsService = ref.read(notificationsProvider); _nodeService = ref.read(nodeServiceChangeNotifierProvider); _tradesService = ref.read(tradesServiceProvider); NotificationApi.prefs = _prefs; NotificationApi.notificationsService = _notificationsService; unawaited(ref.read(baseCurrenciesProvider).update()); await _nodeService.updateDefaults(); await _notificationsService.init( nodeService: _nodeService, tradesService: _tradesService, prefs: _prefs, ); await _prefs.init(); ref.read(priceAnd24hChangeNotifierProvider).start(true); await _wallets.load(_prefs); 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 (_prefs.isAutoBackupEnabled) { switch (_prefs.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; } } } Future _loadChangeNowStandardCurrencies() async { if (ref .read(availableChangeNowCurrenciesStateProvider.state) .state .isNotEmpty && ref .read(availableFloatingRatePairsStateProvider.state) .state .isNotEmpty) { return; } final response = await ChangeNowAPI.instance.getAvailableCurrencies(); final response2 = await ChangeNowAPI.instance.getAvailableFloatingRatePairs(); if (response.value != null) { ref.read(availableChangeNowCurrenciesStateProvider.state).state = response.value!; if (response2.value != null) { ref.read(availableFloatingRatePairsStateProvider.state).state = response2.value!; if (response.value!.length > 1) { if (ref.read(estimatedRateExchangeFormProvider).from == null) { if (response.value!.where((e) => e.ticker == "btc").isNotEmpty) { await ref.read(estimatedRateExchangeFormProvider).updateFrom( response.value!.firstWhere((e) => e.ticker == "btc"), false); } } if (ref.read(estimatedRateExchangeFormProvider).to == null) { if (response.value!.where((e) => e.ticker == "doge").isNotEmpty) { await ref.read(estimatedRateExchangeFormProvider).updateTo( response.value!.firstWhere((e) => e.ticker == "doge"), false); } } } } else { Logging.instance.log( "Failed to load changeNOW available floating rate pairs: ${response2.exception?.errorMessage}", level: LogLevel.Error); ref.read(changeNowEstimatedInitialLoadStatusStateProvider.state).state = ChangeNowLoadStatus.failed; return; } } else { Logging.instance.log( "Failed to load changeNOW currencies: ${response.exception?.errorMessage}", level: LogLevel.Error); await Future.delayed(const Duration(seconds: 1)); ref.read(changeNowEstimatedInitialLoadStatusStateProvider.state).state = ChangeNowLoadStatus.failed; return; } ref.read(changeNowEstimatedInitialLoadStatusStateProvider.state).state = ChangeNowLoadStatus.success; } Future _loadFixedRateMarkets() async { Logging.instance.log("Starting initial fixed rate market data loading...", level: LogLevel.Info); if (ref.read(fixedRateMarketPairsStateProvider.state).state.isNotEmpty) { return; } final response3 = await ChangeNowAPI.instance.getAvailableFixedRateMarkets(); if (response3.value != null) { ref.read(fixedRateMarketPairsStateProvider.state).state = response3.value!; if (ref.read(fixedRateExchangeFormProvider).market == null) { final matchingMarkets = response3.value!.where((e) => e.to == "doge" && e.from == "btc"); if (matchingMarkets.isNotEmpty) { await ref .read(fixedRateExchangeFormProvider) .updateMarket(matchingMarkets.first, true); } } Logging.instance.log("Initial fixed rate market data loading complete.", level: LogLevel.Info); } else { Logging.instance.log( "Failed to load changeNOW fixed rate markets: ${response3.exception?.errorMessage}", level: LogLevel.Error); ref.read(changeNowFixedInitialLoadStatusStateProvider.state).state = ChangeNowLoadStatus.failed; return; } ref.read(changeNowFixedInitialLoadStatusStateProvider.state).state = ChangeNowLoadStatus.success; } Future _loadChangeNowData() async { List> concurrentFutures = []; concurrentFutures.add(_loadChangeNowStandardCurrencies()); if (kFixedRateEnabled) { concurrentFutures.add(_loadFixedRateMarkets()); } } @override void initState() { final colorScheme = DB.instance .get(boxName: DB.boxNameTheme, key: "colorScheme") as String?; ThemeType themeType; switch (colorScheme) { case "dark": themeType = ThemeType.dark; break; case "light": default: themeType = ThemeType.light; } loadingCompleter = Completer(); WidgetsBinding.instance.addObserver(this); // load locale and prefs ref .read(localeServiceChangeNotifierProvider.notifier) .loadLocale(notify: false); _prefs = ref.read(prefsChangeNotifierProvider); _wallets = ref.read(walletsChangeNotifierProvider); WidgetsBinding.instance.addPostFrameCallback((_) async { ref.read(colorThemeProvider.state).state = StackColors.fromStackColorTheme( themeType == ThemeType.dark ? DarkColors() : LightColors()); 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; } }); super.initState(); } @override dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @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; } } /// 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 (!_prefs.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 Stack Wallet backup", biometricsAuthenticationTitle: "Restore Stack 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(colorThemeProvider.state).state; return MaterialApp( key: GlobalKey(), navigatorKey: navigatorKey, title: 'Stack Wallet', onGenerateRoute: RouteGenerator.generateRoute, theme: ThemeData( extensions: [colorScheme], highlightColor: colorScheme.highlight, brightness: Brightness.light, 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, 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: FutureBuilder( future: load(), builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done) { // FlutterNativeSplash.remove(); if (_wallets.hasWallets || _prefs.hasPin) { // return HomeView(); // run without awaiting if (Constants.enableExchange) { _loadChangeNowData(); } String? startupWalletId; if (ref.read(prefsChangeNotifierProvider).gotoWalletOnStartup) { startupWalletId = ref.read(prefsChangeNotifierProvider).startupWalletId; } // TODO proper desktop auth view if (Util.isDesktop) { Future.delayed(Duration.zero).then((value) => Navigator.of(context).pushNamedAndRemoveUntil( DesktopHomeView.routeName, (route) => false)); return Container(); } return LockscreenView( isInitialAppLogin: true, routeOnSuccess: HomeView.routeName, routeOnSuccessArguments: startupWalletId, biometricsAuthenticationTitle: "Unlock Stack", biometricsLocalizedReason: "Unlock your stack wallet using biometrics", biometricsCancelButtonString: "Cancel", ); } 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(); } }, ), ); } }