diff --git a/app/lib/apps/chatbot/chatbot_widget.dart b/app/lib/apps/chatbot/chatbot_widget.dart index 0fb6861d6..249bb7742 100644 --- a/app/lib/apps/chatbot/chatbot_widget.dart +++ b/app/lib/apps/chatbot/chatbot_widget.dart @@ -3,7 +3,6 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:threebotlogin/apps/chatbot/chatbot_config.dart'; import 'package:threebotlogin/browser.dart'; import 'package:threebotlogin/helpers/logger.dart'; -import 'package:threebotlogin/widgets/layout_drawer.dart'; class ChatbotWidget extends StatefulWidget { final String email; @@ -85,15 +84,13 @@ class _ChatbotState extends State @override Widget build(BuildContext context) { super.build(context); - return LayoutDrawer( - titleText: 'Support', - content: Column( + return Column( children: [ Expanded( child: Container(child: iaWebview), ), ], - )); + ); } @override diff --git a/app/lib/apps/farmers/farmers_widget.dart b/app/lib/apps/farmers/farmers_widget.dart index b422741df..98ed42b6a 100644 --- a/app/lib/apps/farmers/farmers_widget.dart +++ b/app/lib/apps/farmers/farmers_widget.dart @@ -14,7 +14,6 @@ import 'package:threebotlogin/helpers/logger.dart'; import 'package:threebotlogin/models/wallet_data.dart'; import 'package:threebotlogin/screens/scan_screen.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; -import 'package:threebotlogin/widgets/layout_drawer.dart'; bool created = false; @@ -153,15 +152,13 @@ class _FarmersState extends State @override Widget build(BuildContext context) { super.build(context); - return LayoutDrawer( - titleText: 'Farming', - content: Column( + return Column( children: [ Expanded( child: Container(child: iaWebView), ), ], - )); + ); } @override diff --git a/app/lib/apps/news/news_screen.dart b/app/lib/apps/news/news_screen.dart index 4e3d9dad9..2844f4ac3 100644 --- a/app/lib/apps/news/news_screen.dart +++ b/app/lib/apps/news/news_screen.dart @@ -4,7 +4,6 @@ import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:http/http.dart' as http; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:threebotlogin/helpers/globals.dart'; -import 'package:threebotlogin/widgets/layout_drawer.dart'; import 'package:xml2json/xml2json.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:timeago/timeago.dart' as timeago; @@ -60,27 +59,24 @@ class _NewsScreenState extends State { @override Widget build(BuildContext context) { - return LayoutDrawer( - titleText: 'News', - content: RefreshIndicator( - onRefresh: () async => _pagingController.refresh(), - child: PagedListView>( - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate>( - itemBuilder: (context, entry, index) => - buildArticleCard(entry, context), - firstPageProgressIndicatorBuilder: (context) => Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 8), - Text('Loading Articles...', - style: Theme.of(context).textTheme.bodyLarge!.copyWith( - color: Theme.of(context).colorScheme.onSurface, - fontWeight: FontWeight.bold)), - ], - ), + return RefreshIndicator( + onRefresh: () async => _pagingController.refresh(), + child: PagedListView>( + pagingController: _pagingController, + builderDelegate: PagedChildBuilderDelegate>( + itemBuilder: (context, entry, index) => + buildArticleCard(entry, context), + firstPageProgressIndicatorBuilder: (context) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 8), + Text('Loading Articles...', + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold)), + ], ), ), ), diff --git a/app/lib/apps/news/news_widget.dart b/app/lib/apps/news/news_widget.dart index 54a6b19cc..d4dc1266a 100644 --- a/app/lib/apps/news/news_widget.dart +++ b/app/lib/apps/news/news_widget.dart @@ -6,7 +6,6 @@ import 'package:threebotlogin/clipboard_hack/clipboard_hack.dart'; import 'package:threebotlogin/events/events.dart'; import 'package:threebotlogin/events/go_home_event.dart'; import 'package:threebotlogin/helpers/logger.dart'; -import 'package:threebotlogin/widgets/layout_drawer.dart'; import 'package:url_launcher/url_launcher.dart'; bool created = false; @@ -83,15 +82,13 @@ class _NewsState extends State with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { super.build(context); - return LayoutDrawer( - titleText: 'News', - content: Column( + return Column( children: [ Expanded( child: Container(child: iaWebView), ), ], - )); + ); } @override diff --git a/app/lib/apps/notifications/notifications.dart b/app/lib/apps/notifications/notifications.dart new file mode 100644 index 000000000..c62038a51 --- /dev/null +++ b/app/lib/apps/notifications/notifications.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:threebotlogin/app.dart'; +import 'package:threebotlogin/apps/farmers/farmers_user_data.dart'; +import 'package:threebotlogin/events/events.dart'; +import 'package:threebotlogin/events/go_home_event.dart'; +import 'package:threebotlogin/screens/notifications_screen.dart'; + +class Notifications implements App { + static final Notifications _singleton = Notifications._internal(); + static const Widget _notificationsWidget = NotificationsScreen(); + + factory Notifications() { + return _singleton; + } + + Notifications._internal(); + + @override + Future widget() async { + return _notificationsWidget; + } + + @override + void clearData() { + clearAllData(); + } + + @override + bool emailVerificationRequired() { + return true; + } + + @override + bool pinRequired() { + return true; + } + + @override + void back() { + Events().emit(GoHomeEvent()); + } +} diff --git a/app/lib/apps/notifications/notifications_user_data.dart b/app/lib/apps/notifications/notifications_user_data.dart new file mode 100644 index 000000000..080b3f2f0 --- /dev/null +++ b/app/lib/apps/notifications/notifications_user_data.dart @@ -0,0 +1,8 @@ +import 'package:shared_preferences/shared_preferences.dart'; + + +Future?> getNotificationSettings() async { + final prefs = await SharedPreferences.getInstance(); + var notifications = prefs.getStringList('notifications'); + return notifications; +} \ No newline at end of file diff --git a/app/lib/apps/wallet/wallet_widget.dart b/app/lib/apps/wallet/wallet_widget.dart index 12244a231..a51645dad 100644 --- a/app/lib/apps/wallet/wallet_widget.dart +++ b/app/lib/apps/wallet/wallet_widget.dart @@ -16,7 +16,6 @@ import 'package:threebotlogin/models/wallet_data.dart'; import 'package:threebotlogin/screens/scan_screen.dart'; import 'package:threebotlogin/services/crypto_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; -import 'package:threebotlogin/widgets/layout_drawer.dart'; bool created = false; @@ -184,15 +183,13 @@ class _WalletState extends State @override Widget build(BuildContext context) { super.build(context); - return LayoutDrawer( - titleText: 'Wallet', - content: Column( - children: [ - Expanded( - child: Container(child: iaWebView), - ), - ], - )); + return Column( + children: [ + Expanded( + child: Container(child: iaWebView), + ), + ], + ); } @override diff --git a/app/lib/helpers/country_code.dart b/app/lib/helpers/country_code.dart index 91987550d..fbf035add 100644 --- a/app/lib/helpers/country_code.dart +++ b/app/lib/helpers/country_code.dart @@ -1,194 +1,194 @@ final Map countryNameToCode = { - 'Afghanistan': 'AF', - 'Albania': 'AL', - 'Algeria': 'DZ', - 'Andorra': 'AD', - 'Angola': 'AO', - 'Argentina': 'AR', - 'Armenia': 'AM', - 'Australia': 'AU', - 'Austria': 'AT', - 'Azerbaijan': 'AZ', - 'Bahamas': 'BS', - 'Bahrain': 'BH', - 'Bangladesh': 'BD', - 'Barbados': 'BB', - 'Belarus': 'BY', - 'Belgium': 'BE', - 'Belize': 'BZ', - 'Benin': 'BJ', - 'Bhutan': 'BT', - 'Bolivia': 'BO', - 'Bosnia and Herzegovina': 'BA', - 'Botswana': 'BW', - 'Brazil': 'BR', - 'Brunei': 'BN', - 'Bulgaria': 'BG', - 'Burkina Faso': 'BF', - 'Burundi': 'BI', - 'Cabo Verde': 'CV', - 'Cambodia': 'KH', - 'Cameroon': 'CM', - 'Canada': 'CA', - 'Central African Republic': 'CF', - 'Chad': 'TD', - 'Chile': 'CL', - 'China': 'CN', - 'Colombia': 'CO', - 'Comoros': 'KM', - 'Congo': 'CG', - 'Costa Rica': 'CR', - 'Croatia': 'HR', - 'Cuba': 'CU', - 'Cyprus': 'CY', - 'Czech Republic': 'CZ', - 'Denmark': 'DK', - 'Djibouti': 'DJ', - 'Dominica': 'DM', - 'Dominican Republic': 'DO', - 'Ecuador': 'EC', - 'Egypt': 'EG', - 'El Salvador': 'SV', - 'Equatorial Guinea': 'GQ', - 'Eritrea': 'ER', - 'Estonia': 'EE', - 'Eswatini': 'SZ', - 'Ethiopia': 'ET', - 'Fiji': 'FJ', - 'Finland': 'FI', - 'France': 'FR', - 'Gabon': 'GA', - 'Gambia': 'GM', - 'Georgia': 'GE', - 'Germany': 'DE', - 'Ghana': 'GH', - 'Greece': 'GR', - 'Grenada': 'GD', - 'Guatemala': 'GT', - 'Guinea': 'GN', - 'Guinea-Bissau': 'GW', - 'Guyana': 'GY', - 'Haiti': 'HT', - 'Honduras': 'HN', - 'Hungary': 'HU', - 'Iceland': 'IS', - 'India': 'IN', - 'Indonesia': 'ID', - 'Iran': 'IR', - 'Iraq': 'IQ', - 'Ireland': 'IE', - 'Israel': 'IL', - 'Italy': 'IT', - 'Jamaica': 'JM', - 'Japan': 'JP', - 'Jordan': 'JO', - 'Kazakhstan': 'KZ', - 'Kenya': 'KE', - 'Kiribati': 'KI', - 'Kuwait': 'KW', - 'Kyrgyzstan': 'KG', - 'Laos': 'LA', - 'Latvia': 'LV', - 'Lebanon': 'LB', - 'Lesotho': 'LS', - 'Liberia': 'LR', - 'Libya': 'LY', - 'Liechtenstein': 'LI', - 'Lithuania': 'LT', - 'Luxembourg': 'LU', - 'Madagascar': 'MG', - 'Malawi': 'MW', - 'Malaysia': 'MY', - 'Maldives': 'MV', - 'Mali': 'ML', - 'Malta': 'MT', - 'Marshall Islands': 'MH', - 'Mauritania': 'MR', - 'Mauritius': 'MU', - 'Mexico': 'MX', - 'Micronesia': 'FM', - 'Moldova': 'MD', - 'Monaco': 'MC', - 'Mongolia': 'MN', - 'Montenegro': 'ME', - 'Morocco': 'MA', - 'Mozambique': 'MZ', - 'Myanmar': 'MM', - 'Namibia': 'NA', - 'Nauru': 'NR', - 'Nepal': 'NP', - 'Netherlands': 'NL', - 'New Zealand': 'NZ', - 'Nicaragua': 'NI', - 'Niger': 'NE', - 'Nigeria': 'NG', - 'North Korea': 'KP', - 'North Macedonia': 'MK', - 'Norway': 'NO', - 'Oman': 'OM', - 'Pakistan': 'PK', - 'Palau': 'PW', - 'Palestine State': 'PS', - 'Panama': 'PA', - 'Papua New Guinea': 'PG', - 'Paraguay': 'PY', - 'Peru': 'PE', - 'Philippines': 'PH', - 'Poland': 'PL', - 'Portugal': 'PT', - 'Qatar': 'QA', - 'Romania': 'RO', - 'Russia': 'RU', - 'Rwanda': 'RW', - 'Saint Kitts and Nevis': 'KN', - 'Saint Lucia': 'LC', - 'Saint Vincent and the Grenadines': 'VC', - 'Samoa': 'WS', - 'San Marino': 'SM', - 'Sao Tome and Principe': 'ST', - 'Saudi Arabia': 'SA', - 'Senegal': 'SN', - 'Serbia': 'RS', - 'Seychelles': 'SC', - 'Sierra Leone': 'SL', - 'Singapore': 'SG', - 'Slovakia': 'SK', - 'Slovenia': 'SI', - 'Solomon Islands': 'SB', - 'Somalia': 'SO', - 'South Africa': 'ZA', - 'South Korea': 'KR', - 'South Sudan': 'SS', - 'Spain': 'ES', - 'Sri Lanka': 'LK', - 'Sudan': 'SD', - 'Suriname': 'SR', - 'Sweden': 'SE', - 'Switzerland': 'CH', - 'Syria': 'SY', - 'Taiwan': 'TW', - 'Tajikistan': 'TJ', - 'Tanzania': 'TZ', - 'Thailand': 'TH', - 'Timor-Leste': 'TL', - 'Togo': 'TG', - 'Tonga': 'TO', - 'Trinidad and Tobago': 'TT', - 'Tunisia': 'TN', - 'Turkey': 'TR', - 'Turkmenistan': 'TM', - 'Tuvalu': 'TV', - 'Uganda': 'UG', - 'Ukraine': 'UA', - 'United Arab Emirates': 'AE', - 'United Kingdom': 'GB', - 'United States': 'US', - 'Uruguay': 'UY', - 'Uzbekistan': 'UZ', - 'Vanuatu': 'VU', - 'Venezuela': 'VE', - 'Vietnam': 'VN', - 'Yemen': 'YE', - 'Zambia': 'ZM', - 'Zimbabwe': 'ZW', - }; \ No newline at end of file + 'Afghanistan': 'AF', + 'Albania': 'AL', + 'Algeria': 'DZ', + 'Andorra': 'AD', + 'Angola': 'AO', + 'Argentina': 'AR', + 'Armenia': 'AM', + 'Australia': 'AU', + 'Austria': 'AT', + 'Azerbaijan': 'AZ', + 'Bahamas': 'BS', + 'Bahrain': 'BH', + 'Bangladesh': 'BD', + 'Barbados': 'BB', + 'Belarus': 'BY', + 'Belgium': 'BE', + 'Belize': 'BZ', + 'Benin': 'BJ', + 'Bhutan': 'BT', + 'Bolivia': 'BO', + 'Bosnia and Herzegovina': 'BA', + 'Botswana': 'BW', + 'Brazil': 'BR', + 'Brunei': 'BN', + 'Bulgaria': 'BG', + 'Burkina Faso': 'BF', + 'Burundi': 'BI', + 'Cabo Verde': 'CV', + 'Cambodia': 'KH', + 'Cameroon': 'CM', + 'Canada': 'CA', + 'Central African Republic': 'CF', + 'Chad': 'TD', + 'Chile': 'CL', + 'China': 'CN', + 'Colombia': 'CO', + 'Comoros': 'KM', + 'Congo': 'CG', + 'Costa Rica': 'CR', + 'Croatia': 'HR', + 'Cuba': 'CU', + 'Cyprus': 'CY', + 'Czech Republic': 'CZ', + 'Denmark': 'DK', + 'Djibouti': 'DJ', + 'Dominica': 'DM', + 'Dominican Republic': 'DO', + 'Ecuador': 'EC', + 'Egypt': 'EG', + 'El Salvador': 'SV', + 'Equatorial Guinea': 'GQ', + 'Eritrea': 'ER', + 'Estonia': 'EE', + 'Eswatini': 'SZ', + 'Ethiopia': 'ET', + 'Fiji': 'FJ', + 'Finland': 'FI', + 'France': 'FR', + 'Gabon': 'GA', + 'Gambia': 'GM', + 'Georgia': 'GE', + 'Germany': 'DE', + 'Ghana': 'GH', + 'Greece': 'GR', + 'Grenada': 'GD', + 'Guatemala': 'GT', + 'Guinea': 'GN', + 'Guinea-Bissau': 'GW', + 'Guyana': 'GY', + 'Haiti': 'HT', + 'Honduras': 'HN', + 'Hungary': 'HU', + 'Iceland': 'IS', + 'India': 'IN', + 'Indonesia': 'ID', + 'Iran': 'IR', + 'Iraq': 'IQ', + 'Ireland': 'IE', + 'Israel': 'IL', + 'Italy': 'IT', + 'Jamaica': 'JM', + 'Japan': 'JP', + 'Jordan': 'JO', + 'Kazakhstan': 'KZ', + 'Kenya': 'KE', + 'Kiribati': 'KI', + 'Kuwait': 'KW', + 'Kyrgyzstan': 'KG', + 'Laos': 'LA', + 'Latvia': 'LV', + 'Lebanon': 'LB', + 'Lesotho': 'LS', + 'Liberia': 'LR', + 'Libya': 'LY', + 'Liechtenstein': 'LI', + 'Lithuania': 'LT', + 'Luxembourg': 'LU', + 'Madagascar': 'MG', + 'Malawi': 'MW', + 'Malaysia': 'MY', + 'Maldives': 'MV', + 'Mali': 'ML', + 'Malta': 'MT', + 'Marshall Islands': 'MH', + 'Mauritania': 'MR', + 'Mauritius': 'MU', + 'Mexico': 'MX', + 'Micronesia': 'FM', + 'Moldova': 'MD', + 'Monaco': 'MC', + 'Mongolia': 'MN', + 'Montenegro': 'ME', + 'Morocco': 'MA', + 'Mozambique': 'MZ', + 'Myanmar': 'MM', + 'Namibia': 'NA', + 'Nauru': 'NR', + 'Nepal': 'NP', + 'Netherlands': 'NL', + 'New Zealand': 'NZ', + 'Nicaragua': 'NI', + 'Niger': 'NE', + 'Nigeria': 'NG', + 'North Korea': 'KP', + 'North Macedonia': 'MK', + 'Norway': 'NO', + 'Oman': 'OM', + 'Pakistan': 'PK', + 'Palau': 'PW', + 'Palestine State': 'PS', + 'Panama': 'PA', + 'Papua New Guinea': 'PG', + 'Paraguay': 'PY', + 'Peru': 'PE', + 'Philippines': 'PH', + 'Poland': 'PL', + 'Portugal': 'PT', + 'Qatar': 'QA', + 'Romania': 'RO', + 'Russia': 'RU', + 'Rwanda': 'RW', + 'Saint Kitts and Nevis': 'KN', + 'Saint Lucia': 'LC', + 'Saint Vincent and the Grenadines': 'VC', + 'Samoa': 'WS', + 'San Marino': 'SM', + 'Sao Tome and Principe': 'ST', + 'Saudi Arabia': 'SA', + 'Senegal': 'SN', + 'Serbia': 'RS', + 'Seychelles': 'SC', + 'Sierra Leone': 'SL', + 'Singapore': 'SG', + 'Slovakia': 'SK', + 'Slovenia': 'SI', + 'Solomon Islands': 'SB', + 'Somalia': 'SO', + 'South Africa': 'ZA', + 'South Korea': 'KR', + 'South Sudan': 'SS', + 'Spain': 'ES', + 'Sri Lanka': 'LK', + 'Sudan': 'SD', + 'Suriname': 'SR', + 'Sweden': 'SE', + 'Switzerland': 'CH', + 'Syria': 'SY', + 'Taiwan': 'TW', + 'Tajikistan': 'TJ', + 'Tanzania': 'TZ', + 'Thailand': 'TH', + 'Timor-Leste': 'TL', + 'Togo': 'TG', + 'Tonga': 'TO', + 'Trinidad and Tobago': 'TT', + 'Tunisia': 'TN', + 'Turkey': 'TR', + 'Turkmenistan': 'TM', + 'Tuvalu': 'TV', + 'Uganda': 'UG', + 'Ukraine': 'UA', + 'United Arab Emirates': 'AE', + 'United Kingdom': 'GB', + 'United States': 'US', + 'Uruguay': 'UY', + 'Uzbekistan': 'UZ', + 'Vanuatu': 'VU', + 'Venezuela': 'VE', + 'Vietnam': 'VN', + 'Yemen': 'YE', + 'Zambia': 'ZM', + 'Zimbabwe': 'ZW', +}; diff --git a/app/lib/helpers/flags.dart b/app/lib/helpers/flags.dart index c99101b72..c69c3348f 100644 --- a/app/lib/helpers/flags.dart +++ b/app/lib/helpers/flags.dart @@ -96,10 +96,9 @@ class Flags { Globals().registrarURL = (await Flags().getFlagValueByFeatureName('registrar-url')).toString(); - Globals().activationServiceAddress = (await Flags() - .getFlagValueByFeatureName('activation-service-address')) - .toString(); - + Globals().activationServiceAddress = + (await Flags().getFlagValueByFeatureName('activation-service-address')) + .toString(); } Future hasFlagValueByFeatureName(String name) async { diff --git a/app/lib/jrouter.dart b/app/lib/jrouter.dart index 15e31eb5e..528b28e0c 100644 --- a/app/lib/jrouter.dart +++ b/app/lib/jrouter.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:threebotlogin/app.dart'; import 'package:threebotlogin/apps/council/council.dart'; import 'package:threebotlogin/apps/dao/dao.dart'; +import 'package:threebotlogin/apps/notifications/notifications.dart'; import 'package:threebotlogin/apps/wallet/wallet.dart'; import 'package:threebotlogin/screens/identity_verification_screen.dart'; import 'package:threebotlogin/screens/preference_screen.dart'; @@ -87,6 +88,14 @@ class JRouter { view: await Council().widget(), ), app: Dao()), + AppInfo( + route: Route( + path: '/notifications', + name: 'Notifications', + icon: Icons.how_to_vote_outlined, + view: await Notifications().widget(), + ), + app: null), AppInfo( route: Route( path: '/sign', diff --git a/app/lib/main.dart b/app/lib/main.dart index 2054121a2..df41efe49 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -71,7 +71,7 @@ Future main() async { ), (String taskId) async { logger.i('[BackgroundFetch] Task: $taskId'); - await checkNodeStatus(); + await checkNodeStatus(taskId); BackgroundFetch.finish(taskId); }, (String taskId) async { diff --git a/app/lib/providers/app_bar_provider.dart b/app/lib/providers/app_bar_provider.dart new file mode 100644 index 000000000..57e871222 --- /dev/null +++ b/app/lib/providers/app_bar_provider.dart @@ -0,0 +1,4 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final appBarActionsBuilderProvider = StateProvider Function(BuildContext context)?>((ref) => null); \ No newline at end of file diff --git a/app/lib/screens/council_screen.dart b/app/lib/screens/council_screen.dart index d960bf9c8..12a381da1 100644 --- a/app/lib/screens/council_screen.dart +++ b/app/lib/screens/council_screen.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:threebotlogin/widgets/council/councils.dart'; -import 'package:threebotlogin/widgets/layout_drawer.dart'; import 'package:validators/validators.dart'; class CouncilScreen extends StatefulWidget { @@ -157,6 +156,6 @@ class _CouncilScreenState extends State { ], )); })); - return LayoutDrawer(titleText: 'Council', content: content); + return content; } } diff --git a/app/lib/screens/dao_screen.dart b/app/lib/screens/dao_screen.dart index e8a59acc9..15609a98d 100644 --- a/app/lib/screens/dao_screen.dart +++ b/app/lib/screens/dao_screen.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:tfchain_client/models/dao.dart'; import 'package:threebotlogin/helpers/logger.dart'; -import 'package:threebotlogin/widgets/layout_drawer.dart'; import 'package:threebotlogin/widgets/dao/proposals.dart'; import 'package:threebotlogin/services/tfchain_service.dart'; @@ -149,6 +148,6 @@ class _DaoPageState extends State with SingleTickerProviderStateMixin { ), ); } - return LayoutDrawer(titleText: 'Dao', content: content); + return content; } } diff --git a/app/lib/screens/farm_screen.dart b/app/lib/screens/farm_screen.dart index 266942fd2..79102a9d4 100644 --- a/app/lib/screens/farm_screen.dart +++ b/app/lib/screens/farm_screen.dart @@ -7,13 +7,13 @@ import 'package:threebotlogin/helpers/globals.dart'; import 'package:threebotlogin/helpers/logger.dart'; import 'package:threebotlogin/models/farm.dart'; import 'package:threebotlogin/models/wallet.dart'; +import 'package:threebotlogin/providers/app_bar_provider.dart'; import 'package:threebotlogin/providers/wallets_provider.dart'; import 'package:threebotlogin/services/crypto_service.dart'; import 'package:threebotlogin/services/gridproxy_service.dart'; import 'package:threebotlogin/services/tfchain_service.dart'; import 'package:threebotlogin/widgets/add_farm.dart'; import 'package:threebotlogin/widgets/farm_item.dart'; -import 'package:threebotlogin/widgets/layout_drawer.dart'; class FarmScreen extends ConsumerStatefulWidget { const FarmScreen({super.key}); @@ -37,12 +37,25 @@ class _FarmScreenState extends ConsumerState super.initState(); _tabController = TabController(length: 2, vsync: this); listFarms(); + + Future.microtask(() { + ref.read(appBarActionsBuilderProvider.notifier).state = + (BuildContext context) { + return [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () => _openAddFarmOverlay(), + ), + ]; + }; + }); } @override void dispose() { - super.dispose(); + ref.read(appBarActionsBuilderProvider.notifier).state = null; _tabController.dispose(); + super.dispose(); } listWallets() async { @@ -215,9 +228,9 @@ class _FarmScreenState extends ConsumerState @override Widget build(BuildContext context) { - Widget mainWidget; + Widget mainContent; if (loading) { - mainWidget = Center( + mainContent = Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -232,7 +245,7 @@ class _FarmScreenState extends ConsumerState ], )); } else if (failed) { - mainWidget = Center( + mainContent = Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -252,59 +265,43 @@ class _FarmScreenState extends ConsumerState ), ); } else { - mainWidget = DefaultTabController( - length: 2, - child: Column( - children: [ - PreferredSize( - preferredSize: const Size.fromHeight(50.0), - child: Container( - color: Theme.of(context).scaffoldBackgroundColor, - child: TabBar( - controller: _tabController, - labelColor: Theme.of(context).colorScheme.primary, - indicatorColor: Theme.of(context).colorScheme.primary, - unselectedLabelColor: - Theme.of(context).colorScheme.onSurface, - dividerColor: Theme.of(context).scaffoldBackgroundColor, - labelStyle: Theme.of(context).textTheme.titleLarge, - unselectedLabelStyle: - Theme.of(context).textTheme.titleMedium, - tabs: const [ - Tab(text: 'V3'), - Tab(text: 'V4'), - ], - ), - ), + mainContent = Column( + children: [ + PreferredSize( + preferredSize: const Size.fromHeight(50.0), + child: Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: TabBar( + controller: _tabController, + labelColor: Theme.of(context).colorScheme.primary, + indicatorColor: Theme.of(context).colorScheme.primary, + unselectedLabelColor: Theme.of(context).colorScheme.onSurface, + dividerColor: Theme.of(context).scaffoldBackgroundColor, + labelStyle: Theme.of(context).textTheme.titleLarge, + unselectedLabelStyle: Theme.of(context).textTheme.titleMedium, + tabs: const [ + Tab(text: 'V3'), + Tab(text: 'V4'), + ], ), - Expanded( - child: TabBarView(controller: _tabController, children: [ - RefreshIndicator( - onRefresh: listFarms, - child: listFarmsWidget(v3Farms, false), - ), - RefreshIndicator( - onRefresh: listFarms, - child: listFarmsWidget(v4Farms, true), - ), - ]), - ) - ], - )); + ), + ), + Expanded( + child: TabBarView(controller: _tabController, children: [ + RefreshIndicator( + onRefresh: listFarms, + child: listFarmsWidget(v3Farms, false), + ), + RefreshIndicator( + onRefresh: listFarms, + child: listFarmsWidget(v4Farms, true), + ), + ]), + ) + ], + ); } - return LayoutDrawer( - titleText: 'Farming', - content: mainWidget, - appBarActions: loading - ? [] - : [ - IconButton( - onPressed: _openAddFarmOverlay, - icon: const Icon( - Icons.add, - )) - ], - ); + return mainContent; } _openAddFarmOverlay() { diff --git a/app/lib/screens/home_screen.dart b/app/lib/screens/home_screen.dart index b44a63933..8fabdabca 100644 --- a/app/lib/screens/home_screen.dart +++ b/app/lib/screens/home_screen.dart @@ -10,6 +10,7 @@ import 'package:threebotlogin/events/go_sign_event.dart'; import 'package:threebotlogin/events/go_support_event.dart'; import 'package:threebotlogin/events/identity_callback_event.dart'; import 'package:threebotlogin/events/phone_event.dart'; +import 'package:threebotlogin/providers/app_bar_provider.dart'; import 'package:threebotlogin/events/events.dart'; import 'package:threebotlogin/events/go_home_event.dart'; import 'package:threebotlogin/events/go_wallet_event.dart'; @@ -23,224 +24,320 @@ import 'package:threebotlogin/services/socket_service.dart'; import 'package:threebotlogin/services/uni_link_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/email_verification_needed.dart'; +import 'package:threebotlogin/widgets/app_bottom_nav.dart'; +import 'package:threebotlogin/widgets/app_drawer.dart'; +import 'package:threebotlogin/widgets/keep_alive.dart'; import 'package:uni_links/uni_links.dart'; -/* Screen shows tab bar and all pages defined in router.dart */ class HomeScreen extends ConsumerStatefulWidget { - const HomeScreen({super.key, this.initialLink, this.backendConnection}); final String? initialLink; final BackendConnection? backendConnection; + final int initialTabIndex; + + const HomeScreen({ + super.key, + this.initialLink, + this.backendConnection, + this.initialTabIndex = 0, + }); @override ConsumerState createState() => _HomeScreenState(); } class _HomeScreenState extends ConsumerState - with SingleTickerProviderStateMixin { + with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { + late TabController _tabController; Globals globals = Globals(); StreamSubscription? _sub; - String? initialLink; - bool timeoutExpiredInBackground = true; - bool pinCheckOpen = false; + bool _timeoutExpiredInBackground = true; + bool _isPinCheckOpen = false; + final List _screens = Globals().router.getContent(); + final List _screenTitles = [ + 'Home', + 'News', + 'Wallet', + 'Farming', + 'Dao', + 'Identity', + 'Settings', + 'Council', + 'Notification Settings', + ]; @override - void dispose() { - _sub?.cancel(); - globals.tabController.removeListener(_handleTabSelection); - globals.tabController.dispose(); - super.dispose(); + void initState() { + super.initState(); + _tabController = TabController( + initialIndex: widget.initialTabIndex, + length: _screens.length, + vsync: this); + + globals.tabController = _tabController; + + _tabController.addListener(_handleTabSelection); + _tabController.addListener(() { + if (!_tabController.indexIsChanging) { + if (mounted) { + setState(() {}); + } + } + }); + + _setupEventHandlers(); + _initUniLinks(); + Future.microtask(() { + ref.read(walletsNotifier.notifier); + }); + } + + void _setupEventHandlers() { + final navigationEvents = { + GoHomeEvent().runtimeType: 0, + GoNewsEvent().runtimeType: 1, + GoSupportEvent().runtimeType: 3, + GoSettingsEvent().runtimeType: 6, + GoReservationsEvent().runtimeType: 5, + }; + + navigationEvents.forEach((eventType, tabIndex) { + Events().onEvent(eventType, (_) { + _selectScreen(tabIndex); + }); + }); + + Events().onEvent(GoWalletEvent().runtimeType, (_) { + if (_isPinCheckOpen) return; + const walletTabIndex = 2; + if (Globals().router.pinRequired(walletTabIndex)) { + _performPinCheck(successTabIndex: walletTabIndex); + } else { + _selectScreen(walletTabIndex); + } + }); + + Events().onEvent(NewLoginEvent().runtimeType, (NewLoginEvent event) { + if (widget.backendConnection != null) { + openLogin(context, event.loginData!, widget.backendConnection!); + } + }); + Events().onEvent(NewSignEvent().runtimeType, (NewSignEvent event) { + if (widget.backendConnection != null) { + openSign(context, event.signData!, widget.backendConnection!); + } + }); + Events().onEvent(EmailEvent().runtimeType, (EmailEvent event) { + if (mounted && context.mounted) { + emailVerificationDialog(context); + } + }); + Events().onEvent(IdentityCallbackEvent().runtimeType, + (IdentityCallbackEvent event) async { + if (mounted) { + Future.delayed(Duration.zero, () { + _selectScreen(0); + if (context.mounted && event.type != null) { + showIdentityMessage(context, event.type!); + } + }); + } + }); + Events().onEvent(PhoneEvent().runtimeType, (PhoneEvent event) { + if (mounted && context.mounted) { + phoneVerification(context); + } + }); } - void checkPinAndNavigateIfSuccess(int indexIfAuthIsSuccess) async { + Future _performPinCheck({int? successTabIndex}) async { + _isPinCheckOpen = true; String? pin = await getPin(); - pinCheckOpen = true; - - bool? authenticated = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => AuthenticationScreen( - correctPin: pin!, - userMessage: 'Please enter your PIN code', + + bool? authenticated = false; + if (mounted && pin != null && context.mounted) { + authenticated = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AuthenticationScreen( + correctPin: pin, + userMessage: 'Please enter your PIN code', + ), ), - ), - ); + ); + } else if (mounted && pin == null && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('PIN is not set. Please set up a PIN.')), + ); + } - pinCheckOpen = false; + _isPinCheckOpen = false; - if (authenticated != null && authenticated) { + if (mounted && authenticated == true) { ref.read(lastPausedProvider.notifier).state = DateTime.now().millisecondsSinceEpoch; - timeoutExpiredInBackground = false; - globals.tabController.animateTo(indexIfAuthIsSuccess); + _timeoutExpiredInBackground = false; + if (successTabIndex != null) { + _selectScreen(successTabIndex); + } } } - _handleTabSelection() async { - if (!globals.tabController.indexIsChanging) { - return; + void _selectScreen(int index) { + if (index >= 0 && index < _tabController.length) { + _tabController.animateTo( + index, + duration: const Duration(milliseconds: 100), + curve: Curves.easeOut, + ); } + } - if (Globals().router.pinRequired(globals.tabController.index) && - timeoutExpiredInBackground && - !pinCheckOpen) { - int authenticatedAppIndex = globals.tabController.index; - globals.tabController.animateTo(globals.tabController.previousIndex); + void _handleBottomNavItemTap(int bottomNavIndex) { + int screenIndexToNavigateTo; + switch (bottomNavIndex) { + case 0: // Tap the 1st item (Home) -> Tab Index 0 + screenIndexToNavigateTo = 0; + break; + case 1: // Tap the 2nd item (Wallet) -> Tab Index 2 + screenIndexToNavigateTo = 2; + break; + case 2: // Tap the 3rd item (Farming) -> Tab Index 3 + screenIndexToNavigateTo = 3; + break; + case 3: // Tap the 4th item (Settings) -> Tab Index 6 + screenIndexToNavigateTo = 6; + break; + default: + screenIndexToNavigateTo = 0; // Default to Home + } + _selectScreen(screenIndexToNavigateTo); + } - checkPinAndNavigateIfSuccess(authenticatedAppIndex); + _handleTabSelection() async { + final currentIndex = _tabController.index; + final previousIndex = _tabController.previousIndex; + + if (!mounted) return; + + // Handle PIN requirement + if (Globals().router.pinRequired(currentIndex) && + _timeoutExpiredInBackground && + !_isPinCheckOpen) { + _tabController.animateTo(previousIndex, + duration: const Duration(seconds: 0)); + await _performPinCheck(successTabIndex: currentIndex); + return; } - if (Globals().router.emailMustBeVerified(globals.tabController.index) && + if (Globals().router.emailMustBeVerified(currentIndex) && !Globals().emailVerified.value) { - globals.tabController.animateTo(globals.tabController.previousIndex); - await emailVerificationDialog(context); + _tabController.animateTo(previousIndex, + duration: const Duration(seconds: 0)); + if (mounted && context.mounted) { + await emailVerificationDialog(context); + } + return; } - if (globals.tabController.index != 2 && Globals().paymentRequest != null) { + if (currentIndex != 2 && Globals().paymentRequest != null) { Globals().paymentRequest = null; Globals().paymentRequestIsUsed = false; - } - - if (globals.tabController.previousIndex == 2 && + } else if (previousIndex == 2 && Globals().paymentRequest != null && Globals().paymentRequestIsUsed == true) { Globals().paymentRequest = null; } } - close(GoHomeEvent e) { - int homeTab = 0; - globals.tabController.animateTo(homeTab); - } - - @override - void initState() { - super.initState(); - initUniLinks(); - - globals.tabController = TabController( - initialIndex: 0, length: Globals().router.routes.length, vsync: this); - globals.tabController.addListener(_handleTabSelection); - - Events().onEvent(GoHomeEvent().runtimeType, close); - - Events().onEvent(GoHomeEvent().runtimeType, (GoHomeEvent event) { - globals.tabController.animateTo(0, duration: const Duration(seconds: 0)); - }); - - Events().onEvent(GoNewsEvent().runtimeType, (GoNewsEvent event) { - globals.tabController.animateTo(1, duration: const Duration(seconds: 0)); - }); - - // Needed to hardcode this to prevent double tapping and gaining access without knowing the pincode with the current logic that was implemented. - Events().onEvent(GoWalletEvent().runtimeType, (GoWalletEvent event) { - if (pinCheckOpen) { - return; - } - - int tabIndex = 2; - - if (Globals().router.pinRequired(tabIndex)) { - checkPinAndNavigateIfSuccess(tabIndex); - } - }); - - Events().onEvent(GoSupportEvent().runtimeType, (GoSupportEvent event) { - globals.tabController.animateTo(3, duration: const Duration(seconds: 0)); - }); - - Events().onEvent(GoSettingsEvent().runtimeType, (GoSettingsEvent event) { - globals.tabController.animateTo(4, duration: const Duration(seconds: 0)); - }); - - Events().onEvent(GoReservationsEvent().runtimeType, - (GoReservationsEvent event) { - globals.tabController.animateTo(5, duration: const Duration(seconds: 0)); - }); - - Events().onEvent(NewLoginEvent().runtimeType, (NewLoginEvent event) { - openLogin(context, event.loginData!, widget.backendConnection!); - }); - - Events().onEvent(NewSignEvent().runtimeType, (NewSignEvent event) { - openSign(context, event.signData!, widget.backendConnection!); - }); - - Events().onEvent(EmailEvent().runtimeType, (EmailEvent event) { - emailVerification(context); - }); - - Events().onEvent(IdentityCallbackEvent().runtimeType, - (IdentityCallbackEvent event) async { - Future(() { - globals.tabController - .animateTo(0, duration: const Duration(seconds: 0)); - showIdentityMessage(context, event.type!); - }); - }); - - Events().onEvent(PhoneEvent().runtimeType, (PhoneEvent event) { - phoneVerification(context); - }); - } - - Future initUniLinks() async { + Future _initUniLinks() async { Events().onEvent( UniLinkEvent(null, null).runtimeType, UniLinkService.handleUniLink); - initialLink = widget.initialLink; - - if (initialLink != null) { - Events().emit(UniLinkEvent(Uri.parse(initialLink!), context)); + if (widget.initialLink != null) { + Events().emit(UniLinkEvent(Uri.parse(widget.initialLink!), context)); } _sub = getLinksStream().listen((String? incomingLink) { - if (!mounted) { + if (!mounted || incomingLink == null) { return; } - Events().emit(UniLinkEvent(Uri.parse(incomingLink!), context)); + if (context.mounted) { + Events().emit(UniLinkEvent(Uri.parse(incomingLink), context)); + } }); } + @override + void dispose() { + _sub?.cancel(); + _tabController.removeListener(_handleTabSelection); + _tabController.dispose(); + super.dispose(); + } + + @override @override Widget build(BuildContext context) { - ProviderScope.containerOf(context, listen: false) - .read(walletsNotifier.notifier); + super.build(context); + + final currentIndex = _tabController.index; + final actionsBuilder = ref.watch(appBarActionsBuilderProvider); + List appBarActions = + actionsBuilder != null ? actionsBuilder(context) : []; + final bool showAppBarAndNav = currentIndex != 0; + return Scaffold( resizeToAvoidBottomInset: false, - appBar: PreferredSize( - preferredSize: const Size.fromHeight(0), - child: AppBar( - automaticallyImplyLeading: true, - ), - ), - body: DefaultTabController( - length: Globals().router.routes.length, - child: WillPopScope( - onWillPop: onWillPop, - child: Scaffold( - body: SafeArea( - child: TabBarView( - controller: globals.tabController, - physics: const NeverScrollableScrollPhysics(), - children: Globals().router.getContent(), - )), - ), + appBar: showAppBarAndNav + ? AppBar( + title: Text(_screenTitles[currentIndex]), + actions: appBarActions, + toolbarHeight: 60, + automaticallyImplyLeading: true, + ) + : null, + body: WillPopScope( + onWillPop: _onWillPop, + child: SafeArea( + child: TabBarView( + controller: _tabController, + physics: const NeverScrollableScrollPhysics(), + children: List.generate( + _screens.length, + (index) => KeepAlivePage(child: _screens[index]), + ), + ) ), ), + drawer: showAppBarAndNav + ? AppDrawer( + onItemSelected: _selectScreen, + ) + : null, + bottomNavigationBar: showAppBarAndNav + ? AppBottomNavigationBar( + currentTabIndex: currentIndex, + onItemSelected: _handleBottomNavItemTap, + ) + : null, ); } - Future onWillPop() { - if (globals.tabController.index == 0) { - return Future(() => true); // if home screen exit + Future _onWillPop() async { + if (_tabController.index == 0) { + return true; } - if (Globals().router.routes[globals.tabController.index].app == null) { - Events().emit(GoHomeEvent()); // if not an app, eg settings, go home + + final currentRoute = Globals().router.routes[_tabController.index]; + if (currentRoute.app == null) { + Events().emit(GoHomeEvent()); + } else { + if (mounted && currentRoute.app != null) { + currentRoute.app!.back(); + } } - Globals() - .router - .routes[globals.tabController.index] - .app! - .back(); // if app ask app to handle back event - return Future(() => false); + return false; } + + @override + bool get wantKeepAlive => true; } diff --git a/app/lib/screens/identity_verification_screen.dart b/app/lib/screens/identity_verification_screen.dart index c676c02b6..14b5f91f6 100644 --- a/app/lib/screens/identity_verification_screen.dart +++ b/app/lib/screens/identity_verification_screen.dart @@ -14,7 +14,6 @@ import 'package:threebotlogin/services/pkid_service.dart'; import 'package:threebotlogin/services/tools_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/widgets/custom_dialog.dart'; -import 'package:threebotlogin/widgets/layout_drawer.dart'; import 'package:threebotlogin/widgets/phone_widget.dart'; class IdentityVerificationScreen extends StatefulWidget { @@ -726,9 +725,6 @@ class IdentityVerificationScreenState ); } - return LayoutDrawer( - titleText: 'Identity', - content: content, - ); + return content; } } diff --git a/app/lib/screens/main_screen.dart b/app/lib/screens/main_screen.dart index 66d2d263d..cff350034 100644 --- a/app/lib/screens/main_screen.dart +++ b/app/lib/screens/main_screen.dart @@ -211,7 +211,6 @@ class _AppState extends State { currentPin: null, ))); } - // await Navigator.push(context, MaterialPageRoute(builder: (context) => UnregisteredScreen())); await Navigator.of(context).pushReplacement(PageRouteBuilder( transitionDuration: const Duration(seconds: 1), pageBuilder: (_, __, ___) => HomeScreen( diff --git a/app/lib/screens/notifications_screen.dart b/app/lib/screens/notifications_screen.dart new file mode 100644 index 000000000..557905f76 --- /dev/null +++ b/app/lib/screens/notifications_screen.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class NotificationsScreen extends StatefulWidget { + const NotificationsScreen({super.key}); + + @override + _NotificationsScreenState createState() => _NotificationsScreenState(); +} + +class _NotificationsScreenState extends State { + bool _nodeStatusNotificationEnabled = true; + + static const String _nodeStatusNotificationEnabledKey = + 'nodeStatusNotificationEnabled'; + + @override + void initState() { + super.initState(); + _loadNotificationPreference(); + } + + void _loadNotificationPreference() async { + final prefs = await SharedPreferences.getInstance(); + setState(() { + _nodeStatusNotificationEnabled = + prefs.getBool(_nodeStatusNotificationEnabledKey) ?? true; + }); + } + + void _setNodeStatusNotification(bool newValue) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_nodeStatusNotificationEnabledKey, newValue); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SwitchListTile( + title: const Text('Enable node status notifications'), + value: _nodeStatusNotificationEnabled, + onChanged: (bool newValue) { + setState(() { + _nodeStatusNotificationEnabled = newValue; + }); + _setNodeStatusNotification(newValue); + }, + secondary: const Icon(Icons.notifications), + ), + ], + ), + ); + } +} diff --git a/app/lib/screens/preference_screen.dart b/app/lib/screens/preference_screen.dart index d61a7038b..af3e31aa9 100644 --- a/app/lib/screens/preference_screen.dart +++ b/app/lib/screens/preference_screen.dart @@ -24,7 +24,6 @@ import 'package:threebotlogin/services/pkid_service.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/services/wallet_service.dart'; import 'package:threebotlogin/widgets/custom_dialog.dart'; -import 'package:threebotlogin/widgets/layout_drawer.dart'; import 'package:threebotlogin/providers/theme_provider.dart'; import 'package:threebotlogin/widgets/wallets/warning_dialog.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -94,172 +93,165 @@ class _PreferenceScreenState extends ConsumerState { } else { isDarkMode = themeMode == ThemeMode.dark; } - return LayoutDrawer( - titleText: 'Settings', - content: ListView( - children: [ - const ListTile( - title: Text('Global settings'), - ), - FutureBuilder( - future: checkBiometrics(), - builder: (context, snapshot) { - if (snapshot.hasData) { - if (snapshot.data == true) { - return FutureBuilder( - future: getBiometricDeviceName(), - builder: (context, snapshot) { - if (snapshot.hasData) { - if (snapshot.data == 'Not found') { - return Container(); - } - biometricDeviceName = snapshot.data; - return CheckboxListTile( - secondary: biometricDeviceName == 'Face ID' || - biometricDeviceName == 'Face unlock' - ? Image.asset( - 'assets/face-id.png', - color: Theme.of(context) - .colorScheme - .onSurface, - height: 24.0, - width: 24.0, - ) - : const Icon(Icons.fingerprint), - value: finger, - title: Text(snapshot.data.toString()), - activeColor: - Theme.of(context).colorScheme.primary, - onChanged: (bool? newValue) async { - _toggleFingerprint(newValue!); - }, - ); - } else { + return ListView( + children: [ + const ListTile( + title: Text('Global settings'), + ), + FutureBuilder( + future: checkBiometrics(), + builder: (context, snapshot) { + if (snapshot.hasData) { + if (snapshot.data == true) { + return FutureBuilder( + future: getBiometricDeviceName(), + builder: (context, snapshot) { + if (snapshot.hasData) { + if (snapshot.data == 'Not found') { return Container(); } - }); - } else { - return Container(); - } + biometricDeviceName = snapshot.data; + return CheckboxListTile( + secondary: biometricDeviceName == 'Face ID' || + biometricDeviceName == 'Face unlock' + ? Image.asset( + 'assets/face-id.png', + color: + Theme.of(context).colorScheme.onSurface, + height: 24.0, + width: 24.0, + ) + : const Icon(Icons.fingerprint), + value: finger, + title: Text(snapshot.data.toString()), + activeColor: Theme.of(context).colorScheme.primary, + onChanged: (bool? newValue) async { + _toggleFingerprint(newValue!); + }, + ); + } else { + return Container(); + } + }); } else { return Container(); } - }), - ListTile( - leading: const Icon(Icons.lock), - title: const Text('Change PIN'), - onTap: () async { - _changePincode(); + } else { + return Container(); + } + }), + ListTile( + leading: const Icon(Icons.lock), + title: const Text('Change PIN'), + onTap: () async { + _changePincode(); + }, + ), + ListTile( + leading: const Icon(Icons.brightness_6_outlined), + title: const Text('Appearance'), + trailing: GestureDetector( + onTap: () { + ref.read(themeModeNotifier.notifier).toggleTheme(); }, - ), - ListTile( - leading: const Icon(Icons.brightness_6_outlined), - title: const Text('Appearance'), - trailing: GestureDetector( - onTap: () { - ref.read(themeModeNotifier.notifier).toggleTheme(); - }, - child: Container( - width: 40, - height: 20, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.black - : Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular(10), - boxShadow: const [ - BoxShadow( - color: Colors.black26, - blurRadius: 4, - offset: Offset(2, 2), - ), - ], - ), - child: Stack( - children: [ - AnimatedPositioned( - duration: const Duration(milliseconds: 300), - left: isDarkMode ? 20 : 0, - child: Container( - width: 20, - height: 20, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.black26, - blurRadius: 4, - offset: Offset(2, 2), - ), - ], - ), - child: Center( - child: Icon( - isDarkMode - ? Icons.nightlight_round - : Icons.wb_sunny, - color: isDarkMode - ? Colors.black - : Theme.of(context).colorScheme.primary, - size: 14, + child: Container( + width: 40, + height: 20, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.black + : Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(10), + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 4, + offset: Offset(2, 2), + ), + ], + ), + child: Stack( + children: [ + AnimatedPositioned( + duration: const Duration(milliseconds: 300), + left: isDarkMode ? 20 : 0, + child: Container( + width: 20, + height: 20, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black26, + blurRadius: 4, + offset: Offset(2, 2), ), + ], + ), + child: Center( + child: Icon( + isDarkMode ? Icons.nightlight_round : Icons.wb_sunny, + color: isDarkMode + ? Colors.black + : Theme.of(context).colorScheme.primary, + size: 14, ), ), ), - ], - ), + ), + ], ), ), ), - ListTile( - leading: const Icon(Icons.perm_device_information), - title: Text('Version: $version - $buildNumber'), - onTap: () { - _showVersionInfo(); - }, - ), - ListTile( - leading: const Icon(Icons.info_outline), - title: const Text('Terms and conditions'), - onTap: () async => {await _showTermsAndConds()}, + ), + ListTile( + leading: const Icon(Icons.perm_device_information), + title: Text('Version: $version - $buildNumber'), + onTap: () { + _showVersionInfo(); + }, + ), + ListTile( + leading: const Icon(Icons.info_outline), + title: const Text('Terms and conditions'), + onTap: () async => {await _showTermsAndConds()}, + ), + ListTile( + leading: const Icon(Icons.logout_outlined), + title: Text( + 'Log Out', + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(color: Theme.of(context).colorScheme.onSurface), ), - ListTile( - leading: const Icon(Icons.logout_outlined), - title: Text( - 'Log Out', - style: Theme.of(context) - .textTheme - .bodyLarge! - .copyWith(color: Theme.of(context).colorScheme.onSurface), - ), - onTap: _showDialog, + onTap: _showDialog, + ), + ExpansionTile( + title: const Text( + 'Advanced settings', ), - ExpansionTile( - title: const Text( - 'Advanced settings', - ), - children: [ - ListTile( - leading: Icon( - Icons.remove_circle, - color: Theme.of(context).colorScheme.error, - ), - title: Text( - 'Delete Account', - style: Theme.of(context) - .textTheme - .bodyLarge! - .copyWith(color: Theme.of(context).colorScheme.error), - ), - onTap: () { - _showDialog(delete: true); - }, + children: [ + ListTile( + leading: Icon( + Icons.remove_circle, + color: Theme.of(context).colorScheme.error, ), - ], - ), - ], - ), + title: Text( + 'Delete Account', + style: Theme.of(context) + .textTheme + .bodyLarge! + .copyWith(color: Theme.of(context).colorScheme.error), + ), + onTap: () { + _showDialog(delete: true); + }, + ), + ], + ), + ], ); } diff --git a/app/lib/screens/registered_screen.dart b/app/lib/screens/registered_screen.dart index c9d289471..b354bfe67 100644 --- a/app/lib/screens/registered_screen.dart +++ b/app/lib/screens/registered_screen.dart @@ -4,15 +4,13 @@ import 'package:threebotlogin/widgets/home_card.dart'; import 'package:threebotlogin/widgets/home_logo.dart'; class RegisteredScreen extends StatefulWidget { - static final RegisteredScreen _singleton = RegisteredScreen._internal(); + static final RegisteredScreen _singleton = const RegisteredScreen._internal(); factory RegisteredScreen() { return _singleton; } - RegisteredScreen._internal() { - //init - } + const RegisteredScreen._internal(); @override State createState() => _RegisteredScreenState(); @@ -20,8 +18,6 @@ class RegisteredScreen extends StatefulWidget { class _RegisteredScreenState extends State with WidgetsBindingObserver { - // We will treat this error as a singleton - bool showSettings = false; bool showPreference = false; @@ -31,6 +27,7 @@ class _RegisteredScreenState extends State body: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox( height: MediaQuery.of(context).size.height * 0.3, @@ -51,12 +48,12 @@ class _RegisteredScreenState extends State ], ), ), - Container( - padding: const EdgeInsets.only(left: 10, right: 10, top: 50), - height: MediaQuery.of(context).size.height * 0.6, - width: MediaQuery.of(context).size.width, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), child: Column( mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: MediaQuery.of(context).size.width / 1.2, @@ -76,7 +73,7 @@ class _RegisteredScreenState extends State ]), ), ), - const Spacer(), + const SizedBox(height: 45), const Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, @@ -119,10 +116,26 @@ class _RegisteredScreenState extends State name: 'Settings', icon: Icons.settings, pageNumber: 6), ], ), - const SizedBox(height: 40), const Row( - children: [Spacer(), CrispChatbot(), SizedBox(width: 20)], - ) + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + HomeCardWidget( + name: 'Notification Settings', + icon: Icons.notifications, + pageNumber: 8, + fullWidth: true, + ), + ], + ), + const SizedBox(height: 70), + const Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: EdgeInsets.only(right: 20.0), + child: CrispChatbot(), + ), + ), ], ), ) diff --git a/app/lib/screens/signing/sign_with_text.dart b/app/lib/screens/signing/sign_with_text.dart index db094141a..0498e1230 100644 --- a/app/lib/screens/signing/sign_with_text.dart +++ b/app/lib/screens/signing/sign_with_text.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:threebotlogin/screens/signing/signing_mixin.dart'; @@ -102,8 +101,8 @@ class _SignWithTextScreenState extends ConsumerState : Text('Sign', style: Theme.of(context) .textTheme - .titleLarge - !.copyWith( + .titleLarge! + .copyWith( color: Theme.of(context) .colorScheme .onPrimaryContainer, diff --git a/app/lib/screens/signing/signing_mixin.dart b/app/lib/screens/signing/signing_mixin.dart index ae50e5efa..55da3e2c3 100644 --- a/app/lib/screens/signing/signing_mixin.dart +++ b/app/lib/screens/signing/signing_mixin.dart @@ -258,8 +258,8 @@ mixin SigningMixin on ConsumerState { child: Text(wallet.name, style: Theme.of(context) .textTheme - .bodyMedium - !.copyWith( + .bodyMedium! + .copyWith( color: Theme.of(context).colorScheme.onSurface, )), diff --git a/app/lib/screens/wallets/wallet_screen.dart b/app/lib/screens/wallets/wallet_screen.dart index 8ed72e801..ef7226d4a 100644 --- a/app/lib/screens/wallets/wallet_screen.dart +++ b/app/lib/screens/wallets/wallet_screen.dart @@ -6,7 +6,7 @@ import 'package:threebotlogin/models/wallet.dart'; import 'package:threebotlogin/providers/wallets_provider.dart'; import 'package:threebotlogin/services/shared_preference_service.dart'; import 'package:threebotlogin/services/wallet_service.dart'; -import 'package:threebotlogin/widgets/layout_drawer.dart'; +import 'package:threebotlogin/providers/app_bar_provider.dart'; import 'package:threebotlogin/widgets/wallets/add_wallet.dart'; import 'package:threebotlogin/widgets/wallets/wallet_card.dart'; import 'package:hashlib/hashlib.dart'; @@ -34,12 +34,42 @@ class _WalletScreenState extends ConsumerState { walletRef.reloadBalances(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _setAppBarActions(); + } + @override void dispose() { walletRef.stopReloadingBalance(); + ref.read(appBarActionsBuilderProvider.notifier).state = null; super.dispose(); } + void _setAppBarActions() { + Future.microtask(() { + ref.read(appBarActionsBuilderProvider.notifier).state = + (BuildContext context) { + return [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () { + showModalBottomSheet( + isScrollControlled: true, + useSafeArea: true, + isDismissible: false, + constraints: const BoxConstraints(maxWidth: double.infinity), + context: context, + builder: (ctx) => NewWallet(wallets: wallets), + ); + }, + ), + ]; + }; + }); + } + @override Widget build(BuildContext context) { wallets = ref.watch(walletsNotifier); @@ -93,19 +123,7 @@ class _WalletScreenState extends ConsumerState { })); } - return LayoutDrawer( - titleText: 'Wallet', - content: mainWidget, - appBarActions: loading && !failed - ? [] - : [ - IconButton( - onPressed: _openAddWalletOverlay, - icon: const Icon( - Icons.add, - )) - ], - ); + return mainWidget; } listMyWallets() async { @@ -145,18 +163,6 @@ class _WalletScreenState extends ConsumerState { } } - _openAddWalletOverlay() { - showModalBottomSheet( - isScrollControlled: true, - useSafeArea: true, - isDismissible: false, - constraints: const BoxConstraints(maxWidth: double.infinity), - context: context, - builder: (ctx) => NewWallet( - wallets: wallets, - )); - } - Future _addInitialWallet() async { const walletName = 'Daily'; final derivedSeed = await getDerivedSeed(WalletConfig().appId()); diff --git a/app/lib/services/background_service.dart b/app/lib/services/background_service.dart index 45a7af78a..5da3dd5a8 100644 --- a/app/lib/services/background_service.dart +++ b/app/lib/services/background_service.dart @@ -1,6 +1,11 @@ import 'package:background_fetch/background_fetch.dart'; import 'package:threebotlogin/services/nodes_check_service.dart'; import 'notification_service.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:threebotlogin/helpers/logger.dart'; + +const String _nodeStatusNotificationEnabledKey = + 'nodeStatusNotificationEnabled'; void backgroundFetchHeadlessTask(HeadlessTask task) async { final String taskId = task.taskId; @@ -10,14 +15,28 @@ void backgroundFetchHeadlessTask(HeadlessTask task) async { BackgroundFetch.finish(taskId); return; } - await checkNodeStatus(); + final prefs = await SharedPreferences.getInstance(); + final bool notificationsEnabled = + prefs.getBool(_nodeStatusNotificationEnabledKey) ?? true; - BackgroundFetch.finish(taskId); + logger.i( + 'Background Fetch Headless Task: $taskId, Notifications Enabled: $notificationsEnabled'); + + if (!notificationsEnabled) { + logger.i( + '[BackgroundFetch] Node status notifications are disabled. Finishing task: $taskId'); + BackgroundFetch.finish(taskId); + return; + } + await checkNodeStatus(taskId); } -Future checkNodeStatus() async { +Future checkNodeStatus(String taskId) async { final offlineNodes = await NodeCheckService.pingNodesInBackground(); - if (offlineNodes.isEmpty) return; + if (offlineNodes.isEmpty) { + BackgroundFetch.finish(taskId); + return; + } final now = DateTime.now().millisecondsSinceEpoch; final sevenDaysAgoTimestamp = @@ -33,7 +52,10 @@ Future checkNodeStatus() async { return downtime.inMinutes % checkInterval.inMinutes < 15; }).toList(); - if (nodesToNotify.isEmpty) return; + if (nodesToNotify.isEmpty) { + BackgroundFetch.finish(taskId); + return; + } const groupKey = 'offline_nodes'; final StringBuffer bodyBuffer = StringBuffer(); @@ -54,6 +76,7 @@ Future checkNodeStatus() async { body: bodyBuffer.toString().trim(), groupKey: groupKey, ); + BackgroundFetch.finish(taskId); } Duration _getCheckInterval(Duration downtime) { diff --git a/app/lib/widgets/app_bottom_nav.dart b/app/lib/widgets/app_bottom_nav.dart new file mode 100644 index 000000000..55ece22b3 --- /dev/null +++ b/app/lib/widgets/app_bottom_nav.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +class BottomNavItemData { + final IconData icon; + final String label; + final int tabIndex; + const BottomNavItemData({ + required this.icon, + required this.label, + required this.tabIndex, + }); +} + +class AppBottomNavigationBar extends StatelessWidget { + final int currentTabIndex; + final ValueChanged onItemSelected; + + const AppBottomNavigationBar({ + super.key, + required this.currentTabIndex, + required this.onItemSelected, + }); + + final List _bottomNavItems = const [ + BottomNavItemData(icon: Icons.home, label: 'Home', tabIndex: 0), + BottomNavItemData( + icon: Icons.account_balance_wallet, label: 'Wallet', tabIndex: 2), + BottomNavItemData(icon: Icons.storage, label: 'Farming', tabIndex: 3), + BottomNavItemData(icon: Icons.settings, label: 'Settings', tabIndex: 6), + ]; + + int _getItemIndex(int tabIndex) { + for (int i = 0; i < _bottomNavItems.length; i++) { + if (_bottomNavItems[i].tabIndex == tabIndex) { + return i; + } + } + return 0; + } + + @override + Widget build(BuildContext context) { + final bottomNavTheme = BottomNavigationBarTheme.of(context); + + return BottomNavigationBar( + onTap: onItemSelected, + currentIndex: _getItemIndex(currentTabIndex), + selectedIconTheme: bottomNavTheme.selectedIconTheme, + unselectedIconTheme: bottomNavTheme.unselectedIconTheme, + selectedItemColor: bottomNavTheme.selectedItemColor, + unselectedItemColor: bottomNavTheme.unselectedItemColor, + showUnselectedLabels: true, + selectedFontSize: 12.0, + unselectedFontSize: 12.0, + type: BottomNavigationBarType.fixed, + items: _bottomNavItems + .map((item) => BottomNavigationBarItem( + icon: Icon(item.icon), + label: item.label, + )) + .toList(), + ); + } +} diff --git a/app/lib/widgets/app_drawer.dart b/app/lib/widgets/app_drawer.dart new file mode 100644 index 000000000..41405116b --- /dev/null +++ b/app/lib/widgets/app_drawer.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:threebotlogin/helpers/globals.dart'; + +class DrawerItemData { + final IconData icon; + final String label; + final int tabIndex; + + const DrawerItemData({ + required this.icon, + required this.label, + required this.tabIndex, + }); +} + +class AppDrawer extends StatelessWidget { + final ValueChanged onItemSelected; + final Globals globals = Globals(); + + AppDrawer({ + super.key, + required this.onItemSelected, + }); + + final List _allDrawerItems = const [ + DrawerItemData(icon: Icons.home, label: 'Home', tabIndex: 0), + DrawerItemData(icon: Icons.article, label: 'News', tabIndex: 1), + DrawerItemData( + icon: Icons.account_balance_wallet, label: 'Wallet', tabIndex: 2), + DrawerItemData(icon: Icons.storage, label: 'Farming', tabIndex: 3), + DrawerItemData(icon: Icons.how_to_vote_outlined, label: 'Dao', tabIndex: 4), + DrawerItemData(icon: Icons.person, label: 'Identity', tabIndex: 5), + DrawerItemData(icon: Icons.settings, label: 'Settings', tabIndex: 6), + DrawerItemData( + icon: Icons.how_to_vote_outlined, label: 'Council', tabIndex: 7), + DrawerItemData( + icon: Icons.notifications, label: 'Notifications', tabIndex: 8), + ]; + + @override + Widget build(BuildContext context) { + final List visibleDrawerItems = [ + _allDrawerItems[0], + _allDrawerItems[1], + _allDrawerItems[2], + if (globals.canSeeFarmers) _allDrawerItems[3], + _allDrawerItems[4], + _allDrawerItems[5], + _allDrawerItems[6], + if (globals.council) _allDrawerItems[7], + _allDrawerItems[8], + ]; + + return Drawer( + elevation: 5, + width: MediaQuery.of(context).size.width * 2 / 3, + child: Column( + children: [ + SizedBox( + height: 110, + child: DrawerHeader( + decoration: BoxDecoration( + border: Border( + bottom: + BorderSide(color: Theme.of(context).colorScheme.primary), + ), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + 'assets/TF_log_horizontal.svg', + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurface, BlendMode.srcIn), + fit: BoxFit.contain, + ), + ), + ), + ), + ...visibleDrawerItems.map( + (item) => ListTile( + minLeadingWidth: 10, + leading: Padding( + padding: const EdgeInsets.only(left: 10), + child: Icon(item.icon, size: 18)), + title: Text(item.label), + onTap: () { + Navigator.pop(context); + onItemSelected(item.tabIndex); + }, + ), + ), + ], + ), + ); + } +} diff --git a/app/lib/widgets/home_card.dart b/app/lib/widgets/home_card.dart index a5fd2e711..71b612a39 100644 --- a/app/lib/widgets/home_card.dart +++ b/app/lib/widgets/home_card.dart @@ -35,7 +35,7 @@ class HomeCardWidget extends StatelessWidget { Container( padding: const EdgeInsets.all(10), height: size / 7, - width: fullWidth ? size * 2 / 2.5 + 2 * margin : size / 2.5, + width: fullWidth ? size * 0.8 : size / 2.5, child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, @@ -45,11 +45,16 @@ class HomeCardWidget extends StatelessWidget { color: Theme.of(context).colorScheme.onPrimaryContainer, ), const SizedBox(width: 7), - Text( - name, - style: Theme.of(context).textTheme.titleMedium!.copyWith( - color: Theme.of(context).colorScheme.onPrimaryContainer, - fontWeight: FontWeight.bold), + Flexible( + child: Text( + name, + style: Theme.of(context).textTheme.titleMedium!.copyWith( + color: + Theme.of(context).colorScheme.onPrimaryContainer, + fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), ) ], ), diff --git a/app/lib/widgets/keep_alive.dart b/app/lib/widgets/keep_alive.dart new file mode 100644 index 000000000..09e82843a --- /dev/null +++ b/app/lib/widgets/keep_alive.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class KeepAlivePage extends StatefulWidget { + final Widget child; + + const KeepAlivePage({super.key, required this.child}); + + @override + _KeepAlivePageState createState() => _KeepAlivePageState(); +} + +class _KeepAlivePageState extends State with AutomaticKeepAliveClientMixin { + @override + Widget build(BuildContext context) { + super.build(context); + return widget.child; + } + + @override + bool get wantKeepAlive => true; +} diff --git a/git_hooks/pre-commit b/git_hooks/pre-commit index 2bcaba443..c6c691711 100755 --- a/git_hooks/pre-commit +++ b/git_hooks/pre-commit @@ -13,17 +13,18 @@ fi STAGED_DART_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.dart$') if [ -n "$STAGED_DART_FILES" ]; then + echo "Formatting staged Dart files..." + echo "$STAGED_DART_FILES" | xargs -n1 dart format + echo "Analyzing staged Dart files..." dart analyze --fatal-infos $STAGED_DART_FILES ANALYZE_EXIT_CODE=$? - if [ $ANALYZE_EXIT_CODE -eq 0 ]; then - exit 0 + if [ $ANALYZE_EXIT_CODE -ne 0 ]; then + echo "Applying fixes..." + echo "$STAGED_DART_FILES" | xargs -n1 dart fix --apply fi - echo "Applying fixes..." - echo "$STAGED_DART_FILES" | xargs -n1 dart fix --apply - echo "Re-adding fixed Dart files to the commit..." echo "$STAGED_DART_FILES" | xargs git add fi