dev
dart 442 lines 15.1 KB
Raw
1 import 'dart:async';
2 import 'package:cake_wallet/entities/preferences_key.dart';
3 import 'package:cake_wallet/di.dart';
4 import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart';
5 import 'package:cake_wallet/src/screens/dashboard/pages/cake_features_page.dart';
6 import 'package:cake_wallet/src/screens/dashboard/widgets/page_indicator.dart';
7 import 'package:cake_wallet/src/screens/dashboard/widgets/new_main_navbar_widget.dart';
8 import 'package:cake_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_listener_widget.dart';
9 import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
10 import 'package:cake_wallet/src/widgets/evm_switcher.dart';
11 import 'package:cake_wallet/src/widgets/gradient_background.dart';
12 import 'package:cake_wallet/src/widgets/haven_wallet_removal_popup.dart';
13 import 'package:cake_wallet/src/widgets/services_updates_widget.dart';
14 import 'package:cake_wallet/src/widgets/vulnerable_seeds_popup.dart';
15 import 'package:cake_wallet/utils/device_info.dart';
16 import 'package:cake_wallet/utils/feature_flag.dart';
17 import 'package:cake_wallet/utils/version_comparator.dart';
18 import 'package:cake_wallet/view_model/dashboard/cake_features_view_model.dart';
19 import 'package:cake_wallet/generated/i18n.dart';
20 import 'package:cake_wallet/routes.dart';
21 import 'package:cake_wallet/src/screens/yat_emoji_id.dart';
22 import 'package:cake_wallet/utils/responsive_layout_util.dart';
23 import 'package:cake_wallet/utils/show_pop_up.dart';
24 import 'package:flutter/material.dart';
25 import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
26 import 'package:cake_wallet/src/screens/base_page.dart';
27 import 'package:cake_wallet/src/screens/dashboard/widgets/menu_widget.dart';
28 import 'package:cake_wallet/src/screens/dashboard/pages/balance/balance_page.dart';
29 import 'package:cake_wallet/src/screens/dashboard/pages/transactions_page.dart';
30 import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator.dart';
31 import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
32 import 'package:flutter_mobx/flutter_mobx.dart';
33 import 'package:flutter_svg/flutter_svg.dart';
34 import 'package:mobx/mobx.dart';
35 import 'package:shared_preferences/shared_preferences.dart';
36 import 'package:cake_wallet/main.dart';
37 import 'package:cake_wallet/src/screens/release_notes/release_notes_screen.dart';
38 import 'package:cake_wallet/themes/core/theme_extension.dart';
39
40 class DashboardPage extends StatefulWidget {
41 DashboardPage({
42 required this.bottomSheetService,
43 required this.balancePage,
44 required this.dashboardViewModel,
45 required this.addressListViewModel,
46 });
47
48 final BalancePage balancePage;
49 final BottomSheetService bottomSheetService;
50 final DashboardViewModel dashboardViewModel;
51 final WalletAddressListViewModel addressListViewModel;
52
53 @override
54 State<DashboardPage> createState() => _DashboardPageState();
55 }
56
57 class _DashboardPageState extends State<DashboardPage> {
58 @override
59 void initState() {
60 super.initState();
61
62 bool isMobileLayout =
63 responsiveLayoutUtil.screenWidth < ResponsiveLayoutUtilBase.kMobileThreshold;
64
65 reaction((_) => responsiveLayoutUtil.screenWidth, (screenWidth) {
66 // Check if it was previously in mobile layout, and now changing to desktop
67 if (isMobileLayout &&
68 screenWidth > ResponsiveLayoutUtilBase.kDesktopMaxDashBoardWidthConstraint) {
69 setState(() {
70 isMobileLayout = false;
71 });
72 }
73
74 // Check if it was previously in desktop layout, and now changing to mobile
75 if (!isMobileLayout &&
76 screenWidth <= ResponsiveLayoutUtilBase.kDesktopMaxDashBoardWidthConstraint) {
77 setState(() {
78 isMobileLayout = true;
79 });
80 }
81 });
82 }
83
84 @override
85 Widget build(BuildContext context) {
86 Widget dashboardChild;
87
88 final dashboardPageView = RefreshIndicator(
89 displacement: responsiveLayoutUtil.screenHeight * 0.1,
90 onRefresh: () async => await widget.dashboardViewModel.refreshDashboard(),
91 child: SingleChildScrollView(
92 physics: AlwaysScrollableScrollPhysics(),
93 child: Container(
94 height: responsiveLayoutUtil.screenHeight,
95 child: _DashboardPageView(
96 balancePage: widget.balancePage,
97 bottomSheetService: widget.bottomSheetService,
98 dashboardViewModel: widget.dashboardViewModel,
99 addressListViewModel: widget.addressListViewModel,
100 ),
101 ),
102 ),
103 );
104
105 if (DeviceInfo.instance.isDesktop) {
106 if (responsiveLayoutUtil.screenWidth >
107 ResponsiveLayoutUtilBase.kDesktopMaxDashBoardWidthConstraint) {
108 dashboardChild = getIt.get<DesktopSidebarWrapper>();
109 } else {
110 dashboardChild = dashboardPageView;
111 }
112 } else if (responsiveLayoutUtil.shouldRenderMobileUI) {
113 dashboardChild = dashboardPageView;
114 } else {
115 dashboardChild = getIt.get<DesktopSidebarWrapper>();
116 }
117
118 return Scaffold(body: dashboardChild);
119 }
120 }
121
122 class _DashboardPageView extends BasePage {
123 _DashboardPageView({
124 required this.bottomSheetService,
125 required this.balancePage,
126 required this.dashboardViewModel,
127 required this.addressListViewModel,
128 });
129
130 final BalancePage balancePage;
131
132 @override
133 bool get gradientBackground => true;
134
135 @override
136 Widget Function(BuildContext, Widget) get rootWrapper =>
137 (BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold);
138
139 @override
140 bool get resizeToAvoidBottomInset => false;
141
142 @override
143 Widget get endDrawer =>
144 MenuWidget(dashboardViewModel, ValueKey('dashboard_page_drawer_menu_widget_key'));
145
146 @override
147 Widget leading(BuildContext context) {
148 if (FeatureFlag.isEVMChainSwitcherEnabled &&
149 dashboardViewModel.isEVMWallet &&
150 dashboardViewModel.availableChains.isNotEmpty) {
151 return TextButton(
152 style: TextButton.styleFrom(
153 minimumSize: Size(50, 30),
154 tapTargetSize: MaterialTapTargetSize.shrinkWrap,
155 alignment: Alignment.centerLeft,
156 ),
157 onPressed: () => showDialog(
158 context: context,
159 builder: (context) => EvmSwitcher(
160 chains: dashboardViewModel.availableChains,
161 currentChain: dashboardViewModel.currentChain,
162 onChainSelected: (chainId) => dashboardViewModel.selectChain(chainId),
163 hiddenChainIds: dashboardViewModel.settingsStore.evmHiddenChainIds,
164 onHiddenChanged: (hidden) =>
165 dashboardViewModel.settingsStore.setEvmHiddenChainIds(hidden),
166 ),
167 ),
168 child: Container(
169 child: SvgPicture.asset(
170 'assets/images/evm_switcher.svg',
171 color: Theme.of(context).colorScheme.onSurfaceVariant,
172 height: 30,
173 ),
174 ),
175 );
176 }
177
178 return Observer(
179 builder: (context) {
180 return ServicesUpdatesWidget(
181 key: ValueKey('dashboard_page_services_update_button_key'),
182 dashboardViewModel.getServicesStatus(),
183 enabled: dashboardViewModel.isEnabledBulletinAction,
184 );
185 },
186 );
187 }
188
189 @override
190 Widget middle(BuildContext context) {
191 return SyncIndicator(
192 key: ValueKey('dashboard_page_sync_indicator_button_key'),
193 dashboardViewModel: dashboardViewModel,
194 onTap: () => Navigator.of(context, rootNavigator: true).pushNamed(Routes.connectionSync),
195 );
196 }
197
198 @override
199 Widget trailing(BuildContext context) {
200 return Container(
201 alignment: Alignment.centerRight,
202 width: 42,
203 child: TextButton(
204 key: ValueKey('dashboard_page_wallet_menu_button_key'),
205 onPressed: () => onOpenEndDrawer(),
206 child: Semantics(
207 label: S.of(context).wallet_menu,
208 child: SvgPicture.asset(
209 'assets/images/menu.svg',
210 color: Theme.of(context).colorScheme.primary,
211 ),
212 ),
213 ),
214 );
215 }
216
217 final DashboardViewModel dashboardViewModel;
218 final BottomSheetService bottomSheetService;
219 final WalletAddressListViewModel addressListViewModel;
220
221 int get initialPage => dashboardViewModel.shouldShowMarketPlaceInDashboard ? 1 : 0;
222 ObservableList<Widget> pages = ObservableList<Widget>();
223 bool _isEffectsInstalled = false;
224
225 @override
226 Widget body(BuildContext context) {
227 final controller = PageController(initialPage: initialPage);
228
229 reaction(
230 (_) => dashboardViewModel.shouldShowMarketPlaceInDashboard,
231 (bool value) {
232 if (!dashboardViewModel.shouldShowMarketPlaceInDashboard) {
233 controller.jumpToPage(0);
234 }
235 pages.clear();
236 _isEffectsInstalled = false;
237 _setEffects(context);
238
239 if (value) {
240 controller.jumpToPage(1);
241 } else {
242 controller.jumpToPage(0);
243 }
244 },
245 );
246 _setEffects(context);
247
248 return SafeArea(
249 minimum: EdgeInsets.only(bottom: 0),
250 child: BottomSheetListener(
251 bottomSheetService: bottomSheetService,
252 child: Container(
253 child: Stack(
254 alignment: Alignment.bottomCenter,
255 children: <Widget>[
256 Observer(
257 builder: (context) {
258 return PageView.builder(
259 key: const ValueKey('dashboard_page_view_key'),
260 controller: controller,
261 physics: const BouncingScrollPhysics(),
262 itemCount: pages.length,
263 itemBuilder: (context, index) => pages[index],
264 );
265 },
266 ),
267 Stack(
268 alignment: Alignment.bottomCenter,
269 children: <Widget>[
270 IgnorePointer(
271 child: Container(
272 alignment: Alignment.bottomCenter,
273 height: 220,
274 decoration: BoxDecoration(
275 gradient: LinearGradient(
276 begin: Alignment.topCenter,
277 end: Alignment.bottomCenter,
278 colors: <Color>[
279 context.customColors.backgroundGradientColor.withAlpha(5),
280 context.customColors.backgroundGradientColor.withAlpha(50),
281 context.customColors.backgroundGradientColor.withAlpha(125),
282 context.customColors.backgroundGradientColor.withAlpha(150),
283 context.customColors.backgroundGradientColor.withAlpha(200),
284 context.customColors.backgroundGradientColor,
285 context.customColors.backgroundGradientColor,
286 ],
287 ),
288 ),
289 ),
290 ),
291 Container(
292 margin: EdgeInsets.only(bottom: 110),
293 child: Semantics(
294 container: true,
295 label: 'Page indicator',
296 hint:
297 'Swipe left or right to change page, or double tap buttons below to navigate directly.',
298 child: ExcludeSemantics(
299 excluding: false,
300 child: PageIndicator(
301 controller: controller,
302 dashboardViewModel: dashboardViewModel,
303 ),
304 ),
305 ),
306 ),
307 NewMainNavBar(
308 dashboardViewModel: dashboardViewModel,
309 selectedIndex: 0,
310 onItemTap: (index) {}),
311 ],
312 ),
313 ],
314 ),
315 ),
316 ),
317 );
318 }
319
320 void _setEffects(BuildContext context) async {
321 if (_isEffectsInstalled || !context.mounted) {
322 return;
323 }
324 if (dashboardViewModel.shouldShowMarketPlaceInDashboard) {
325 pages.add(
326 Semantics(
327 label: S.of(context).apps,
328 child: CakeFeaturesPage(
329 dashboardViewModel: dashboardViewModel,
330 cakeFeaturesViewModel: getIt.get<CakeFeaturesViewModel>(),
331 ),
332 ),
333 );
334 }
335 pages.add(Semantics(label: S.of(context).balance_page, child: balancePage));
336 pages.add(
337 Semantics(
338 label: S.of(context).history,
339 child: TransactionsPage(dashboardViewModel: dashboardViewModel),
340 ),
341 );
342 _isEffectsInstalled = true;
343
344 _showReleaseNotesPopup(context);
345
346 _showVulnerableSeedsPopup(context);
347
348 _showHavenPopup(context);
349
350 var needToPresentYat = false;
351
352 rootKey.currentState?.isInactive.listen(
353 (inactive) {
354 if (needToPresentYat) {
355 Future<void>.delayed(Duration(milliseconds: 500)).then(
356 (_) {
357 showPopUp<void>(
358 context: navigatorKey.currentContext!,
359 builder: (_) => YatEmojiId(dashboardViewModel.yatStore.emoji),
360 );
361 needToPresentYat = false;
362 },
363 );
364 }
365 },
366 );
367
368 dashboardViewModel.yatStore.emojiIncommingStream.listen(
369 (String emoji) {
370 if (!_isEffectsInstalled || emoji.isEmpty) {
371 return;
372 }
373
374 needToPresentYat = true;
375 },
376 );
377 }
378
379 void _showReleaseNotesPopup(BuildContext context) async {
380 final sharedPrefs = await SharedPreferences.getInstance();
381 final currentAppVersion =
382 VersionComparator.getExtendedVersionNumber(dashboardViewModel.settingsStore.appVersion);
383 final lastSeenAppVersion = sharedPrefs.getInt(PreferencesKey.lastSeenAppVersion);
384 final isNewInstall = sharedPrefs.getBool(PreferencesKey.isNewInstall);
385
386 if (currentAppVersion != lastSeenAppVersion && !isNewInstall!) {
387 Future<void>.delayed(
388 Duration(seconds: 1),
389 () {
390 showPopUp<void>(
391 context: context,
392 builder: (BuildContext context) {
393 return ReleaseNotesScreen(
394 title: 'Version ${dashboardViewModel.settingsStore.appVersion}',
395 );
396 },
397 );
398 },
399 );
400
401 sharedPrefs.setInt(PreferencesKey.lastSeenAppVersion, currentAppVersion);
402 } else if (isNewInstall!) {
403 sharedPrefs.setInt(PreferencesKey.lastSeenAppVersion, currentAppVersion);
404 }
405 }
406
407 void _showVulnerableSeedsPopup(BuildContext context) async {
408 final List<String> affectedWalletNames = await dashboardViewModel.checkAffectedWallets();
409
410 if (affectedWalletNames.isNotEmpty) {
411 Future<void>.delayed(
412 Duration(seconds: 1),
413 () {
414 showPopUp<void>(
415 context: context,
416 builder: (BuildContext context) {
417 return VulnerableSeedsPopup(affectedWalletNames);
418 },
419 );
420 },
421 );
422 }
423 }
424
425 void _showHavenPopup(BuildContext context) async {
426 final List<String> havenWalletList = await dashboardViewModel.checkForHavenWallets();
427
428 if (havenWalletList.isNotEmpty) {
429 Future<void>.delayed(
430 Duration(seconds: 1),
431 () {
432 showPopUp<void>(
433 context: context,
434 builder: (BuildContext context) {
435 return HavenWalletRemovalPopup(havenWalletList);
436 },
437 );
438 },
439 );
440 }
441 }
442 }