diff --git a/android/app/build.gradle b/android/app/build.gradle index 1d9c3c7..a51fad4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -23,6 +23,7 @@ if (flutterVersionName == null) { apply plugin: 'com.android.application' apply plugin: 'com.google.gms.google-services' +apply plugin: 'com.google.firebase.crashlytics' apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" @@ -79,4 +80,5 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation platform('com.google.firebase:firebase-bom:28.0.1') implementation 'com.google.firebase:firebase-analytics-ktx' + implementation 'com.google.firebase:firebase-crashlytics-ktx' } diff --git a/android/build.gradle b/android/build.gradle index d6087e6..c4d5ec6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -9,6 +9,7 @@ buildscript { classpath 'com.android.tools.build:gradle:4.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.google.gms:google-services:4.3.8' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.0' } } diff --git a/lib/app/app.dart b/lib/app/app.dart index 9212f68..d6a8b5f 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -3,6 +3,7 @@ import 'package:feed/core/services/environment_service.dart'; import 'package:feed/core/services/explode_service.dart'; import 'package:feed/core/services/key_storage_service.dart'; import 'package:feed/firebase/analytics.dart'; +import 'package:feed/firebase/crashlytics.dart'; import 'package:feed/firebase/dynamic_links.dart'; import 'package:feed/core/services/hive_service/hive_service.dart'; import 'package:feed/core/services/hive_service/hive_service_impl.dart'; @@ -27,7 +28,6 @@ import 'package:stacked_firebase_auth/stacked_firebase_auth.dart'; import 'package:stacked_services/stacked_services.dart'; import 'package:stacked/stacked_annotations.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart'; - import 'injection.dart'; /// The @StackedApp annotation generates @@ -93,8 +93,11 @@ import 'injection.dart'; LazySingleton(classType: FeedService), LazySingleton(classType: DynamicLinksService), LazySingleton(classType: AnalyticsService), + LazySingleton( + classType: CrashlyticsService, + resolveUsing: CrashlyticsService.getInstance), ], - logger: StackedLogger(), + logger: StackedLogger(loggerOutputs: [CrashlyticsOutput]), ) class AppSetup { /** Serves no purpose besides having an annotation attached to it */ diff --git a/lib/app/app.locator.dart b/lib/app/app.locator.dart index 5dc8f9e..6499eaf 100644 --- a/lib/app/app.locator.dart +++ b/lib/app/app.locator.dart @@ -24,6 +24,7 @@ import '../core/services/topic_service.dart'; import '../core/services/user_service.dart'; import '../core/services/video_service.dart'; import '../firebase/analytics.dart'; +import '../firebase/crashlytics.dart'; import '../firebase/dynamic_links.dart'; import '../remote/api/api_service.dart'; import '../remote/api/api_service_impl.dart'; @@ -73,4 +74,5 @@ Future setupLocator( locator.registerLazySingleton(() => FeedService()); locator.registerLazySingleton(() => DynamicLinksService()); locator.registerLazySingleton(() => AnalyticsService()); + locator.registerLazySingleton(() => CrashlyticsService.getInstance()); } diff --git a/lib/app/app.logger.dart b/lib/app/app.logger.dart index c0eebf6..6abf36f 100644 --- a/lib/app/app.logger.dart +++ b/lib/app/app.logger.dart @@ -10,6 +10,8 @@ import 'package:flutter/foundation.dart'; import 'package:logger/logger.dart'; +import '../firebase/crashlytics.dart'; + class SimpleLogPrinter extends LogPrinter { final String className; final bool printCallingFunctionName; @@ -120,13 +122,6 @@ class MultipleLoggerOutput extends LogOutput { } } -class LogAllTheTimeFilter extends LogFilter { - @override - bool shouldLog(LogEvent event) { - return true; - } -} - Logger getLogger( String className, { bool printCallingFunctionName = true, @@ -135,7 +130,6 @@ Logger getLogger( String? showOnlyClass, }) { return Logger( - filter: LogAllTheTimeFilter(), printer: SimpleLogPrinter( className, printCallingFunctionName: printCallingFunctionName, @@ -145,6 +139,7 @@ Logger getLogger( ), output: MultipleLoggerOutput([ ConsoleOutput(), + CrashlyticsOutput(), ]), ); } diff --git a/lib/core/services/environment_service.dart b/lib/core/services/environment_service.dart index 531e201..a8c7638 100644 --- a/lib/core/services/environment_service.dart +++ b/lib/core/services/environment_service.dart @@ -8,14 +8,14 @@ class EnvironmentService { /// Returns the value associated with the key String getValue(String key, {bool verbose = false}) { - final value = env[key] ?? NoKey; + final value = dotenv.env[key] ?? NoKey; if (verbose) log.v('key:$key value:$value'); return value; } static Future getInstance() async { print('Load environment'); - await load(fileName: ".env"); + await dotenv.load(fileName: ".env"); print('Environement loaded'); return Future.value(EnvironmentService()); } diff --git a/lib/core/services/user_service.dart b/lib/core/services/user_service.dart index 701e3b6..57fb8ab 100644 --- a/lib/core/services/user_service.dart +++ b/lib/core/services/user_service.dart @@ -5,6 +5,7 @@ import 'package:feed/core/models/app_models.dart'; import 'package:feed/core/services/hive_service/hive_service.dart'; import 'package:feed/core/services/key_storage_service.dart'; +import 'package:feed/firebase/analytics.dart'; import 'package:feed/remote/api/api_service.dart'; import 'package:feed/remote/client.dart'; import 'package:stacked_firebase_auth/stacked_firebase_auth.dart'; @@ -15,6 +16,7 @@ class UserService { final _apiService = locator(); final _remoteClient = locator(); final _hiveService = locator(); + final _crashlytics = locator(); final _authService = locator(); User? _loggedInUser; @@ -89,6 +91,7 @@ class UserService { _loggedInUser = authUser; _remoteClient.updateToken(newToken: authUser.token); + _crashlytics.setUserIdentifier(currentUser.id); return true; } diff --git a/lib/firebase/analytics.dart b/lib/firebase/analytics.dart index 5f222e8..2b70596 100644 --- a/lib/firebase/analytics.dart +++ b/lib/firebase/analytics.dart @@ -1,9 +1,50 @@ import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_analytics/observer.dart'; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:flutter/foundation.dart'; class AnalyticsService { final FirebaseAnalytics _analytics = FirebaseAnalytics(); FirebaseAnalyticsObserver getAnalyticsObserver() => FirebaseAnalyticsObserver(analytics: _analytics); + + Future logAppOpen() async { + if (kReleaseMode) { + await _analytics.logAppOpen(); + } + } + + void logLogin() { + if (kReleaseMode) { + _analytics.logLogin(loginMethod: 'google'); + } + } + + Future logShare(String itemId, + {String contentType = 'Post', String method = 'in_app_share'}) async { + if (kReleaseMode) { + await _analytics.logShare( + contentType: contentType, + itemId: itemId, + method: method, + ); + } + } + + Future logEvent(String name, {Map params = const {}}) async { + if (kReleaseMode) { + await _analytics.logEvent( + name: name, + parameters: params, + ); + } + } + + void setUserIdentifier(String userId) { + if (kReleaseMode) { + FirebaseCrashlytics.instance.setUserIdentifier(userId); + _analytics.setUserId(userId); + } + } } diff --git a/lib/firebase/crashlytics.dart b/lib/firebase/crashlytics.dart index e69de29..c1258a7 100644 --- a/lib/firebase/crashlytics.dart +++ b/lib/firebase/crashlytics.dart @@ -0,0 +1,73 @@ +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:flutter/foundation.dart'; +import 'package:logger/logger.dart'; + +class CrashlyticsService { + static CrashlyticsService? _instance; + + static CrashlyticsService getInstance() { + if (_instance == null) { + _instance = CrashlyticsService._(FirebaseCrashlytics.instance); + } + + return _instance!; + } + + final FirebaseCrashlytics _crashlyticsService; + CrashlyticsService._( + this._crashlyticsService, + ); + + void recordFlutterErrorToCrashlytics(FlutterErrorDetails details) { + _crashlyticsService.recordFlutterError(details); + } + + Future logToCrashlytics( + Level level, List lines, StackTrace stacktrace, + {required bool logwarnings}) async { + if (level == Level.error || level == Level.wtf) { + await _crashlyticsService.recordError( + lines.join('\n'), + stacktrace, + printDetails: true, + fatal: true, + ); + } + if (level == Level.warning && logwarnings) { + await _crashlyticsService.recordError( + lines.join('\n'), + stacktrace, + printDetails: true, + ); + } + if (level == Level.info || level == Level.verbose || level == Level.debug) { + await _crashlyticsService.log(lines.join('\n')); + } + } + + Future setCustomKeysToTrack(String key, dynamic value) async { + await _crashlyticsService.setCustomKey(key, value); + } + + // Be very careful when you excute this code it will crash the app + // So, be sure to remove it after usage + void crashApp() { + _crashlyticsService.crash(); + } +} + +class CrashlyticsOutput extends LogOutput { + final bool logWarnings; + CrashlyticsOutput({this.logWarnings = false}); + + @override + void output(OutputEvent event) { + try { + CrashlyticsService.getInstance().logToCrashlytics( + event.level, event.lines, StackTrace.current, + logwarnings: logWarnings); + } catch (e) { + print('CRASHLYTICS FAILED: $e'); + } + } +} diff --git a/lib/ui/base/auth_viewmodel.dart b/lib/ui/base/auth_viewmodel.dart index 98f32c7..c2776de 100644 --- a/lib/ui/base/auth_viewmodel.dart +++ b/lib/ui/base/auth_viewmodel.dart @@ -3,14 +3,16 @@ import 'package:feed/app/app.logger.dart'; import 'package:feed/app/app.router.dart'; import 'package:feed/core/enums/bottom_sheet.dart'; import 'package:feed/core/services/user_service.dart'; +import 'package:feed/firebase/analytics.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; abstract class AuthenticationViewModel extends BaseViewModel { - final BottomSheetService _bottomSheetService = locator(); - final SnackbarService _snackbarService = locator(); - final NavigationService _navigationService = locator(); - final UserService _userService = locator(); + final _bottomSheetService = locator(); + final _snackbarService = locator(); + final _navigationService = locator(); + final _userService = locator(); + final _analytics = locator(); final _log = getLogger("Authentication ViewModel"); Future showConstraint({ @@ -61,6 +63,7 @@ abstract class AuthenticationViewModel extends BaseViewModel { _log.i( "User Login Successful : Logged in user: ${_userService.currentUser}"); isProfileExists = await _userService.isUserProfileExists(); + _analytics.logLogin(); setBusy(false); diff --git a/pubspec.lock b/pubspec.lock index a428f73..8a082bf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -70,7 +70,7 @@ packages: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.5" build_runner_core: dependency: transitive description: @@ -84,14 +84,14 @@ packages: name: built_collection url: "https://pub.dartlang.org" source: hosted - version: "5.0.0" + version: "5.1.0" built_value: dependency: transitive description: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "8.0.6" + version: "8.1.0" characters: dependency: transitive description: @@ -119,7 +119,7 @@ packages: name: cli_util url: "https://pub.dartlang.org" source: hosted - version: "0.3.0" + version: "0.3.1" clock: dependency: transitive description: @@ -147,7 +147,7 @@ packages: name: connectivity url: "https://pub.dartlang.org" source: hosted - version: "3.0.3" + version: "3.0.6" connectivity_for_web: dependency: transitive description: @@ -168,14 +168,14 @@ packages: name: connectivity_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" convert: dependency: transitive description: name: convert url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" crypto: dependency: transitive description: @@ -210,7 +210,7 @@ packages: name: equatable url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" fake_async: dependency: transitive description: @@ -224,14 +224,14 @@ packages: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.1.2" file: dependency: transitive description: name: file url: "https://pub.dartlang.org" source: hosted - version: "6.1.0" + version: "6.1.2" firebase: dependency: transitive description: @@ -266,21 +266,21 @@ packages: name: firebase_auth url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.4.1" firebase_auth_platform_interface: dependency: transitive description: name: firebase_auth_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "4.2.4" + version: "4.3.1" firebase_auth_web: dependency: transitive description: name: firebase_auth_web url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.1" firebase_core: dependency: "direct main" description: @@ -302,13 +302,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + firebase_crashlytics: + dependency: "direct main" + description: + name: firebase_crashlytics + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + firebase_crashlytics_platform_interface: + dependency: transitive + description: + name: firebase_crashlytics_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.6" firebase_dynamic_links: dependency: "direct main" description: name: firebase_dynamic_links url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.6" fixnum: dependency: transitive description: @@ -334,7 +348,7 @@ packages: name: flutter_dotenv url: "https://pub.dartlang.org" source: hosted - version: "4.0.0-nullsafety.1" + version: "5.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -351,14 +365,14 @@ packages: name: freezed url: "https://pub.dartlang.org" source: hosted - version: "0.14.1+3" + version: "0.14.2" freezed_annotation: dependency: "direct main" description: name: freezed_annotation url: "https://pub.dartlang.org" source: hosted - version: "0.14.1" + version: "0.14.2" frontend_server_client: dependency: transitive description: @@ -624,7 +638,7 @@ packages: name: package_info url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.2" path: dependency: transitive description: @@ -638,7 +652,7 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.2" path_provider_linux: dependency: transitive description: @@ -659,7 +673,7 @@ packages: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" path_provider_windows: dependency: transitive description: @@ -673,7 +687,7 @@ packages: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.11.0" + version: "1.11.1" petitparser: dependency: transitive description: @@ -757,7 +771,7 @@ packages: name: share url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.4" shared_preferences: dependency: "direct main" description: @@ -832,7 +846,7 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" source_helper: dependency: transitive description: @@ -860,7 +874,7 @@ packages: name: stacked url: "https://pub.dartlang.org" source: hosted - version: "2.1.8" + version: "2.1.9" stacked_firebase_auth: dependency: "direct main" description: @@ -874,14 +888,14 @@ packages: name: stacked_generator url: "https://pub.dartlang.org" source: hosted - version: "0.4.6" + version: "0.4.8" stacked_services: dependency: "direct main" description: name: stacked_services url: "https://pub.dartlang.org" source: hosted - version: "0.8.5" + version: "0.8.8" stream_channel: dependency: transitive description: @@ -951,7 +965,7 @@ packages: name: video_player url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" + version: "2.1.6" video_player_platform_interface: dependency: transitive description: @@ -1056,7 +1070,7 @@ packages: name: youtube_explode_dart url: "https://pub.dartlang.org" source: hosted - version: "1.9.4" + version: "1.9.6" sdks: dart: ">=2.12.0 <3.0.0" flutter: ">=2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 96012c1..0659af3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,30 +30,31 @@ dependencies: # A secure internal NoSQL Database hive: ^2.0.4 path_provider: ^2.0.1 + shared_preferences: ^2.0.6 # graphql for fetching data from API graphql: ^5.0.0-nullsafety.4 # could be handy for hiding keys in [.env] file - flutter_dotenv: ^4.0.0-nullsafety.1 + flutter_dotenv: ^5.0.0 # fetches [version][build-info].. package_info: ^2.0.0 # a videoplayer to play youtube videos flick_video_player: ^0.3.1 + share: ^2.0.1 + youtube_explode_dart: ^1.9.4 + visibility_detector: ^0.2.0 # firebase integration + firebase_core: ^1.1.1 firebase_auth: ^1.1.4 stacked_firebase_auth: ^0.2.5 - firebase_core: ^1.1.1 firebase_dynamic_links: ^2.0.4 - - share: ^2.0.1 - youtube_explode_dart: ^1.9.4 - visibility_detector: ^0.2.0 + firebase_crashlytics: ^2.0.6 firebase_analytics: ^8.1.2 - shared_preferences: ^2.0.6 + dev_dependencies: flutter_test: diff --git a/test/helpers/test_helpers.mocks.dart b/test/helpers/test_helpers.mocks.dart index 0695b1d..b4ab925 100644 --- a/test/helpers/test_helpers.mocks.dart +++ b/test/helpers/test_helpers.mocks.dart @@ -313,7 +313,7 @@ class MockBottomSheetService extends _i1.Mock super.noSuchMethod(Invocation.method(#setCustomSheetBuilders, [builders]), returnValueForMissingStub: null); @override - _i4.Future<_i13.SheetResponse?> showBottomSheet( + _i4.Future<_i13.SheetResponse?> showBottomSheet( {String? title, String? description, String? confirmButtonTitle = r'Ok', @@ -335,10 +335,10 @@ class MockBottomSheetService extends _i1.Mock #exitBottomSheetDuration: exitBottomSheetDuration, #enterBottomSheetDuration: enterBottomSheetDuration }), - returnValue: Future<_i13.SheetResponse?>.value()) - as _i4.Future<_i13.SheetResponse?>); + returnValue: Future<_i13.SheetResponse?>.value()) + as _i4.Future<_i13.SheetResponse?>); @override - _i4.Future<_i13.SheetResponse?> showCustomSheet( + _i4.Future<_i13.SheetResponse?> showCustomSheet( {dynamic variant, String? title, String? description, @@ -356,6 +356,7 @@ class MockBottomSheetService extends _i1.Mock bool? isScrollControlled = false, String? barrierLabel = r'', dynamic customData, + R? data, bool? enableDrag = true, Duration? exitBottomSheetDuration, Duration? enterBottomSheetDuration}) => @@ -378,14 +379,15 @@ class MockBottomSheetService extends _i1.Mock #isScrollControlled: isScrollControlled, #barrierLabel: barrierLabel, #customData: customData, + #data: data, #enableDrag: enableDrag, #exitBottomSheetDuration: exitBottomSheetDuration, #enterBottomSheetDuration: enterBottomSheetDuration }), - returnValue: Future<_i13.SheetResponse?>.value()) - as _i4.Future<_i13.SheetResponse?>); + returnValue: Future<_i13.SheetResponse?>.value()) + as _i4.Future<_i13.SheetResponse?>); @override - void completeSheet(_i13.SheetResponse? response) => + void completeSheet(_i13.SheetResponse? response) => super.noSuchMethod(Invocation.method(#completeSheet, [response]), returnValueForMissingStub: null); }