import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:warp_api/data_fb_generated.dart'; import 'package:warp_api/types.dart'; import 'package:warp_api/warp_api.dart'; import 'package:badges/badges.dart' as Badges; import 'package:path/path.dart' as p; import 'about.dart'; import 'account.dart'; import 'animated_qr.dart'; import 'budget.dart'; import 'coin/coins.dart'; import 'contact.dart'; import 'db.dart'; import 'history.dart'; import 'generated/l10n.dart'; import 'main.dart'; import 'message.dart'; import 'note.dart'; import 'reset.dart'; import 'store.dart'; class HomePage extends StatefulWidget { @override HomeState createState() => HomeState(); } class HomeState extends State { StreamSubscription? _syncDispose; Timer? _syncTimer; Timer? _priceTimer; @override void initState() { super.initState(); contacts.fetchContacts(); Future.microtask(() async { await syncStatus.update(); active.updateBalances(); await priceStore.updateChart(); await Future.delayed(Duration(seconds: 3)); await syncStatus.sync(false); _syncTimer?.cancel(); _syncTimer = Timer.periodic(Duration(seconds: 15), (Timer t) async { syncStatus.sync(false); if (active.id != 0) { active.updateBalances(); } }); _priceTimer?.cancel(); _priceTimer = Timer.periodic(Duration(minutes: 5), (Timer t) async { await priceStore.updateChart(); }); }); _syncDispose = syncStream.listen((p) { if (p is String) { final pjs = jsonDecode(p); final progress = Progress.fromJson(pjs); syncStatus.setProgress(progress); } final syncedHeight = syncStatus.getDbSyncedHeight(); if (syncedHeight != null) { final h = syncedHeight.height; syncStatus.setSyncHeight(h, syncedHeight.timestamp); eta.checkpoint(h, DateTime.now()); active.update(); final progress = syncStatus.progress; if (progress != null && isMobile()) { FlutterForegroundTask.updateService( notificationText: "$progress %"); } syncStats.add(DateTime.now(), syncedHeight.height); } }); } @override void dispose() { _syncDispose?.cancel(); _syncTimer?.cancel(); _priceTimer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) => Observer(builder: (context) { // ignore: unused_local_variable final _simpleMode = settings.simpleMode; return HomeInnerPage(key: UniqueKey()); }); } class HomeInnerPage extends StatefulWidget { HomeInnerPage({Key? key}) : super(key: key); @override HomeInnerState createState() => HomeInnerState(); } class HomeInnerState extends State with SingleTickerProviderStateMixin { TabController? _tabController; int _tabIndex = 0; final contactKey = GlobalKey(); @override void initState() { super.initState(); if (Platform.isAndroid) _initForegroundTask(); final tabController = TabController(length: settings.simpleMode ? 4 : 7, vsync: this); tabController.addListener(() { setState(() { _tabIndex = tabController.index; }); }); _tabController = tabController; } @override Widget build(BuildContext context) { final s = S.of(context); final theme = Theme.of(context); final simpleMode = settings.simpleMode; final contactTabIndex = simpleMode ? 3 : 6; Widget button = Container(); if (_tabIndex == 0) button = FloatingActionButton( onPressed: _onSend, backgroundColor: theme.colorScheme.secondary, child: Icon(Icons.send), ); else if (_tabIndex == contactTabIndex) button = FloatingActionButton( onPressed: _onAddContact, backgroundColor: theme.colorScheme.secondary, child: Icon(Icons.add), ); else if (_tabIndex == 1) button = FloatingActionButton( onPressed: _onSend, backgroundColor: theme.colorScheme.secondary, child: Icon(Icons.send), ); return Observer(builder: (context) { // ignore: unused_local_variable final _unused1 = active.dataEpoch; // ignore: unused_local_variable final _unused2 = syncStatus.paused; // ignore: unused_local_variable final _unused3 = syncStatus.syncing; final rescanMsg; if (syncStatus.paused) rescanMsg = s.resumeScan; else if (syncStatus.syncing) rescanMsg = s.cancelScan; else rescanMsg = s.rescan; final unread = active.unread; final messageTab = unread != 0 ? Tab(child: Badges.Badge( child: Text(s.messages), badgeContent: Text('$unread'))) : Tab(text: s.messages); if (active.id == 0) { Future.microtask(() async { Navigator.of(context).pushReplacementNamed('/accounts'); }); return SizedBox(); // Show a placeholder } final menu = PopupMenuButton( itemBuilder: (context) { return [ PopupMenuItem(child: Text(s.accounts), value: "Accounts"), PopupMenuItem(child: Text(s.backup), value: "Backup"), PopupMenuItem(child: Text(rescanMsg), value: "Rescan"), if (!simpleMode) PopupMenuItem(child: Text(s.pools), value: "Pools"), if (!simpleMode) PopupMenuItem(child: PopupMenuButton( child: Text(s.advanced), itemBuilder: (_) => [ if (!isMobile()) PopupMenuItem(child: Text(s.encryptDatabase), value: "Encrypt"), PopupMenuItem(child: Text(s.rewindToCheckpoint), value: "Rewind"), PopupMenuItem(child: Text(s.convertToWatchonly), enabled: active.canPay, value: "Cold"), PopupMenuItem(child: Text(s.signOffline), enabled: active.canPay, value: "Sign"), PopupMenuItem(child: Text(s.broadcast), value: "Broadcast"), PopupMenuItem(child: Text(s.multipay), value: "MultiPay"), PopupMenuItem(child: Text(s.keyTool), enabled: active.canPay, value: "KeyTool"), PopupMenuItem(child: Text(s.sweep), enabled: active.canPay, value: "Sweep"), ], onSelected: _onMenu)), // if (!simpleMode && !isMobile()) // PopupMenuItem(child: Text(s.ledger), value: "Ledger"), if (settings.isDeveloper) PopupMenuItem(child: Text(s.expert), value: "Expert"), PopupMenuItem(child: Text(s.settings), value: "Settings"), PopupMenuItem(child: Text(s.help), value: "Help"), PopupMenuItem(child: Text(s.about), value: "About"), ]; }, onSelected: _onMenu, ); showAboutOnce(this.context); return WithForegroundTask(child: Scaffold( appBar: AppBar( centerTitle: true, title: GestureDetector( onTap: () => settings.tapDeveloperMode(context), child: Text("${active.account.name}")), bottom: TabBar( controller: _tabController, isScrollable: true, tabs: [ Tab(text: s.account), messageTab, if (!simpleMode) Tab(text: s.notes), Tab(text: s.history), if (!simpleMode) Tab(text: s.budget), if (!simpleMode) Tab(text: s.tradingPl), Tab(text: s.contacts), ], ), actions: [menu], ), body: TabBarView( controller: _tabController, children: [ AccountPage(), MessageWidget(messageKey), if (!simpleMode) NoteWidget(), HistoryWidget(), if (!simpleMode) BudgetWidget(), if (!simpleMode) PnLWidget(), ContactsTab(key: contactKey), ], ), floatingActionButton: button, )); }); } _onSend() { Navigator.of(context).pushNamed('/send'); } _onMenu(String choice) { switch (choice) { case "Accounts": Navigator.of(context).pushNamed('/accounts'); break; case "Backup": _backup(); break; case "Rescan": _rescan(); break; case "Pools": Navigator.of(context).pushNamed('/pools'); break; case "Rewind": _rewind(); break; case "Cold": _cold(); break; case "MultiPay": _multiPay(); break; case "Sign": _sign(); break; case "Broadcast": _broadcast(); break; case "KeyTool": _keyTool(); break; case "Sweep": _sweep(); break; case "Expert": Navigator.of(context).pushNamed('/dev'); break; case "Encrypt": _encryptDb(); break; case "Ledger": _ledger(); break; case "Settings": _settings(); break; case "Help": launchUrl(Uri.parse(DOC_URL)); break; case "About": showAbout(this.context); break; } } _backup() async { final didAuthenticate = await authenticate(context, S.of(context).pleaseAuthenticateToShowAccountSeed); if (didAuthenticate) { Navigator.of(context).pushNamed('/backup'); } } _keyTool() async { final didAuthenticate = await authenticate(context, S.of(context).pleaseAuthenticateToShowAccountSeed); if (didAuthenticate) { Navigator.of(context).pushNamed('/keytool'); } } _sweep() async { final s = S.of(context); final keyController = TextEditingController(); int pool = 0; final checkKey = (String? key) { final k = key ?? ''; if (!WarpApi.isValidTransparentKey(k)) return s.invalidKey; return null; }; final confirmed = await showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( title: Text(s.sweep), content: SingleChildScrollView(child: Column( mainAxisSize: MainAxisSize.min, children: [ TextFormField( decoration: InputDecoration(labelText: s.transparentKey), controller: keyController, validator: checkKey, ), FormBuilderRadioGroup( orientation: OptionsOrientation.horizontal, decoration: InputDecoration(labelText: s.toPool), name: 'pool', initialValue: 0, onChanged: (v) { pool = v ?? 0; }, options: [ FormBuilderFieldOption( child: Text('T'), value: 0), FormBuilderFieldOption( child: Text('S'), value: 1), if (active.coinDef.supportsUA) FormBuilderFieldOption( child: Text('O'), value: 2), ]), ], )), actions: confirmButtons(context, () => Navigator.of(context).pop(true)) ) ) ?? false; if (confirmed) { final txid = await WarpApi.sweepTransparent(syncStatus.latestHeight, keyController.text, pool, settings.anchorOffset); showSnackBar(s.txId(txid)); } } _rewind() async { final s = S.of(context); int? height; final checkpoints = WarpApi.getCheckpoints(active.coin); final items = checkpoints.map((c) => DropdownMenuItem(value: c.height, child: Text('${noteDateFormat.format(DateTime.fromMillisecondsSinceEpoch(c.timestamp * 1000))} - ${c.height}') )).toList(); final confirmed = await showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( title: Text(s.rewindToCheckpoint), content: SingleChildScrollView(child: Column( mainAxisSize: MainAxisSize.min, children: [ DropdownButtonFormField(items: items, hint: Text(s.selectCheckpoint), onChanged: (v) { height = v; })] )), actions: confirmButtons(context, () => Navigator.of(context).pop(true)) ) ) ?? false; final h = height; if (confirmed && h != null) { showSnackBar(s.blockReorgDetectedRewind(h)); WarpApi.rewindTo(h); } } _cold() { showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( title: Text(S.of(context).coldStorage), content: Text(S.of(context).doYouWantToDeleteTheSecretKeyAndConvert), actions: confirmButtons(context, _convertToWatchOnly, okLabel: S.of(context).delete))); } _rescan() { if (syncStatus.paused) syncStatus.setPause(false); else if (syncStatus.syncing) cancelScan(context); else rescan(context); } _multiPay() { Navigator.of(context).pushNamed('/multipay'); } _sign() async { final String? tx; if (settings.qrOffline) { tx = await scanMultiCode(context); } else { final res = await pickFile(); if (res == null) return; final path = res.files.single.path!; final file = File(path); tx = file.readAsStringSync(); } if (tx != null) Navigator.of(context).pushNamed('/sign', arguments: tx); } _broadcast() async { String? rawTx; if (settings.qrOffline) { rawTx = await scanMultiCode(context); } else { final result = await pickFile(); if (result != null) { String path = result.files.single.path!; File f = File(path); rawTx = f.readAsStringSync(); } } if (rawTx != null) { try { final res = WarpApi.broadcast(rawTx); showSnackBar(res); } on String catch (e) { showSnackBar(e); } } } _ledger() async { // final result = await FilePicker.platform.pickFiles(); // // if (result != null) { // final res = WarpApi.ledgerSign(active.coin, result.files.single.path!); // final snackBar = SnackBar(content: Text(res)); // rootScaffoldMessengerKey.currentState?.showSnackBar(snackBar); // } } _encryptDb() async { final s = S.of(context); final hasPasswd = settings.dbPasswd.isNotEmpty; final oldPasswdController = TextEditingController(); final newPasswdController = TextEditingController(); final repeatPasswdController = TextEditingController(); final confirmed = await showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( title: Text(s.encryptDatabase), contentPadding: EdgeInsets.all(16), content: Form(child: SingleChildScrollView(child: Column(children: [ if (hasPasswd) TextFormField( decoration: InputDecoration(labelText: s.currentPassword), obscureText: true, controller: oldPasswdController), TextFormField( decoration: InputDecoration(labelText: s.newPassword), obscureText: true, controller: newPasswdController), TextFormField( decoration: InputDecoration(labelText: s.repeatNewPassword), obscureText: true, controller: repeatPasswdController), ]))), actions: confirmButtons( context, () => Navigator.of(context).pop(true), cancelValue: false))) ?? false; if (confirmed) { if (oldPasswdController.text != settings.dbPasswd) showSnackBar(s.currentPasswordIncorrect); else if (newPasswdController.text != repeatPasswdController.text) { showSnackBar(s.newPasswordsDoNotMatch); } else { final passwd = newPasswdController.text; for (var c in coins) { final tempPath = p.join(settings.tempDir, c.dbName); WarpApi.cloneDbWithPasswd(c.coin, tempPath, passwd); } final prefs = await SharedPreferences.getInstance(); prefs.setBool('recover', true); showSnackBar(s.databaseEncrypted); await showRestartMessage(); } } } _convertToWatchOnly() { WarpApi.convertToWatchOnly(active.coin, active.id); active.canPay = false; Navigator.of(context).pop(); } _settings() { Navigator.of(context).pushNamed('/settings'); } _onAddContact() async { await addContact(context, ContactT()); } void _initForegroundTask() { FlutterForegroundTask.init( androidNotificationOptions: AndroidNotificationOptions( channelId: '$APP_NAME Sync', channelName: '$APP_NAME Foreground Notification', channelDescription: 'YWallet Synchronization', channelImportance: NotificationChannelImportance.LOW, priority: NotificationPriority.HIGH, iconData: const NotificationIconData( resType: ResourceType.drawable, resPrefix: ResourcePrefix.ic, name: 'notification', ), ), iosNotificationOptions: const IOSNotificationOptions( showNotification: true, playSound: false, ), foregroundTaskOptions: const ForegroundTaskOptions( isOnceEvent: true, autoRunOnBoot: false, allowWakeLock: true, allowWifiLock: true, ), ); FlutterForegroundTask.setOnLockScreenVisibility(false); } }