dev
dart 351 lines 11 KB
Raw
1 import 'dart:async';
2 import 'dart:io';
3
4 import 'package:cake_wallet/bitcoin/bitcoin.dart';
5 import 'package:cake_wallet/core/auth_service.dart';
6 import 'package:cake_wallet/core/node_switching_service.dart';
7 import 'package:cake_wallet/core/totp_request_details.dart';
8 import 'package:cake_wallet/core/trade_monitor.dart';
9 import 'package:cake_wallet/entities/qr_scanner.dart';
10 import 'package:cake_wallet/reactions/wallet_utils.dart';
11 import 'package:cake_wallet/routes.dart';
12 import 'package:cake_wallet/src/screens/auth/auth_page.dart';
13 import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_enter_code_page.dart';
14 import 'package:cake_wallet/store/app_store.dart';
15 import 'package:cake_wallet/store/authentication_store.dart';
16 import 'package:cake_wallet/utils/device_info.dart';
17 import 'package:cake_wallet/view_model/link_view_model.dart';
18 import 'package:cw_core/utils/print_verbose.dart';
19 import 'package:cw_core/utils/socket_health_logger.dart';
20 import 'package:cw_core/wallet_base.dart';
21 import 'package:cw_core/wallet_type.dart';
22 import 'package:flutter/material.dart';
23 import 'package:mobx/mobx.dart';
24 import 'package:trezor_connect/trezor_connect.dart';
25 import 'package:uni_links/uni_links.dart';
26
27 class Root extends StatefulWidget {
28 Root({
29 required Key key,
30 required this.authenticationStore,
31 required this.appStore,
32 required this.child,
33 required this.navigatorKey,
34 required this.authService,
35 required this.linkViewModel,
36 required this.tradeMonitor,
37 required this.nodeSwitchingService,
38 required this.trezorConnect,
39 this.initialQuickAction,
40 required this.quickActionsStream,
41 }) : super(key: key);
42
43 final AuthenticationStore authenticationStore;
44 final AppStore appStore;
45 final GlobalKey<NavigatorState> navigatorKey;
46 final AuthService authService;
47 final Widget child;
48 final LinkViewModel linkViewModel;
49 final TradeMonitor tradeMonitor;
50 final NodeSwitchingService nodeSwitchingService;
51 final TrezorConnect trezorConnect;
52 final String? initialQuickAction;
53 final Stream<Uri?> quickActionsStream;
54
55 @override
56 RootState createState() => RootState();
57 }
58
59 class RootState extends State<Root> with WidgetsBindingObserver {
60 RootState()
61 : _isInactiveController = StreamController<bool>.broadcast(),
62 _isInactive = false,
63 _requestAuth = true,
64 _postFrameCallback = false;
65
66 Stream<bool> get isInactive => _isInactiveController.stream;
67 StreamController<bool> _isInactiveController;
68 bool _isInactive;
69 bool _postFrameCallback;
70 bool _requestAuth;
71
72 StreamSubscription<Uri?>? stream;
73 ReactionDisposer? _walletReactionDisposer;
74 ReactionDisposer? _deepLinksReactionDisposer;
75
76 @override
77 void initState() {
78 WidgetsBinding.instance.addObserver(this);
79
80 widget.authService.requireAuth().then((value) {
81 WidgetsBinding.instance.addPostFrameCallback((_) {
82 setState(() => _requestAuth = value);
83 });
84 });
85 _isInactiveController = StreamController<bool>.broadcast();
86 _isInactive = false;
87 _postFrameCallback = false;
88 super.initState();
89 if (DeviceInfo.instance.isMobile) {
90 initUniLinks();
91 }
92 }
93
94 @override
95 void dispose() {
96 stream?.cancel();
97 _walletReactionDisposer?.call();
98 _deepLinksReactionDisposer?.call();
99 super.dispose();
100 }
101
102 /// handle app links while the app is already started
103 /// whether its in the foreground or in the background.
104 Future<void> initUniLinks() async {
105 try {
106 stream = uriLinkStream.listen((Uri? uri) {
107 handleDeepLinking(uri);
108 });
109
110 // listen for quick actions
111 widget.quickActionsStream.listen((Uri? uri) {
112 handleDeepLinking(uri);
113 });
114
115 handleDeepLinking(await getInitialUri());
116
117 if (widget.initialQuickAction != null) {
118 final uri = Uri.parse('cakewallet://quickaction/${widget.initialQuickAction}');
119 handleDeepLinking(uri);
120 }
121 } catch (e) {
122 printV(e);
123 }
124 }
125
126 void handleDeepLinking(Uri? uri) async {
127 if (uri == null || !mounted) return;
128
129 widget.linkViewModel.currentLink = uri;
130
131 if (uri.toString().startsWith(widget.trezorConnect.callbackBackUri)) {
132 widget.trezorConnect.handleCallback(uri);
133 return;
134 }
135
136 bool requireAuth = await widget.authService.requireAuth();
137
138 if (widget.authenticationStore.state == AuthenticationState.allowedCreate) {
139 requireAuth = false;
140 }
141
142 if (!requireAuth &&
143 (widget.authenticationStore.state == AuthenticationState.allowed ||
144 widget.authenticationStore.state == AuthenticationState.allowedCreate)) {
145 _navigateToDeepLinkScreen();
146 return;
147 }
148
149 _deepLinksReactionDisposer = reaction(
150 (_) => widget.authenticationStore.state,
151 (AuthenticationState state) {
152 if (state == AuthenticationState.allowed || state == AuthenticationState.allowedCreate) {
153 if (widget.appStore.wallet == null) {
154 waitForWalletInstance(context);
155 } else {
156 _navigateToDeepLinkScreen();
157 }
158 _deepLinksReactionDisposer?.call();
159 _deepLinksReactionDisposer = null;
160 }
161 },
162 );
163 }
164
165 @override
166 void didChangeAppLifecycleState(AppLifecycleState state) {
167 switch (state) {
168 case AppLifecycleState.paused:
169 if (isQrScannerShown) {
170 return;
171 }
172
173 if (!_isInactive && widget.authenticationStore.state == AuthenticationState.allowed) {
174 setState(() => _setInactive(true));
175 }
176
177 if (widget.appStore.wallet?.type == WalletType.bitcoin) {
178 bitcoin!.stopPayjoinSessions(widget.appStore.wallet!);
179 }
180
181 widget.tradeMonitor.stopTradeMonitoring();
182
183 break;
184 case AppLifecycleState.resumed:
185
186 // Reset inactive state when app resumes
187 if (_isInactive) setState(() => _setInactive(false));
188
189 widget.authService.requireAuth().then((value) {
190 if (mounted) {
191 setState(() {
192 _requestAuth = value;
193 });
194 }
195 });
196 if (widget.appStore.wallet?.type == WalletType.bitcoin &&
197 widget.appStore.settingsStore.usePayjoin) {
198 bitcoin!.resumePayjoinSessions(widget.appStore.wallet!);
199 }
200
201 widget.tradeMonitor.resumeTradeMonitoring();
202
203 // Trigger node health check when app resumes
204 widget.nodeSwitchingService.performHealthCheck();
205
206 // Electrum Wallet socket health check and reconnection flow
207 final wallet = widget.appStore.wallet;
208 if (wallet != null && isElectrumWallet(wallet.type)) {
209 SocketHealthLogger().logHealthCheck(
210 walletType: wallet.type,
211 walletName: wallet.name,
212 syncStatus: wallet.syncStatus.toString(),
213 wasReconnected: false,
214 trigger: 'app_resume',
215 );
216
217 wallet.checkSocketHealth().then((isHealthy) {
218 SocketHealthLogger().logHealthCheck(
219 walletType: wallet.type,
220 walletName: wallet.name,
221 isHealthy: isHealthy,
222 syncStatus: wallet.syncStatus.toString(),
223 wasReconnected: true,
224 trigger: 'app_resume_socket_health_check',
225 );
226 });
227 }
228
229 break;
230 default:
231 break;
232 }
233 }
234
235 @override
236 void didChangePlatformBrightness() {
237 // Only handle theme changes when the app is active (not in background)
238 if (_isInactive) return;
239
240 if (widget.appStore.themeStore.themeMode == ThemeMode.system) {
241 Future.delayed(Duration(milliseconds: Platform.isIOS ? 500 : 0), () {
242 // Double-check that app is still active before applying theme change
243 if (_isInactive) return;
244
245 final systemTheme = widget.appStore.themeStore.getThemeFromSystem();
246 if (widget.appStore.themeStore.currentTheme != systemTheme) {
247 widget.appStore.themeStore.setTheme(systemTheme);
248 }
249 });
250 }
251 }
252
253 @override
254 Widget build(BuildContext context) {
255 // this only happens when the app has been in the background for some time
256 // this does NOT trigger when the app is started from the "closed" state!
257 if (_isInactive && !_postFrameCallback && _requestAuth) {
258 _postFrameCallback = true;
259 WidgetsBinding.instance.addPostFrameCallback((_) {
260 widget.navigatorKey.currentState?.pushNamed(
261 Routes.unlock,
262 arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) {
263 if (!isAuthenticatedSuccessfully) {
264 return;
265 }
266 final useTotp = widget.appStore.settingsStore.useTOTP2FA;
267 final shouldUseTotp2FAToAccessWallets =
268 widget.appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet;
269 if (useTotp && shouldUseTotp2FAToAccessWallets) {
270 _reset();
271 auth.close(
272 route: Routes.totpAuthCodePage,
273 arguments: TotpAuthArgumentsModel(
274 onTotpAuthenticationFinished:
275 (bool isAuthenticatedSuccessfully, TotpAuthCodePageState totpAuth) {
276 if (!isAuthenticatedSuccessfully) {
277 return;
278 }
279 _reset();
280 totpAuth.close(
281 route: widget.linkViewModel.getRouteToGo(),
282 arguments: widget.linkViewModel.getRouteArgs(),
283 );
284 widget.linkViewModel.currentLink = null;
285 },
286 isForSetup: false,
287 isClosable: false,
288 ),
289 );
290 } else {
291 _reset();
292 auth.close(
293 route: widget.linkViewModel.getRouteToGo(),
294 arguments: widget.linkViewModel.getRouteArgs(),
295 );
296 widget.linkViewModel.currentLink = null;
297 }
298 },
299 );
300 });
301 }
302
303 return WillPopScope(
304 onWillPop: () async => false,
305 child: widget.child,
306 );
307 }
308
309 void _reset() {
310 setState(() {
311 _postFrameCallback = false;
312 _setInactive(false);
313 });
314
315 // Apply any missed theme changes after successful authentication
316 if (widget.appStore.themeStore.themeMode == ThemeMode.system) {
317 Future.delayed(Duration(milliseconds: 100), () {
318 final systemTheme = widget.appStore.themeStore.getThemeFromSystem();
319 if (widget.appStore.themeStore.currentTheme != systemTheme) {
320 widget.appStore.themeStore.setTheme(systemTheme);
321 }
322 });
323 }
324 }
325
326 void _setInactive(bool value) {
327 _isInactive = value;
328 _isInactiveController.add(value);
329 }
330
331 void waitForWalletInstance(BuildContext context) {
332 WidgetsBinding.instance.addPostFrameCallback((_) {
333 if (mounted) {
334 _walletReactionDisposer = reaction(
335 (_) => widget.appStore.wallet,
336 (WalletBase? wallet) {
337 if (wallet != null) {
338 _navigateToDeepLinkScreen();
339 _walletReactionDisposer?.call();
340 _walletReactionDisposer = null;
341 }
342 },
343 );
344 }
345 });
346 }
347
348 void _navigateToDeepLinkScreen() {
349 widget.linkViewModel.handleLink();
350 }
351 }