diff --git a/board_games_companion/android/app/src/main/AndroidManifest.xml b/board_games_companion/android/app/src/main/AndroidManifest.xml index c3397fa4..08551b06 100644 --- a/board_games_companion/android/app/src/main/AndroidManifest.xml +++ b/board_games_companion/android/app/src/main/AndroidManifest.xml @@ -26,5 +26,9 @@ + + + diff --git a/board_games_companion/ios/Runner/Info.plist b/board_games_companion/ios/Runner/Info.plist index 84bc890d..2314430d 100644 --- a/board_games_companion/ios/Runner/Info.plist +++ b/board_games_companion/ios/Runner/Info.plist @@ -51,5 +51,7 @@ CADisableMinimumFrameDurationOnPhone + FirebaseAutomaticScreenReportingEnabled + diff --git a/board_games_companion/lib/app.dart b/board_games_companion/lib/app.dart index 99a3fa75..2f15055d 100644 --- a/board_games_companion/lib/app.dart +++ b/board_games_companion/lib/app.dart @@ -1,5 +1,4 @@ import 'package:board_games_companion/pages/edit_playthrough/playthrough_note_page.dart'; -import 'package:board_games_companion/pages/games/games_view_model.dart'; import 'package:board_games_companion/pages/settings/settings_view_model.dart'; import 'package:firebase_analytics/observer.dart'; import 'package:flutter/material.dart'; @@ -18,16 +17,13 @@ import 'pages/edit_playthrough/edit_playthrough_page.dart'; import 'pages/edit_playthrough/edit_playthrough_view_model.dart'; import 'pages/edit_playthrough/playthrough_note_view_model.dart'; import 'pages/home/home_page.dart'; +import 'pages/home/home_view_model.dart'; import 'pages/players/player_page.dart'; import 'pages/players/players_view_model.dart'; import 'pages/playthroughs/playthroughs_page.dart'; import 'pages/playthroughs/playthroughs_view_model.dart'; -import 'pages/search_board_games/search_board_games_view_model.dart'; import 'pages/settings/settings_page.dart'; -import 'services/analytics_service.dart'; import 'services/preferences_service.dart'; -import 'services/rate_and_review_service.dart'; -import 'stores/board_games_filters_store.dart'; import 'utilities/analytics_route_observer.dart'; class BoardGamesCompanionApp extends StatefulWidget { @@ -61,23 +57,11 @@ class BoardGamesCompanionAppState extends State { onGenerateRoute: (RouteSettings routeSettings) { switch (routeSettings.name) { case HomePage.pageRoute: - final analyticsService = getIt(); - final rateAndReviewService = getIt(); - final playersViewModel = getIt(); - final boardGamesFiltersStore = getIt(); - final gamesViewModel = getIt(); - final searchViewModel = getIt(); + final viewModel = getIt(); return MaterialPageRoute( settings: routeSettings, - builder: (_) => HomePage( - analyticsService: analyticsService, - rateAndReviewService: rateAndReviewService, - gamesViewModel: gamesViewModel, - playersViewModel: playersViewModel, - searchViewModel: searchViewModel, - boardGamesFiltersStore: boardGamesFiltersStore, - ), + builder: (_) => HomePage(viewModel: viewModel), ); case BoardGamesDetailsPage.pageRoute: diff --git a/board_games_companion/lib/common/analytics.dart b/board_games_companion/lib/common/analytics.dart index 466c181b..648e403a 100644 --- a/board_games_companion/lib/common/analytics.dart +++ b/board_games_companion/lib/common/analytics.dart @@ -1,7 +1,6 @@ class Analytics { static const String filterCollection = 'filter_collection'; static const String sortCollection = 'sort_collection'; - static const String viewPage = 'view_page'; static const String viewGameStats = 'view_game_stats'; static const String viewGameDetails = 'view_game_details'; static const String viewHotBoardGame = 'view_hot_board_game'; @@ -10,6 +9,7 @@ class Analytics { static const String editPlaythrough = 'edit_playthough'; static const String logPlaythrough = 'log_playthough'; static const String importBggPlays = 'import_bgg_plays'; + static const String openGamesPlaylist = 'open_games_playlist'; static const String routeName = 'route_name'; diff --git a/board_games_companion/lib/extensions/route_extensions.dart b/board_games_companion/lib/extensions/route_extensions.dart new file mode 100644 index 00000000..ba7d6eb0 --- /dev/null +++ b/board_games_companion/lib/extensions/route_extensions.dart @@ -0,0 +1,58 @@ +import 'package:flutter/widgets.dart'; + +import '../pages/about/about_page.dart'; +import '../pages/board_game_details/board_game_details_page.dart'; +import '../pages/edit_playthrough/edit_playthrough_page.dart'; +import '../pages/edit_playthrough/playthrough_note_page.dart'; +import '../pages/home/home_page.dart'; +import '../pages/players/player_page.dart'; +import '../pages/playthroughs/playthroughs_page.dart'; +import '../pages/settings/settings_page.dart'; + +extension RouteExtensions on Route { + String toScreenName() { + switch (settings.name) { + case AboutPage.pageRoute: + return 'About'; + case BoardGamesDetailsPage.pageRoute: + return 'Board Games Details'; + case EditPlaythroughPage.pageRoute: + return 'Edit Playthrough'; + case HomePage.pageRoute: + return 'Home'; + case PlaythroughsPage.pageRoute: + return 'Playthroughs'; + case PlaythroughNotePage.pageRoute: + return 'Playthrough Note'; + case PlayerPage.pageRoute: + return 'Player'; + case SettingsPage.pageRoute: + return 'Settings'; + default: + return 'Undefined'; + } + } + + String toScreenClassName() { + switch (settings.name) { + case AboutPage.pageRoute: + return 'AboutPage'; + case BoardGamesDetailsPage.pageRoute: + return 'BoardGamesDetailsPage'; + case EditPlaythroughPage.pageRoute: + return 'EditPlaythroughPage'; + case HomePage.pageRoute: + return 'HomePage'; + case PlaythroughsPage.pageRoute: + return 'PlaythroughsPage'; + case PlaythroughNotePage.pageRoute: + return 'PlaythroughNotePage'; + case PlayerPage.pageRoute: + return 'PlayerPage'; + case SettingsPage.pageRoute: + return 'SettingsPage'; + default: + return 'Undefined'; + } + } +} diff --git a/board_games_companion/lib/injectable.config.dart b/board_games_companion/lib/injectable.config.dart index 37997b38..63c070df 100644 --- a/board_games_companion/lib/injectable.config.dart +++ b/board_games_companion/lib/injectable.config.dart @@ -14,6 +14,7 @@ import 'pages/edit_playthrough/edit_playthrough_view_model.dart' as _i27; import 'pages/edit_playthrough/playthrough_note_view_model.dart' as _i29; import 'pages/games/collection_search_result_view_model.dart' as _i26; import 'pages/games/games_view_model.dart' as _i28; +import 'pages/home/home_view_model.dart' as _i37; import 'pages/players/players_view_model.dart' as _i11; import 'pages/playthroughs/playthrough_statistics_view_model.dart' as _i30; import 'pages/playthroughs/playthroughs_game_settings_view_model.dart' as _i31; @@ -27,7 +28,7 @@ import 'services/board_games_filters_service.dart' as _i4; import 'services/board_games_geek_service.dart' as _i19; import 'services/board_games_service.dart' as _i20; import 'services/file_service.dart' as _i6; -import 'services/injectable_register_module.dart' as _i37; +import 'services/injectable_register_module.dart' as _i38; import 'services/player_service.dart' as _i9; import 'services/playthroughs_service.dart' as _i21; import 'services/preferences_service.dart' as _i12; @@ -138,10 +139,17 @@ _i1.GetIt $initGetIt(_i1.GetIt get, gh.factory<_i36.BoardGameDetailsViewModel>(() => _i36.BoardGameDetailsViewModel( get<_i25.BoardGamesStore>(), get<_i17.AnalyticsService>())); + gh.factory<_i37.HomeViewModel>(() => _i37.HomeViewModel( + get<_i17.AnalyticsService>(), + get<_i13.RateAndReviewService>(), + get<_i11.PlayersViewModel>(), + get<_i18.BoardGamesFiltersStore>(), + get<_i28.GamesViewModel>(), + get<_i34.SearchBoardGamesViewModel>())); return get; } -class _$RegisterModule extends _i37.RegisterModule { +class _$RegisterModule extends _i38.RegisterModule { @override _i7.FirebaseAnalytics get firebaseAnalytics => _i7.FirebaseAnalytics(); } diff --git a/board_games_companion/lib/pages/home/home_page.dart b/board_games_companion/lib/pages/home/home_page.dart index 754b8215..bdc83f39 100644 --- a/board_games_companion/lib/pages/home/home_page.dart +++ b/board_games_companion/lib/pages/home/home_page.dart @@ -1,14 +1,9 @@ -import 'package:board_games_companion/pages/games/games_view_model.dart'; -import 'package:board_games_companion/pages/players/players_view_model.dart'; -import 'package:board_games_companion/pages/search_board_games/search_board_games_view_model.dart'; +import 'package:board_games_companion/pages/home/home_view_model.dart'; import 'package:convex_bottom_bar/convex_bottom_bar.dart'; import 'package:flutter/material.dart'; import '../../common/app_colors.dart'; import '../../common/dimensions.dart'; -import '../../services/analytics_service.dart'; -import '../../services/rate_and_review_service.dart'; -import '../../stores/board_games_filters_store.dart'; import '../../widgets/bottom_tab_icon.dart'; import '../../widgets/common/page_container.dart'; import '../base_page_state.dart'; @@ -19,23 +14,13 @@ import 'home_page_drawer.dart'; class HomePage extends StatefulWidget { const HomePage({ - required this.analyticsService, - required this.rateAndReviewService, - required this.gamesViewModel, - required this.playersViewModel, - required this.searchViewModel, - required this.boardGamesFiltersStore, + required this.viewModel, Key? key, }) : super(key: key); static const String pageRoute = '/home'; - final AnalyticsService analyticsService; - final RateAndReviewService rateAndReviewService; - final GamesViewModel gamesViewModel; - final PlayersViewModel playersViewModel; - final SearchBoardGamesViewModel searchViewModel; - final BoardGamesFiltersStore boardGamesFiltersStore; + final HomeViewModel viewModel; static final GlobalKey homePageGlobalKey = GlobalKey(); @@ -73,13 +58,13 @@ class HomePageState extends BasePageState with SingleTickerProviderSta controller: tabController, children: [ GamesPage( - widget.gamesViewModel, - widget.boardGamesFiltersStore, - widget.analyticsService, - widget.rateAndReviewService, + widget.viewModel.gamesViewModel, + widget.viewModel.boardGamesFiltersStore, + widget.viewModel.analyticsService, + widget.viewModel.rateAndReviewService, ), - SearchBoardGamesPage(viewModel: widget.searchViewModel), - PlayersPage(playersViewModel: widget.playersViewModel), + SearchBoardGamesPage(viewModel: widget.viewModel.searchBoardGamesViewModel), + PlayersPage(playersViewModel: widget.viewModel.playersViewModel), ], ), ), @@ -108,6 +93,7 @@ class HomePageState extends BasePageState with SingleTickerProviderSta initialActiveIndex: _initialTabIndex, activeColor: AppColors.accentColor, color: AppColors.inactiveBottomTabColor, + onTap: (int tabIndex) => widget.viewModel.trackTabChange(tabIndex), ), ), ); diff --git a/board_games_companion/lib/pages/home/home_view_model.dart b/board_games_companion/lib/pages/home/home_view_model.dart new file mode 100644 index 00000000..22e07c78 --- /dev/null +++ b/board_games_companion/lib/pages/home/home_view_model.dart @@ -0,0 +1,48 @@ +// ignore_for_file: library_private_types_in_public_api + +import 'package:injectable/injectable.dart'; +import 'package:mobx/mobx.dart'; +import 'package:tuple/tuple.dart'; + +import '../../services/analytics_service.dart'; +import '../../services/rate_and_review_service.dart'; +import '../../stores/board_games_filters_store.dart'; +import '../games/games_view_model.dart'; +import '../players/players_view_model.dart'; +import '../search_board_games/search_board_games_view_model.dart'; + +part 'home_view_model.g.dart'; + +@injectable +class HomeViewModel = _HomeViewModelBase with _$HomeViewModel; + +abstract class _HomeViewModelBase with Store { + _HomeViewModelBase( + this.analyticsService, + this.rateAndReviewService, + this.playersViewModel, + this.boardGamesFiltersStore, + this.gamesViewModel, + this.searchBoardGamesViewModel, + ); + + final AnalyticsService analyticsService; + final RateAndReviewService rateAndReviewService; + final PlayersViewModel playersViewModel; + final BoardGamesFiltersStore boardGamesFiltersStore; + final GamesViewModel gamesViewModel; + final SearchBoardGamesViewModel searchBoardGamesViewModel; + + static const Map> _screenViewByTabIndex = { + 0: Tuple2('Games', 'GamesPage'), + 1: Tuple2('Search', 'SearchBoardGamesPage'), + 2: Tuple2('Players', 'PlayersPage'), + }; + + Future trackTabChange(int tabIndex) async { + await analyticsService.logScreenView( + screenName: _screenViewByTabIndex[tabIndex]!.item1, + screenClass: _screenViewByTabIndex[tabIndex]!.item2, + ); + } +} diff --git a/board_games_companion/lib/pages/home/home_view_model.g.dart b/board_games_companion/lib/pages/home/home_view_model.g.dart new file mode 100644 index 00000000..260d4818 --- /dev/null +++ b/board_games_companion/lib/pages/home/home_view_model.g.dart @@ -0,0 +1,18 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'home_view_model.dart'; + +// ************************************************************************** +// StoreGenerator +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers + +mixin _$HomeViewModel on _HomeViewModelBase, Store { + @override + String toString() { + return ''' + + '''; + } +} diff --git a/board_games_companion/lib/pages/playthroughs/playthroughs_page.dart b/board_games_companion/lib/pages/playthroughs/playthroughs_page.dart index c913839f..4efae388 100644 --- a/board_games_companion/lib/pages/playthroughs/playthroughs_page.dart +++ b/board_games_companion/lib/pages/playthroughs/playthroughs_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:board_games_companion/utilities/launcher_helper.dart'; import 'package:convex_bottom_bar/convex_bottom_bar.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; @@ -45,6 +47,9 @@ class PlaythroughsPageState extends BasePageState bool _showImportGamesLoadingIndicator = false; + static final GlobalKey playthroughsPageGlobalKey = + GlobalKey(); + @override void initState() { super.initState(); @@ -58,75 +63,80 @@ class PlaythroughsPageState extends BasePageState @override Widget build(BuildContext context) { - final scaffold = Scaffold( - resizeToAvoidBottomInset: false, - appBar: AppBar( - title: Text(widget.viewModel.boardGame.name, style: AppTheme.titleTextStyle), - actions: [ - IconButton( - icon: const Icon(Icons.music_note, color: AppColors.accentColor), - onPressed: () async => _openGamesMusicPlaylist(context), - ), - Observer( - builder: (_) { - if (!widget.viewModel.hasUser) { - return const SizedBox.shrink(); - } - - return IconButton( - icon: const Icon(Icons.download, color: AppColors.accentColor), - onPressed: () => _importBggPlays(), - ); - }, - ), - IconButton( - icon: const Icon(Icons.info, color: AppColors.accentColor), - onPressed: () => _navigateToBoardGameDetails(context), - ), - ], - ), - body: SafeArea( - child: PageContainer( - child: TabBarView( - controller: tabController, - children: const [ - PlaythroughStatistcsPage(), - PlaythroughsHistoryPage(), - PlaythroughsLogGamePage(), - PlaythroughsGameSettingsPage() - ], - ), + final scaffold = ScaffoldMessenger( + key: playthroughsPageGlobalKey, + child: Scaffold( + resizeToAvoidBottomInset: false, + appBar: AppBar( + title: Text(widget.viewModel.boardGame.name, style: AppTheme.titleTextStyle), + actions: [ + IconButton( + icon: const Icon(Icons.music_note, color: AppColors.accentColor), + onPressed: () async => _openGamesMusicPlaylist(context), + ), + Observer( + builder: (_) { + if (!widget.viewModel.hasUser) { + return const SizedBox.shrink(); + } + + return IconButton( + icon: const Icon(Icons.download, color: AppColors.accentColor), + onPressed: () => _importBggPlays(), + ); + }, + ), + IconButton( + icon: const Icon(Icons.info, color: AppColors.accentColor), + onPressed: () => _navigateToBoardGameDetails(context), + ), + ], ), - ), - bottomNavigationBar: ConvexAppBar( - controller: tabController, - backgroundColor: AppColors.bottomTabBackgroundColor, - top: -Dimensions.bottomTabTopHeight, - items: const [ - TabItem( - title: AppText.playthroughPageStatsBottomTabTitle, - icon: BottomTabIcon(iconData: Icons.multiline_chart), - activeIcon: BottomTabIcon(iconData: Icons.multiline_chart, isActive: true), - ), - TabItem( - title: AppText.playthroughPageHistoryBottomTabTitle, - icon: BottomTabIcon(iconData: Icons.history), - activeIcon: BottomTabIcon(iconData: Icons.history, isActive: true), - ), - TabItem( - title: AppText.playthroughPageLogGameBottomTabTitle, - icon: BottomTabIcon(iconData: Icons.casino), - activeIcon: BottomTabIcon(iconData: Icons.casino, isActive: true), + body: SafeArea( + child: PageContainer( + child: TabBarView( + controller: tabController, + children: const [ + PlaythroughStatistcsPage(), + PlaythroughsHistoryPage(), + PlaythroughsLogGamePage(), + PlaythroughsGameSettingsPage() + ], + ), ), - TabItem( - title: AppText.playthroughPageGameSettingsLogGameBottomTabTitle, - icon: BottomTabIcon(iconData: Icons.settings_applications_sharp), - activeIcon: BottomTabIcon(iconData: Icons.settings_applications_sharp, isActive: true), - ), - ], - initialActiveIndex: _initialTabIndex, - activeColor: AppColors.accentColor, - color: AppColors.inactiveBottomTabColor, + ), + bottomNavigationBar: ConvexAppBar( + controller: tabController, + backgroundColor: AppColors.bottomTabBackgroundColor, + top: -Dimensions.bottomTabTopHeight, + items: const [ + TabItem( + title: AppText.playthroughPageStatsBottomTabTitle, + icon: BottomTabIcon(iconData: Icons.multiline_chart), + activeIcon: BottomTabIcon(iconData: Icons.multiline_chart, isActive: true), + ), + TabItem( + title: AppText.playthroughPageHistoryBottomTabTitle, + icon: BottomTabIcon(iconData: Icons.history), + activeIcon: BottomTabIcon(iconData: Icons.history, isActive: true), + ), + TabItem( + title: AppText.playthroughPageLogGameBottomTabTitle, + icon: BottomTabIcon(iconData: Icons.casino), + activeIcon: BottomTabIcon(iconData: Icons.casino, isActive: true), + ), + TabItem( + title: AppText.playthroughPageGameSettingsLogGameBottomTabTitle, + icon: BottomTabIcon(iconData: Icons.settings_applications_sharp), + activeIcon: + BottomTabIcon(iconData: Icons.settings_applications_sharp, isActive: true), + ), + ], + initialActiveIndex: _initialTabIndex, + activeColor: AppColors.accentColor, + color: AppColors.inactiveBottomTabColor, + onTap: (int tabIndex) => widget.viewModel.trackTabChange(tabIndex), + ), ), ); @@ -150,6 +160,7 @@ class PlaythroughsPageState extends BasePageState } Future _openGamesMusicPlaylist(BuildContext context) async { + unawaited(widget.viewModel.trackOpenGamesPlaylist()); await LauncherHelper.launchUri( context, widget.viewModel.gamePlaylistUrl, diff --git a/board_games_companion/lib/pages/playthroughs/playthroughs_view_model.dart b/board_games_companion/lib/pages/playthroughs/playthroughs_view_model.dart index c604085f..2ab53a20 100644 --- a/board_games_companion/lib/pages/playthroughs/playthroughs_view_model.dart +++ b/board_games_companion/lib/pages/playthroughs/playthroughs_view_model.dart @@ -8,6 +8,7 @@ import 'package:board_games_companion/stores/user_store.dart'; import 'package:collection/collection.dart' show IterableExtension; import 'package:injectable/injectable.dart'; import 'package:mobx/mobx.dart'; +import 'package:tuple/tuple.dart'; import 'package:uuid/uuid.dart'; import '../../common/analytics.dart'; @@ -38,6 +39,13 @@ abstract class _PlaythroughsViewModel with Store { static const baseMelodiceUrl = 'https://melodice.org'; static const melodicePlaylistUrl = '$baseMelodiceUrl/playlist'; + static const Map> _screenViewByTabIndex = { + 0: Tuple2('Statistics', 'PlaythroughStatistcsPage'), + 1: Tuple2('History', 'PlaythroughsHistoryPage'), + 2: Tuple2('Log Games', 'PlaythroughsLogGamePage'), + 3: Tuple2('Settings', 'PlaythroughsGameSettingsPage'), + }; + final PlayersStore _playersStore; final PlaythroughsStore _playthroughsStore; final AnalyticsService _analyticsService; @@ -152,4 +160,21 @@ abstract class _PlaythroughsViewModel with Store { } } } + + Future trackOpenGamesPlaylist() async { + await _analyticsService.logEvent( + name: Analytics.openGamesPlaylist, + parameters: { + Analytics.boardGameIdParameter: boardGame.id, + Analytics.boardGameNameParameter: boardGame.name, + }, + ); + } + + Future trackTabChange(int tabIndex) async { + await _analyticsService.logScreenView( + screenName: _screenViewByTabIndex[tabIndex]!.item1, + screenClass: _screenViewByTabIndex[tabIndex]!.item2, + ); + } } diff --git a/board_games_companion/lib/services/analytics_service.dart b/board_games_companion/lib/services/analytics_service.dart index 7126276d..b133851e 100644 --- a/board_games_companion/lib/services/analytics_service.dart +++ b/board_games_companion/lib/services/analytics_service.dart @@ -17,4 +17,15 @@ class AnalyticsService { await _firebaseAnalytics.logEvent(name: name, parameters: parameters); await _rateAndReviewService.increaseNumberOfSignificantActions(); } + + Future logScreenView({ + required String screenName, + required String screenClass, + }) async { + await _firebaseAnalytics.setCurrentScreen( + screenName: screenName, + screenClassOverride: screenClass, + ); + await _rateAndReviewService.increaseNumberOfSignificantActions(); + } } diff --git a/board_games_companion/lib/utilities/analytics_route_observer.dart b/board_games_companion/lib/utilities/analytics_route_observer.dart index 934333c6..28537595 100644 --- a/board_games_companion/lib/utilities/analytics_route_observer.dart +++ b/board_games_companion/lib/utilities/analytics_route_observer.dart @@ -1,4 +1,5 @@ import 'package:basics/basics.dart'; +import 'package:board_games_companion/extensions/route_extensions.dart'; import 'package:flutter/cupertino.dart'; import 'package:injectable/injectable.dart'; @@ -17,6 +18,17 @@ class AnalyticsRouteObserver extends RouteObserver> { final AnalyticsService _analtyicsService; + @override + void didPop(Route route, Route? previousRoute) { + super.didPop(route, previousRoute); + + // MK Manually logging a screen view as per https://firebase.google.com/docs/analytics/screenviews#dart + _analtyicsService.logScreenView( + screenName: route.toScreenName(), + screenClass: route.toScreenClassName(), + ); + } + @override void didPush(Route route, Route? previousRoute) { super.didPush(route, previousRoute); @@ -26,9 +38,10 @@ class AnalyticsRouteObserver extends RouteObserver> { return; } - _analtyicsService.logEvent( - name: Analytics.viewPage, - parameters: {Analytics.routeName: routeName!}, + // MK Manually logging a screen view as per https://firebase.google.com/docs/analytics/screenviews#dart + _analtyicsService.logScreenView( + screenName: route.toScreenName(), + screenClass: route.toScreenClassName(), ); switch (routeName) {