From 2725edaf27a6040e0529db0bdc46e30f16f90f1f Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 11 Dec 2023 15:21:35 +0100 Subject: [PATCH 1/6] API: Adding Maintenance error guards for requests --- lib/api/client_api.dart | 4 +++- lib/api/rss.dart | 4 +++- lib/api/search.dart | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/api/client_api.dart b/lib/api/client_api.dart index 7bca4b8..c715166 100644 --- a/lib/api/client_api.dart +++ b/lib/api/client_api.dart @@ -40,7 +40,9 @@ class ApiClient { final path = Uri.https(ApiClient.baseUrl, 'Platform/Destiny2/Manifest'); final response = await client.get(path, headers: headers); - if (response.statusCode >= 400) { + if (response.statusCode == 503) { + throw const HttpException("Unable to load data from Bungie. Bungie.net servers are down for maintenance."); + } else if (response.statusCode >= 400) { throw HttpException(response.body); } diff --git a/lib/api/rss.dart b/lib/api/rss.dart index fc0f92e..d401ed0 100644 --- a/lib/api/rss.dart +++ b/lib/api/rss.dart @@ -15,7 +15,9 @@ class Rss { final path = Uri.https(ApiClient.baseUrl, "Platform/Content/Rss/NewsArticles/$pagination", {"includebody": "true", "lc": locale}); final response = await _client.client.get(path, headers: _client.headers); - if (response.statusCode >= 400) { + if (response.statusCode == 503) { + throw const HttpException("Unable to load data from Bungie. Bungie.net servers are down for maintenance."); + } else if (response.statusCode >= 400) { throw HttpException(response.body); } return List.from(jsonDecode(utf8.decode(response.bodyBytes))['Response']['NewsArticles'].map((account) => NewsArticle.fromJson(account))); diff --git a/lib/api/search.dart b/lib/api/search.dart index 83e7279..0ba0920 100644 --- a/lib/api/search.dart +++ b/lib/api/search.dart @@ -17,7 +17,9 @@ class Search { body: jsonEncode({ "displayNamePrefix": displayNamePrefix }) ); - if (response.statusCode >= 400) { + if (response.statusCode == 503) { + throw const HttpException("Unable to load data from Bungie. Bungie.net servers are down for maintenance."); + } else if (response.statusCode >= 400) { throw HttpException(response.body); } return List.from(jsonDecode(utf8.decode(response.bodyBytes))['Response']['searchResults'].map((account) => BungieAccountData.fromJson(account))); From 3f7ece8c03dad4886d7b0e6ea3da1650c47386a5 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 12 Dec 2023 09:29:35 +0100 Subject: [PATCH 2/6] API: Handling API errors with FutureBuilder catch --- lib/src/views/home_view.dart | 3 ++ lib/src/widgets/maintenance_error.dart | 39 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 lib/src/widgets/maintenance_error.dart diff --git a/lib/src/views/home_view.dart b/lib/src/views/home_view.dart index 60614d1..6bea2c9 100644 --- a/lib/src/views/home_view.dart +++ b/lib/src/views/home_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; +import 'package:guardian_dock/src/widgets/maintenance_error.dart'; import 'package:guardian_dock/src/widgets/persistent_search_bar.dart'; import 'package:guardian_dock/src/widgets/rss/rss_news_feed.dart'; @@ -46,6 +47,8 @@ class _HomeViewState extends State { ), ) ); + } else if (snapshot.hasError) { + return Scaffold(body: MaintenanceError(error: snapshot.error)); } return Scaffold( resizeToAvoidBottomInset: false, diff --git a/lib/src/widgets/maintenance_error.dart b/lib/src/widgets/maintenance_error.dart new file mode 100644 index 0000000..4f71fb0 --- /dev/null +++ b/lib/src/widgets/maintenance_error.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +class MaintenanceError extends StatelessWidget { + final Object? error; + + const MaintenanceError({required this.error, super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(40), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Icon( + Icons.error, + size: 75, + color: Theme.of(context).colorScheme.error + ), + SizedBox(height: MediaQuery.of(context).size.height * .02), + Flexible( + child: Text( + error.toString().replaceFirst(RegExp('HttpException: '), ''), + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.error, + ), + ), + ) + ] + ), + ), + ); + } +} From 09a938c83e49e92df97a6b2fc2fbeb70141d0b9d Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 12 Dec 2023 14:47:21 +0100 Subject: [PATCH 3/6] Tests: Add API Maintenance Error widget --- test/views/article_view_test.dart | 2 ++ test/views/home_view_test.dart | 34 +++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/test/views/article_view_test.dart b/test/views/article_view_test.dart index 68e7404..c03dcf5 100644 --- a/test/views/article_view_test.dart +++ b/test/views/article_view_test.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:network_image_mock/network_image_mock.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; @@ -25,6 +26,7 @@ void main() { setUpAll(() { final client = MockClient(); GetIt.I.registerSingleton(ApiClient(client: client)); + GetIt.I.registerSingleton(const FlutterSecureStorage()); when(client.get(Uri.https(ApiClient.baseUrl, "Platform/Destiny2/Manifest"), headers: anyNamed("headers"))).thenAnswer((_) async { return http.Response( diff --git a/test/views/home_view_test.dart b/test/views/home_view_test.dart index 216f817..45cd815 100644 --- a/test/views/home_view_test.dart +++ b/test/views/home_view_test.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:http/http.dart' as http; @@ -14,9 +15,11 @@ import 'home_view_test.mocks.dart'; @GenerateMocks([http.Client]) void main() { + final client = MockClient(); + setUpAll(() { - final client = MockClient(); GetIt.I.registerSingleton(ApiClient(client: client)); + GetIt.I.registerSingleton(const FlutterSecureStorage()); when(client.post(Uri.https(ApiClient.baseUrl, "Platform/User/Search/GlobalName/0/"), headers: anyNamed("headers"), body: anyNamed("body"))).thenAnswer((_) async { return http.Response( @@ -1256,7 +1259,7 @@ void main() { debugDumpRenderTree(); } - expect(find.text('View all your statistics on the same platform.'), findsOneWidget); + expect(find.textContaining("View all your statistics on the same platform.", findRichText: true), findsOneWidget); expect(find.byType(TextField), findsOneWidget); }); @@ -1281,4 +1284,31 @@ void main() { expect(find.text("aled"), findsOneWidget); }); }); + + testWidgets('Bungie Maintenance Error', (WidgetTester tester) async { + await mockNetworkImagesFor(() async { + when(client.get(Uri.https(ApiClient.baseUrl, "Platform/Destiny2/Manifest"), headers: anyNamed("headers"))).thenAnswer((_) async { + return http.Response( + '''{ + "ErrorCode": 5, + "ThrottleSeconds": 0, + "ErrorStatus": "SystemDisabled", + "Message": "This system is temporarily disabled for maintenance.", + "MessageData": {} + }''', + 503 + ); + }); + await tester.pumpWidget(GuardianDock()); + + try { + await tester.pumpAndSettle(); + } catch (e) { + debugDumpRenderTree(); + } + + expect(find.byWidgetPredicate((widget) => widget is Icon && widget.icon == Icons.error), findsOneWidget); + expect(find.textContaining(RegExp('Unable to load data from Bungie.')), findsOneWidget); + }); + }); } From 5372a98d9a4865e2c698bbb2312c99a75cbe5680 Mon Sep 17 00:00:00 2001 From: Arthur Date: Wed, 13 Dec 2023 12:03:19 +0100 Subject: [PATCH 4/6] Dependencies: Adding url_launcher for utils --- pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pubspec.yaml b/pubspec.yaml index 1f1e115..49c4f5d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,6 +45,7 @@ dependencies: mocktail: ^1.0.1 network_image_mock: ^2.1.1 flutter_widget_from_html: ^0.14.6 + url_launcher: ^6.2.2 dev_dependencies: flutter_test: From c2002b06d57e1404b30bfd754a9e775bd3651919 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 18 Dec 2023 12:16:21 +0100 Subject: [PATCH 5/6] API: Adding buttons to check Twitter and reload screen --- android/app/src/main/AndroidManifest.xml | 8 +- ios/Runner.xcodeproj/project.pbxproj | 9 ++ ios/Runner/Base.lproj/Main.storyboard | 55 ++++++------ ios/Runner/Info.plist | 106 ++++++++++++----------- lib/src/theme.dart | 7 +- lib/src/views/home_view.dart | 16 ++-- lib/src/widgets/maintenance_error.dart | 55 +++++++++++- 7 files changed, 169 insertions(+), 87 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 885049c..781a703 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + + + + + + diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 8eb5b9e..fd563c7 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -475,10 +475,13 @@ DEVELOPMENT_TEAM = 6MURAC78AD; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Guardian Dock"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 0.1; PRODUCT_BUNDLE_IDENTIFIER = dock.guardian.guardianDock; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -661,10 +664,13 @@ DEVELOPMENT_TEAM = 6MURAC78AD; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Guardian Dock"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 0.1; PRODUCT_BUNDLE_IDENTIFIER = dock.guardian.guardianDock; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -687,10 +693,13 @@ DEVELOPMENT_TEAM = 6MURAC78AD; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Guardian Dock"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); + MARKETING_VERSION = 0.1; PRODUCT_BUNDLE_IDENTIFIER = dock.guardian.guardianDock; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard index bbb83ca..c8017a0 100644 --- a/ios/Runner/Base.lproj/Main.storyboard +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -1,26 +1,29 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index d8797f0..f0f1737 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -1,51 +1,55 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Guardian Dock - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - guardian_dock - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - - + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Guardian Dock + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + guardian_dock + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSApplicationQueriesSchemes + + https + + LSRequiresIPhoneOS + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIStatusBarStyle + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/lib/src/theme.dart b/lib/src/theme.dart index 5372d14..18a14d7 100644 --- a/lib/src/theme.dart +++ b/lib/src/theme.dart @@ -32,8 +32,9 @@ const ColorScheme appScheme = ColorScheme( class AppTheme { static final ThemeData defaultTheme = ThemeData( - fontFamily: 'NeueHaasDisplay', - scaffoldBackgroundColor: appScheme.background, - colorScheme: appScheme + fontFamily: 'NeueHaasDisplay', + scaffoldBackgroundColor: appScheme.background, + colorScheme: appScheme, + useMaterial3: true ); } diff --git a/lib/src/views/home_view.dart b/lib/src/views/home_view.dart index 6bea2c9..23d5eb1 100644 --- a/lib/src/views/home_view.dart +++ b/lib/src/views/home_view.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; -import 'package:guardian_dock/src/widgets/maintenance_error.dart'; import 'package:guardian_dock/src/widgets/persistent_search_bar.dart'; +import 'package:guardian_dock/src/widgets/maintenance_error.dart'; import 'package:guardian_dock/src/widgets/rss/rss_news_feed.dart'; import 'package:guardian_dock/src/widgets/custom_appbar.dart'; import 'package:guardian_dock/api/models/news_article.dart'; @@ -19,7 +19,6 @@ class HomeView extends StatefulWidget { class _HomeViewState extends State { List fetchedArticles = []; bool isLoadingNewsArticle = false; - late Future future; Future getManifest() async => await GetIt.I().getManifest(); @@ -29,14 +28,13 @@ class _HomeViewState extends State { @override void initState() { - future = Future.wait([getNewsArticles(), getManifest()]); super.initState(); } @override Widget build(BuildContext context) { return FutureBuilder( - future: future, + future: Future.wait([getNewsArticles(), getManifest()]), builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return Center( @@ -48,7 +46,15 @@ class _HomeViewState extends State { ) ); } else if (snapshot.hasError) { - return Scaffold(body: MaintenanceError(error: snapshot.error)); + return Scaffold( + body: MaintenanceError( + error: snapshot.error, + onReload: () { + setState(() {}); + Future.delayed(const Duration(seconds: 5)); + }, + ) + ); } return Scaffold( resizeToAvoidBottomInset: false, diff --git a/lib/src/widgets/maintenance_error.dart b/lib/src/widgets/maintenance_error.dart index 4f71fb0..6a17dae 100644 --- a/lib/src/widgets/maintenance_error.dart +++ b/lib/src/widgets/maintenance_error.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:simple_icons/simple_icons.dart'; +import 'package:url_launcher/url_launcher.dart'; class MaintenanceError extends StatelessWidget { final Object? error; + final void Function() onReload; - const MaintenanceError({required this.error, super.key}); + const MaintenanceError({required this.error, required this.onReload, super.key}); @override Widget build(BuildContext context) { @@ -30,6 +33,56 @@ class MaintenanceError extends StatelessWidget { color: Theme.of(context).colorScheme.error, ), ), + ), + SizedBox(height: MediaQuery.of(context).size.height * .02), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + child: ElevatedButton.icon( + onPressed: () async { + canLaunchUrl(Uri.parse("https://twitter.com/BungieHelp")).then((bool result) async { + await launchUrl( + Uri.parse("https://twitter.com/BungieHelp"), + mode: LaunchMode.externalApplication + ); + }); + }, + label: Text("@BungieHelp", + style: TextStyle( + color: Theme.of(context).colorScheme.onBackground + ), + ), + icon: Icon( + size: 20, + SimpleIcons.x, + color: Theme.of(context).colorScheme.onBackground, + ), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.error.withOpacity(.35) + ), + ), + ), + Flexible( + child: ElevatedButton.icon( + onPressed: onReload, + label: Text("Reload", + style: TextStyle( + color: Theme.of(context).colorScheme.onBackground + ), + ), + icon: Icon( + size: 20, + Icons.refresh, + color: Theme.of(context).colorScheme.onBackground, + ), + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.error.withOpacity(.35) + ), + ), + ) + ], ) ] ), From 389f048225024861cae9a9722ca293654d8481ac Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 18 Dec 2023 12:22:31 +0100 Subject: [PATCH 6/6] Dependencies: Adding Simple Icons Dep --- pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pubspec.yaml b/pubspec.yaml index 49c4f5d..eb9848d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,6 +46,7 @@ dependencies: network_image_mock: ^2.1.1 flutter_widget_from_html: ^0.14.6 url_launcher: ^6.2.2 + simple_icons: ^10.1.3 dev_dependencies: flutter_test: