dev
dart 566 lines 20.2 KB
Raw
1 import 'dart:async';
2 import 'dart:io';
3 import 'dart:ui';
4
5 import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
6 import 'package:cake_wallet/app_scroll_behavior.dart';
7 import 'package:cake_wallet/core/auth_service.dart';
8 import 'package:cake_wallet/core/background_sync.dart';
9 import 'package:cake_wallet/core/node_switching_service.dart';
10 import 'package:cake_wallet/core/reset_service.dart';
11 import 'package:cake_wallet/core/secure_storage.dart';
12 import 'package:cake_wallet/core/trade_monitor.dart';
13 import 'package:cake_wallet/di.dart';
14 import 'package:cake_wallet/entities/contact.dart';
15 import 'package:cake_wallet/entities/default_settings_migration.dart';
16 import 'package:cake_wallet/entities/get_encryption_key.dart';
17 import 'package:cake_wallet/entities/haven_seed_store.dart';
18 import 'package:cake_wallet/entities/language_service.dart';
19 import 'package:cake_wallet/entities/node_check.dart';
20 import 'package:cake_wallet/entities/template.dart';
21 import 'package:cake_wallet/entities/transaction_description.dart';
22 import 'package:cake_wallet/exchange/exchange_template.dart';
23 import 'package:cake_wallet/exchange/trade_legacy.dart';
24 import 'package:cake_wallet/generated/i18n.dart';
25 import 'package:cake_wallet/locales/locale.dart';
26 import "package:cake_wallet/new-ui/widgets/money/money_settings_provider.dart";
27 import 'package:cake_wallet/order/order.dart';
28 import 'package:cake_wallet/reactions/bootstrap.dart';
29 import 'package:cake_wallet/router.dart' as Router;
30 import 'package:cake_wallet/routes.dart';
31 import 'package:cake_wallet/src/screens/root/root.dart';
32 import 'package:cake_wallet/store/app_store.dart';
33 import 'package:cake_wallet/store/authentication_store.dart';
34 import 'package:cake_wallet/store/settings_store.dart';
35 import 'package:cake_wallet/test_asset_bundles.dart';
36 import 'package:cake_wallet/themes/utils/theme_provider.dart';
37 import 'package:cake_wallet/utils/device_info.dart';
38 import 'package:cake_wallet/utils/exception_handler.dart';
39 import 'package:cake_wallet/utils/feature_flag.dart';
40 import 'package:cake_wallet/utils/responsive_layout_util.dart';
41 import 'package:cake_wallet/view_model/link_view_model.dart';
42 import 'package:cake_wallet/zcash/zcash.dart';
43 import 'package:cw_core/address_info.dart';
44 import 'package:cw_core/cake_hive.dart';
45 import 'package:cw_core/db/sqlite.dart';
46 import 'package:cw_core/erc20_token_legacy.dart' show performErc20TokenHiveMigration;
47 import 'package:cw_core/hive_type_ids.dart';
48 import 'package:cw_core/key.dart';
49 import 'package:cw_core/mweb_utxo.dart';
50 import 'package:cw_core/node.dart';
51 import 'package:cw_core/node_legacy.dart' show performNodeHiveMigration;
52 import 'package:cw_core/payjoin_session.dart';
53 import 'package:cw_core/root_dir.dart';
54 import 'package:cw_core/spl_token_legacy.dart' show performSplTokenHiveMigration;
55 import 'package:cw_core/tron_token_legacy.dart' show performTronTokenHiveMigration;
56 import 'package:cw_core/unspent_coins_info.dart';
57 import 'package:cw_core/utils/print_verbose.dart';
58 import 'package:cw_core/utils/proxy_logger/memory_proxy_logger.dart';
59 import 'package:cw_core/utils/proxy_wrapper.dart';
60 import 'package:cw_core/utils/tor/abstract.dart';
61 import 'package:cw_core/wallet_info.dart';
62 import 'package:cw_core/wallet_type.dart';
63 import 'package:cw_core/window_size.dart';
64 import 'package:flutter/foundation.dart';
65 import 'package:flutter/material.dart';
66 import 'package:flutter/services.dart';
67 import 'package:flutter_daemon/flutter_daemon.dart';
68 import 'package:flutter_mobx/flutter_mobx.dart';
69 import 'package:hive/hive.dart';
70 import 'package:quick_actions/quick_actions.dart';
71 import 'package:logging/logging.dart';
72 import 'package:shared_preferences/shared_preferences.dart';
73 import 'package:trezor_connect/trezor_connect.dart';
74
75 final navigatorKey = GlobalKey<NavigatorState>();
76 final rootKey = GlobalKey<RootState>();
77 final RouteObserver<PageRoute<dynamic>> routeObserver = RouteObserver<PageRoute<dynamic>>();
78 final quickActionsStream = StreamController<Uri?>.broadcast();
79
80 Future<void> main({Key? topLevelKey}) async {
81 await runAppWithZone(topLevelKey: topLevelKey);
82 }
83
84 Future<void> runAppWithZone({Key? topLevelKey}) async {
85 bool isAppRunning = false;
86
87 await runZonedGuarded(() async {
88 WidgetsFlutterBinding.ensureInitialized();
89
90 final Completer<String?> initialShortcutCompleter = Completer<String?>();
91
92 // Quick Actions plugin supports only iOS/Android platforms
93 if (Platform.isIOS || Platform.isAndroid) {
94 const QuickActions quickActions = QuickActions();
95
96 quickActions.initialize((String shortcutType) {
97 // Complete the completer only once (for cold starts)
98 if (!initialShortcutCompleter.isCompleted) {
99 initialShortcutCompleter.complete(shortcutType);
100 }
101
102 // Convert the shortcut type to a URI and add it to the stream
103 final uri = Uri.parse('cakewallet://quickaction/$shortcutType');
104 quickActionsStream.sink.add(uri);
105 });
106
107 quickActions.setShortcutItems(<ShortcutItem>[
108 const ShortcutItem(
109 type: 'send', icon: 'send', localizedTitle: 'Send', localizedSubtitle: 'Send funds'),
110 const ShortcutItem(
111 type: 'receive',
112 icon: 'receive',
113 localizedTitle: 'Receive',
114 localizedSubtitle: 'Receive funds')
115 ]);
116
117 // Fallback in case the initial shortcut is not received (normal cold start)
118 Future<void>.delayed(const Duration(milliseconds: 500), () {
119 if (!initialShortcutCompleter.isCompleted) {
120 initialShortcutCompleter.complete(null);
121 }
122 });
123 } else {
124 initialShortcutCompleter.complete(null);
125 }
126
127 final initialQuickAction = await initialShortcutCompleter.future;
128 FlutterError.onError = ExceptionHandler.onError;
129
130 /// A callback that is invoked when an unhandled error occurs in the root
131 /// isolate.
132 PlatformDispatcher.instance.onError = (error, stack) {
133 ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stack));
134
135 return true;
136 };
137
138 await FlutterDaemon().unmarkBackgroundSync();
139 await initDb();
140
141 try {
142 CakeTor.instance = await CakeTorInstance.getInstance();
143 } catch (e) {
144 printV("Failed to initialize tor: $e");
145 }
146
147 try {
148 await linuxSymlinkSharedPreferences();
149 } catch (e) {
150 printV("Failed to symlink linux preferences: $e");
151 }
152
153 await initializeAppAtRoot();
154
155 if (kDebugMode) {
156 final appDocDir = await getAppDir();
157
158 final ledgerFile = File('${appDocDir.path}/ledger_log.txt');
159 if (!ledgerFile.existsSync()) ledgerFile.createSync();
160 Logger.root.onRecord.listen((event) async {
161 final content = ledgerFile.readAsStringSync();
162 ledgerFile.writeAsStringSync("$content\n${event.message}");
163 });
164 }
165
166 if (FeatureFlag.hasDevOptions) {
167 ProxyWrapper.logger = MemoryProxyLogger();
168 }
169
170 if (!Platform.isWindows) {
171 var zcashPassword = await secureStorageShared.read(key: "com.cakewallet.cw_zcash/zec.db");
172 if (zcashPassword == null || zcashPassword.isEmpty) {
173 zcashPassword = generateKey().substring(0, 32);
174 secureStorageShared.write(key: "com.cakewallet.cw_zcash/zec.db", value: zcashPassword);
175 }
176 zcash?.unlockDatabase(zcashPassword);
177 }
178
179 // Basically when we're running a test
180 if (topLevelKey != null) {
181 runApp(
182 DefaultAssetBundle(
183 bundle: TestAssetBundle(),
184 child: App(
185 key: topLevelKey,
186 initialQuickAction: initialQuickAction,
187 quickActionsStream: quickActionsStream.stream),
188 ),
189 );
190 } else {
191 runApp(App(
192 key: topLevelKey,
193 initialQuickAction: initialQuickAction,
194 quickActionsStream: quickActionsStream.stream));
195 }
196
197 isAppRunning = true;
198 }, (error, stackTrace) async {
199 if (!isAppRunning) {
200 runApp(
201 TopLevelErrorWidget(error: error, stackTrace: stackTrace),
202 );
203 }
204
205 await ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stackTrace));
206 });
207 }
208
209 Future<void> initializeAppAtRoot({bool reInitializing = false}) async {
210 if (!reInitializing) await setDefaultMinimumWindowSize();
211 await CakeHive.close();
212 await initializeAppConfigs();
213 }
214
215 Future<void> initializeAppConfigs({bool loadWallet = true}) async {
216 setRootDirFromEnv();
217 final appDir = await getAppDir();
218 CakeHive.init(appDir.path);
219
220 if (!CakeHive.isAdapterRegistered(Contact.typeId)) {
221 CakeHive.registerAdapter(ContactAdapter());
222 }
223
224 if (!CakeHive.isAdapterRegistered(TransactionDescription.typeId)) {
225 CakeHive.registerAdapter(TransactionDescriptionAdapter());
226 }
227
228 if (!CakeHive.isAdapterRegistered(AddressInfo.typeId)) {
229 CakeHive.registerAdapter(AddressInfoAdapter());
230 }
231
232 if (!CakeHive.isAdapterRegistered(WALLET_TYPE_TYPE_ID)) {
233 CakeHive.registerAdapter(WalletTypeAdapter());
234 }
235
236 if (!CakeHive.isAdapterRegistered(Template.typeId)) {
237 CakeHive.registerAdapter(TemplateAdapter());
238 }
239
240 if (!CakeHive.isAdapterRegistered(ExchangeTemplate.typeId)) {
241 CakeHive.registerAdapter(ExchangeTemplateAdapter());
242 }
243
244 if (!CakeHive.isAdapterRegistered(Order.typeId)) {
245 CakeHive.registerAdapter(OrderAdapter());
246 }
247
248 if (!CakeHive.isAdapterRegistered(UnspentCoinsInfo.typeId)) {
249 CakeHive.registerAdapter(UnspentCoinsInfoAdapter());
250 }
251
252 if (!CakeHive.isAdapterRegistered(AnonpayInvoiceInfo.typeId)) {
253 CakeHive.registerAdapter(AnonpayInvoiceInfoAdapter());
254 }
255
256 if (!CakeHive.isAdapterRegistered(HavenSeedStore.typeId)) {
257 CakeHive.registerAdapter(HavenSeedStoreAdapter());
258 }
259
260 if (!CakeHive.isAdapterRegistered(MwebUtxo.typeId)) {
261 CakeHive.registerAdapter(MwebUtxoAdapter());
262 }
263
264 if (!CakeHive.isAdapterRegistered(PayjoinSession.typeId)) {
265 CakeHive.registerAdapter(PayjoinSessionAdapter());
266 }
267
268 await performHiveMigration();
269 await performErc20TokenHiveMigration();
270 await performSplTokenHiveMigration();
271 await performTronTokenHiveMigration();
272
273 final secureStorage = secureStorageShared;
274 final transactionDescriptionsBoxKey =
275 await getEncryptionKey(secureStorage: secureStorage, forKey: TransactionDescription.boxKey);
276 final ordersBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: Order.boxKey);
277 final contacts = await CakeHive.openBox<Contact>(Contact.boxName);
278 final transactionDescriptions = await CakeHive.openBox<TransactionDescription>(
279 TransactionDescription.boxName,
280 encryptionKey: transactionDescriptionsBoxKey);
281 await performTradeHiveMigration(secureStorage);
282 await performNodeHiveMigration();
283 await validateBuiltinNodes();
284
285 final orders = await CakeHive.openBox<Order>(Order.boxName, encryptionKey: ordersBoxKey);
286 final templates = await CakeHive.openBox<Template>(Template.boxName);
287 final exchangeTemplates = await CakeHive.openBox<ExchangeTemplate>(ExchangeTemplate.boxName);
288 final anonpayInvoiceInfo = await CakeHive.openBox<AnonpayInvoiceInfo>(AnonpayInvoiceInfo.boxName);
289 final unspentCoinsInfoSource = await CakeHive.openBox<UnspentCoinsInfo>(UnspentCoinsInfo.boxName);
290 final payjoinSessionSource = await CakeHive.openBox<PayjoinSession>(PayjoinSession.boxName);
291
292 final havenSeedStoreBoxKey =
293 await getEncryptionKey(secureStorage: secureStorage, forKey: HavenSeedStore.boxKey);
294 final havenSeedStore = await CakeHive.openBox<HavenSeedStore>(HavenSeedStore.boxName,
295 encryptionKey: havenSeedStoreBoxKey);
296
297 await initialSetup(
298 loadWallet: loadWallet,
299 sharedPreferences: await SharedPreferences.getInstance(),
300 contactSource: contacts,
301 ordersSource: orders,
302 unspentCoinsInfoSource: unspentCoinsInfoSource,
303 // fiatConvertationService: fiatConvertationService,
304 templates: templates,
305 exchangeTemplates: exchangeTemplates,
306 transactionDescriptions: transactionDescriptions,
307 secureStorage: secureStorage,
308 payjoinSessionSource: payjoinSessionSource,
309 anonpayInvoiceInfo: anonpayInvoiceInfo,
310 havenSeedStore: havenSeedStore,
311 initialMigrationVersion: 70,
312 );
313 }
314
315 Future<void> initialSetup({
316 required bool loadWallet,
317 required SharedPreferences sharedPreferences,
318 required Box<Contact> contactSource,
319 required Box<Order> ordersSource,
320 // required FiatConvertationService fiatConvertationService,
321 required Box<Template> templates,
322 required Box<ExchangeTemplate> exchangeTemplates,
323 required Box<TransactionDescription> transactionDescriptions,
324 required SecureStorage secureStorage,
325 required Box<AnonpayInvoiceInfo> anonpayInvoiceInfo,
326 required Box<UnspentCoinsInfo> unspentCoinsInfoSource,
327 required Box<PayjoinSession> payjoinSessionSource,
328 required Box<HavenSeedStore> havenSeedStore,
329 required int initialMigrationVersion,
330 }) async {
331 LanguageService.loadLocaleList();
332 await defaultSettingsMigration(
333 secureStorage: secureStorage,
334 version: initialMigrationVersion,
335 sharedPreferences: sharedPreferences,
336 contactSource: contactSource,
337 havenSeedStore: havenSeedStore,
338 );
339 await setup(
340 contactSource: contactSource,
341 ordersSource: ordersSource,
342 templates: templates,
343 exchangeTemplates: exchangeTemplates,
344 transactionDescriptionBox: transactionDescriptions,
345 anonpayInvoiceInfoSource: anonpayInvoiceInfo,
346 unspentCoinsInfoSource: unspentCoinsInfoSource,
347 payjoinSessionSource: payjoinSessionSource,
348 navigatorKey: navigatorKey,
349 secureStorage: secureStorage,
350 );
351 final settingsStore = getIt<SettingsStore>();
352 await checkCurrentNodes(sharedPreferences, settingsStore);
353
354 await getIt.get<ResetService>().resetAuthDataOnNewInstall(sharedPreferences);
355
356 await bootstrapOffline();
357 if (!settingsStore.currentBuiltinTor) {
358 bootstrapOnline(navigatorKey, loadWallet: loadWallet);
359 }
360 }
361
362 class App extends StatefulWidget {
363 App({this.key, this.initialQuickAction, required this.quickActionsStream});
364
365 final Key? key;
366 final String? initialQuickAction;
367 final Stream<Uri?> quickActionsStream;
368 @override
369 AppState createState() => AppState();
370 }
371
372 class AppState extends State<App> with SingleTickerProviderStateMixin {
373 @override
374 Widget build(BuildContext context) {
375 return Observer(
376 builder: (BuildContext context) {
377 final appStore = getIt.get<AppStore>();
378 final authService = getIt.get<AuthService>();
379 final linkViewModel = getIt.get<LinkViewModel>();
380 final tradeMonitor = getIt.get<TradeMonitor>();
381 final nodeSwitchingService = getIt.get<NodeSwitchingService>();
382 final settingsStore = appStore.settingsStore;
383 final statusBarColor = Colors.transparent;
384 final authenticationStore = getIt.get<AuthenticationStore>();
385 final initialRoute = authenticationStore.state == AuthenticationState.uninitialized
386 ? Routes.welcome
387 : settingsStore.currentBuiltinTor
388 ? Routes.startTor
389 : Routes.login;
390 final currentTheme = appStore.themeStore.currentTheme;
391 final statusBarBrightness =
392 currentTheme.type == currentTheme.isDark ? Brightness.light : Brightness.dark;
393 final statusBarIconBrightness =
394 currentTheme.type == currentTheme.isDark ? Brightness.light : Brightness.dark;
395 SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
396 statusBarColor: statusBarColor,
397 statusBarBrightness: statusBarBrightness,
398 statusBarIconBrightness: statusBarIconBrightness));
399
400 final appRouteObserver = AppRouteObserver();
401
402 return Root(
403 key: widget.key ?? rootKey,
404 appStore: appStore,
405 initialQuickAction: widget.initialQuickAction,
406 quickActionsStream: widget.quickActionsStream,
407 authenticationStore: authenticationStore,
408 navigatorKey: navigatorKey,
409 authService: authService,
410 linkViewModel: linkViewModel,
411 tradeMonitor: tradeMonitor,
412 nodeSwitchingService: nodeSwitchingService,
413 trezorConnect: getIt<TrezorConnect>(),
414 child: ThemeProvider(
415 themeStore: appStore.themeStore,
416 materialAppBuilder: (context, theme, darkTheme, themeMode) => MaterialApp(
417 navigatorObservers: [routeObserver, appRouteObserver],
418 navigatorKey: navigatorKey,
419 debugShowCheckedModeBanner: false,
420 builder: (context, child) => MediaQuery(
421 data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling),
422 child: MoneySettingsProvider(settingsStore: settingsStore, child: child!),
423 ),
424 theme: theme,
425 darkTheme: darkTheme,
426 themeMode: themeMode,
427 localizationsDelegates: localizationDelegates,
428 supportedLocales: S.delegate.supportedLocales,
429 locale: Locale(appStore.settingsStore.languageCode),
430 onGenerateRoute: (settings) => Router.createRoute(settings),
431 initialRoute: initialRoute,
432 scrollBehavior: AppScrollBehavior(),
433 home: _Home(),
434 ),
435 ),
436 );
437 },
438 );
439 }
440 }
441
442 class _Home extends StatefulWidget {
443 const _Home();
444
445 @override
446 State<_Home> createState() => _HomeState();
447 }
448
449 class _HomeState extends State<_Home> {
450 @override
451 void didChangeDependencies() {
452 _setOrientation(context);
453
454 super.didChangeDependencies();
455 }
456
457 void _setOrientation(BuildContext context) {
458 if (!DeviceInfo.instance.isDesktop) {
459 if (responsiveLayoutUtil.shouldRenderMobileUI) {
460 SystemChrome.setPreferredOrientations(
461 [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
462 } else {
463 SystemChrome.setPreferredOrientations(
464 [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
465 }
466 }
467 }
468
469 @override
470 Widget build(BuildContext context) {
471 return const SizedBox.shrink();
472 }
473 }
474
475 class TopLevelErrorWidget extends StatelessWidget {
476 const TopLevelErrorWidget({
477 required this.error,
478 required this.stackTrace,
479 super.key,
480 });
481
482 final Object error;
483 final StackTrace stackTrace;
484
485 @override
486 Widget build(BuildContext context) {
487 return MaterialApp(
488 debugShowCheckedModeBanner: false,
489 scrollBehavior: AppScrollBehavior(),
490 home: Scaffold(
491 body: SingleChildScrollView(
492 child: Container(
493 margin: EdgeInsets.only(top: 50, left: 20, right: 20, bottom: 20),
494 child: Column(
495 children: [
496 Text(
497 'Error:\n${error.toString()}',
498 style: Theme.of(context).textTheme.bodyLarge!.copyWith(
499 fontSize: 22,
500 ),
501 ),
502 Text(
503 'Stack trace:\n${stackTrace.toString()}',
504 style: Theme.of(context).textTheme.bodyMedium!.copyWith(
505 fontSize: 16,
506 ),
507 ),
508 ],
509 ),
510 ),
511 ),
512 ),
513 );
514 }
515 }
516
517 @pragma('vm:entry-point')
518 Future<void> backgroundSync() async {
519 bool shouldUnmark = false;
520 try {
521 printV("Background sync triggered");
522 printV("- WidgetsFlutterBinding.ensureInitialized()");
523 WidgetsFlutterBinding.ensureInitialized();
524 printV("- DartPluginRegistrant.ensureInitialized()");
525 DartPluginRegistrant.ensureInitialized();
526 printV("- FlutterDaemon.markBackgroundSync()");
527 final val = await FlutterDaemon().markBackgroundSync();
528 if (val) {
529 printV("Background sync already in progress");
530 return;
531 }
532 shouldUnmark = true;
533 printV("Starting background sync");
534 final backgroundSync = BackgroundSync();
535 await initializeAppConfigs(loadWallet: false);
536 await backgroundSync.sync();
537 printV("Background sync completed");
538 } finally {
539 if (shouldUnmark) {
540 printV("Unmarking background sync");
541 await FlutterDaemon().unmarkBackgroundSync();
542 } else {
543 printV("Not unmarking background sync");
544 }
545 }
546 }
547
548 class AppRouteObserver extends RouteObserver<PageRoute<dynamic>> {
549 final AppStore appStore = getIt.get<AppStore>();
550
551 @override
552 void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
553 super.didPush(route, previousRoute);
554 if (route is PageRoute) {
555 appStore.currentRouteName = route.settings.name;
556 }
557 }
558
559 @override
560 void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
561 super.didPop(route, previousRoute);
562 if (previousRoute is PageRoute) {
563 appStore.currentRouteName = previousRoute.settings.name;
564 }
565 }
566 }