From 05bc52a9e2fa496b1d226ef1a1a625fb8074528a Mon Sep 17 00:00:00 2001 From: Daniel Poss Date: Sat, 22 Mar 2025 20:24:56 -0500 Subject: [PATCH 1/4] Add error handling and redirect to Whoops page for unrecoverable errors --- lib/main.dart | 21 ++++++-- lib/pages/errorPages/whoops_page.dart | 18 +++++++ lib/routes.dart | 1 + lib/services/clashbot_service_impl.dart | 51 +++++++++++++++---- lib/stores/v2-stores/error_handler.store.dart | 13 +++++ 5 files changed, 92 insertions(+), 12 deletions(-) create mode 100644 lib/pages/errorPages/whoops_page.dart diff --git a/lib/main.dart b/lib/main.dart index d43c203..85afb2f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:clashbot_flutter/core/config/env.dart'; import 'package:clashbot_flutter/globals/color_schemes.dart'; import 'package:clashbot_flutter/models/clash_team.dart'; import 'package:clashbot_flutter/models/model_first_time.dart'; +import 'package:clashbot_flutter/pages/errorPages/whoops_page.dart'; import 'package:clashbot_flutter/pages/home/page/home_v2.dart'; import 'package:clashbot_flutter/pages/home/page/widgets/team_card.dart'; import 'package:clashbot_flutter/pages/intro/welcome_page.dart'; @@ -72,18 +73,32 @@ GoRouter router = GoRouter( path: HOME_ROUTE, builder: (BuildContext context, GoRouterState state) { return HomeV2(); + }, + redirect: (context, state) { + ErrorHandlerStore errorHandlerStore = + context.read(); + if (errorHandlerStore.irreconcilable) { + return WHOOPS_ROUTE; + } + return null; + }), + GoRoute( + name: 'whoops', + path: WHOOPS_ROUTE, + builder: (BuildContext context, GoRouterState state) { + return const WhoopsPage(); }), ]), ], - errorBuilder: (context, state) => Scaffold( + errorBuilder: (context, state) => const Scaffold( body: Center( child: Column( - children: const [ + children: [ Text('Page Not Found', style: headerStyle), ], ), )), - debugLogDiagnostics: true); + debugLogDiagnostics: false); void main() async { runApp(MyApp()); diff --git a/lib/pages/errorPages/whoops_page.dart b/lib/pages/errorPages/whoops_page.dart new file mode 100644 index 0000000..9484854 --- /dev/null +++ b/lib/pages/errorPages/whoops_page.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class WhoopsPage extends StatelessWidget { + const WhoopsPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Whoops!'), + ), + body: const Center( + child: Text( + 'Something went wrong and we were not able to load what we needed. Our bot is working tirelessly to resolve the issue! Please check back at a later time.', + )), + ); + } +} diff --git a/lib/routes.dart b/lib/routes.dart index 8bc5a35..72afc45 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -2,3 +2,4 @@ const String GETTING_STARTED_ROUTE = '/getting-started'; const String TEAMS_DASHBOARD_ROUTE = '/teams'; const String SETTINGS_ROUTE = '/settings'; const String HOME_ROUTE = '/home'; +const String WHOOPS_ROUTE = '/whoops'; diff --git a/lib/services/clashbot_service_impl.dart b/lib/services/clashbot_service_impl.dart index b06184a..f47c179 100644 --- a/lib/services/clashbot_service_impl.dart +++ b/lib/services/clashbot_service_impl.dart @@ -5,6 +5,7 @@ import 'package:clashbot_flutter/models/clash_tournament.dart'; import 'package:clashbot_flutter/models/clashbot_user.dart'; import 'package:clashbot_flutter/models/tentative_queue.dart'; import 'package:clashbot_flutter/services/clashbot_service.dart'; +import 'package:clashbot_flutter/stores/v2-stores/error_handler.store.dart'; class ClashBotServiceImpl implements ClashBotService { UserApi userApi; @@ -13,9 +14,16 @@ class ClashBotServiceImpl implements ClashBotService { SubscriptionApi subscriptionApi; TentativeApi tentativeApi; TournamentApi tournamentApi; + ErrorHandlerStore errorHandlerStore; - ClashBotServiceImpl(this.userApi, this.teamApi, this.championsApi, - this.subscriptionApi, this.tentativeApi, this.tournamentApi); + ClashBotServiceImpl( + this.userApi, + this.teamApi, + this.championsApi, + this.subscriptionApi, + this.tentativeApi, + this.tournamentApi, + this.errorHandlerStore); @override Future createPlayer( @@ -24,14 +32,23 @@ class ClashBotServiceImpl implements ClashBotService { CreateUserRequest(discordId: id, name: name, serverId: defaultServerId); return userApi .createUser(id, createUserRequest: createUserRequest) - .then(playerToClashBotUser); + .then(playerToClashBotUser) + .catchError((error) { + errorHandlerStore.setErrorMessage("Whoops! Failed to create player."); + return error; + }); } @override Future> getChampions(String id) { return championsApi .retrieveUsersPreferredChampions(id, id) - .then(fromChampionsToStringList); + .then(fromChampionsToStringList) + .catchError((error) { + errorHandlerStore + .setErrorMessage("Whoops! Failed to retrieve user's champions."); + return error; + }); } @override @@ -40,7 +57,7 @@ class ClashBotServiceImpl implements ClashBotService { .getUser(id, discordId: id) .then(playerToClashBotUser) .catchError((error) { - print('Error getting player: $error'); + errorHandlerStore.setErrorMessage("Whoops! Failed to retrieve user."); throw error; }); } @@ -56,7 +73,8 @@ class ClashBotServiceImpl implements ClashBotService { } return subscriptions; }).catchError((error) { - print('Error getting player subscriptions: $error'); + errorHandlerStore + .setErrorMessage("Whoops! Failed to retrieve user's subscriptions."); throw error; }); ; @@ -71,14 +89,24 @@ class ClashBotServiceImpl implements ClashBotService { return championsApi .createListOfPreferredChampionsForUser(id, id, champions: Champions(champions: championsList)) - .then(fromChampionsToStringList); + .then(fromChampionsToStringList) + .catchError((error) { + errorHandlerStore + .setErrorMessage("Whoops! Failed to overwrite user's champions."); + return error; + }); } @override Future> removeChampion(String id, String champion) { return championsApi .removePreferredChampionForUser(id, id, List.of([champion])) - .then(fromChampionsToStringList); + .then(fromChampionsToStringList) + .catchError((error) { + errorHandlerStore + .setErrorMessage("Whoops! Failed to remove user's champion."); + return error; + }); } @override @@ -88,7 +116,12 @@ class ClashBotServiceImpl implements ClashBotService { return championsApi .addToPreferredChampionsForUser(id, id, champions: Champions(champions: championsList)) - .then(fromChampionsToStringList); + .then(fromChampionsToStringList) + .catchError((error) { + errorHandlerStore + .setErrorMessage("Whoops! Failed to update user's champions."); + return error; + }); } @override diff --git a/lib/stores/v2-stores/error_handler.store.dart b/lib/stores/v2-stores/error_handler.store.dart index 9d5caaa..c800414 100644 --- a/lib/stores/v2-stores/error_handler.store.dart +++ b/lib/stores/v2-stores/error_handler.store.dart @@ -8,6 +8,19 @@ abstract class _ErrorHandlerStore with Store { @observable String errorMessage = ''; + @observable + bool irreconcilable = false; + + @action + void setIrreconcilable() { + irreconcilable = true; + } + + @action + void clearIrreconcilable() { + irreconcilable = false; + } + @action void setErrorMessage(String message) { errorMessage = message; From c79aae67a755adb86451932b1b2518dc73981c0e Mon Sep 17 00:00:00 2001 From: Daniel Poss Date: Sun, 23 Mar 2025 01:02:25 -0500 Subject: [PATCH 2/4] Refactor Discord OAuth client ID handling and enhance Whoops page layout --- lib/core/config/env.dart | 2 + lib/globals/credentials.dart | 4 +- lib/globals/global_settings.dart | 3 +- lib/main.dart | 26 +++++----- lib/pages/errorPages/whoops_page.dart | 25 ++++++++-- lib/services/clashbot_service_impl.dart | 65 +++++++++++++++++++++---- lib/storybook_main.dart | 23 +++++++-- 7 files changed, 116 insertions(+), 32 deletions(-) diff --git a/lib/core/config/env.dart b/lib/core/config/env.dart index d84fb23..d91794e 100644 --- a/lib/core/config/env.dart +++ b/lib/core/config/env.dart @@ -11,4 +11,6 @@ abstract class Env { static String clashbotServiceUrl = _Env.clashbotServiceUrl; @EnviedField(varName: 'MOCK_DISCORD_SERVICE') static String mockDiscordService = _Env.mockDiscordService; + @EnviedField(varName: 'DISCORD_CLIENT_ID') + static String discordClientId = _Env.discordClientId; } diff --git a/lib/globals/credentials.dart b/lib/globals/credentials.dart index 9b164b9..16cab5e 100644 --- a/lib/globals/credentials.dart +++ b/lib/globals/credentials.dart @@ -1,7 +1,9 @@ import 'dart:io'; +import 'package:clashbot_flutter/core/config/env.dart'; + abstract class Credentials { - static const APP_DISCORD_OAUTH_CLIENT_ID = "837629412328734740"; + // "837629412328734740"; // static const APP_DISCORD_OAUTH_CLIENT_ID = "839586949748228156"; static const APP_DISCORD_OAUTH_REDIRECT_URI = "http://localhost:4200/login"; static const SCOPE = ['identify', 'guilds']; diff --git a/lib/globals/global_settings.dart b/lib/globals/global_settings.dart index 6d622eb..e81c1fd 100644 --- a/lib/globals/global_settings.dart +++ b/lib/globals/global_settings.dart @@ -1,4 +1,5 @@ import 'package:clashbot_flutter/clients/discord_client.dart'; +import 'package:clashbot_flutter/core/config/env.dart'; import 'package:clashbot_flutter/globals/credentials.dart'; import 'package:oauth2_client/oauth2_helper.dart'; @@ -39,7 +40,7 @@ OAuth2Helper setupOauth2Helper() { var oAuth2Helper = OAuth2Helper( client, grantType: OAuth2Helper.authorizationCode, - clientId: Credentials.APP_DISCORD_OAUTH_CLIENT_ID, + clientId: Env.discordClientId, scopes: Credentials.SCOPE, ); return oAuth2Helper; diff --git a/lib/main.dart b/lib/main.dart index 85afb2f..970c156 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,20 +1,15 @@ -import 'dart:developer' as developer; - import 'package:clash_bot_api/api.dart'; import 'package:clashbot_flutter/core/config/env.dart'; import 'package:clashbot_flutter/globals/color_schemes.dart'; -import 'package:clashbot_flutter/models/clash_team.dart'; import 'package:clashbot_flutter/models/model_first_time.dart'; import 'package:clashbot_flutter/pages/errorPages/whoops_page.dart'; import 'package:clashbot_flutter/pages/home/page/home_v2.dart'; -import 'package:clashbot_flutter/pages/home/page/widgets/team_card.dart'; import 'package:clashbot_flutter/pages/intro/welcome_page.dart'; import 'package:clashbot_flutter/routes.dart'; import 'package:clashbot_flutter/services/clashbot_service.dart'; import 'package:clashbot_flutter/services/clashbot_service_impl.dart'; import 'package:clashbot_flutter/services/discord_service.dart'; import 'package:clashbot_flutter/services/discord_service_impl.dart'; -import 'package:clashbot_flutter/services/discord_service_mock_impl.dart'; import 'package:clashbot_flutter/services/riot_resources_service.dart'; import 'package:clashbot_flutter/services/riot_resources_service_impl.dart'; import 'package:clashbot_flutter/stores/application_details.store.dart'; @@ -31,8 +26,8 @@ import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:mobx/mobx.dart'; import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'package:validators/validators.dart'; -import 'package:storybook_flutter/storybook_flutter.dart'; import 'generated/git_info.dart'; import 'globals/global_settings.dart'; @@ -133,23 +128,24 @@ class _MyAppState extends State { providers: [ ChangeNotifierProvider(create: (context) => widget.modelFirstTime), ChangeNotifierProvider(create: (context) => widget.modelTheme), + Provider(create: (_) => ErrorHandlerStore()), Provider( create: (_) => DiscordServiceImpl(setupOauth2Helper())), Provider( create: (_) => ApiClient(basePath: Env.clashbotServiceUrl)), - ProxyProvider( - update: (_, apiClient, __) => ClashBotServiceImpl( + ProxyProvider2( + update: (_, apiClient, errorHandlerStore, __) => ClashBotServiceImpl( UserApi(apiClient), TeamApi(apiClient), ChampionsApi(apiClient), SubscriptionApi(apiClient), TentativeApi(apiClient), - TournamentApi(apiClient))), + TournamentApi(apiClient), + errorHandlerStore)), Provider( create: (_) => RiotResourceServiceImpl()), Provider( create: (_) => ClashBotEventsService()), - Provider(create: (_) => ErrorHandlerStore()), ProxyProvider2( update: (_, clashBotService, errorHandlerStore, __) => ClashStore(clashBotService, errorHandlerStore)), @@ -324,11 +320,15 @@ class MainContainer extends StatelessWidget { mainAxisSize: MainAxisSize.min, // spacing: 1.0, children: [ - IconButton( + IconButton( icon: const Icon(Icons.code), tooltip: 'GitHub', - onPressed: () { - // Add your GitHub link or functionality here + onPressed: () async { + const url = 'https://github.com/Poss111/ClashBotFlutter'; + if (await canLaunchUrl(Uri(path: url))) { + await launchUrl(Uri(path: url), mode: LaunchMode.inAppBrowserView, + webOnlyWindowName: '_blank'); + } }, ), const Text( diff --git a/lib/pages/errorPages/whoops_page.dart b/lib/pages/errorPages/whoops_page.dart index 9484854..2391456 100644 --- a/lib/pages/errorPages/whoops_page.dart +++ b/lib/pages/errorPages/whoops_page.dart @@ -9,10 +9,27 @@ class WhoopsPage extends StatelessWidget { appBar: AppBar( title: const Text('Whoops!'), ), - body: const Center( - child: Text( - 'Something went wrong and we were not able to load what we needed. Our bot is working tirelessly to resolve the issue! Please check back at a later time.', - )), + body: Container( + child: Column( + children: [ + Flex( + direction: Axis.vertical, + children: [ + Center( + child: Text( + 'Something went wrong and we were not able to load what we needed. Our bot is working tirelessly to resolve the issue!' + ), + ), + Center( + child: Text( + ' Please check back at a later time.', + ), + ), + ], + ), + ], + ), + ), ); } } diff --git a/lib/services/clashbot_service_impl.dart b/lib/services/clashbot_service_impl.dart index f47c179..791437e 100644 --- a/lib/services/clashbot_service_impl.dart +++ b/lib/services/clashbot_service_impl.dart @@ -133,7 +133,12 @@ class ClashBotServiceImpl implements ClashBotService { } return userApi .createUsersSelectedServers(id, id, servers: Servers(servers: servers)) - .then(serversToServerIdList); + .then(serversToServerIdList) + .catchError((error) { + errorHandlerStore + .setErrorMessage("Whoops! Failed to create user's selected servers."); + return error; + }); } @override @@ -141,7 +146,12 @@ class ClashBotServiceImpl implements ClashBotService { String id, List selectedServers) { return userApi .removeUsersSelectedServers(id, id, selectedServers) - .then(serversToServerIdList); + .then(serversToServerIdList) + .catchError((error) { + errorHandlerStore + .setErrorMessage("Whoops! Failed to remove user's selected servers."); + return error; + }); } @override @@ -153,21 +163,34 @@ class ClashBotServiceImpl implements ClashBotService { } return userApi .addUsersSelectedServers(id, id, servers: Servers(servers: servers)) - .then(serversToServerIdList); + .then(serversToServerIdList) + .catchError((error) { + errorHandlerStore + .setErrorMessage("Whoops! Failed to update user's selected servers."); + return error; + }); } @override Future addToTeam(String id, Role role, String teamId) { return teamApi .assignUserToTeam(id, teamId, id, PositionDetails(role: role)) - .then((team) => teamToClashTeam(team ?? Team())); + .then((team) => teamToClashTeam(team ?? Team())) + .catchError((error) { + errorHandlerStore.setErrorMessage("Whoops! Failed to add user to team."); + return error; + }); } @override Future addToTentativeQueue(String id, String tentativeId) { return tentativeApi .assignUserToATentativeQueue(id, tentativeId, id) - .then(tentativeToTentativeQueue); + .then(tentativeToTentativeQueue) + .catchError((error) { + errorHandlerStore.setErrorMessage("Whoops! Failed to add user to tentative queue."); + return error; + }); } @override @@ -187,6 +210,10 @@ class ClashBotServiceImpl implements ClashBotService { } } return teams; + }) + .catchError((error) { + errorHandlerStore.setErrorMessage("Whoops! Failed to retrieve teams."); + return error; }); } @@ -209,6 +236,10 @@ class ClashBotServiceImpl implements ClashBotService { } } return tentativeQueues; + }) + .catchError((error) { + errorHandlerStore.setErrorMessage("Whoops! Failed to retrieve tentative queues."); + return error; }); } @@ -216,7 +247,11 @@ class ClashBotServiceImpl implements ClashBotService { Future removeFromTeam(String id, String teamId) { return teamApi .removeUserFromTeam(id, teamId, id) - .then((team) => teamToClashTeam(team ?? Team())); + .then((team) => teamToClashTeam(team ?? Team())) + .catchError((error) { + errorHandlerStore.setErrorMessage("Whoops! Failed to remove user from team."); + return error; + }); } @override @@ -224,7 +259,11 @@ class ClashBotServiceImpl implements ClashBotService { String id, String tentativeId) { return tentativeApi .removeUserFromTentativeQueue(id, tentativeId, id) - .then(tentativeToTentativeQueue); + .then(tentativeToTentativeQueue) + .catchError((error) { + errorHandlerStore.setErrorMessage("Whoops! Failed to remove user from tentative queue."); + return error; + }); } @override @@ -259,7 +298,11 @@ class ClashBotServiceImpl implements ClashBotService { tournamentName: tournamentName, tournamentDay: tournamentDay)); return teamApi .createTeam(discordId, teamRequired) - .then((team) => teamToClashTeam(team ?? Team())); + .then((team) => teamToClashTeam(team ?? Team())) + .catchError((error) { + errorHandlerStore.setErrorMessage("Whoops! Failed to create team."); + return error; + }); } @override @@ -274,7 +317,11 @@ class ClashBotServiceImpl implements ClashBotService { tournamentName: tournamentName, tournamentDay: tournamentDay), tentativePlayers: [TentativePlayer(discordId: discordId)])) - .then(tentativeToTentativeQueue); + .then(tentativeToTentativeQueue) + .catchError((error) { + errorHandlerStore.setErrorMessage("Whoops! Failed to create tentative queue."); + return error; + }); } @override diff --git a/lib/storybook_main.dart b/lib/storybook_main.dart index a18fdbb..5ee9ca8 100644 --- a/lib/storybook_main.dart +++ b/lib/storybook_main.dart @@ -5,6 +5,7 @@ import 'package:clashbot_flutter/models/clash_tournament.dart'; import 'package:clashbot_flutter/models/clashbot_user.dart'; import 'package:clashbot_flutter/models/discord_guild.dart'; import 'package:clashbot_flutter/models/discord_user.dart'; +import 'package:clashbot_flutter/pages/errorPages/whoops_page.dart'; import 'package:clashbot_flutter/pages/home/page/widgets/calendar_widget.dart'; import 'package:clashbot_flutter/pages/home/page/widgets/team_card.dart'; import 'package:clashbot_flutter/services/clashbot_service_impl.dart'; @@ -132,7 +133,8 @@ void main() { ChampionsApi(apiClient), SubscriptionApi(apiClient), TentativeApi(apiClient), - TournamentApi(apiClient)), + TournamentApi(apiClient), + errorHandlerStore), errorHandlerStore)), ProxyProvider4( @@ -149,7 +151,8 @@ class ClashBotStorybookApp extends StatelessWidget { return Storybook(initialStory: "4Filled", stories: [ StoryCalendarWidgetWTournaments(context), StoryCalendarWidgetWTournamentsLoading(context), - StoryTeamCard() + StoryTeamCard(), + WhoopsPageStory() ]); } @@ -230,6 +233,16 @@ class ClashBotStorybookApp extends StatelessWidget { }); } + Story WhoopsPageStory() { + return Story( + name: "WhoopsPage", + description: "A page for that the app is not usable.", + builder: (context) { + return const WhoopsPage(); + }, + ); + } + Story StoryCalendarWidgetWTournamentsLoading(BuildContext context) { DiscordDetailsStore discordDetailsStore = context.read(); @@ -269,7 +282,8 @@ class ClashBotStorybookApp extends StatelessWidget { new ChampionsApi(context.read()), new SubscriptionApi(context.read()), new TentativeApi(context.read()), - new TournamentApi(context.read())), + new TournamentApi(context.read()), + new ErrorHandlerStore()), context.read()); clashStoreW5Tournies.addCallInProgress('getTournaments'); return Story( @@ -324,7 +338,8 @@ Story StoryCalendarWidgetWTournaments(BuildContext context) { new ChampionsApi(context.read()), new SubscriptionApi(context.read()), new TentativeApi(context.read()), - new TournamentApi(context.read())), + new TournamentApi(context.read()), + new ErrorHandlerStore()), context.read()); return Story( name: "Widgets/Calendar/filled", From 8126621bc72172808317077848730b56fda3df55 Mon Sep 17 00:00:00 2001 From: Daniel Poss Date: Sun, 23 Mar 2025 20:55:17 -0500 Subject: [PATCH 3/4] Update environment configuration for ClashBot and enhance error handling in application --- .env | 5 +- lib/core/config/env.dart | 2 +- lib/globals/credentials.dart | 4 - lib/main.dart | 37 +++---- lib/pages/errorPages/whoops_page.dart | 34 +++---- lib/pages/home/page/home_v2.dart | 112 ++++++++++++---------- lib/stores/application_details.store.dart | 16 +++- lib/stores/discord_details.store.dart | 52 ++++++++-- lib/stores/v2-stores/clash.store.dart | 34 +++++-- 9 files changed, 178 insertions(+), 118 deletions(-) diff --git a/.env b/.env index e8d30e2..3a57798 100644 --- a/.env +++ b/.env @@ -1,2 +1,3 @@ -CLASHBOT_SERVICE_URL=CLASHBOT_SERVICE_URL -MOCK_DISCORD_SERVICE=MOCK_DISCORD_SERVICE \ No newline at end of file +CLASHBOT_SERVICE_URL=http://localhost:8080 +MOCK_DISCORD_SERVICE=false +discordClientId=837629412328734740 \ No newline at end of file diff --git a/lib/core/config/env.dart b/lib/core/config/env.dart index d91794e..39d3d14 100644 --- a/lib/core/config/env.dart +++ b/lib/core/config/env.dart @@ -11,6 +11,6 @@ abstract class Env { static String clashbotServiceUrl = _Env.clashbotServiceUrl; @EnviedField(varName: 'MOCK_DISCORD_SERVICE') static String mockDiscordService = _Env.mockDiscordService; - @EnviedField(varName: 'DISCORD_CLIENT_ID') + @EnviedField() static String discordClientId = _Env.discordClientId; } diff --git a/lib/globals/credentials.dart b/lib/globals/credentials.dart index 16cab5e..dca946a 100644 --- a/lib/globals/credentials.dart +++ b/lib/globals/credentials.dart @@ -1,10 +1,6 @@ import 'dart:io'; -import 'package:clashbot_flutter/core/config/env.dart'; - abstract class Credentials { - // "837629412328734740"; - // static const APP_DISCORD_OAUTH_CLIENT_ID = "839586949748228156"; static const APP_DISCORD_OAUTH_REDIRECT_URI = "http://localhost:4200/login"; static const SCOPE = ['identify', 'guilds']; static final appUserCredentialsFile = new File("~/.myapp/credentials.json"); diff --git a/lib/main.dart b/lib/main.dart index 970c156..d70009b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -68,14 +68,6 @@ GoRouter router = GoRouter( path: HOME_ROUTE, builder: (BuildContext context, GoRouterState state) { return HomeV2(); - }, - redirect: (context, state) { - ErrorHandlerStore errorHandlerStore = - context.read(); - if (errorHandlerStore.irreconcilable) { - return WHOOPS_ROUTE; - } - return null; }), GoRoute( name: 'whoops', @@ -134,14 +126,15 @@ class _MyAppState extends State { Provider( create: (_) => ApiClient(basePath: Env.clashbotServiceUrl)), ProxyProvider2( - update: (_, apiClient, errorHandlerStore, __) => ClashBotServiceImpl( - UserApi(apiClient), - TeamApi(apiClient), - ChampionsApi(apiClient), - SubscriptionApi(apiClient), - TentativeApi(apiClient), - TournamentApi(apiClient), - errorHandlerStore)), + update: (_, apiClient, errorHandlerStore, __) => + ClashBotServiceImpl( + UserApi(apiClient), + TeamApi(apiClient), + ChampionsApi(apiClient), + SubscriptionApi(apiClient), + TentativeApi(apiClient), + TournamentApi(apiClient), + errorHandlerStore)), Provider( create: (_) => RiotResourceServiceImpl()), Provider( @@ -186,11 +179,6 @@ class Main extends StatelessWidget { brightness: Brightness.dark, colorScheme: darkColorScheme, useMaterial3: true, - textTheme: const TextTheme( - // headlineLarge: TextStyle(fontSize: 72.0, fontWeight: FontWeight.bold), - // titleLarge: TextStyle(fontSize: 36.0, fontStyle: FontStyle.italic), - // bodyMedium: TextStyle(fontSize: 14.0, fontFamily: 'Hind'), - ), snackBarTheme: SnackBarThemeData( behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( @@ -320,14 +308,15 @@ class MainContainer extends StatelessWidget { mainAxisSize: MainAxisSize.min, // spacing: 1.0, children: [ - IconButton( + IconButton( icon: const Icon(Icons.code), tooltip: 'GitHub', onPressed: () async { const url = 'https://github.com/Poss111/ClashBotFlutter'; if (await canLaunchUrl(Uri(path: url))) { - await launchUrl(Uri(path: url), mode: LaunchMode.inAppBrowserView, - webOnlyWindowName: '_blank'); + await launchUrl(Uri(path: url), + mode: LaunchMode.inAppBrowserView, + webOnlyWindowName: '_blank'); } }, ), diff --git a/lib/pages/errorPages/whoops_page.dart b/lib/pages/errorPages/whoops_page.dart index 2391456..0ccf679 100644 --- a/lib/pages/errorPages/whoops_page.dart +++ b/lib/pages/errorPages/whoops_page.dart @@ -9,26 +9,22 @@ class WhoopsPage extends StatelessWidget { appBar: AppBar( title: const Text('Whoops!'), ), - body: Container( - child: Column( - children: [ - Flex( - direction: Axis.vertical, - children: [ - Center( - child: Text( - 'Something went wrong and we were not able to load what we needed. Our bot is working tirelessly to resolve the issue!' - ), - ), - Center( - child: Text( - ' Please check back at a later time.', - ), - ), - ], + body: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 20, + children: [ + Center( + child: Text( + 'Something went wrong and we were not able to load what we needed. Our bot is working tirelessly to resolve the issue!', + style: Theme.of(context).textTheme.headlineSmall, ), - ], - ), + ), + Text( + 'Please check back at a later time.', + style: Theme.of(context).textTheme.headlineSmall, + ), + ], ), ); } diff --git a/lib/pages/home/page/home_v2.dart b/lib/pages/home/page/home_v2.dart index 4dfca58..c29c596 100644 --- a/lib/pages/home/page/home_v2.dart +++ b/lib/pages/home/page/home_v2.dart @@ -3,11 +3,13 @@ import 'dart:developer' as developer; import 'package:clash_bot_api/api.dart'; import 'package:clashbot_flutter/models/clash_team.dart'; import 'package:clashbot_flutter/models/discord_guild.dart'; +import 'package:clashbot_flutter/pages/errorPages/whoops_page.dart'; import 'package:clashbot_flutter/pages/home/page/widgets/calendar_widget.dart'; import 'package:clashbot_flutter/pages/home/page/widgets/events_widget.dart'; import 'package:clashbot_flutter/pages/home/page/widgets/server_chip_list.dart'; import 'package:clashbot_flutter/stores/discord_details.store.dart'; import 'package:clashbot_flutter/stores/v2-stores/clash.store.dart'; +import 'package:clashbot_flutter/stores/v2-stores/error_handler.store.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_svg/svg.dart'; @@ -92,60 +94,66 @@ class _HomeV2State extends State { ClashStore clashStore = context.read(); DiscordDetailsStore discordDetailsStore = context.read(); + ErrorHandlerStore errorHandlerStore = context.read(); return Scaffold( - body: LayoutBuilder( - builder: (context, constraints) { - if (constraints.maxWidth > 500) { - return Row( - children: [ - Flexible( - flex: 1, - child: Flex(direction: Axis.vertical, children: [ - const ServerChipList(), - CalendarWidget( - focusedDay: _focusedDay, - selectedDay: _selectedDay, - clashStore: clashStore, - discordDetailsStore: discordDetailsStore), - Expanded( - child: Card.filled( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.blueGrey - : Colors.blueAccent, - margin: const EdgeInsets.all(16.0), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: SvgPicture.asset( - 'svgs/ClashBot-HomePage.svg', - semanticsLabel: 'Clash Bot Home Page', - width: 100, - height: 600, - ), + body: Observer( + builder: (_) => errorHandlerStore.irreconcilable + ? const WhoopsPage() + : LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth > 500) { + return Row( + children: [ + Flexible( + flex: 1, + child: Flex(direction: Axis.vertical, children: [ + const ServerChipList(), + CalendarWidget( + focusedDay: _focusedDay, + selectedDay: _selectedDay, + clashStore: clashStore, + discordDetailsStore: discordDetailsStore), + Expanded( + child: Card.filled( + color: Theme.of(context).brightness == + Brightness.dark + ? Colors.blueGrey + : Colors.blueAccent, + margin: const EdgeInsets.all(16.0), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: SvgPicture.asset( + 'svgs/ClashBot-HomePage.svg', + semanticsLabel: 'Clash Bot Home Page', + width: 100, + height: 600, + ), + ), + ), + ), + ]), ), - ), - ), - ]), - ), - const Flexible( - flex: 2, - child: EventsListWidget(), - ), - ], - ); - } else { - return Column( - children: [ - ServerChipList(), - CalendarWidget( - focusedDay: _focusedDay, - selectedDay: _selectedDay, - clashStore: clashStore, - discordDetailsStore: discordDetailsStore), - EventsListWidget(), - ], - ); - } - }, + const Flexible( + flex: 2, + child: EventsListWidget(), + ), + ], + ); + } else { + return Column( + children: [ + ServerChipList(), + CalendarWidget( + focusedDay: _focusedDay, + selectedDay: _selectedDay, + clashStore: clashStore, + discordDetailsStore: discordDetailsStore), + EventsListWidget(), + ], + ); + } + }, + ), ), floatingActionButton: Observer( builder: (_) => clashStore.canCreateTeam diff --git a/lib/stores/application_details.store.dart b/lib/stores/application_details.store.dart index e41e3ab..39c4aea 100644 --- a/lib/stores/application_details.store.dart +++ b/lib/stores/application_details.store.dart @@ -10,6 +10,8 @@ import 'package:collection/collection.dart'; import 'package:mobx/mobx.dart'; import 'dart:developer' as developer; +import 'package:url_launcher/url_launcher.dart'; + part 'application_details.store.g.dart'; class ApplicationDetailsStore = _ApplicationDetailsStore @@ -23,9 +25,7 @@ abstract class _ApplicationDetailsStore with Store { _ApplicationDetailsStore(this._clashStore, this._discordDetailsStore, this._riotChampionStore, this._errorHandlerStore) { reaction((_) => _discordDetailsStore.discordUser, (_) { - developer.log("reaction: _discordDetailsStore.discordUser"); if (_discordDetailsStore.discordUser.id != '0') { - developer.log("Refreshing clash bot user"); _clashStore.refreshClashBotUser(_discordDetailsStore.discordUser.id); _clashStore.refreshSelectedServers(); _discordDetailsStore.fetchUserGuilds(); @@ -40,6 +40,18 @@ abstract class _ApplicationDetailsStore with Store { _clashStore.clashBotUser.selectedServers); } }); + + reaction( + (_) => (_discordDetailsStore.failedToLoad, _clashStore.failedToLoad), + (failedToLoad) { + if (failedToLoad.$1 && failedToLoad.$2) { + _errorHandlerStore.setIrreconcilable(); + } else if (failedToLoad.$2) { + _errorHandlerStore.setIrreconcilable(); + } else { + _errorHandlerStore.clearIrreconcilable(); + } + }); } @observable diff --git a/lib/stores/discord_details.store.dart b/lib/stores/discord_details.store.dart index 434f75a..5a5d7da 100644 --- a/lib/stores/discord_details.store.dart +++ b/lib/stores/discord_details.store.dart @@ -5,6 +5,7 @@ import 'package:clashbot_flutter/models/discord_user.dart'; import 'package:clashbot_flutter/services/discord_service.dart'; import 'package:clashbot_flutter/stores/v2-stores/error_handler.store.dart'; import 'package:mobx/mobx.dart'; +import 'dart:developer' as developer; part 'discord_details.store.g.dart'; @@ -27,6 +28,9 @@ abstract class _DiscordDetailsStore with Store { @observable ObservableList callsInProgress = ObservableList(); + @observable + bool failedToLoad = false; + @computed bool get loadingData => callsInProgress.isNotEmpty; @@ -40,46 +44,82 @@ abstract class _DiscordDetailsStore with Store { Map get discordGuildMap => {for (var guild in discordGuilds) guild.id: guild}; + @computed + bool get irreconcilableError => failedToLoad && discordUser.id == '0'; + + @action + void loadingUserDetailsFailed() { + developer.log("loadingUserDetailsFailed"); + developer.log("failedToLoad: ${failedToLoad}"); + failedToLoad = true; + developer.log("failedToLoad: ${failedToLoad}"); + } + + @action + void userDetailsSuccessfullyLoaded() { + failedToLoad = false; + } + + @action + void addCallInProgress(String call) { + callsInProgress.add(call); + } + + @action + void removeCallInProgress(String call) { + callsInProgress.remove(call); + } + + @action + void clearCallsInProgress() { + callsInProgress.clear(); + } + @action Future fetchUserDetails(String discordId) async { - callsInProgress.add('fetchUserDetails'); + addCallInProgress('fetchUserDetails'); var foundUser; try { + userDetailsSuccessfullyLoaded(); foundUser = await _discordService.fetchUserDetails(discordId); discordIdToName.putIfAbsent(discordId, () => foundUser.username); } on Exception catch (error) { _errorHandlerStore.errorMessage = 'Failed to fetch Discord User details due to ${error.toString()}'; } - callsInProgress.remove('fetchUserDetails'); + removeCallInProgress('fetchUserDetails'); } @action Future fetchCurrentUserDetails() async { - callsInProgress.add('fetchCurrentUserDetails'); + addCallInProgress('fetchCurrentUserDetails'); final future = _discordService.fetchCurrentUserDetails(); try { + userDetailsSuccessfullyLoaded(); DiscordUser updatedUser = await future; discordUser = updatedUser.copy(); discordIdToName.putIfAbsent(updatedUser.id, () => updatedUser.username); } on Exception catch (error) { _errorHandlerStore.errorMessage = 'Failed to fetch Discord User details due to ${error.toString()}'; + loadingUserDetailsFailed(); } - callsInProgress.remove('fetchCurrentUserDetails'); + removeCallInProgress('fetchCurrentUserDetails'); } @action Future fetchUserGuilds() async { - callsInProgress.add('fetchUserGuilds'); + addCallInProgress('fetchUserGuilds'); final future = _discordService.fetchUserGuilds(); try { + userDetailsSuccessfullyLoaded(); List guilds = await future; discordGuilds.clear(); discordGuilds.addAll(guilds); } on Exception catch (error) { _errorHandlerStore.errorMessage = error.toString(); + loadingUserDetailsFailed(); } - callsInProgress.remove('fetchUserGuilds'); + removeCallInProgress('fetchUserGuilds'); } } diff --git a/lib/stores/v2-stores/clash.store.dart b/lib/stores/v2-stores/clash.store.dart index fd1b961..16fd807 100644 --- a/lib/stores/v2-stores/clash.store.dart +++ b/lib/stores/v2-stores/clash.store.dart @@ -56,6 +56,19 @@ abstract class _ClashStore with Store { callsInProgress.contains(_ClashStore.refreshClashTournamentsCall) || callsInProgress.contains(_ClashStore.refreshClashTeamsCall); + @observable + bool failedToLoad = false; + + @action + void loadingUserDetailsFailed() { + failedToLoad = true; + } + + @action + void userDetailsSuccessfullyLoaded() { + failedToLoad = false; + } + @action void addCallInProgress(String call) { callsInProgress.add(call); @@ -74,10 +87,15 @@ abstract class _ClashStore with Store { @action Future refreshClashBotUser(String id) async { refreshingUser = true; - callsInProgress.add(_ClashStore.refreshClashBotUserCall); - clashBotUser = await _clashService.getPlayer(id); - setSelectedServer(clashBotUser.selectedServers); - callsInProgress.remove(_ClashStore.refreshClashBotUserCall); + userDetailsSuccessfullyLoaded(); + addCallInProgress(_ClashStore.refreshClashBotUserCall); + try { + clashBotUser = await _clashService.getPlayer(id); + setSelectedServer(clashBotUser.selectedServers); + } catch (e) { + loadingUserDetailsFailed(); + } + removeCallInProgress(_ClashStore.refreshClashBotUserCall); refreshingUser = false; } @@ -186,20 +204,20 @@ abstract class _ClashStore with Store { @action Future refreshClashTournaments(String id) async { - callsInProgress.add(_ClashStore.refreshClashTournamentsCall); + addCallInProgress(_ClashStore.refreshClashTournamentsCall); tournaments = ObservableList.of(await _clashService.retrieveTournaments(id)); - callsInProgress.remove(_ClashStore.refreshClashTournamentsCall); + removeCallInProgress(_ClashStore.refreshClashTournamentsCall); } @action Future refreshClashTeams( String id, List preferredServers) async { - callsInProgress.add(_ClashStore.refreshClashTeamsCall); + addCallInProgress(_ClashStore.refreshClashTeamsCall); var futureClashTeams = await _clashService.getClashTeams(id, preferredServers); clashTeams = ObservableList.of(futureClashTeams); - callsInProgress.remove(_ClashStore.refreshClashTeamsCall); + removeCallInProgress(_ClashStore.refreshClashTeamsCall); } @action From 97fc3b1aef18b559f841c26e9c0c34360ca9b3ed Mon Sep 17 00:00:00 2001 From: Daniel Poss Date: Sun, 23 Mar 2025 22:03:23 -0500 Subject: [PATCH 4/4] Update Discord client ID in environment configuration and CI workflows --- .env | 2 +- .github/workflows/build-pr.yml | 27 +++++++++++++++++++++------ .github/workflows/main.yml | 3 ++- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/.env b/.env index 3a57798..55ed1c6 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ CLASHBOT_SERVICE_URL=http://localhost:8080 MOCK_DISCORD_SERVICE=false -discordClientId=837629412328734740 \ No newline at end of file +discordClientId=1234567890 \ No newline at end of file diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 9608da9..076a32f 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -31,11 +31,16 @@ jobs: - name: Download Dependencies run: flutter pub get - - name: Create environment file + - name: Create environment file env: CLASHBOT_SERVICE_URL: http://localhost:8080/api/v2 MOCK_DISCORD_SERVICE: false - run: ./ciScripts/build-env-file.sh CLASHBOT_SERVICE_URL ${CLASHBOT_SERVICE_URL} MOCK_DISCORD_SERVICE ${MOCK_DISCORD_SERVICE} + DISCORD_CLIENT_ID: 1234567 + run: | + ./ciScripts/build-env-file.sh \ + CLASHBOT_SERVICE_URL ${CLASHBOT_SERVICE_URL} \ + MOCK_DISCORD_SERVICE ${MOCK_DISCORD_SERVICE} \ + discordClientId ${DISCORD_CLIENT_ID} - name: Unit Tests run: flutter test "test/unit/" >> $GITHUB_STEP_SUMMARY @@ -59,11 +64,16 @@ jobs: - name: Download Dependencies run: flutter pub get - - name: Create environment file + - name: Create environment file env: CLASHBOT_SERVICE_URL: http://localhost:8080/api/v2 MOCK_DISCORD_SERVICE: false - run: ./ciScripts/build-env-file.sh CLASHBOT_SERVICE_URL ${CLASHBOT_SERVICE_URL} MOCK_DISCORD_SERVICE ${MOCK_DISCORD_SERVICE} + DISCORD_CLIENT_ID: 123456 + run: | + ./ciScripts/build-env-file.sh \ + CLASHBOT_SERVICE_URL ${CLASHBOT_SERVICE_URL} \ + MOCK_DISCORD_SERVICE ${MOCK_DISCORD_SERVICE} \ + discordClientId ${DISCORD_CLIENT_ID} - name: Generate Envied Files run: flutter pub run build_runner build --delete-conflicting-outputs @@ -100,11 +110,16 @@ jobs: - name: Check Flutter version run: flutter --version - - name: Create environment file + - name: Create environment file env: CLASHBOT_SERVICE_URL: http://localhost:8080/api/v2 MOCK_DISCORD_SERVICE: false - run: ./ciScripts/build-env-file.sh CLASHBOT_SERVICE_URL ${CLASHBOT_SERVICE_URL} MOCK_DISCORD_SERVICE ${MOCK_DISCORD_SERVICE} + DISCORD_CLIENT_ID: 1234567 + run: | + ./ciScripts/build-env-file.sh \ + CLASHBOT_SERVICE_URL ${CLASHBOT_SERVICE_URL} \ + MOCK_DISCORD_SERVICE ${MOCK_DISCORD_SERVICE} \ + discordClientId ${DISCORD_CLIENT_ID} - name: Generate Envied Files run: flutter pub run build_runner build --delete-conflicting-outputs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0e7743c..48eb185 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,7 +34,8 @@ jobs: env: CLASHBOT_SERVICE_URL: ${{ vars.CLASHBOT_SERVICE_URL }} MOCK_DISCORD_SERVICE: false - run: ./ciScripts/build-env-file.sh CLASHBOT_SERVICE_URL ${CLASHBOT_SERVICE_URL} MOCK_DISCORD_SERVICE ${MOCK_DISCORD_SERVICE} + DISCORD_CLIENT_ID: ${{ vars.DISCORD_CLIENT_ID }} + run: ./ciScripts/build-env-file.sh CLASHBOT_SERVICE_URL ${CLASHBOT_SERVICE_URL} MOCK_DISCORD_SERVICE ${MOCK_DISCORD_SERVICE} discordClientId ${DISCORD_CLIENT_ID} - name: Generate Envied Files run: |