From dac65670dc774310d7ca258f8d5e8127f2199e63 Mon Sep 17 00:00:00 2001 From: GwanLiZa <144007144+GwanLiZa@users.noreply.github.com> Date: Fri, 4 Oct 2024 03:21:42 +0900 Subject: [PATCH] feat: add analytics (#423) * feat: add analytics * fix: build error * fix: naming error * refactor: logEvent * fix: 'id' was assigned -1 * feat: add page source enum * refactor: get name and parameter function * refactor: page source enum's order * chore: update dependencies * feat: add analytics * feat: add analystics * chore: upgrade Podfile.lock * feat: add router observer * chore(deps): remove flutter_hooks * feat: add analytics repository helper * feat: add AutoRouteObserver * feat: add pageview event for feed page * feat: add pageview event for category page * feat: add pageview event for main bottom tab navigator * feat: add pageview event for profile page * feat: add pageview event for list page * fix: remove null event parameter * feat: add pageview event for search page * feat: add pageview event for write body page * feat: click back button event * feat: add pageview event for detail page * feat: add pageview event for notice edit page * feat: add pageview event for write config page * feat: add pageview event for add hashtag page * feat: add pageview event for write config preview page * feat: add pageview event for notice write consent page * feat: add pageview event for notice edit body page * feat: add pageview event for notice edit additional page * feat: add pageview event for notice edit preview page * feat: add pageview event for profile setting page * feat: add pageview event for profile setting information page * refactor: remove unused imports * feat: add analytics * fix: language toggle merge error * fix: always invoke ai translation click event even if condition is not met * fix: additional notice event condition * refactor: separate analytics repositories * fix: wrong import * refactor: cleanup import * feat: link firebase analytics repository to MultipleAnalyticsRepository * chore(deps): add amplitude * feat: use amplitude analytics * feat: register FirebaseAnalyticsRepository to injectable * chore(deps): add flutter_smartlook * feat: integrate smartlook * chore(deps): add flutter_dotenv * feat: use dotenv * chore(ci/cd): copy dotenv * chore: update Podfile.lock * refactor: analytics instance getting * chore: remove logging in FirebaseAnalyticsRepository * fix: try~catch in MultipleAnalyticsRepository * refactor: use injected AnalyticsRepository * feat: add ai translation action event * fix: make activated language button non clickable * fix: wrong condition for ai translation * refactor: bottom navigation page click action * fix: notice can be null issue in detail page * fix: notice can be null issue in notice edit page * refactor: string -> Language enum * fix: ai translate condition * fix: add type parameter in publish agree event * fix: don't render child of single notice shell layout if no data in bloc * refactor: make camelToSnake as global function not method * feat: deadline selector view event --------- Co-authored-by: 2paperstar --- .github/workflows/test.yml | 4 + .github/workflows/upload.yml | 4 + .gitignore | 3 + ios/Podfile.lock | 138 ++++++++------ lib/app/app.dart | 9 +- .../pages/ziggle_bottom_navigation_page.dart | 128 ++++++++----- .../presentation/widgets/ziggle_app_bar.dart | 3 + .../widgets/ziggle_back_button.dart | 15 +- .../core/data/models/analytics_event.dart | 145 +++++++++++++++ .../amplitude_analytics_repository.dart | 44 +++++ .../firebase_analytics_repository.dart | 33 ++++ .../mock_analytics_repository.dart | 25 +++ .../multiple_analytics_repository.dart | 55 ++++++ .../smartlook_analytics_repository.dart | 33 ++++ .../modules/core/domain/enums/event_type.dart | 6 + .../core/domain/enums/page_source.dart | 20 ++ .../repositories/analytics_repository.dart | 21 +++ .../layouts/group_creation_layout.dart | 2 + .../pages/group_management_main_page.dart | 2 + .../presentation/bloc/notice_bloc.dart | 15 +- .../layouts/single_notice_shell_layout.dart | 7 +- .../presentation/pages/category_page.dart | 31 +++- .../presentation/pages/detail_page.dart | 21 ++- .../notices/presentation/pages/feed_page.dart | 30 ++- .../notices/presentation/pages/list_page.dart | 28 ++- .../pages/notice_edit_body_page.dart | 54 ++++-- .../presentation/pages/notice_edit_page.dart | 53 +++++- .../pages/notice_edit_preview_page.dart | 26 ++- .../pages/notice_write_body_page.dart | 62 +++++-- .../pages/notice_write_config_page.dart | 52 +++++- .../pages/notice_write_consent_page.dart | 50 ++++- .../pages/notice_write_preview_page.dart | 24 ++- .../pages/notice_write_select_tags_page.dart | 39 +++- .../presentation/pages/search_page.dart | 24 ++- .../pages/write_additional_notice_page.dart | 63 +++++-- .../widgets/deadline_selector.dart | 31 +++- .../presentation/widgets/list_layout.dart | 38 +++- .../presentation/widgets/notice_renderer.dart | 25 ++- .../user/presentation/bloc/auth_bloc.dart | 18 +- .../presentation/pages/information_page.dart | 37 +++- .../user/presentation/pages/license_page.dart | 2 + .../presentation/pages/packages_page.dart | 2 + .../user/presentation/pages/profile_page.dart | 52 +++++- .../user/presentation/pages/setting_page.dart | 67 +++++-- lib/app/router_observer.dart | 55 ++++++ lib/app/values/strings.dart | 30 ++- lib/main.dart | 2 + pubspec.lock | 174 +++++++++--------- pubspec.yaml | 4 + 49 files changed, 1459 insertions(+), 347 deletions(-) create mode 100644 lib/app/modules/core/data/models/analytics_event.dart create mode 100644 lib/app/modules/core/data/repositories/amplitude_analytics_repository.dart create mode 100644 lib/app/modules/core/data/repositories/firebase_analytics_repository.dart create mode 100644 lib/app/modules/core/data/repositories/mock_analytics_repository.dart create mode 100644 lib/app/modules/core/data/repositories/multiple_analytics_repository.dart create mode 100644 lib/app/modules/core/data/repositories/smartlook_analytics_repository.dart create mode 100644 lib/app/modules/core/domain/enums/event_type.dart create mode 100644 lib/app/modules/core/domain/enums/page_source.dart create mode 100644 lib/app/modules/core/domain/repositories/analytics_repository.dart create mode 100644 lib/app/router_observer.dart diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0ea01602d..49eb020b1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -101,6 +101,10 @@ jobs: bundler-cache: true working-directory: ${{ env.directory }} + - name: Copy dotenv + if: env.run == 'true' + run: cat ${{ secrets.DOTENV }} > .env + - name: Generate files if: env.run == 'true' run: | diff --git a/.github/workflows/upload.yml b/.github/workflows/upload.yml index ae093034e..a1eb8f233 100644 --- a/.github/workflows/upload.yml +++ b/.github/workflows/upload.yml @@ -82,6 +82,10 @@ jobs: ~/Library/Caches/CocoaPods key: cocoapods-${{ hashFiles('ios/Podfile.lock') }} + - name: Copy dotenv + if: env.run == 'true' + run: cat ${{ secrets.DOTENV }} > .env + - name: Generate files run: | dart run slang diff --git a/.gitignore b/.gitignore index 4b26d770d..a42e62b99 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,6 @@ app.*.map.json *.freezed.dart *.gen.dart locator.config.dart + +# env +.env \ No newline at end of file diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 4853e2989..eb170b946 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,36 +1,40 @@ PODS: - - device_info_plus (0.0.1): + - Amplitude (8.18.0): + - AnalyticsConnector (~> 1.0.0) + - amplitude_flutter (0.0.1): + - Amplitude (= 8.18.0) - Flutter - - Firebase/Analytics (11.0.0): + - AnalyticsConnector (1.0.3) + - Firebase/Analytics (11.2.0): - Firebase/Core - - Firebase/Core (11.0.0): + - Firebase/Core (11.2.0): - Firebase/CoreOnly - - FirebaseAnalytics (~> 11.0.0) - - Firebase/CoreOnly (11.0.0): - - FirebaseCore (= 11.0.0) - - Firebase/Crashlytics (11.0.0): + - FirebaseAnalytics (~> 11.2.0) + - Firebase/CoreOnly (11.2.0): + - FirebaseCore (= 11.2.0) + - Firebase/Crashlytics (11.2.0): - Firebase/CoreOnly - - FirebaseCrashlytics (~> 11.0.0) - - Firebase/Messaging (11.0.0): + - FirebaseCrashlytics (~> 11.2.0) + - Firebase/Messaging (11.2.0): - Firebase/CoreOnly - - FirebaseMessaging (~> 11.0.0) - - firebase_analytics (11.3.0): - - Firebase/Analytics (= 11.0.0) + - FirebaseMessaging (~> 11.2.0) + - firebase_analytics (11.3.3): + - Firebase/Analytics (= 11.2.0) - firebase_core - Flutter - - firebase_core (3.4.1): - - Firebase/CoreOnly (= 11.0.0) + - firebase_core (3.6.0): + - Firebase/CoreOnly (= 11.2.0) - Flutter - - firebase_crashlytics (4.1.0): - - Firebase/Crashlytics (= 11.0.0) + - firebase_crashlytics (4.1.3): + - Firebase/Crashlytics (= 11.2.0) - firebase_core - Flutter - - firebase_messaging (15.1.0): - - Firebase/Messaging (= 11.0.0) + - firebase_messaging (15.1.3): + - Firebase/Messaging (= 11.2.0) - firebase_core - Flutter - - FirebaseAnalytics (11.0.0): - - FirebaseAnalytics/AdIdSupport (= 11.0.0) + - FirebaseAnalytics (11.2.0): + - FirebaseAnalytics/AdIdSupport (= 11.2.0) - FirebaseCore (~> 11.0) - FirebaseInstallations (~> 11.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0) @@ -38,24 +42,24 @@ PODS: - GoogleUtilities/Network (~> 8.0) - "GoogleUtilities/NSData+zlib (~> 8.0)" - nanopb (~> 3.30910.0) - - FirebaseAnalytics/AdIdSupport (11.0.0): + - FirebaseAnalytics/AdIdSupport (11.2.0): - FirebaseCore (~> 11.0) - FirebaseInstallations (~> 11.0) - - GoogleAppMeasurement (= 11.0.0) + - GoogleAppMeasurement (= 11.2.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/Network (~> 8.0) - "GoogleUtilities/NSData+zlib (~> 8.0)" - nanopb (~> 3.30910.0) - - FirebaseCore (11.0.0): + - FirebaseCore (11.2.0): - FirebaseCoreInternal (~> 11.0) - GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Logger (~> 8.0) - - FirebaseCoreExtension (11.1.0): + - FirebaseCoreExtension (11.3.0): - FirebaseCore (~> 11.0) - - FirebaseCoreInternal (11.1.0): + - FirebaseCoreInternal (11.3.0): - "GoogleUtilities/NSData+zlib (~> 8.0)" - - FirebaseCrashlytics (11.0.0): + - FirebaseCrashlytics (11.2.0): - FirebaseCore (~> 11.0) - FirebaseInstallations (~> 11.0) - FirebaseRemoteConfigInterop (~> 11.0) @@ -64,12 +68,12 @@ PODS: - GoogleUtilities/Environment (~> 8.0) - nanopb (~> 3.30910.0) - PromisesObjC (~> 2.4) - - FirebaseInstallations (11.1.0): + - FirebaseInstallations (11.3.0): - FirebaseCore (~> 11.0) - GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0) - PromisesObjC (~> 2.4) - - FirebaseMessaging (11.0.0): + - FirebaseMessaging (11.2.0): - FirebaseCore (~> 11.0) - FirebaseInstallations (~> 11.0) - GoogleDataTransport (~> 10.0) @@ -78,8 +82,8 @@ PODS: - GoogleUtilities/Reachability (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0) - nanopb (~> 3.30910.0) - - FirebaseRemoteConfigInterop (11.1.0) - - FirebaseSessions (11.1.0): + - FirebaseRemoteConfigInterop (11.3.0) + - FirebaseSessions (11.3.0): - FirebaseCore (~> 11.0) - FirebaseCoreExtension (~> 11.0) - FirebaseInstallations (~> 11.0) @@ -96,29 +100,31 @@ PODS: - flutter_inappwebview/Core (0.0.1): - Flutter - OrderedSet (~> 5.0) - - flutter_keyboard_visibility (0.0.1): + - flutter_keyboard_visibility_temp_fork (0.0.1): - Flutter - flutter_local_notifications (0.0.1): - Flutter - flutter_secure_storage (6.0.0): - Flutter + - flutter_smartlook (4.1.24): + - Flutter - flutter_web_auth_2 (3.0.0): - Flutter - - GoogleAppMeasurement (11.0.0): - - GoogleAppMeasurement/AdIdSupport (= 11.0.0) + - GoogleAppMeasurement (11.2.0): + - GoogleAppMeasurement/AdIdSupport (= 11.2.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/Network (~> 8.0) - "GoogleUtilities/NSData+zlib (~> 8.0)" - nanopb (~> 3.30910.0) - - GoogleAppMeasurement/AdIdSupport (11.0.0): - - GoogleAppMeasurement/WithoutAdIdSupport (= 11.0.0) + - GoogleAppMeasurement/AdIdSupport (11.2.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 11.2.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/Network (~> 8.0) - "GoogleUtilities/NSData+zlib (~> 8.0)" - nanopb (~> 3.30910.0) - - GoogleAppMeasurement/WithoutAdIdSupport (11.0.0): + - GoogleAppMeasurement/WithoutAdIdSupport (11.2.0): - GoogleUtilities/AppDelegateSwizzler (~> 8.0) - GoogleUtilities/MethodSwizzler (~> 8.0) - GoogleUtilities/Network (~> 8.0) @@ -185,6 +191,8 @@ PODS: - PromisesObjC (2.4.0) - PromisesSwift (2.4.0): - PromisesObjC (= 2.4.0) + - quill_native_bridge (0.0.1): + - Flutter - share_plus (0.0.1): - Flutter - sqflite (0.0.3): @@ -196,7 +204,7 @@ PODS: - Flutter DEPENDENCIES: - - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - amplitude_flutter (from `.symlinks/plugins/amplitude_flutter/ios`) - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) @@ -204,15 +212,17 @@ DEPENDENCIES: - FirebaseMessaging - Flutter (from `Flutter`) - flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`) - - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) + - flutter_keyboard_visibility_temp_fork (from `.symlinks/plugins/flutter_keyboard_visibility_temp_fork/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) + - flutter_smartlook (from `.symlinks/plugins/flutter_smartlook/ios`) - flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`) - GoogleUtilities - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - quill_native_bridge (from `.symlinks/plugins/quill_native_bridge/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - sqflite (from `.symlinks/plugins/sqflite/darwin`) - uni_links (from `.symlinks/plugins/uni_links/ios`) @@ -220,6 +230,8 @@ DEPENDENCIES: SPEC REPOS: trunk: + - Amplitude + - AnalyticsConnector - Firebase - FirebaseAnalytics - FirebaseCore @@ -239,8 +251,8 @@ SPEC REPOS: - PromisesSwift EXTERNAL SOURCES: - device_info_plus: - :path: ".symlinks/plugins/device_info_plus/ios" + amplitude_flutter: + :path: ".symlinks/plugins/amplitude_flutter/ios" firebase_analytics: :path: ".symlinks/plugins/firebase_analytics/ios" firebase_core: @@ -253,12 +265,14 @@ EXTERNAL SOURCES: :path: Flutter flutter_inappwebview: :path: ".symlinks/plugins/flutter_inappwebview/ios" - flutter_keyboard_visibility: - :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" + flutter_keyboard_visibility_temp_fork: + :path: ".symlinks/plugins/flutter_keyboard_visibility_temp_fork/ios" flutter_local_notifications: :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_secure_storage: :path: ".symlinks/plugins/flutter_secure_storage/ios" + flutter_smartlook: + :path: ".symlinks/plugins/flutter_smartlook/ios" flutter_web_auth_2: :path: ".symlinks/plugins/flutter_web_auth_2/ios" image_picker_ios: @@ -269,6 +283,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" + quill_native_bridge: + :path: ".symlinks/plugins/quill_native_bridge/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" sqflite: @@ -279,28 +295,31 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: - device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d - Firebase: 9f574c08c2396885b5e7e100ed4293d956218af9 - firebase_analytics: 1a66fe8d4375eccff44671ea37897683a78b2675 - firebase_core: ba84e940cf5cbbc601095f86556560937419195c - firebase_crashlytics: e4f04180f443d5a8b56fbc0685bdbd7d90dd26f0 - firebase_messaging: 15d8b557010f3bb7b98d0302e1c7c8fbcd244425 - FirebaseAnalytics: 27eb78b97880ea4a004839b9bac0b58880f5a92a - FirebaseCore: 3cf438f431f18c12cdf2aaf64434648b63f7e383 - FirebaseCoreExtension: aa5c9779c2d0d39d83f1ceb3fdbafe80c4feecfa - FirebaseCoreInternal: adefedc9a88dbe393c4884640a73ec9e8e790f8c - FirebaseCrashlytics: 745d8f0221fe49c62865391d1bf56f5a12eeec0b - FirebaseInstallations: d0a8fea5a6fa91abc661591cf57c0f0d70863e57 - FirebaseMessaging: d2d1d9c62c46dd2db49a952f7deb5b16ad2c9742 - FirebaseRemoteConfigInterop: abf8b1bbc0bf1b84abd22b66746926410bf91a87 - FirebaseSessions: 78f137e68dc01ca71606169ba4ac73b98c13752a + Amplitude: 184def4f87aa26f94a93a7faa334e06b1cae704d + amplitude_flutter: 0f617f89d62b10c40a0ab14849e660a7c12e4e46 + AnalyticsConnector: a53214d38ae22734c6266106c0492b37832633a9 + Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c + firebase_analytics: fbc57838bdb94eef1e0ff504f127d974ff2981ad + firebase_core: 2bedc3136ec7c7b8561c6123ed0239387b53f2af + firebase_crashlytics: 37d104d457b51760b48504a93a12b3bf70995d77 + firebase_messaging: 15d114e1a41fc31e4fbabcd48d765a19eec94a38 + FirebaseAnalytics: c36efd5710c60c17558650fa58c2066eca7e9265 + FirebaseCore: a282032ae9295c795714ded2ec9c522fc237f8da + FirebaseCoreExtension: 30bb063476ef66cd46925243d64ad8b2c8ac3264 + FirebaseCoreInternal: ac26d09a70c730e497936430af4e60fb0c68ec4e + FirebaseCrashlytics: cfc69af5b53565dc6a5e563788809b5778ac4eac + FirebaseInstallations: 58cf94dabf1e2bb2fa87725a9be5c2249171cda0 + FirebaseMessaging: c9ec7b90c399c7a6100297e9d16f8a27fc7f7152 + FirebaseRemoteConfigInterop: c3a5c31b3c22079f41ba1dc645df889d9ce38cb9 + FirebaseSessions: 655ff17f3cc1a635cbdc2d69b953878001f9e25b Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_inappwebview: 3d32228f1304635e7c028b0d4252937730bbc6cf - flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 + flutter_keyboard_visibility_temp_fork: 442dadca3b81868a225cd6a2f605bffff1215844 flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 + flutter_smartlook: 2f8ffb62fb0197739e3a412ddb4e65eb67d8a237 flutter_web_auth_2: 051cf9f5dc366f31b5dcc4e2952c2b954767be8a - GoogleAppMeasurement: 6e49ffac7d3f2c3ded9cc663f912a13b67bbd0de + GoogleAppMeasurement: 76d4f8b36b03bd8381fa9a7fe2cc7f99c0a2e93a GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 @@ -311,6 +330,7 @@ SPEC CHECKSUMS: permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 + quill_native_bridge: e5afa7d49c08cf68c52a5e23bc272eba6925c622 share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec uni_links: d97da20c7701486ba192624d99bffaaffcfc298a diff --git a/lib/app/app.dart b/lib/app/app.dart index a533e1163..c17756431 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -12,6 +13,7 @@ import 'package:ziggle/app/modules/user/presentation/bloc/auth_bloc.dart'; import 'package:ziggle/app/modules/user/presentation/bloc/developer_option_bloc.dart'; import 'package:ziggle/app/modules/user/presentation/bloc/user_bloc.dart'; import 'package:ziggle/app/router.dart'; +import 'package:ziggle/app/router_observer.dart'; import 'package:ziggle/app/values/palette.dart'; import 'package:ziggle/app/values/theme.dart'; import 'package:ziggle/gen/strings.g.dart'; @@ -31,7 +33,12 @@ class App extends StatelessWidget { onTap: () => FocusManager.instance.primaryFocus?.unfocus(), child: MaterialApp.router( theme: AppTheme.theme, - routerConfig: _appRouter.config(), + routerConfig: _appRouter.config( + navigatorObservers: () => [ + AutoRouteObserver(), + sl(), + ], + ), locale: TranslationProvider.of(context).flutterLocale, supportedLocales: AppLocaleUtils.supportedLocales, localizationsDelegates: GlobalMaterialLocalizations.delegates, diff --git a/lib/app/modules/common/presentation/pages/ziggle_bottom_navigation_page.dart b/lib/app/modules/common/presentation/pages/ziggle_bottom_navigation_page.dart index ddb6ed691..f8a538c74 100644 --- a/lib/app/modules/common/presentation/pages/ziggle_bottom_navigation_page.dart +++ b/lib/app/modules/common/presentation/pages/ziggle_bottom_navigation_page.dart @@ -1,14 +1,28 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_pressable.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/router.gr.dart'; import 'package:ziggle/app/values/palette.dart'; import 'package:ziggle/gen/assets.gen.dart'; @RoutePage() -class ZiggleBottomNavigationPage extends StatelessWidget { +class ZiggleBottomNavigationPage extends StatefulWidget { const ZiggleBottomNavigationPage({super.key}); + @override + State createState() => + _ZiggleBottomNavigationPageState(); +} + +class _ZiggleBottomNavigationPageState extends State + with AutoRouteAwareStateMixin { + AnalyticsEvent _currentEvent = const AnalyticsEvent.feed(); + + @override + void didPopNext() => AnalyticsRepository.pageView(_currentEvent); + @override Widget build(BuildContext context) { return AutoTabsRouter.tabBar( @@ -17,60 +31,76 @@ class ZiggleBottomNavigationPage extends StatelessWidget { CategoryRoute(), ProfileRoute(), ], - builder: (context, child, tabController) => Scaffold( - body: child, - bottomNavigationBar: Container( - decoration: const BoxDecoration( - color: Colors.white, - border: Border(top: BorderSide(color: Palette.grayBorder)), - ), - child: SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 18), - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - BottomNavigationBarItem( - icon: Assets.icons.feed.svg(), - activeIcon: Assets.icons.feedActive.svg(), - ), - BottomNavigationBarItem( - icon: Assets.icons.category.svg(), - activeIcon: Assets.icons.categoryActive.svg(), - ), - BottomNavigationBarItem( - icon: Assets.icons.profile.svg(), - activeIcon: Assets.icons.profileActive.svg(), - ), - ] - .indexed - .map( - (e) => Expanded( - child: ZigglePressable( - onPressed: () => tabController.animateTo(e.$1), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - width: 24, - child: tabController.index == e.$1 - ? e.$2.activeIcon - : e.$2.icon, - ), - ], + builder: (context, child, tabController) { + _currentEvent = const [ + AnalyticsEvent.feed(), + AnalyticsEvent.category(), + AnalyticsEvent.profile(), + ][tabController.index]; + return Scaffold( + body: child, + bottomNavigationBar: Container( + decoration: const BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Palette.grayBorder)), + ), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + BottomNavigationBarItem( + icon: Assets.icons.feed.svg(), + activeIcon: Assets.icons.feedActive.svg(), + ), + BottomNavigationBarItem( + icon: Assets.icons.category.svg(), + activeIcon: Assets.icons.categoryActive.svg(), + ), + BottomNavigationBarItem( + icon: Assets.icons.profile.svg(), + activeIcon: Assets.icons.profileActive.svg(), + ), + ] + .indexed + .map( + (e) => Expanded( + child: ZigglePressable( + onPressed: () { + AnalyticsRepository.click( + [ + const AnalyticsEvent.feed(), + const AnalyticsEvent.category(), + const AnalyticsEvent.profile() + ][e.$1], + ); + tabController.animateTo(e.$1); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 24, + child: tabController.index == e.$1 + ? e.$2.activeIcon + : e.$2.icon, + ), + ], + ), ), ), ), - ), - ) - .toList(), + ) + .toList(), + ), ), ), ), - ), - ), + ); + }, ); } } diff --git a/lib/app/modules/common/presentation/widgets/ziggle_app_bar.dart b/lib/app/modules/common/presentation/widgets/ziggle_app_bar.dart index 730e4742c..4bbbe8538 100644 --- a/lib/app/modules/common/presentation/widgets/ziggle_app_bar.dart +++ b/lib/app/modules/common/presentation/widgets/ziggle_app_bar.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_back_button.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_logo.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; import 'package:ziggle/app/values/palette.dart'; import 'package:ziggle/gen/assets.gen.dart'; @@ -57,11 +58,13 @@ class ZiggleAppBar extends StatelessWidget implements PreferredSizeWidget { required Widget title, List actions = const [], Color? backgroundColor, + required PageSource from, }) => ZiggleAppBar( backgroundColor: backgroundColor, leading: ZiggleBackButton( label: backLabel, + from: from, ), title: title, actions: actions, diff --git a/lib/app/modules/common/presentation/widgets/ziggle_back_button.dart b/lib/app/modules/common/presentation/widgets/ziggle_back_button.dart index afab868d6..46b8fe985 100644 --- a/lib/app/modules/common/presentation/widgets/ziggle_back_button.dart +++ b/lib/app/modules/common/presentation/widgets/ziggle_back_button.dart @@ -1,18 +1,29 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_button.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/values/palette.dart'; import 'package:ziggle/gen/assets.gen.dart'; class ZiggleBackButton extends StatelessWidget { - const ZiggleBackButton({super.key, required this.label}); + const ZiggleBackButton({ + super.key, + required this.label, + this.from = PageSource.unknown, + }); + final PageSource from; final String label; @override Widget build(BuildContext context) { return ZiggleButton.text( - onPressed: () => context.maybePop(), + onPressed: () { + AnalyticsRepository.click(AnalyticsEvent.back(from)); + context.maybePop(); + }, child: Text.rich( TextSpan( children: [ diff --git a/lib/app/modules/core/data/models/analytics_event.dart b/lib/app/modules/core/data/models/analytics_event.dart new file mode 100644 index 000000000..6eadfda47 --- /dev/null +++ b/lib/app/modules/core/data/models/analytics_event.dart @@ -0,0 +1,145 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:ziggle/app/modules/core/domain/enums/language.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; +import 'package:ziggle/app/modules/notices/domain/enums/notice_reaction.dart'; +import 'package:ziggle/app/modules/notices/domain/enums/notice_type.dart'; +import 'package:ziggle/gen/strings.g.dart'; + +part 'analytics_event.freezed.dart'; +part 'analytics_event.g.dart'; + +@freezed +class AnalyticsEvent with _$AnalyticsEvent { + const AnalyticsEvent._(); + factory AnalyticsEvent.fromJson(Map json) => + _$AnalyticsEventFromJson(json); + + // 메인화면 이벤트 + const factory AnalyticsEvent.feed() = _Feed; + const factory AnalyticsEvent.category() = _Category; + const factory AnalyticsEvent.categoryType(NoticeType noticeType) = + _CategoryType; + const factory AnalyticsEvent.list(NoticeType noticeType) = _List; + const factory AnalyticsEvent.profile() = _Profile; + const factory AnalyticsEvent.search([PageSource? from]) = _Search; + const factory AnalyticsEvent.write([PageSource? from]) = _Write; + const factory AnalyticsEvent.back(PageSource from) = _Back; + + // 공지 관련 이벤트 + const factory AnalyticsEvent.notice(int id, [PageSource? from]) = _Notice; + const factory AnalyticsEvent.noticeReaction( + int id, NoticeReaction noticeReaction, PageSource from) = _NoticeReaction; + const factory AnalyticsEvent.noticeShare(int id, PageSource from) = + _NoticeShare; + const factory AnalyticsEvent.noticeCopy(int id) = _NoticeCopy; + const factory AnalyticsEvent.noticeEdit(int id) = _NoticeEdit; + const factory AnalyticsEvent.noticeDelete(int id) = _NoticeDelete; + const factory AnalyticsEvent.noticeSendNotification(int id) = + _NoticeSendNotification; + + // 공지 작성 이벤트 + const factory AnalyticsEvent.writeToggleLanguage(Language lang) = + _WriteToggleLanguage; + const factory AnalyticsEvent.writeAddPhoto() = _WriteAddPhoto; + const factory AnalyticsEvent.writeUseAiTranslation() = _WriteUseAiTranslation; + const factory AnalyticsEvent.writeAbortUseAiTranslation() = + _WriteAbortUseAiTranslation; + const factory AnalyticsEvent.writeUndoUseAiTranslation() = + _WriteUndoUseAiTranslation; + const factory AnalyticsEvent.writeConfig() = _WriteConfig; + const factory AnalyticsEvent.writeConfigChangeAccount() = + _WriteConfigChangeAccount; + const factory AnalyticsEvent.writeConfigChangeAccountTo(int id) = + _WriteConfigChangeAccountTo; + const factory AnalyticsEvent.writeConfigAddDeadline() = + _WriteConfigAddDeadline; + const factory AnalyticsEvent.writeConfigSetDeadline() = + _WriteConfigSetDeadline; + const factory AnalyticsEvent.writeConfigSetDeadlineCancel() = + _WriteConfigSetDeadlineCancel; + const factory AnalyticsEvent.writeConfigChangeDeadline() = + _WriteConfigChangeDeadline; + const factory AnalyticsEvent.writeConfigDeleteDeadline() = + _WriteConfigDeleteDeadline; + const factory AnalyticsEvent.writeConfigCategory(NoticeType noticeType) = + _WriteConfigCategory; + const factory AnalyticsEvent.writeConfigAddHashtag() = _WriteConfigAddHashtag; + const factory AnalyticsEvent.writeConfigDoneHashtag() = + _WriteConfigDoneHashtag; + const factory AnalyticsEvent.writeConfigAddHashtagAutocomplete() = + _WriteConfigAddHashtagAutocomplete; + const factory AnalyticsEvent.writeConfigAddHashtagDelete() = + _WriteConfigAddHashtagDelete; + const factory AnalyticsEvent.writeConfigDeleteHashtag() = + _WriteConfigDeleteHashtag; + const factory AnalyticsEvent.writeConfigPreview() = _WriteConfigPreview; + const factory AnalyticsEvent.writeConfigPublish() = _WriteConfigPublish; + const factory AnalyticsEvent.writeConfigPublishAgree( + String value, String type) = _WriteConfigPublishAgree; + const factory AnalyticsEvent.writeConfigPublishUpload() = + _WriteConfigPublishUpload; + +// 공지 수정 이벤트 + const factory AnalyticsEvent.noticeEditPublish(int id) = _NoticeEditPublish; + const factory AnalyticsEvent.noticeEditBody(int id) = _NoticeEditBody; + const factory AnalyticsEvent.noticeEditBodyToggleLanguage(Language lang) = + _NoticeEditBodyToggleLanguage; + const factory AnalyticsEvent.noticeEditBodyUseAiTranslation() = + _NoticeEditBodyUseAiTranslation; + const factory AnalyticsEvent.noticeEditBodyAbortUseAiTranslation() = + _NoticeEditBodyAbortUseAiTranslation; + const factory AnalyticsEvent.noticeEditBodyUndoUseAiTranslation() = + _NoticeEditBodyUndoUseAiTranslation; + const factory AnalyticsEvent.noticeEditEnglish(int id) = _NoticeEditEnglish; + const factory AnalyticsEvent.noticeEditAdditional(int id) = + _NoticeEditAdditional; + const factory AnalyticsEvent.noticeEditAdditionalToggleLanguage( + Language lang) = _NoticeEditAdditionalToggleLanguage; + const factory AnalyticsEvent.noticeEditAdditionalDone() = + _NoticeEditAdditionalDone; + const factory AnalyticsEvent.noticeEditChangeDeadline([int? id]) = + _NoticeEditChangeDeadline; + const factory AnalyticsEvent.noticeEditSetDeadline() = _NoticeEditSetDeadline; + const factory AnalyticsEvent.noticeEditSetDeadlineCancel() = + _NoticeEditSetDeadlineCancel; + const factory AnalyticsEvent.noticeEditPreview(int id) = _NoticeEditPreview; + +// 프로필 페이지 이벤트 + const factory AnalyticsEvent.profileSetting() = _ProfileSetting; + const factory AnalyticsEvent.profileMyNotices() = _ProfileMyNotices; + const factory AnalyticsEvent.profileFeedback() = _ProfileFeedback; + const factory AnalyticsEvent.profileLogout(PageSource from) = _ProfileLogout; + const factory AnalyticsEvent.profileWithdraw() = _ProfileWithdraw; + const factory AnalyticsEvent.profileLogin(PageSource from) = _ProfileLogin; + const factory AnalyticsEvent.profileSettingEnableNotification() = + _ProfileSettingEnableNotification; + const factory AnalyticsEvent.profileSettingLanguage(AppLocale lang) = + _ProfileSettingLanguage; + const factory AnalyticsEvent.profileSettingInformation() = + _ProfileSettingInformation; + const factory AnalyticsEvent.profileSettingInformationTos() = + _ProfileSettingInformationTos; + const factory AnalyticsEvent.profileSettingInformationPrivacy() = + _ProfileSettingInformationPrivacy; + const factory AnalyticsEvent.profileSettingInformationLicense() = + _ProfileSettingInformationLicense; + + Map get parameters { + final json = toJson(); + json.remove('runtimeType'); + json.removeWhere((key, value) => value == null); + return json.cast(); + } + + String get name { + final json = toJson(); + return _camelToSnake(json['runtimeType']); + } +} + +String _camelToSnake(String text) { + return text.replaceAllMapped( + RegExp(r'[A-Z]'), + (Match match) => '_${match.group(0)!.toLowerCase()}', + ); +} diff --git a/lib/app/modules/core/data/repositories/amplitude_analytics_repository.dart b/lib/app/modules/core/data/repositories/amplitude_analytics_repository.dart new file mode 100644 index 000000000..ddf6e0517 --- /dev/null +++ b/lib/app/modules/core/data/repositories/amplitude_analytics_repository.dart @@ -0,0 +1,44 @@ +import 'package:amplitude_flutter/amplitude.dart'; +import 'package:injectable/injectable.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/enums/event_type.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; +import 'package:ziggle/app/modules/user/domain/entities/user_entity.dart'; +import 'package:ziggle/app/values/strings.dart'; + +@singleton +class AmplitudeAnalyticsRepository implements AnalyticsRepository { + late final _instance = Amplitude.getInstance()..init(Strings.amplitudeApiKey); + + @override + logChangeUser(UserEntity? user) { + if (user == null) { + _instance + ..setUserId(null) + ..clearUserProperties(); + return; + } + _instance + ..setUserId(user.uuid) + ..setUserProperties({ + 'studentId': user.studentId, + 'email': user.email, + }); + } + + @override + logEvent(EventType type, AnalyticsEvent event) { + _instance.logEvent( + '${type.name}_${event.name}', + eventProperties: event.parameters, + ); + } + + @override + logScreen(String screenName) { + _instance.logEvent( + 'screen_view', + eventProperties: {'screenName': screenName}, + ); + } +} diff --git a/lib/app/modules/core/data/repositories/firebase_analytics_repository.dart b/lib/app/modules/core/data/repositories/firebase_analytics_repository.dart new file mode 100644 index 000000000..61cf1cc8c --- /dev/null +++ b/lib/app/modules/core/data/repositories/firebase_analytics_repository.dart @@ -0,0 +1,33 @@ +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:injectable/injectable.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/enums/event_type.dart'; +import 'package:ziggle/app/modules/user/domain/entities/user_entity.dart'; + +import '../../domain/repositories/analytics_repository.dart'; + +@singleton +class FirebaseAnalyticsRepository implements AnalyticsRepository { + static final _analytics = FirebaseAnalytics.instance; + + @override + logChangeUser(UserEntity? user) { + _analytics + ..setUserId(id: user?.uuid) + ..setUserProperty(name: 'studentId', value: user?.studentId) + ..setUserProperty(name: 'email', value: user?.email); + } + + @override + logScreen(String screenName) { + _analytics.logScreenView(screenName: screenName); + } + + @override + logEvent(EventType type, AnalyticsEvent event) { + _analytics.logEvent( + name: '${type.name}_${event.name}', + parameters: event.parameters, + ); + } +} diff --git a/lib/app/modules/core/data/repositories/mock_analytics_repository.dart b/lib/app/modules/core/data/repositories/mock_analytics_repository.dart new file mode 100644 index 000000000..091464d12 --- /dev/null +++ b/lib/app/modules/core/data/repositories/mock_analytics_repository.dart @@ -0,0 +1,25 @@ +import 'dart:developer'; + +import 'package:injectable/injectable.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/enums/event_type.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; +import 'package:ziggle/app/modules/user/domain/entities/user_entity.dart'; + +@Singleton(as: AnalyticsRepository) +@dev +@test +class MockAnalyticsRepository implements AnalyticsRepository { + @override + logChangeUser(UserEntity? user) => + log('MockAnalyticsRepository.logChangeUser, user: $user'); + + @override + logEvent(EventType type, AnalyticsEvent event) => log( + 'MockAnalyticsRepository.logEvent, type: ${type.name}, event: ${event.name}, parameters: ${event.parameters}', + ); + + @override + logScreen(String screenName) => + log('MockAnalyticsRepository.logScreen, screenName: $screenName'); +} diff --git a/lib/app/modules/core/data/repositories/multiple_analytics_repository.dart b/lib/app/modules/core/data/repositories/multiple_analytics_repository.dart new file mode 100644 index 000000000..897120c7c --- /dev/null +++ b/lib/app/modules/core/data/repositories/multiple_analytics_repository.dart @@ -0,0 +1,55 @@ +import 'package:injectable/injectable.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/data/repositories/amplitude_analytics_repository.dart'; +import 'package:ziggle/app/modules/core/data/repositories/firebase_analytics_repository.dart'; +import 'package:ziggle/app/modules/core/data/repositories/smartlook_analytics_repository.dart'; +import 'package:ziggle/app/modules/core/domain/enums/event_type.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; +import 'package:ziggle/app/modules/user/domain/entities/user_entity.dart'; + +@Singleton(as: AnalyticsRepository) +@prod +class MultipleAnalyticsRepository implements AnalyticsRepository { + final FirebaseAnalyticsRepository _firebaseAnalyticsRepository; + final AmplitudeAnalyticsRepository _amplitudeAnalyticsRepository; + final SmartlookAnalyticsRepository _smartlookAnalyticsRepository; + + late final _repositories = [ + _firebaseAnalyticsRepository, + _amplitudeAnalyticsRepository, + _smartlookAnalyticsRepository, + ]; + + MultipleAnalyticsRepository( + this._firebaseAnalyticsRepository, + this._amplitudeAnalyticsRepository, + this._smartlookAnalyticsRepository, + ); + + @override + logChangeUser(UserEntity? user) { + for (final repository in _repositories) { + try { + repository.logChangeUser(user); + } catch (_) {} + } + } + + @override + logEvent(EventType type, AnalyticsEvent event) { + for (final repository in _repositories) { + try { + repository.logEvent(type, event); + } catch (_) {} + } + } + + @override + logScreen(String screenName) { + for (final repository in _repositories) { + try { + repository.logScreen(screenName); + } catch (_) {} + } + } +} diff --git a/lib/app/modules/core/data/repositories/smartlook_analytics_repository.dart b/lib/app/modules/core/data/repositories/smartlook_analytics_repository.dart new file mode 100644 index 000000000..b1a814156 --- /dev/null +++ b/lib/app/modules/core/data/repositories/smartlook_analytics_repository.dart @@ -0,0 +1,33 @@ +import 'package:flutter_smartlook/flutter_smartlook.dart'; +import 'package:injectable/injectable.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/enums/event_type.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; +import 'package:ziggle/app/modules/user/domain/entities/user_entity.dart'; + +@singleton +class SmartlookAnalyticsRepository implements AnalyticsRepository { + final _instance = Smartlook.instance; + + @override + logChangeUser(UserEntity? user) { + if (user == null) return; + _instance.user + ..setIdentifier(user.uuid) + ..setEmail(user.email); + } + + @override + logEvent(EventType type, AnalyticsEvent event) { + final properties = Properties(); + event.parameters.forEach((key, value) { + properties.putString(key, value: value.toString()); + }); + _instance.trackEvent('${type.name}_${event.name}', properties: properties); + } + + @override + logScreen(String screenName) { + _instance.trackNavigationEnter(screenName); + } +} diff --git a/lib/app/modules/core/domain/enums/event_type.dart b/lib/app/modules/core/domain/enums/event_type.dart new file mode 100644 index 000000000..b2191a58c --- /dev/null +++ b/lib/app/modules/core/domain/enums/event_type.dart @@ -0,0 +1,6 @@ +enum EventType { + click, + pageview, + action, + view, +} diff --git a/lib/app/modules/core/domain/enums/page_source.dart b/lib/app/modules/core/domain/enums/page_source.dart new file mode 100644 index 000000000..af0aadb13 --- /dev/null +++ b/lib/app/modules/core/domain/enums/page_source.dart @@ -0,0 +1,20 @@ +enum PageSource { + category, + detail, + feed, + list, + profile, + search, + setting, + settingInformation, + settingInformationLicense, + noticeEdit, + noticeEditBody, + noticeEditAdditional, + noticeEditPreview, + write, + writeConfig, + writeConfigAddHashtag, + writeConfigPreview, + unknown, +} diff --git a/lib/app/modules/core/domain/repositories/analytics_repository.dart b/lib/app/modules/core/domain/repositories/analytics_repository.dart new file mode 100644 index 000000000..5641569dc --- /dev/null +++ b/lib/app/modules/core/domain/repositories/analytics_repository.dart @@ -0,0 +1,21 @@ +import 'package:ziggle/app/di/locator.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/enums/event_type.dart'; +import 'package:ziggle/app/modules/user/domain/entities/user_entity.dart'; + +abstract class AnalyticsRepository { + logChangeUser(UserEntity? user); + logScreen(String screenName); + logEvent(EventType type, AnalyticsEvent event); + + static AnalyticsRepository get _instance => sl(); + + static void click(AnalyticsEvent event) => + _instance.logEvent(EventType.click, event); + static void pageView(AnalyticsEvent event) => + _instance.logEvent(EventType.pageview, event); + static void view(AnalyticsEvent event) => + _instance.logEvent(EventType.view, event); + static void action(AnalyticsEvent event) => + _instance.logEvent(EventType.action, event); +} diff --git a/lib/app/modules/groups/presentation/layouts/group_creation_layout.dart b/lib/app/modules/groups/presentation/layouts/group_creation_layout.dart index 3d74b4f42..ebb70ffc1 100644 --- a/lib/app/modules/groups/presentation/layouts/group_creation_layout.dart +++ b/lib/app/modules/groups/presentation/layouts/group_creation_layout.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_app_bar.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_progress_bar.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; import 'package:ziggle/app/values/palette.dart'; import 'package:ziggle/gen/strings.g.dart'; @@ -28,6 +29,7 @@ class GroupCreationLayout extends StatelessWidget { color: Colors.transparent, child: ZiggleAppBar.compact( backLabel: context.t.common.cancel, + from: PageSource.unknown, title: Text(context.t.group.creation.title), ), ), diff --git a/lib/app/modules/groups/presentation/pages/group_management_main_page.dart b/lib/app/modules/groups/presentation/pages/group_management_main_page.dart index 0ef286da5..ab7600f6e 100644 --- a/lib/app/modules/groups/presentation/pages/group_management_main_page.dart +++ b/lib/app/modules/groups/presentation/pages/group_management_main_page.dart @@ -1,6 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_app_bar.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; import 'package:ziggle/app/values/palette.dart'; import 'package:ziggle/gen/assets.gen.dart'; import 'package:ziggle/gen/strings.g.dart'; @@ -14,6 +15,7 @@ class GroupManagementMainPage extends StatelessWidget { return Scaffold( appBar: ZiggleAppBar.compact( backLabel: context.t.user.myInfo, + from: PageSource.unknown, title: Text(context.t.group.managementMain.header), actions: [ GestureDetector( diff --git a/lib/app/modules/notices/presentation/bloc/notice_bloc.dart b/lib/app/modules/notices/presentation/bloc/notice_bloc.dart index 5e78d2812..3b7b52a31 100644 --- a/lib/app/modules/notices/presentation/bloc/notice_bloc.dart +++ b/lib/app/modules/notices/presentation/bloc/notice_bloc.dart @@ -1,6 +1,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:injectable/injectable.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/enums/event_type.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/modules/notices/domain/entities/notice_entity.dart'; import 'package:ziggle/app/modules/notices/domain/enums/notice_reaction.dart'; import 'package:ziggle/app/modules/notices/domain/repositories/notice_repository.dart'; @@ -10,8 +13,10 @@ part 'notice_bloc.freezed.dart'; @injectable class NoticeBloc extends Bloc { final NoticeRepository _repository; + final AnalyticsRepository _analyticsRepository; - NoticeBloc(this._repository) : super(const _Initial()) { + NoticeBloc(this._repository, this._analyticsRepository) + : super(const _Initial()) { on<_Load>((event, emit) async { emit(_Loaded(event.entity)); emit(_Loaded(await _repository.getNotice(event.entity.id))); @@ -19,12 +24,20 @@ class NoticeBloc extends Bloc { on<_SendNotification>((event, emit) async { if (state.entity == null) return; emit(_Loading(state.entity!.copyWith(publishedAt: DateTime.now()))); + _analyticsRepository.logEvent( + EventType.action, + AnalyticsEvent.noticeSendNotification(state.entity!.id), + ); final entity = await _repository.sendNotification(state.entity!.id); emit(_Loaded(entity)); }); on<_Delete>((event, emit) async { if (state.entity == null) return; emit(_Loading(state.entity!)); + _analyticsRepository.logEvent( + EventType.action, + AnalyticsEvent.noticeDelete(state.entity!.id), + ); await _repository.deleteNotice(state.entity!.id); emit(const _Deleted()); }); diff --git a/lib/app/modules/notices/presentation/layouts/single_notice_shell_layout.dart b/lib/app/modules/notices/presentation/layouts/single_notice_shell_layout.dart index 782c8446e..9568423b4 100644 --- a/lib/app/modules/notices/presentation/layouts/single_notice_shell_layout.dart +++ b/lib/app/modules/notices/presentation/layouts/single_notice_shell_layout.dart @@ -21,7 +21,12 @@ class SingleNoticeShellLayout extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (_) => sl()..add(NoticeEvent.load(notice)), - child: const AutoRouter(), + child: BlocBuilder(builder: (context, state) { + if (state.entity == null) { + return const Center(child: CircularProgressIndicator()); + } + return const AutoRouter(); + }), ); } } diff --git a/lib/app/modules/notices/presentation/pages/category_page.dart b/lib/app/modules/notices/presentation/pages/category_page.dart index 88c01bb80..ad93c1014 100644 --- a/lib/app/modules/notices/presentation/pages/category_page.dart +++ b/lib/app/modules/notices/presentation/pages/category_page.dart @@ -3,6 +3,9 @@ import 'package:flutter/material.dart'; import 'package:ziggle/app/modules/common/presentation/extensions/toast.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_app_bar.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_pressable.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/modules/notices/domain/enums/notice_type.dart'; import 'package:ziggle/app/modules/user/presentation/bloc/user_bloc.dart'; import 'package:ziggle/app/router.gr.dart'; @@ -10,16 +13,32 @@ import 'package:ziggle/app/values/palette.dart'; import 'package:ziggle/gen/strings.g.dart'; @RoutePage() -class CategoryPage extends StatelessWidget { +class CategoryPage extends StatefulWidget { const CategoryPage({super.key}); + @override + State createState() => _CategoryPageState(); +} + +class _CategoryPageState extends State + with AutoRouteAwareStateMixin { + @override + void didChangeTabRoute(previousRoute) => + AnalyticsRepository.pageView(const AnalyticsEvent.category()); + @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Palette.grayLight, appBar: ZiggleAppBar.main( - onTapSearch: () => const SearchRoute().push(context), + onTapSearch: () { + AnalyticsRepository.click( + const AnalyticsEvent.search(PageSource.category)); + const SearchRoute().push(context); + }, onTapWrite: () { + AnalyticsRepository.click( + const AnalyticsEvent.write(PageSource.category)); if (UserBloc.userOrNull(context) == null) { return context.showToast( context.t.user.login.description, @@ -54,8 +73,12 @@ class CategoryPage extends StatelessWidget { if (category.$1 != 0) const SizedBox(width: 10), Expanded( child: ZigglePressable( - onPressed: () => ListRoute(type: category.$2) - .push(context), + onPressed: () { + AnalyticsRepository.click( + AnalyticsEvent.categoryType( + category.$2)); + ListRoute(type: category.$2).push(context); + }, decoration: BoxDecoration( color: category.$2.backgroundColor, borderRadius: const BorderRadius.all( diff --git a/lib/app/modules/notices/presentation/pages/detail_page.dart b/lib/app/modules/notices/presentation/pages/detail_page.dart index 85ed727b2..3b9c6f562 100644 --- a/lib/app/modules/notices/presentation/pages/detail_page.dart +++ b/lib/app/modules/notices/presentation/pages/detail_page.dart @@ -2,19 +2,38 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_app_bar.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/modules/notices/presentation/bloc/notice_bloc.dart'; import 'package:ziggle/app/modules/notices/presentation/widgets/notice_renderer.dart'; import 'package:ziggle/gen/strings.g.dart'; @RoutePage() -class DetailPage extends StatelessWidget { +class DetailPage extends StatefulWidget { const DetailPage({super.key}); + @override + State createState() => _DetailPageState(); +} + +class _DetailPageState extends State + with AutoRouteAwareStateMixin { + @override + void didPush() => AnalyticsRepository.pageView(AnalyticsEvent.notice( + context.read().state.entity!.id, + )); + @override + void didPopNext() => AnalyticsRepository.pageView(AnalyticsEvent.notice( + context.read().state.entity!.id, + )); + @override Widget build(BuildContext context) { return Scaffold( appBar: ZiggleAppBar.compact( backLabel: context.t.notice.detail.back, + from: PageSource.detail, title: Text(context.t.notice.detail.title), ), body: const _Layout(), diff --git a/lib/app/modules/notices/presentation/pages/feed_page.dart b/lib/app/modules/notices/presentation/pages/feed_page.dart index a16c01c2a..622bb227d 100644 --- a/lib/app/modules/notices/presentation/pages/feed_page.dart +++ b/lib/app/modules/notices/presentation/pages/feed_page.dart @@ -4,6 +4,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ziggle/app/di/locator.dart'; import 'package:ziggle/app/modules/common/presentation/extensions/toast.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_app_bar.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/modules/notices/domain/enums/notice_type.dart'; import 'package:ziggle/app/modules/notices/presentation/bloc/notice_list_bloc.dart'; import 'package:ziggle/app/modules/notices/presentation/widgets/list_layout.dart'; @@ -13,16 +16,35 @@ import 'package:ziggle/app/values/palette.dart'; import 'package:ziggle/gen/strings.g.dart'; @RoutePage() -class FeedPage extends StatelessWidget { +class FeedPage extends StatefulWidget { const FeedPage({super.key}); + @override + State createState() => _FeedPageState(); +} + +class _FeedPageState extends State + with AutoRouteAwareStateMixin { + @override + void didInitTabRoute(previousRoute) => + AnalyticsRepository.pageView(const AnalyticsEvent.feed()); + @override + void didChangeTabRoute(previousRoute) => + AnalyticsRepository.pageView(const AnalyticsEvent.feed()); + @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Palette.grayLight, appBar: ZiggleAppBar.main( - onTapSearch: () => const SearchRoute().push(context), + onTapSearch: () { + AnalyticsRepository.click( + const AnalyticsEvent.search(PageSource.feed)); + const SearchRoute().push(context); + }, onTapWrite: () { + AnalyticsRepository.click( + const AnalyticsEvent.write(PageSource.feed)); if (UserBloc.userOrNull(context) == null) { return context.showToast( context.t.user.login.description, @@ -34,7 +56,9 @@ class FeedPage extends StatelessWidget { body: BlocProvider( create: (_) => sl() ..add(const NoticeListEvent.load(NoticeType.all)), - child: const ListLayout(), + child: const ListLayout( + noticeType: NoticeType.all, + ), ), ); } diff --git a/lib/app/modules/notices/presentation/pages/list_page.dart b/lib/app/modules/notices/presentation/pages/list_page.dart index 4a9b9bb88..e72835930 100644 --- a/lib/app/modules/notices/presentation/pages/list_page.dart +++ b/lib/app/modules/notices/presentation/pages/list_page.dart @@ -3,6 +3,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ziggle/app/di/locator.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_app_bar.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/modules/notices/domain/enums/notice_type.dart'; import 'package:ziggle/app/modules/notices/presentation/bloc/notice_list_bloc.dart'; import 'package:ziggle/app/modules/notices/presentation/widgets/list_layout.dart'; @@ -10,22 +13,39 @@ import 'package:ziggle/app/values/palette.dart'; import 'package:ziggle/gen/strings.g.dart'; @RoutePage() -class ListPage extends StatelessWidget { +class ListPage extends StatefulWidget { const ListPage({super.key, required this.type}); final NoticeType type; + @override + State createState() => _ListPageState(); +} + +class _ListPageState extends State + with AutoRouteAwareStateMixin { + @override + void didPush() => + AnalyticsRepository.pageView(AnalyticsEvent.list(widget.type)); + @override + void didPopNext() => + AnalyticsRepository.pageView(AnalyticsEvent.list(widget.type)); + @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Palette.grayLight, appBar: ZiggleAppBar.compact( backLabel: context.t.notice.list, - title: Text(type.getName(context)), + from: PageSource.list, + title: Text(widget.type.getName(context)), ), body: BlocProvider( - create: (_) => sl()..add(NoticeListEvent.load(type)), - child: const ListLayout(), + create: (_) => + sl()..add(NoticeListEvent.load(widget.type)), + child: ListLayout( + noticeType: widget.type, + ), ), ); } diff --git a/lib/app/modules/notices/presentation/pages/notice_edit_body_page.dart b/lib/app/modules/notices/presentation/pages/notice_edit_body_page.dart index 3850a9320..2867faa64 100644 --- a/lib/app/modules/notices/presentation/pages/notice_edit_body_page.dart +++ b/lib/app/modules/notices/presentation/pages/notice_edit_body_page.dart @@ -8,9 +8,11 @@ import 'package:ziggle/app/di/locator.dart'; import 'package:ziggle/app/modules/common/presentation/extensions/toast.dart'; import 'package:ziggle/app/modules/common/presentation/functions/noop.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_app_bar.dart'; -import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_back_button.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_button.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; import 'package:ziggle/app/modules/core/domain/enums/language.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/modules/notices/domain/entities/notice_entity.dart'; import 'package:ziggle/app/modules/notices/presentation/bloc/ai_bloc.dart'; import 'package:ziggle/app/modules/notices/presentation/bloc/notice_bloc.dart'; @@ -26,11 +28,25 @@ import 'package:ziggle/gen/assets.gen.dart'; import 'package:ziggle/gen/strings.g.dart'; @RoutePage() -class NoticeEditBodyPage extends StatelessWidget { +class NoticeEditBodyPage extends StatefulWidget { const NoticeEditBodyPage({super.key, this.showEnglish = false}); final bool showEnglish; + @override + State createState() => _NoticeEditBodyPageState(); +} + +class _NoticeEditBodyPageState extends State + with AutoRouteAwareStateMixin { + @override + void didPush() => AnalyticsRepository.pageView(AnalyticsEvent.noticeEditBody( + context.read().state.entity!.id)); + @override + void didPopNext() => + AnalyticsRepository.pageView(AnalyticsEvent.noticeEditBody( + context.read().state.entity!.id)); + @override Widget build(BuildContext context) { return BlocProvider( @@ -39,7 +55,7 @@ class NoticeEditBodyPage extends StatelessWidget { listener: (context, state) => state.mapOrNull( error: (error) => context.showToast(error.message), ), - child: _Layout(showEnglish: showEnglish), + child: _Layout(showEnglish: widget.showEnglish), ), ); } @@ -151,8 +167,9 @@ class _LayoutState extends State<_Layout> with SingleTickerProviderStateMixin { .trim() .isEmpty)); return Scaffold( - appBar: ZiggleAppBar( - leading: ZiggleBackButton(label: context.t.common.cancel), + appBar: ZiggleAppBar.compact( + backLabel: context.t.common.cancel, + from: PageSource.noticeEditBody, title: Text( _tabController.index == 0 ? context.t.notice.edit.title @@ -211,7 +228,13 @@ class _LayoutState extends State<_Layout> with SingleTickerProviderStateMixin { child: Row( children: [ LanguageToggle( - onToggle: (v) => _tabController.animateTo(v ? 1 : 0), + onToggle: (v) { + AnalyticsRepository.click( + AnalyticsEvent.noticeEditBodyToggleLanguage( + v ? Language.en : Language.ko), + ); + _tabController.animateTo(v ? 1 : 0); + }, value: _tabController.index != 0), ], ), @@ -239,11 +262,14 @@ class _LayoutState extends State<_Layout> with SingleTickerProviderStateMixin { ), Editor( titleDisabled: _prevNotice.contents[Language.en] != null, - onTranslate: _englishBodyController.plainTextEditingValue.text - .trim() - .isNotEmpty - ? null - : _translate, + onTranslate: () { + AnalyticsRepository.click( + const AnalyticsEvent.noticeEditBodyUseAiTranslation()); + if (_englishBodyController.plainTextEditingValue.text + .trim() + .isNotEmpty) return; + _translate(); + }, titleFocusNode: _englishTitleFocusNode, bodyFocusNode: _englishBodyFocusNode, titleController: _englishTitleController, @@ -263,7 +289,11 @@ class _LayoutState extends State<_Layout> with SingleTickerProviderStateMixin { )); final result = await blocker; result.mapOrNull( - loaded: (result) => _englishBodyController.html = result.body, + loaded: (result) { + AnalyticsRepository.action( + const AnalyticsEvent.noticeEditBodyUseAiTranslation()); + return _englishBodyController.html = result.body; + }, ); } diff --git a/lib/app/modules/notices/presentation/pages/notice_edit_page.dart b/lib/app/modules/notices/presentation/pages/notice_edit_page.dart index fb5bf76f5..b3f3354c0 100644 --- a/lib/app/modules/notices/presentation/pages/notice_edit_page.dart +++ b/lib/app/modules/notices/presentation/pages/notice_edit_page.dart @@ -5,7 +5,10 @@ import 'package:ziggle/app/modules/common/presentation/extensions/toast.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_app_bar.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_button.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_pressable.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; import 'package:ziggle/app/modules/core/domain/enums/language.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/modules/notices/domain/entities/notice_entity.dart'; import 'package:ziggle/app/modules/notices/presentation/bloc/notice_bloc.dart'; import 'package:ziggle/app/modules/notices/presentation/bloc/notice_write_bloc.dart'; @@ -16,9 +19,22 @@ import 'package:ziggle/gen/assets.gen.dart'; import 'package:ziggle/gen/strings.g.dart'; @RoutePage() -class NoticeEditPage extends StatelessWidget { +class NoticeEditPage extends StatefulWidget { const NoticeEditPage({super.key}); + @override + State createState() => _NoticeEditPageState(); +} + +class _NoticeEditPageState extends State + with AutoRouteAwareStateMixin { + @override + void didPush() => AnalyticsRepository.pageView( + AnalyticsEvent.noticeEdit(context.read().state.entity!.id)); + @override + void didPopNext() => AnalyticsRepository.pageView( + AnalyticsEvent.noticeEdit(context.read().state.entity!.id)); + Future _publish(BuildContext context) async { final bloc = context.read(); final blocker = bloc.stream.firstWhere((state) => state.hasResult); @@ -41,12 +57,17 @@ class NoticeEditPage extends StatelessWidget { return Scaffold( appBar: ZiggleAppBar.compact( backLabel: context.t.common.cancel, + from: PageSource.noticeEdit, title: Text(context.t.notice.edit.title), actions: [ BlocBuilder( builder: (context, state) => ZiggleButton.text( disabled: !state.hasChanging, - onPressed: () => _publish(context), + onPressed: () { + AnalyticsRepository.click(AnalyticsEvent.noticeEditPublish( + context.read().state.entity!.id)); + _publish(context); + }, child: Text(context.t.notice.write.publish), ), ), @@ -69,8 +90,11 @@ class NoticeEditPage extends StatelessWidget { disabled: !state.isLoaded || state.entity!.isPublished, icon: Assets.icons.body, title: context.t.notice.edit.editBody, - onPressed: () => - NoticeEditBodyRoute(showEnglish: false).push(context), + onPressed: () { + AnalyticsRepository.click(AnalyticsEvent.noticeEditBody( + context.read().state.entity!.id)); + NoticeEditBodyRoute(showEnglish: false).push(context); + }, ), const SizedBox(height: 10), _ActionButton( @@ -78,21 +102,32 @@ class NoticeEditPage extends StatelessWidget { state.entity!.contents[Language.en] != null, icon: Assets.icons.language, title: context.t.notice.edit.addEnglish, - onPressed: () => - NoticeEditBodyRoute(showEnglish: true).push(context), + onPressed: () { + AnalyticsRepository.click(AnalyticsEvent.noticeEditEnglish( + context.read().state.entity!.id)); + NoticeEditBodyRoute(showEnglish: true).push(context); + }, ), const SizedBox(height: 10), _ActionButton( disabled: false, icon: Assets.icons.add, title: context.t.notice.edit.additional.action, - onPressed: () => - const WriteAdditionalNoticeRoute().push(context), + onPressed: () { + AnalyticsRepository.click( + AnalyticsEvent.noticeEditAdditional( + context.read().state.entity!.id)); + const WriteAdditionalNoticeRoute().push(context); + }, ), const SizedBox(height: 25), ZiggleButton.cta( emphasize: false, - onPressed: () => const NoticeEditPreviewRoute().push(context), + onPressed: () { + AnalyticsRepository.click(AnalyticsEvent.noticeEditPreview( + context.read().state.entity!.id)); + const NoticeEditPreviewRoute().push(context); + }, child: Text(context.t.notice.write.preview), ), ], diff --git a/lib/app/modules/notices/presentation/pages/notice_edit_preview_page.dart b/lib/app/modules/notices/presentation/pages/notice_edit_preview_page.dart index 7bf0ea04e..5c5f075dd 100644 --- a/lib/app/modules/notices/presentation/pages/notice_edit_preview_page.dart +++ b/lib/app/modules/notices/presentation/pages/notice_edit_preview_page.dart @@ -2,7 +2,9 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_app_bar.dart'; -import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_back_button.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/modules/notices/domain/entities/notice_entity.dart'; import 'package:ziggle/app/modules/notices/presentation/bloc/notice_bloc.dart'; import 'package:ziggle/app/modules/notices/presentation/bloc/notice_write_bloc.dart'; @@ -10,14 +12,30 @@ import 'package:ziggle/app/modules/notices/presentation/widgets/notice_renderer. import 'package:ziggle/gen/strings.g.dart'; @RoutePage() -class NoticeEditPreviewPage extends StatelessWidget { +class NoticeEditPreviewPage extends StatefulWidget { const NoticeEditPreviewPage({super.key}); + @override + State createState() => _NoticeEditPreviewPageState(); +} + +class _NoticeEditPreviewPageState extends State + with AutoRouteAwareStateMixin { + @override + void didPush() => + AnalyticsRepository.pageView(AnalyticsEvent.noticeEditPreview( + context.read().state.entity!.id)); + @override + void didPopNext() => + AnalyticsRepository.pageView(AnalyticsEvent.noticeEditPreview( + context.read().state.entity!.id)); + @override Widget build(BuildContext context) { return Scaffold( - appBar: ZiggleAppBar( - leading: ZiggleBackButton(label: context.t.notice.write.configTitle), + appBar: ZiggleAppBar.compact( + backLabel: context.t.notice.write.configTitle, + from: PageSource.noticeEditPreview, title: Text(context.t.notice.write.preview), ), body: Builder( diff --git a/lib/app/modules/notices/presentation/pages/notice_write_body_page.dart b/lib/app/modules/notices/presentation/pages/notice_write_body_page.dart index 0e2c8c5e6..63c803e19 100644 --- a/lib/app/modules/notices/presentation/pages/notice_write_body_page.dart +++ b/lib/app/modules/notices/presentation/pages/notice_write_body_page.dart @@ -12,9 +12,11 @@ import 'package:ziggle/app/di/locator.dart'; import 'package:ziggle/app/modules/common/presentation/extensions/toast.dart'; import 'package:ziggle/app/modules/common/presentation/functions/noop.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_app_bar.dart'; -import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_back_button.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_button.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; import 'package:ziggle/app/modules/core/domain/enums/language.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/modules/notices/presentation/bloc/ai_bloc.dart'; import 'package:ziggle/app/modules/notices/presentation/bloc/notice_write_bloc.dart'; import 'package:ziggle/app/modules/notices/presentation/extensions/quill.dart'; @@ -28,9 +30,21 @@ import 'package:ziggle/gen/assets.gen.dart'; import 'package:ziggle/gen/strings.g.dart'; @RoutePage() -class NoticeWriteBodyPage extends StatelessWidget { +class NoticeWriteBodyPage extends StatefulWidget { const NoticeWriteBodyPage({super.key}); + @override + State createState() => _NoticeWriteBodyPageState(); +} + +class _NoticeWriteBodyPageState extends State + with AutoRouteAwareStateMixin { + @override + void didPush() => AnalyticsRepository.pageView(const AnalyticsEvent.write()); + @override + void didPopNext() => + AnalyticsRepository.pageView(const AnalyticsEvent.write()); + @override Widget build(BuildContext context) { return BlocProvider( @@ -129,13 +143,17 @@ class _LayoutState extends State<_Layout> with SingleTickerProviderStateMixin { .trim() .isEmpty)); return Scaffold( - appBar: ZiggleAppBar( - leading: ZiggleBackButton(label: context.t.common.cancel), + appBar: ZiggleAppBar.compact( + backLabel: context.t.common.cancel, + from: PageSource.write, title: Text(context.t.notice.write.title), actions: [ ZiggleButton.text( disabled: actionDisabled, - onPressed: actionDisabled ? null : _next, + onPressed: () { + AnalyticsRepository.click(const AnalyticsEvent.writeConfig()); + if (!actionDisabled) _next(); + }, child: Text( context.t.common.done, style: const TextStyle( @@ -176,7 +194,13 @@ class _LayoutState extends State<_Layout> with SingleTickerProviderStateMixin { child: Row( children: [ LanguageToggle( - onToggle: (v) => _tabController.animateTo(v ? 1 : 0), + onToggle: (v) { + AnalyticsRepository.click( + AnalyticsEvent.writeToggleLanguage( + v ? Language.en : Language.ko), + ); + _tabController.animateTo(v ? 1 : 0); + }, value: _tabController.index != 0), ], ), @@ -196,11 +220,18 @@ class _LayoutState extends State<_Layout> with SingleTickerProviderStateMixin { if (index == _photos.length) { return GestureDetector( onTap: () async { + AnalyticsRepository.click( + const AnalyticsEvent.writeAddPhoto()); final images = await ImagePicker().pickMultiImage(); if (!mounted) return; - setState(() => _photos.addAll( - images.map((e) => File(e.path)), - )); + setState(() { + _photos.addAll( + images.map((e) => File(e.path)), + ); + AnalyticsRepository.action( + const AnalyticsEvent.writeAddPhoto(), + ); + }); }, child: DottedBorder( color: Palette.gray, @@ -264,11 +295,14 @@ class _LayoutState extends State<_Layout> with SingleTickerProviderStateMixin { BlocBuilder( builder: (context, state) => Editor( translating: state.isLoading, - onTranslate: _englishBodyController.plainTextEditingValue.text - .trim() - .isNotEmpty - ? null - : _translate, + onTranslate: () { + if (_englishBodyController.plainTextEditingValue.text + .trim() + .isNotEmpty) return; + AnalyticsRepository.click( + const AnalyticsEvent.writeUseAiTranslation()); + _translate(); + }, titleFocusNode: _englishTitleFocusNode, bodyFocusNode: _englishBodyFocusNode, titleController: _englishTitleController, diff --git a/lib/app/modules/notices/presentation/pages/notice_write_config_page.dart b/lib/app/modules/notices/presentation/pages/notice_write_config_page.dart index efbe07bbd..70881dd1f 100644 --- a/lib/app/modules/notices/presentation/pages/notice_write_config_page.dart +++ b/lib/app/modules/notices/presentation/pages/notice_write_config_page.dart @@ -7,6 +7,9 @@ import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_bottom_she import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_button.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_pressable.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_toggle_button.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/modules/notices/domain/enums/notice_type.dart'; import 'package:ziggle/app/modules/notices/presentation/bloc/notice_write_bloc.dart'; import 'package:ziggle/app/modules/notices/presentation/widgets/deadline_selector.dart'; @@ -24,7 +27,15 @@ class NoticeWriteConfigPage extends StatefulWidget { State createState() => _NoticeWriteConfigPageState(); } -class _NoticeWriteConfigPageState extends State { +class _NoticeWriteConfigPageState extends State + with AutoRouteAwareStateMixin { + @override + void didPush() => + AnalyticsRepository.pageView(const AnalyticsEvent.writeConfig()); + @override + void didPopNext() => + AnalyticsRepository.pageView(const AnalyticsEvent.writeConfig()); + DateTime? _deadline; NoticeType? _type; final List _tags = []; @@ -53,11 +64,18 @@ class _NoticeWriteConfigPageState extends State { return Scaffold( appBar: ZiggleAppBar.compact( backLabel: context.t.common.cancel, + from: PageSource.writeConfig, title: Text(context.t.notice.write.configTitle), actions: [ ZiggleButton.text( disabled: _type == null, - onPressed: _type == null ? null : _publish, + onPressed: () { + AnalyticsRepository.click( + const AnalyticsEvent.writeConfigPublish()); + if (_type != null) { + _publish(); + } + }, child: Text( context.t.notice.write.publish, style: const TextStyle( @@ -83,7 +101,13 @@ class _NoticeWriteConfigPageState extends State { ZiggleButton.cta( disabled: _type == null, emphasize: false, - onPressed: _type == null ? null : _preview, + onPressed: () { + AnalyticsRepository.click( + const AnalyticsEvent.writeConfigPreview()); + if (_type != null) { + _preview(); + } + }, child: Text(context.t.notice.write.preview), ), ], @@ -130,13 +154,18 @@ class _NoticeWriteConfigPageState extends State { value: _deadline != null, onToggle: (v) async { if (_deadline != null) { + AnalyticsRepository.click( + const AnalyticsEvent.writeConfigDeleteDeadline()); setState(() => _deadline = null); return; } + AnalyticsRepository.click( + const AnalyticsEvent.writeConfigAddDeadline()); final dateTime = await ZiggleBottomSheet.show( context: context, title: context.t.notice.write.deadline.title, builder: (context) => DeadlineSelector( + isEditMode: false, onChanged: (v) => Navigator.pop(context, v), ), ); @@ -150,10 +179,13 @@ class _NoticeWriteConfigPageState extends State { const SizedBox(height: 10), ZigglePressable( onPressed: () async { + AnalyticsRepository.click( + const AnalyticsEvent.writeConfigChangeDeadline()); final dateTime = await ZiggleBottomSheet.show( context: context, title: context.t.notice.write.deadline.title, builder: (context) => DeadlineSelector( + isEditMode: false, initialDateTime: _deadline, onChanged: (v) => Navigator.pop(context, v), ), @@ -220,7 +252,11 @@ class _NoticeWriteConfigPageState extends State { child: AspectRatio( aspectRatio: 1, child: ZigglePressable( - onPressed: () => setState(() => _type = e.$2), + onPressed: () { + AnalyticsRepository.click( + AnalyticsEvent.writeConfigCategory(e.$2)); + setState(() => _type = e.$2); + }, decoration: BoxDecoration( color: _type == e.$2 ? Palette.black @@ -304,6 +340,8 @@ class _NoticeWriteConfigPageState extends State { const SizedBox(height: 10), ZigglePressable( onPressed: () async { + AnalyticsRepository.click( + const AnalyticsEvent.writeConfigAddHashtag()); final tags = await const NoticeWriteSelectTagsRoute() .push>(context); if (!mounted || tags == null) return; @@ -344,7 +382,11 @@ class _NoticeWriteConfigPageState extends State { (tag) => Tag( tag: tag.$2, onDelete: true, - onPressed: () => setState(() => _tags.removeAt(tag.$1)), + onPressed: () { + AnalyticsRepository.click( + const AnalyticsEvent.writeConfigDeleteHashtag()); + setState(() => _tags.removeAt(tag.$1)); + }, ), ) .toList(), diff --git a/lib/app/modules/notices/presentation/pages/notice_write_consent_page.dart b/lib/app/modules/notices/presentation/pages/notice_write_consent_page.dart index 9c1048331..5699f7d0b 100644 --- a/lib/app/modules/notices/presentation/pages/notice_write_consent_page.dart +++ b/lib/app/modules/notices/presentation/pages/notice_write_consent_page.dart @@ -4,6 +4,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ziggle/app/modules/common/presentation/extensions/toast.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_button.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_pressable.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/modules/notices/presentation/bloc/notice_write_bloc.dart'; import 'package:ziggle/app/modules/notices/presentation/widgets/consent_item.dart'; import 'package:ziggle/app/router.gr.dart'; @@ -12,9 +14,22 @@ import 'package:ziggle/gen/assets.gen.dart'; import 'package:ziggle/gen/strings.g.dart'; @RoutePage() -class NoticeWriteConsentPage extends StatelessWidget { +class NoticeWriteConsentPage extends StatefulWidget { const NoticeWriteConsentPage({super.key}); + @override + State createState() => _NoticeWriteConsentPageState(); +} + +class _NoticeWriteConsentPageState extends State + with AutoRouteAwareStateMixin { + @override + void didPush() => + AnalyticsRepository.pageView(const AnalyticsEvent.writeConfigPublish()); + @override + void didPopNext() => + AnalyticsRepository.pageView(const AnalyticsEvent.writeConfigPublish()); + @override Widget build(BuildContext context) { return BlocBuilder( @@ -85,14 +100,26 @@ class _LayoutState extends State<_Layout> { description: context.t.notice.write.consent.notification.description, isChecked: _notification, - onChanged: (v) => setState(() => _notification = v), + onChanged: (v) { + AnalyticsRepository.click( + AnalyticsEvent.writeConfigPublishAgree( + v ? "on" : "off", "notification"), + ); + setState(() => _notification = v); + }, ), const SizedBox(height: 10), ConsentItem( title: context.t.notice.write.consent.edit.title, description: context.t.notice.write.consent.edit.description, isChecked: _edit, - onChanged: (v) => setState(() => _edit = v), + onChanged: (v) { + AnalyticsRepository.click( + AnalyticsEvent.writeConfigPublishAgree( + v ? "on" : "off", "edit"), + ); + setState(() => _edit = v); + }, ), const SizedBox(height: 10), ConsentItem( @@ -100,15 +127,26 @@ class _LayoutState extends State<_Layout> { description: context.t.notice.write.consent.urgent.description, isChecked: _urgent, - onChanged: (v) => setState(() => _urgent = v), + onChanged: (v) { + AnalyticsRepository.click( + AnalyticsEvent.writeConfigPublishAgree( + v ? "on" : "off", "urgent"), + ); + setState(() => _urgent = v); + }, ), const SizedBox(height: 24), BlocBuilder( builder: (context, state) => ZiggleButton.cta( loading: state.isLoading, disabled: !_notification || !_edit || !_urgent, - onPressed: - !(_notification && _edit && _urgent) ? null : _publish, + onPressed: () { + AnalyticsRepository.click( + const AnalyticsEvent.writeConfigPublishUpload()); + if (_notification && _edit && _urgent) { + _publish(); + } + }, child: Text(context.t.notice.write.consent.upload), ), ) diff --git a/lib/app/modules/notices/presentation/pages/notice_write_preview_page.dart b/lib/app/modules/notices/presentation/pages/notice_write_preview_page.dart index 08689a12a..89bcf1eaa 100644 --- a/lib/app/modules/notices/presentation/pages/notice_write_preview_page.dart +++ b/lib/app/modules/notices/presentation/pages/notice_write_preview_page.dart @@ -2,7 +2,9 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_app_bar.dart'; -import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_back_button.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/modules/notices/domain/entities/notice_entity.dart'; import 'package:ziggle/app/modules/notices/presentation/bloc/notice_write_bloc.dart'; import 'package:ziggle/app/modules/notices/presentation/widgets/notice_renderer.dart'; @@ -10,14 +12,28 @@ import 'package:ziggle/app/modules/user/presentation/bloc/user_bloc.dart'; import 'package:ziggle/gen/strings.g.dart'; @RoutePage() -class NoticeWritePreviewPage extends StatelessWidget { +class NoticeWritePreviewPage extends StatefulWidget { const NoticeWritePreviewPage({super.key}); + @override + State createState() => _NoticeWritePreviewPageState(); +} + +class _NoticeWritePreviewPageState extends State + with AutoRouteAwareStateMixin { + @override + void didPush() => + AnalyticsRepository.pageView(const AnalyticsEvent.writeConfigPreview()); + @override + void didPopNext() => + AnalyticsRepository.pageView(const AnalyticsEvent.writeConfigPreview()); + @override Widget build(BuildContext context) { return Scaffold( - appBar: ZiggleAppBar( - leading: ZiggleBackButton(label: context.t.notice.write.configTitle), + appBar: ZiggleAppBar.compact( + backLabel: context.t.notice.write.configTitle, + from: PageSource.writeConfigPreview, title: Text(context.t.notice.write.preview), ), body: BlocBuilder( diff --git a/lib/app/modules/notices/presentation/pages/notice_write_select_tags_page.dart b/lib/app/modules/notices/presentation/pages/notice_write_select_tags_page.dart index 668428674..b9f411bf0 100644 --- a/lib/app/modules/notices/presentation/pages/notice_write_select_tags_page.dart +++ b/lib/app/modules/notices/presentation/pages/notice_write_select_tags_page.dart @@ -3,19 +3,35 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ziggle/app/di/locator.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_app_bar.dart'; -import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_back_button.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_button.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_input.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_pressable.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/modules/notices/presentation/bloc/tag_bloc.dart'; import 'package:ziggle/app/values/palette.dart'; import 'package:ziggle/gen/assets.gen.dart'; import 'package:ziggle/gen/strings.g.dart'; @RoutePage() -class NoticeWriteSelectTagsPage extends StatelessWidget { +class NoticeWriteSelectTagsPage extends StatefulWidget { const NoticeWriteSelectTagsPage({super.key}); + @override + State createState() => + _NoticeWriteSelectTagsPageState(); +} + +class _NoticeWriteSelectTagsPageState extends State + with AutoRouteAwareStateMixin { + @override + void didPush() => AnalyticsRepository.pageView( + const AnalyticsEvent.writeConfigAddHashtag()); + @override + void didPopNext() => AnalyticsRepository.pageView( + const AnalyticsEvent.writeConfigAddHashtag()); + @override Widget build(BuildContext context) { return BlocProvider( @@ -53,12 +69,17 @@ class _LayoutState extends State<_Layout> { @override Widget build(BuildContext context) { return Scaffold( - appBar: ZiggleAppBar( - leading: ZiggleBackButton(label: context.t.notice.write.configTitle), + appBar: ZiggleAppBar.compact( + backLabel: context.t.notice.write.configTitle, + from: PageSource.writeConfigAddHashtag, title: Text(context.t.notice.write.hashtag.title), actions: [ ZiggleButton.text( - onPressed: () => context.maybePop(_tags), + onPressed: () { + AnalyticsRepository.click( + const AnalyticsEvent.writeConfigDoneHashtag()); + context.maybePop(_tags); + }, child: Text(context.t.common.done), ) ], @@ -80,6 +101,8 @@ class _LayoutState extends State<_Layout> { if (_controller.text.isNotEmpty) ZigglePressable( onPressed: () { + AnalyticsRepository.click(const AnalyticsEvent + .writeConfigAddHashtagAutocomplete()); _tags.add(_controller.text.trim().replaceAll(' ', '_')); _controller.clear(); }, @@ -92,7 +115,11 @@ class _LayoutState extends State<_Layout> { Expanded( child: ListView.separated( itemBuilder: (context, index) => ZigglePressable( - onPressed: () => setState(() => _tags.removeAt(index)), + onPressed: () { + AnalyticsRepository.click( + const AnalyticsEvent.writeConfigAddHashtagDelete()); + setState(() => _tags.removeAt(index)); + }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( diff --git a/lib/app/modules/notices/presentation/pages/search_page.dart b/lib/app/modules/notices/presentation/pages/search_page.dart index e1632d6b1..677c4e7a9 100644 --- a/lib/app/modules/notices/presentation/pages/search_page.dart +++ b/lib/app/modules/notices/presentation/pages/search_page.dart @@ -7,6 +7,9 @@ import 'package:ziggle/app/modules/common/presentation/extensions/date_time.dart import 'package:ziggle/app/modules/common/presentation/functions/noop.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_button.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_pressable.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/modules/notices/domain/entities/notice_entity.dart'; import 'package:ziggle/app/modules/notices/domain/enums/notice_type.dart'; import 'package:ziggle/app/modules/notices/presentation/bloc/notice_list_bloc.dart'; @@ -17,9 +20,21 @@ import 'package:ziggle/gen/assets.gen.dart'; import 'package:ziggle/gen/strings.g.dart'; @RoutePage() -class SearchPage extends StatelessWidget { +class SearchPage extends StatefulWidget { const SearchPage({super.key}); + @override + State createState() => _SearchPageState(); +} + +class _SearchPageState extends State + with AutoRouteAwareStateMixin { + @override + void didPush() => AnalyticsRepository.pageView(const AnalyticsEvent.search()); + @override + void didPopNext() => + AnalyticsRepository.pageView(const AnalyticsEvent.search()); + @override Widget build(BuildContext context) { return BlocProvider( @@ -172,8 +187,11 @@ class _LayoutState extends State<_Layout> { ), ), child: ZigglePressable( - onPressed: () => - SingleNoticeShellRoute(notice: notice).push(context), + onPressed: () { + AnalyticsRepository.click( + AnalyticsEvent.notice(notice.id, PageSource.search)); + SingleNoticeShellRoute(notice: notice).push(context); + }, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ diff --git a/lib/app/modules/notices/presentation/pages/write_additional_notice_page.dart b/lib/app/modules/notices/presentation/pages/write_additional_notice_page.dart index 01f3454a4..32bea81dc 100644 --- a/lib/app/modules/notices/presentation/pages/write_additional_notice_page.dart +++ b/lib/app/modules/notices/presentation/pages/write_additional_notice_page.dart @@ -9,7 +9,10 @@ import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_button.dar import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_input.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_pressable.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_toggle_button.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; import 'package:ziggle/app/modules/core/domain/enums/language.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/modules/notices/presentation/bloc/notice_bloc.dart'; import 'package:ziggle/app/modules/notices/presentation/bloc/notice_write_bloc.dart'; import 'package:ziggle/app/modules/notices/presentation/widgets/deadline_selector.dart'; @@ -28,7 +31,18 @@ class WriteAdditionalNoticePage extends StatefulWidget { } class _WriteAdditionalNoticePageState extends State - with SingleTickerProviderStateMixin { + with + SingleTickerProviderStateMixin, + AutoRouteAwareStateMixin { + @override + void didPush() => + AnalyticsRepository.pageView(AnalyticsEvent.noticeEditAdditional( + context.read().state.entity!.id)); + @override + void didPopNext() => + AnalyticsRepository.pageView(AnalyticsEvent.noticeEditAdditional( + context.read().state.entity!.id)); + late final _prevNotice = context.read().state.entity!; late final _draft = context.read().state.draft; DateTime? _deadline; @@ -65,27 +79,32 @@ class _WriteAdditionalNoticePageState extends State return Scaffold( appBar: ZiggleAppBar.compact( backLabel: context.t.common.cancel, + from: PageSource.noticeEditAdditional, title: Text(context.t.notice.write.configTitle), actions: [ ZiggleButton.text( disabled: _content.text.isEmpty || (_enContent?.text.isEmpty ?? false), - onPressed: - _content.text.isEmpty || (_enContent?.text.isEmpty ?? false) - ? null - : () { - context.read().add( - NoticeWriteEvent.addAdditional( - deadline: _deadline, - contents: { - Language.ko: _content.text, - if (_enContent != null) - Language.en: _enContent.text, - }, - ), - ); - context.maybePop(); + onPressed: () { + AnalyticsRepository.click( + const AnalyticsEvent.noticeEditAdditionalDone()); + if (_content.text.isEmpty || + (_enContent?.text.isEmpty ?? false)) { + return; + } + context.read().add( + NoticeWriteEvent.addAdditional( + deadline: _deadline, + contents: { + Language.ko: _content.text, + if (_enContent != null) Language.en: _enContent.text, }, + ), + ); + context.maybePop(); + AnalyticsRepository.action( + const AnalyticsEvent.noticeEditAdditionalDone()); + }, child: Text( context.t.common.done, style: const TextStyle( @@ -107,7 +126,13 @@ class _WriteAdditionalNoticePageState extends State if (_prevNotice.currentDeadline != null) const SizedBox(height: 20), LanguageToggle( - onToggle: (v) => _tabController.animateTo(v ? 1 : 0), + onToggle: (v) { + AnalyticsRepository.click( + AnalyticsEvent.noticeEditAdditionalToggleLanguage( + v ? Language.en : Language.ko), + ); + _tabController.animateTo(v ? 1 : 0); + }, value: _tabController.index != 0, ), ], @@ -174,6 +199,8 @@ class _WriteAdditionalNoticePageState extends State ZiggleToggleButton( value: _deadline != null, onToggle: (v) async { + AnalyticsRepository.click( + AnalyticsEvent.noticeEditChangeDeadline(_prevNotice.id)); if (_deadline != null) { setState(() => _deadline = null); return; @@ -182,6 +209,7 @@ class _WriteAdditionalNoticePageState extends State context: context, title: context.t.notice.write.deadline.title, builder: (context) => DeadlineSelector( + isEditMode: true, initialDateTime: _prevNotice.currentDeadline!.toLocal(), onChanged: (v) => Navigator.pop(context, v), ), @@ -200,6 +228,7 @@ class _WriteAdditionalNoticePageState extends State context: context, title: context.t.notice.write.deadline.title, builder: (context) => DeadlineSelector( + isEditMode: true, initialDateTime: _deadline, onChanged: (v) => Navigator.pop(context, v), ), diff --git a/lib/app/modules/notices/presentation/widgets/deadline_selector.dart b/lib/app/modules/notices/presentation/widgets/deadline_selector.dart index e42af1d15..67e3569f2 100644 --- a/lib/app/modules/notices/presentation/widgets/deadline_selector.dart +++ b/lib/app/modules/notices/presentation/widgets/deadline_selector.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_button.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_date_time_picker.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/gen/strings.g.dart'; class DeadlineSelector extends StatefulWidget { @@ -8,10 +10,12 @@ class DeadlineSelector extends StatefulWidget { super.key, required this.onChanged, this.initialDateTime, + required this.isEditMode, }); final ValueChanged onChanged; final DateTime? initialDateTime; + final bool isEditMode; @override State createState() => _DeadlineSelectorState(); @@ -20,6 +24,14 @@ class DeadlineSelector extends StatefulWidget { class _DeadlineSelectorState extends State { late DateTime? _dateTime = widget.initialDateTime; + @override + void initState() { + super.initState(); + AnalyticsRepository.view(widget.isEditMode + ? const AnalyticsEvent.noticeEditChangeDeadline() + : const AnalyticsEvent.writeConfigSetDeadline()); + } + @override Widget build(BuildContext context) { return Column( @@ -37,7 +49,14 @@ class _DeadlineSelectorState extends State { children: [ Expanded( child: ZiggleButton.cta( - onPressed: () => widget.onChanged(null), + onPressed: () { + widget.isEditMode + ? AnalyticsRepository.click( + const AnalyticsEvent.noticeEditSetDeadlineCancel()) + : AnalyticsRepository.click( + const AnalyticsEvent.writeConfigSetDeadlineCancel()); + widget.onChanged(null); + }, outlined: true, child: Text(context.t.common.cancel), ), @@ -47,7 +66,15 @@ class _DeadlineSelectorState extends State { child: ZiggleButton.cta( onPressed: _dateTime == null ? null - : () => widget.onChanged(_dateTime), + : () { + widget.isEditMode + ? AnalyticsRepository.click( + const AnalyticsEvent.noticeEditSetDeadline()) + : AnalyticsRepository.click( + const AnalyticsEvent.writeConfigSetDeadline()); + + widget.onChanged(_dateTime); + }, disabled: _dateTime == null, child: Text(context.t.notice.write.deadline.confirm), ), diff --git a/lib/app/modules/notices/presentation/widgets/list_layout.dart b/lib/app/modules/notices/presentation/widgets/list_layout.dart index 6953ea1f7..65aa684de 100644 --- a/lib/app/modules/notices/presentation/widgets/list_layout.dart +++ b/lib/app/modules/notices/presentation/widgets/list_layout.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ziggle/app/modules/common/presentation/extensions/toast.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/modules/notices/domain/entities/notice_entity.dart'; import 'package:ziggle/app/modules/notices/domain/enums/notice_reaction.dart'; +import 'package:ziggle/app/modules/notices/domain/enums/notice_type.dart'; import 'package:ziggle/app/modules/notices/presentation/bloc/notice_list_bloc.dart'; import 'package:ziggle/app/modules/notices/presentation/cubit/share_cubit.dart'; import 'package:ziggle/app/modules/notices/presentation/widgets/infinite_scroll.dart'; @@ -12,7 +16,9 @@ import 'package:ziggle/app/router.gr.dart'; import 'package:ziggle/gen/strings.g.dart'; class ListLayout extends StatelessWidget { - const ListLayout({super.key}); + const ListLayout({super.key, required this.noticeType}); + + final NoticeType noticeType; @override Widget build(BuildContext context) { @@ -47,6 +53,13 @@ class ListLayout extends StatelessWidget { final notice = state.notices[index]; return NoticeCard( onLike: () { + AnalyticsRepository.click( + AnalyticsEvent.noticeReaction( + notice.id, + NoticeReaction.like, + noticeType == NoticeType.all + ? PageSource.feed + : PageSource.list)); if (UserBloc.userOrNull(context) == null) { return context.showToast( context.t.user.login.description, @@ -58,11 +71,24 @@ class ListLayout extends StatelessWidget { : NoticeListEvent.addLike(notice), ); }, - onPressed: () => - SingleNoticeShellRoute(notice: notice) - .push(context), - onShare: () => - context.read().share(notice), + onPressed: () { + AnalyticsRepository.click(AnalyticsEvent.notice( + notice.id, + noticeType == NoticeType.all + ? PageSource.feed + : PageSource.list)); + SingleNoticeShellRoute(notice: notice) + .push(context); + }, + onShare: () { + AnalyticsRepository.click( + AnalyticsEvent.noticeShare( + notice.id, + noticeType == NoticeType.all + ? PageSource.feed + : PageSource.list)); + context.read().share(notice); + }, notice: notice, ); }, diff --git a/lib/app/modules/notices/presentation/widgets/notice_renderer.dart b/lib/app/modules/notices/presentation/widgets/notice_renderer.dart index 203a44e78..aceff1c3b 100644 --- a/lib/app/modules/notices/presentation/widgets/notice_renderer.dart +++ b/lib/app/modules/notices/presentation/widgets/notice_renderer.dart @@ -5,6 +5,9 @@ import 'package:intl/intl.dart'; import 'package:ziggle/app/modules/common/presentation/extensions/confirm.dart'; import 'package:ziggle/app/modules/common/presentation/extensions/toast.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_pressable.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/modules/notices/domain/entities/notice_content_entity.dart'; import 'package:ziggle/app/modules/notices/domain/entities/notice_entity.dart'; import 'package:ziggle/app/modules/notices/domain/enums/notice_reaction.dart'; @@ -179,6 +182,8 @@ class NoticeRenderer extends StatelessWidget { ...NoticeReaction.values.map( (reaction) => _ChipButton( onPressed: () { + AnalyticsRepository.click(AnalyticsEvent.noticeReaction( + notice.id, reaction, PageSource.detail)); if (UserBloc.userOrNull(context) == null) { return context.showToast( context.t.user.login.description, @@ -196,12 +201,18 @@ class NoticeRenderer extends StatelessWidget { ), ), _ChipButton( - onPressed: () => context.read().share(notice), + onPressed: () { + AnalyticsRepository.click(AnalyticsEvent.noticeShare( + notice.id, PageSource.detail)); + context.read().share(notice); + }, icon: Assets.icons.share.svg(), text: context.t.notice.detail.share, ), _ChipButton( onPressed: () async { + AnalyticsRepository.click( + AnalyticsEvent.noticeCopy(notice.id)); final result = await context.read().copyLink(notice); if (!result || !context.mounted) return; @@ -333,12 +344,17 @@ class NoticeRenderer extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ _AuthorSettingAction( - onPressed: () => const NoticeEditRoute().push(context), + onPressed: () { + AnalyticsRepository.click(AnalyticsEvent.noticeEdit(notice.id)); + const NoticeEditRoute().push(context); + }, icon: Assets.icons.editPencil, text: context.t.notice.settings.edit.action, ), _AuthorSettingAction( onPressed: () async { + AnalyticsRepository.click( + AnalyticsEvent.noticeDelete(notice.id)); final result = await context.showDialog( title: context.t.notice.settings.delete.title, content: context.t.notice.settings.delete.description, @@ -355,11 +371,14 @@ class NoticeRenderer extends StatelessWidget { icon: Assets.icons.delete, text: context.t.notice.settings.delete.action, ), - if (notice.publishedAt == null) + if (!notice.isPublished) Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ _AuthorSettingAction( onPressed: () async { + AnalyticsRepository.click( + AnalyticsEvent.noticeSendNotification(notice.id)); final result = await context.showDialog( title: context.t.notice.settings.sendNotification.title, content: context diff --git a/lib/app/modules/user/presentation/bloc/auth_bloc.dart b/lib/app/modules/user/presentation/bloc/auth_bloc.dart index 41842f642..1a494c8ec 100644 --- a/lib/app/modules/user/presentation/bloc/auth_bloc.dart +++ b/lib/app/modules/user/presentation/bloc/auth_bloc.dart @@ -2,6 +2,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:injectable/injectable.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/enums/event_type.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/modules/user/domain/repositories/auth_repository.dart'; part 'auth_bloc.freezed.dart'; @@ -9,8 +13,10 @@ part 'auth_bloc.freezed.dart'; @injectable class AuthBloc extends Bloc { final AuthRepository _repository; + final AnalyticsRepository _analyticsRepository; - AuthBloc(this._repository) : super(const AuthState.initial()) { + AuthBloc(this._repository, this._analyticsRepository) + : super(const AuthState.initial()) { on<_Load>((event, emit) async { emit(const _Loading()); return emit.forEach( @@ -22,11 +28,17 @@ class AuthBloc extends Bloc { emit(const _Loading()); try { await _repository.login(); + _analyticsRepository.logEvent( + EventType.action, + AnalyticsEvent.profileLogin(event.source), + ); } catch (e) { emit(_Error(e.toString())); } }); on<_Logout>((event, emit) async { + _analyticsRepository.logEvent( + EventType.action, AnalyticsEvent.profileLogout(event.source)); emit(const _Unauthenticated()); await _repository.logout(); }); @@ -39,8 +51,8 @@ class AuthBloc extends Bloc { @freezed sealed class AuthEvent with _$AuthEvent { const factory AuthEvent.load() = _Load; - const factory AuthEvent.login() = _Login; - const factory AuthEvent.logout() = _Logout; + const factory AuthEvent.login({required PageSource source}) = _Login; + const factory AuthEvent.logout({required PageSource source}) = _Logout; } @freezed diff --git a/lib/app/modules/user/presentation/pages/information_page.dart b/lib/app/modules/user/presentation/pages/information_page.dart index 4e403f843..906b50ccf 100644 --- a/lib/app/modules/user/presentation/pages/information_page.dart +++ b/lib/app/modules/user/presentation/pages/information_page.dart @@ -6,6 +6,9 @@ import 'package:url_launcher/url_launcher_string.dart'; import 'package:ziggle/app/modules/common/presentation/extensions/toast.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_app_bar.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_row_button.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/modules/user/presentation/bloc/developer_option_bloc.dart'; import 'package:ziggle/app/router.gr.dart'; import 'package:ziggle/app/values/strings.dart'; @@ -14,14 +17,28 @@ import 'package:ziggle/gen/strings.g.dart'; const _devModeCount = 10; @RoutePage() -class InformationPage extends StatelessWidget { +class InformationPage extends StatefulWidget { const InformationPage({super.key}); + @override + State createState() => _InformationPageState(); +} + +class _InformationPageState extends State + with AutoRouteAwareStateMixin { + @override + void didPush() => AnalyticsRepository.pageView( + const AnalyticsEvent.profileSettingInformation()); + @override + void didPopNext() => AnalyticsRepository.pageView( + const AnalyticsEvent.profileSettingInformation()); + @override Widget build(BuildContext context) { return Scaffold( appBar: ZiggleAppBar.compact( backLabel: context.t.user.setting.title, + from: PageSource.settingInformation, title: Text(context.t.user.setting.information.title), ), body: SingleChildScrollView( @@ -32,18 +49,30 @@ class InformationPage extends StatelessWidget { children: [ ZiggleRowButton( title: Text(context.t.user.setting.information.termsOfService), - onPressed: () => launchUrlString(Strings.termsOfServiceUrl), + onPressed: () { + AnalyticsRepository.click( + const AnalyticsEvent.profileSettingInformationTos()); + launchUrlString(Strings.termsOfServiceUrl); + }, ), const SizedBox(height: 20), ZiggleRowButton( title: Text(context.t.user.setting.information.privacyPolicy), - onPressed: () => launchUrlString(Strings.privacyPolicyUrl), + onPressed: () { + AnalyticsRepository.click( + const AnalyticsEvent.profileSettingInformationPrivacy()); + launchUrlString(Strings.privacyPolicyUrl); + }, ), const SizedBox(height: 20), ZiggleRowButton( title: Text(context.t.user.setting.information.openSourceLicense), - onPressed: () => const PackagesRoute().push(context), + onPressed: () { + AnalyticsRepository.click( + const AnalyticsEvent.profileSettingInformationLicense()); + const PackagesRoute().push(context); + }, ), const SizedBox(height: 20), FutureBuilder( diff --git a/lib/app/modules/user/presentation/pages/license_page.dart b/lib/app/modules/user/presentation/pages/license_page.dart index 4d0d6ee27..78aa64991 100644 --- a/lib/app/modules/user/presentation/pages/license_page.dart +++ b/lib/app/modules/user/presentation/pages/license_page.dart @@ -4,6 +4,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_app_bar.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; import 'package:ziggle/gen/strings.g.dart'; @RoutePage() @@ -22,6 +23,7 @@ class PackageLicensesPage extends StatelessWidget { return Scaffold( appBar: ZiggleAppBar.compact( backLabel: context.t.common.back, + from: PageSource.settingInformationLicense, title: Text(package), ), body: Scrollbar( diff --git a/lib/app/modules/user/presentation/pages/packages_page.dart b/lib/app/modules/user/presentation/pages/packages_page.dart index 831b4c43b..55e45848a 100644 --- a/lib/app/modules/user/presentation/pages/packages_page.dart +++ b/lib/app/modules/user/presentation/pages/packages_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_app_bar.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_pressable.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; import 'package:ziggle/app/router.gr.dart'; import 'package:ziggle/app/values/palette.dart'; import 'package:ziggle/gen/assets.gen.dart'; @@ -18,6 +19,7 @@ class PackagesPage extends StatelessWidget { return Scaffold( appBar: ZiggleAppBar.compact( backLabel: context.t.common.back, + from: PageSource.unknown, title: Text(context.t.user.setting.information.openSourceLicense), ), body: FutureBuilder( diff --git a/lib/app/modules/user/presentation/pages/profile_page.dart b/lib/app/modules/user/presentation/pages/profile_page.dart index 0a1697eec..c0abd78ad 100644 --- a/lib/app/modules/user/presentation/pages/profile_page.dart +++ b/lib/app/modules/user/presentation/pages/profile_page.dart @@ -6,6 +6,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_button.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_row_button.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/modules/notices/domain/enums/notice_type.dart'; import 'package:ziggle/app/modules/user/domain/entities/user_entity.dart'; import 'package:ziggle/app/modules/user/presentation/bloc/auth_bloc.dart'; @@ -17,9 +20,19 @@ import 'package:ziggle/gen/assets.gen.dart'; import 'package:ziggle/gen/strings.g.dart'; @RoutePage() -class ProfilePage extends StatelessWidget { +class ProfilePage extends StatefulWidget { const ProfilePage({super.key}); + @override + State createState() => _ProfilePageState(); +} + +class _ProfilePageState extends State + with AutoRouteAwareStateMixin { + @override + void didChangeTabRoute(previousRoute) => + AnalyticsRepository.pageView(const AnalyticsEvent.profile()); + @override Widget build(BuildContext context) { return const Scaffold( @@ -73,22 +86,33 @@ class _Layout extends StatelessWidget { ZiggleRowButton( icon: Assets.icons.setting.svg(), title: Text(context.t.user.setting.title), - onPressed: () => const SettingRoute().push(context), + onPressed: () { + AnalyticsRepository.click( + const AnalyticsEvent.profileSetting()); + const SettingRoute().push(context); + }, ), if (authenticated) ...[ const SizedBox(height: 20), ZiggleRowButton( icon: Assets.icons.write.svg(), title: Text(context.t.user.written), - onPressed: () => - ListRoute(type: NoticeType.written).push(context), + onPressed: () { + AnalyticsRepository.click( + const AnalyticsEvent.profileMyNotices()); + ListRoute(type: NoticeType.written).push(context); + }, ), ], const SizedBox(height: 20), ZiggleRowButton( icon: Assets.icons.flag.svg(), title: Text(context.t.user.feedback), - onPressed: () => launchUrlString(Strings.heyDeveloperUrl), + onPressed: () { + AnalyticsRepository.click( + const AnalyticsEvent.profileFeedback()); + launchUrlString(Strings.heyDeveloperUrl); + }, ), const SizedBox(height: 40), if (authenticated) @@ -96,8 +120,13 @@ class _Layout extends StatelessWidget { showChevron: false, title: Text(context.t.user.account.logout), destructive: true, - onPressed: () => - context.read().add(const AuthEvent.logout()), + onPressed: () { + AnalyticsRepository.click( + const AnalyticsEvent.profileLogout(PageSource.profile)); + context + .read() + .add(const AuthEvent.logout(source: PageSource.profile)); + }, ), ], ); @@ -165,8 +194,13 @@ class _Login extends StatelessWidget { ), const SizedBox(height: 8), ZiggleButton.cta( - onPressed: () => - context.read().add(const AuthEvent.login()), + onPressed: () { + AnalyticsRepository.click( + const AnalyticsEvent.profileLogin(PageSource.profile)); + context + .read() + .add(const AuthEvent.login(source: PageSource.profile)); + }, child: Text(context.t.user.login.action), ), ], diff --git a/lib/app/modules/user/presentation/pages/setting_page.dart b/lib/app/modules/user/presentation/pages/setting_page.dart index 6a1cbcaff..6e09b829d 100644 --- a/lib/app/modules/user/presentation/pages/setting_page.dart +++ b/lib/app/modules/user/presentation/pages/setting_page.dart @@ -6,7 +6,10 @@ import 'package:ziggle/app/di/locator.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_app_bar.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_button.dart'; import 'package:ziggle/app/modules/common/presentation/widgets/ziggle_row_button.dart'; +import 'package:ziggle/app/modules/core/data/models/analytics_event.dart'; import 'package:ziggle/app/modules/core/domain/enums/language.dart'; +import 'package:ziggle/app/modules/core/domain/enums/page_source.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; import 'package:ziggle/app/modules/user/domain/repositories/language_setting_repository.dart'; import 'package:ziggle/app/modules/user/domain/repositories/notification_setting_repository.dart'; import 'package:ziggle/app/modules/user/presentation/bloc/auth_bloc.dart'; @@ -18,14 +21,28 @@ import 'package:ziggle/app/values/strings.dart'; import 'package:ziggle/gen/strings.g.dart'; @RoutePage() -class SettingPage extends StatelessWidget { +class SettingPage extends StatefulWidget { const SettingPage({super.key}); + @override + State createState() => _SettingPageState(); +} + +class _SettingPageState extends State + with AutoRouteAwareStateMixin { + @override + void didPush() => + AnalyticsRepository.pageView(const AnalyticsEvent.profileSetting()); + @override + void didPopNext() => + AnalyticsRepository.pageView(const AnalyticsEvent.profileSetting()); + @override Widget build(BuildContext context) { return Scaffold( appBar: ZiggleAppBar.compact( backLabel: context.t.user.myInfo, + from: PageSource.setting, title: Text(context.t.user.setting.title), ), body: SingleChildScrollView( @@ -42,9 +59,13 @@ class SettingPage extends StatelessWidget { return ZiggleButton.cta( loading: authState.isLoading, child: Text(context.t.user.account.login), - onPressed: () => context - .read() - .add(const AuthEvent.login()), + onPressed: () { + AnalyticsRepository.click( + const AnalyticsEvent.profileLogin( + PageSource.setting)); + context.read().add(const AuthEvent.login( + source: PageSource.setting)); + }, ); }, ); @@ -55,16 +76,24 @@ class SettingPage extends StatelessWidget { title: Text(context.t.user.account.logout), destructive: true, showChevron: false, - onPressed: () => context - .read() - .add(const AuthEvent.logout()), + onPressed: () { + AnalyticsRepository.click( + const AnalyticsEvent.profileLogout( + PageSource.setting)); + context.read().add( + const AuthEvent.logout(source: PageSource.setting)); + }, ), const SizedBox(height: 20), ZiggleRowButton( title: Text(context.t.user.account.withdraw), destructive: true, showChevron: false, - onPressed: () => launchUrlString(Strings.withdrawalUrl), + onPressed: () { + AnalyticsRepository.click( + const AnalyticsEvent.profileWithdraw()); + launchUrlString(Strings.withdrawalUrl); + }, ), ], ); @@ -79,8 +108,12 @@ class SettingPage extends StatelessWidget { if (!data) { return ZiggleRowButton( title: Text(context.t.user.setting.notification.enable), - onPressed: () => sl() - .enableNotification(), + onPressed: () { + AnalyticsRepository.click(const AnalyticsEvent + .profileSettingEnableNotification()); + sl() + .enableNotification(); + }, ); } return ZiggleRowButton( @@ -98,6 +131,10 @@ class SettingPage extends StatelessWidget { onPressed: LocaleSettings.currentLocale == AppLocale.ko ? null : () { + AnalyticsRepository.click( + const AnalyticsEvent.profileSettingLanguage( + AppLocale.ko), + ); LocaleSettings.setLocale(AppLocale.ko); sl() .setLanguage(Language.ko); @@ -112,6 +149,10 @@ class SettingPage extends StatelessWidget { onPressed: LocaleSettings.currentLocale == AppLocale.en ? null : () { + AnalyticsRepository.click( + const AnalyticsEvent.profileSettingLanguage( + AppLocale.en), + ); LocaleSettings.setLocale(AppLocale.en); sl() .setLanguage(Language.en); @@ -121,7 +162,11 @@ class SettingPage extends StatelessWidget { _Title(title: context.t.user.setting.information.title), ZiggleRowButton( title: Text(context.t.user.setting.information.title), - onPressed: () => const InformationRoute().push(context), + onPressed: () { + AnalyticsRepository.click( + const AnalyticsEvent.profileSettingInformation()); + const InformationRoute().push(context); + }, ), BlocBuilder( builder: (context, state) => state.enabled diff --git a/lib/app/router_observer.dart b/lib/app/router_observer.dart new file mode 100644 index 000000000..8f07af732 --- /dev/null +++ b/lib/app/router_observer.dart @@ -0,0 +1,55 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:injectable/injectable.dart'; +import 'package:ziggle/app/modules/core/domain/repositories/analytics_repository.dart'; + +@injectable +class AppRouterObserver extends AutoRouterObserver { + final AnalyticsRepository _repository; + + AppRouterObserver(this._repository); + + @override + void didPush(Route route, Route? previousRoute) { + final name = route.settings.name; + if (name != null) { + _repository.logScreen(name); + } + } + + @override + void didPop(Route route, Route? previousRoute) { + final name = previousRoute?.settings.name; + if (name != null) { + _repository.logScreen(name); + } + } + + @override + void didChangeTabRoute(TabPageRoute route, TabPageRoute previousRoute) { + final name = route.routeInfo.name; + _repository.logScreen(name); + } + + @override + void didInitTabRoute(TabPageRoute route, TabPageRoute? previousRoute) { + final name = route.routeInfo.name; + _repository.logScreen(name); + } + + @override + void didReplace({Route? newRoute, Route? oldRoute}) { + final name = newRoute?.settings.name; + if (name != null) { + _repository.logScreen(name); + } + } + + @override + void didRemove(Route route, Route? previousRoute) { + final name = previousRoute?.settings.name; + if (name != null) { + _repository.logScreen(name); + } + } +} diff --git a/lib/app/values/strings.dart b/lib/app/values/strings.dart index df071bd03..8cdd6e8d5 100644 --- a/lib/app/values/strings.dart +++ b/lib/app/values/strings.dart @@ -1,21 +1,17 @@ +import 'package:flutter_dotenv/flutter_dotenv.dart'; + abstract class Strings { Strings._(); - static const idpRedirectScheme = 'ziggle-idp-login-redirect'; - static const idpBaseUrl = 'https://idp.gistory.me'; - static const idpClientId = 'ziggle2023'; - static String get _idpBasePath => '/authorize' - '?client_id=$idpClientId' - '&redirect_uri=$idpRedirectScheme://callback' - '&scope=openid%20profile%20email%20student_id%20offline_access' - '&response_type=code'; - static String get idpPath => '$_idpBasePath&prompt=consent'; - static String get reLoginIdpPath => '$_idpBasePath&prompt=login'; - static const privacyPolicyUrl = - 'https://infoteam-rulrudino.notion.site/ceb9340c0b514497b6d916c4a67590a1'; - static const termsOfServiceUrl = - 'https://infoteam-rulrudino.notion.site/6177be6369e44280a23a65866c51b257'; - static const withdrawalUrl = 'https://idp.gistory.me'; - - static const heyDeveloperUrl = 'https://cs.gistory.me/?service=Ziggle'; + static final amplitudeApiKey = dotenv.get('AMPLITUDE_API_KEY'); + static final smartlookApiKey = dotenv.get('SMARTLOOK_API_KEY'); + static final idpRedirectScheme = dotenv.get('IDP_REDIRECT_SCHEME'); + static final idpBaseUrl = dotenv.get('IDP_BASE_URL'); + static final idpClientId = dotenv.get('IDP_CLIENT_ID'); + static final idpPath = dotenv.get('IDP_PATH'); + static final reLoginIdpPath = dotenv.get('IDP_RE_LOGIN_PATH'); + static final privacyPolicyUrl = dotenv.get('PRIVACY_POLICY_URL'); + static final termsOfServiceUrl = dotenv.get('TERMS_OF_SERVICE_URL'); + static final withdrawalUrl = dotenv.get('WITHDRAWAL_URL'); + static final heyDeveloperUrl = dotenv.get('HEY_DEVELOPER_URL'); } diff --git a/lib/main.dart b/lib/main.dart index 9f3eaf630..14dc087e1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl.dart'; @@ -16,6 +17,7 @@ import 'package:ziggle/gen/strings.g.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + await dotenv.load(); _initCrashlytics(); await _initHive(); await configureDependencies(); diff --git a/pubspec.lock b/pubspec.lock index 49ec67675..9228942ea 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,15 +13,23 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: ddc6f775260b89176d329dee26f88b9469ef46aa3228ff6a0b91caf2b2989692 + sha256: "5534e701a2c505fed1f0799e652dd6ae23bd4d2c4cf797220e5ced5764a7c1c2" url: "https://pub.dev" source: hosted - version: "1.3.42" + version: "1.3.44" _macros: dependency: transitive description: dart source: sdk version: "0.3.2" + amplitude_flutter: + dependency: "direct main" + description: + name: amplitude_flutter + sha256: cc0f22b03444e02bbaf36f0041b60282f9947ca849d6b3e6fe69e6b7b4d49565 + url: "https://pub.dev" + source: hosted + version: "3.16.4" analyzer: dependency: transitive description: @@ -114,10 +122,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04 + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.12" + version: "2.4.13" build_runner_core: dependency: transitive description: @@ -274,18 +282,18 @@ packages: dependency: transitive description: name: dart_quill_delta - sha256: "3046b617add2f66bf3124e2144d06f64d0527cb66bffbb6ba1bb4461ef2c43aa" + sha256: a3552d7dfe4904ab344ccc7bf6453fd2d966b7ef64a945e364ae18dd486b9569 url: "https://pub.dev" source: hosted - version: "10.6.4" + version: "10.8.1" dart_style: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.7" dartx: dependency: transitive description: @@ -302,22 +310,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" - device_info_plus: - dependency: transitive - description: - name: device_info_plus - sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 - url: "https://pub.dev" - source: hosted - version: "10.1.2" - device_info_plus_platform_interface: - dependency: transitive - description: - name: device_info_plus_platform_interface - sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" - url: "https://pub.dev" - source: hosted - version: "7.0.1" diff_match_patch: dependency: transitive description: @@ -426,10 +418,10 @@ packages: dependency: transitive description: name: file_selector_macos - sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385 + sha256: cb284e267f8e2a45a904b5c094d2ba51d0aabfc20b1538ab786d9ef7dc2bf75c url: "https://pub.dev" source: hosted - version: "0.9.4" + version: "0.9.4+1" file_selector_platform_interface: dependency: transitive description: @@ -442,98 +434,98 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69" + sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" url: "https://pub.dev" source: hosted - version: "0.9.3+2" + version: "0.9.3+3" firebase_analytics: dependency: "direct main" description: name: firebase_analytics - sha256: "7e032ade38dec2a92f543ba02c5f72f54ffaa095c60d2132b867eab56de3bc73" + sha256: "2c4e7b548d41b46e8aa08bc3bd1163146be7e6d48f678f2e6dd3114994e42458" url: "https://pub.dev" source: hosted - version: "11.3.0" + version: "11.3.3" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - sha256: b62a2444767d95067a7e36b1d6e335e0b877968574bbbfb656168c46f2e95a13 + sha256: c259ae890c7d4c5d1675d35936be0b1fcd587fce9645948982cd87ad08df6222 url: "https://pub.dev" source: hosted - version: "4.2.2" + version: "4.2.5" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - sha256: bad44f71f96cfca6c16c9dd4f70b85f123ddca7d5dd698977449fadf298b1782 + sha256: "5988d1fd022e55515c2a14811c9b5104c32acde115874a9a69ff7c77c4c05cd9" url: "https://pub.dev" source: hosted - version: "0.5.9+2" + version: "0.5.10+2" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "40921de9795fbf5887ed5c0adfdf4972d5a8d7ae7e1b2bb98dea39bc02626a88" + sha256: "51dfe2fbf3a984787a2e7b8592f2f05c986bfedd6fdacea3f9e0a7beb334de96" url: "https://pub.dev" source: hosted - version: "3.4.1" + version: "3.6.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: f7d7180c7f99babd4b4c517754d41a09a4943a0f7a69b65c894ca5c68ba66315 + sha256: e30da58198a6d4b49d5bce4e852f985c32cb10db329ebef9473db2b9f09ce810 url: "https://pub.dev" source: hosted - version: "5.2.1" + version: "5.3.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: f4ee170441ca141c5f9ee5ad8737daba3ee9c8e7efb6902aee90b4fbd178ce25 + sha256: f967a7138f5d2ffb1ce15950e2a382924239eaa521150a8f144af34e68b3b3e5 url: "https://pub.dev" source: hosted - version: "2.18.0" + version: "2.18.1" firebase_crashlytics: dependency: "direct main" description: name: firebase_crashlytics - sha256: "4c9872020c0d97a161362ee6af7000cfdb8666234ddc290a15252ad379bb235a" + sha256: "6899800fff1af819955aef740f18c4c8600f8b952a2a1ea97bc0872ebb257387" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.1.3" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: ede8a199ff03378857d3c8cbb7fa58d37c27bb5a6b75faf8415ff6925dcaae2a + sha256: "97c47b0a1779a3d4118416a3f0c6c564cc59ad89095e899893204d4b2ad08f4c" url: "https://pub.dev" source: hosted - version: "3.6.41" + version: "3.6.44" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: "29941ba5a3204d80656c0e52103369aa9a53edfd9ceae05a2bb3376f24fda453" + sha256: eb6e28a3a35deda61fe8634967c84215efc19133ba58d8e0fc6c9a2af2cba05e url: "https://pub.dev" source: hosted - version: "15.1.0" + version: "15.1.3" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: d8a4984635f09213302243ea670fe5c42f3261d7d8c7c0a5f7dcd5d6c84be459 + sha256: b316c4ee10d93d32c033644207afc282d9b2b4372f3cf9c6022f3558b3873d2d url: "https://pub.dev" source: hosted - version: "4.5.44" + version: "4.5.46" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: "258b9d637965db7855299b123533609ed95e52350746a723dfd1d8d6f3fac678" + sha256: d7f0147a1a9fe4313168e20154a01fd5cf332898de1527d3930ff77b8c7f5387 url: "https://pub.dev" source: hosted - version: "3.9.0" + version: "3.9.2" fixnum: dependency: transitive description: @@ -571,6 +563,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: "9357883bdd153ab78cbf9ffa07656e336b8bbb2b5a3ca596b0b27e119f7c7d77" + url: "https://pub.dev" + source: hosted + version: "5.1.0" flutter_gen_core: dependency: transitive description: @@ -595,14 +595,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.8.0" - flutter_keyboard_visibility: - dependency: transitive - description: - name: flutter_keyboard_visibility - sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8" - url: "https://pub.dev" - source: hosted - version: "6.0.0" flutter_keyboard_visibility_linux: dependency: transitive description: @@ -627,14 +619,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - flutter_keyboard_visibility_web: + flutter_keyboard_visibility_temp_fork: dependency: transitive description: - name: flutter_keyboard_visibility_web - sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 + name: flutter_keyboard_visibility_temp_fork + sha256: e342172aaa6173a661e822c85a005f8c5d0a04a1d263e00cb9f9155adab9cb7c url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "0.1.1" flutter_keyboard_visibility_windows: dependency: transitive description: @@ -655,10 +647,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: c500d5d9e7e553f06b61877ca6b9c8b92c570a4c8db371038702e8ce57f8a50f + sha256: "49eeef364fddb71515bc78d5a8c51435a68bccd6e4d68e25a942c5e47761ae71" url: "https://pub.dev" source: hosted - version: "17.2.2" + version: "17.2.3" flutter_local_notifications_linux: dependency: transitive description: @@ -692,10 +684,10 @@ packages: dependency: "direct main" description: name: flutter_quill - sha256: "03bb07df02ab83d96f09abfe0d11d639b552971ec5c24cea58b484348fe816a3" + sha256: "27afdee8ea3b7bf5cb3b36f4c195468b24bcaad80036d4daa423bf948e8b5a97" url: "https://pub.dev" source: hosted - version: "10.5.13" + version: "10.8.1" flutter_quill_delta_from_html: dependency: "direct main" description: @@ -752,6 +744,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + flutter_smartlook: + dependency: "direct main" + description: + name: flutter_smartlook + sha256: "3bde33fb8a9755ce7808057c7ac3977a8bceb285b772dd7bf77d28143273c7bf" + url: "https://pub.dev" + source: hosted + version: "4.1.24" flutter_svg: dependency: "direct main" description: @@ -1370,6 +1370,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + quill_native_bridge: + dependency: transitive + description: + name: quill_native_bridge + sha256: "7e2050567c5dae3516b6c399fdd2036971aa16114e19c51832523f2ec1b8faca" + url: "https://pub.dev" + source: hosted + version: "10.6.2" quiver: dependency: transitive description: @@ -1507,18 +1515,18 @@ packages: dependency: transitive description: name: sqflite - sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + sha256: ff5a2436ef8ebdfda748fbfe957f9981524cb5ff11e7bafa8c42771840e8a788 url: "https://pub.dev" source: hosted - version: "2.3.3+1" + version: "2.3.3+2" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "4058172e418eb7e7f2058dcb7657d451a8fc264afa0dea4dbd0f304a57131611" + sha256: "2d8e607db72e9cb7748c9c6e739e2c9618320a5517de693d5a24609c4671b1a4" url: "https://pub.dev" source: hosted - version: "2.5.4+3" + version: "2.5.4+4" stack_trace: dependency: transitive description: @@ -1555,10 +1563,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "51b08572b9f091f8c3eb4d9d4be253f196ff0075d5ec9b10a884026d5b55d7bc" + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" url: "https://pub.dev" source: hosted - version: "3.3.0+2" + version: "3.3.0+3" term_glyph: dependency: transitive description: @@ -1675,10 +1683,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" url_launcher_platform_interface: dependency: transitive description: @@ -1707,10 +1715,10 @@ packages: dependency: transitive description: name: uuid - sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.5.1" vector_graphics: dependency: transitive description: @@ -1771,10 +1779,10 @@ packages: dependency: transitive description: name: web - sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" web_socket: dependency: transitive description: @@ -1799,14 +1807,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.5.4" - win32_registry: - dependency: transitive - description: - name: win32_registry - sha256: "723b7f851e5724c55409bb3d5a32b203b3afe8587eaf5dafb93a5fed8ecda0d6" - url: "https://pub.dev" - source: hosted - version: "1.1.4" window_to_front: dependency: transitive description: @@ -1840,5 +1840,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.5.1 <4.0.0" + dart: ">=3.5.1 <=3.10.100" flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 046ee8ac3..f72027727 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,6 +54,9 @@ dependencies: flutter_quill_delta_from_html: ^1.4.1 share_plus: ^10.0.2 cached_network_image: ^3.4.1 + amplitude_flutter: ^3.16.4 + flutter_smartlook: ^4.1.24 + flutter_dotenv: ^5.1.0 dev_dependencies: flutter_test: @@ -73,6 +76,7 @@ dev_dependencies: flutter: uses-material-design: true assets: + - .env - assets/logo/fire.svg - assets/logo/short.svg - assets/logo/long.svg