diff --git a/README.md b/README.md index 8ce1bc8..5f9fd56 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,36 @@ -# mmcalendar +# Myanmar Calendar -A new Flutter project. +The Myanmar Calendar app is a beautifully designed tool that brings the traditional Myanmar calendar to your fingertips. Built with Flutter, this app offers a seamless experience across devices, ensuring you stay connected with Myanmar’s rich cultural heritage. -## Getting Started +## Libraries -This project is a starting point for a Flutter application. +- [flutter_mmcalendar](https://pub.dev/packages/flutter_mmcalendar) +- [table_calendar](https://pub.dev/packages/table_calendar) -A few resources to get you started if this is your first Flutter project: +## Project Setup -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) +To clone the repo for the first time -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +```bash +git clone https://github.com/mixin27/mmcalendar.git +cd mmcalendar/ +flutter packages get +``` + +Generate `build_runner` and `easy_localization` + +```bash +# build_runner +dart run build_runner build + +# easy_localization +dart run easy_localization:generate -S assets/translations -O lib/src/l10n -o locale_keys.g.dart -f keys +``` + +You will need to create firebase project to configure firebase + +```bash +flutterfire configure +``` + +Go to onesignal, login or create an account and create an app. Then copy onesignalAppId and paste to `.env` file. diff --git a/android/app/build.gradle b/android/app/build.gradle index e2c152e..9f68aa9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -67,7 +67,7 @@ android { // Enables resource shrinking, which is performed by the // Android Gradle plugin. shrinkResources true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + // proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } debug { signingConfig signingConfigs.debug diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 275c391..e69de29 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -1,36 +0,0 @@ -#Flutter Wrapper --keep class io.flutter.app.** { *; } --keep class io.flutter.plugin.** { *; } --keep class io.flutter.util.** { *; } --keep class io.flutter.view.** { *; } --keep class io.flutter.** { *; } --keep class io.flutter.plugins.** { *; } --keep class io.flutter.plugin.editing.** { *; } - -#Firebase --keep class com.google.firebase.** { *; } --keep class com.firebase.** { *; } --keep class org.apache.** { *; } --keepnames class com.fasterxml.jackson.** { *; } --keepnames class javax.servlet.** { *; } --keepnames class org.ietf.jgss.** { *; } --dontwarn org.w3c.dom.** --dontwarn org.joda.time.** --dontwarn org.shaded.apache.** --dontwarn org.ietf.jgss.** --keepattributes Signature --keepattributes *Annotation* --keepattributes EnclosingMethod --keepattributes InnerClasses --keep class androidx.lifecycle.DefaultLifecycleObserver - -#Crashlytics --keepattributes SourceFile,LineNumberTable # Keep file names and line numbers. --keep public class * extends java.lang.Exception --keep class com.ito_technologies.soudan.** { *; } - -#twilio_programmable_video --keep class tvi.webrtc.** { *; } --keep class com.twilio.video.** { *; } --keep class com.twilio.common.** { *; } --keepattributes InnerClasses \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle index c6d41c6..b5dba7b 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -24,7 +24,7 @@ plugins { id "com.google.firebase.firebase-perf" version "1.4.1" apply false id "com.google.firebase.crashlytics" version "2.8.1" apply false // END: FlutterFire Configuration - id "org.jetbrains.kotlin.android" version "1.7.10" apply false + id "org.jetbrains.kotlin.android" version "1.9.10" apply false } include ":app" diff --git a/assets/images/logo.png b/assets/images/logo.png new file mode 100644 index 0000000..34d959e Binary files /dev/null and b/assets/images/logo.png differ diff --git a/lib/app_start_up.dart b/lib/app_start_up.dart new file mode 100644 index 0000000..b28b9cd --- /dev/null +++ b/lib/app_start_up.dart @@ -0,0 +1,119 @@ +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mmcalendar/flutter_mmcalendar.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:mmcalendar/firebase_options.dart'; +import 'package:mmcalendar/src/utils/onesignal/onesignal.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'app_start_up.g.dart'; + +@Riverpod(keepAlive: true) +FutureOr appStartup(AppStartupRef ref) async { + ref.onDispose(() { + // ensure dependent providers are disposed as well + // ref.invalidate(onboardingRepositoryProvider); + }); + + MmCalendarConfig.initDefault( + const MmCalendarOptions( + language: Language.myanmar, + ), + ); + + // await for all initialization code to be complete before returning + // we can use `Future.wait` for independent long run tasks. + await Future.wait([ + // Firebase init + Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ), + initOnesignal(), + + // list of providers to be warmed up + // ref.watch(onboardingRepositoryProvider.future), + // Future.delayed(const Duration(seconds: 5)), + ]); +} + +/// Widget class to manage asynchronous app initialization +class AppStartUpWidget extends ConsumerWidget { + const AppStartUpWidget({ + super.key, + required this.onLoaded, + }); + + final WidgetBuilder onLoaded; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appStartupState = ref.watch(appStartupProvider); + + return appStartupState.when( + data: (_) => onLoaded(context), + loading: () => const AppStartupLoadingWidget(), + error: (e, st) => AppStartupErrorWidget( + message: e.toString(), + onRetry: () => ref.invalidate(appStartupProvider), + ), + ); + } +} + +class AppStartupLoadingWidget extends StatelessWidget { + const AppStartupLoadingWidget({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 100, + height: 100, + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('assets/images/logo.png'), + ), + ), + ), + const SizedBox(height: 20), + const CircularProgressIndicator.adaptive(), + ], + ), + ), + ), + ); + } +} + +class AppStartupErrorWidget extends StatelessWidget { + const AppStartupErrorWidget( + {super.key, required this.message, required this.onRetry}); + final String message; + final VoidCallback onRetry; + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(message, style: Theme.of(context).textTheme.headlineSmall), + const SizedBox(height: 16), + ElevatedButton( + onPressed: onRetry, + child: const Text('Retry'), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 5438604..87b945a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,29 +1,44 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mmcalendar/flutter_mmcalendar.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mmcalendar/firebase_options.dart'; import 'package:mmcalendar/src/features/app/app.dart'; +import 'package:mmcalendar/src/shared/errors/async_error_logger.dart'; +import 'package:mmcalendar/src/shared/errors/error_logger.dart'; import 'package:mmcalendar/src/utils/onesignal/onesignal.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); + MmCalendarConfig.initDefault( + const MmCalendarOptions( + language: Language.myanmar, + ), + ); + await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); + await initOnesignal(); - MmCalendarConfig.initDefault( - const MmCalendarOptions( - language: Language.myanmar, - ), + final container = ProviderContainer( + observers: [AsyncErrorLogger()], ); + // * Register error handlers. For more info, see: + // * https://docs.flutter.dev/testing/errors + final errorLogger = container.read(errorLoggerProvider); + registerErrorHandlers(errorLogger); + runApp( - ProviderScope( + UncontrolledProviderScope( + container: container, child: EasyLocalization( supportedLocales: const [ Locale('en'), @@ -37,3 +52,44 @@ Future main() async { ), ); } + +void registerErrorHandlers(ErrorLogger errorLogger) { + // * Show some error UI if any uncaught exception happens + FlutterError.onError = (FlutterErrorDetails details) { + FlutterError.presentError(details); + errorLogger.logError(details.exception, details.stack); + + if (kReleaseMode) { + // Pass all uncaught "fatal" errors from the framework to Crashlytics + FirebaseCrashlytics.instance.recordFlutterFatalError(details); + } + }; + + // * Handle errors from the underlying platform/OS + PlatformDispatcher.instance.onError = (Object error, StackTrace stack) { + errorLogger.logError(error, stack); + + // Pass all uncaught asynchronous errors that aren't handled by the Flutter framework to Crashlytics + if (kReleaseMode) { + FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); + } + return true; + }; + + // * Show some error UI when any widget in the app fails to build + ErrorWidget.builder = (FlutterErrorDetails details) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.redAccent, + title: const Text('An error occurred'), + ), + body: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 20), + child: Text(details.toString()), + ), + ), + ); + }; +} diff --git a/lib/src/features/app/presentation/app.dart b/lib/src/features/app/presentation/app.dart index 9ab4257..fc51dac 100644 --- a/lib/src/features/app/presentation/app.dart +++ b/lib/src/features/app/presentation/app.dart @@ -1,4 +1,5 @@ import 'package:easy_localization/easy_localization.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:flutter/material.dart'; import 'package:mmcalendar/src/routes/routes.dart'; @@ -16,7 +17,12 @@ class AppWidget extends StatelessWidget { colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF18363E)), useMaterial3: true, ), - routerConfig: _appRouter.config(), + routerConfig: _appRouter.config( + navigatorObservers: () => [ + AppRouteObserver(), + FirebaseAnalyticsObserver(analytics: FirebaseAnalytics.instance), + ], + ), // localization stuffs localizationsDelegates: context.localizationDelegates, diff --git a/lib/src/features/settings/presentation/app_settings/app_settings_page.dart b/lib/src/features/settings/presentation/app_settings/app_settings_page.dart index abf6787..d996a99 100644 --- a/lib/src/features/settings/presentation/app_settings/app_settings_page.dart +++ b/lib/src/features/settings/presentation/app_settings/app_settings_page.dart @@ -1,5 +1,4 @@ import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:iconly/iconly.dart'; import 'package:mmcalendar/src/routes/routes.dart'; @@ -22,14 +21,13 @@ class AppSettingsPage extends StatelessWidget { const AppLanguageListTile(), const RateMeListTile(), const PrivacyPolicyListTile(), - AboutListTile( - icon: const Icon(IconlyLight.document), + const AboutListTile( + icon: Icon(IconlyLight.document), applicationName: 'Myanmar Calendar', applicationVersion: 'v1.0.0', - applicationIcon: const Icon(IconlyBroken.calendar), - applicationLegalese: - 'Copyright (c) ${DateFormat().add_y().format(DateTime.now())} Kyaw Zayar Tun', - child: const Text('License'), + applicationIcon: Icon(IconlyBroken.calendar), + applicationLegalese: 'Copyright (c) 2024 Kyaw Zayar Tun', + child: Text('License'), ), ListTile( onTap: () => context.router.push(const AboutRoute()), diff --git a/lib/src/routes/app_route_observer.dart b/lib/src/routes/app_route_observer.dart new file mode 100644 index 0000000..002d995 --- /dev/null +++ b/lib/src/routes/app_route_observer.dart @@ -0,0 +1,29 @@ +import 'package:flutter/widgets.dart'; + +import 'package:auto_route/auto_route.dart'; + +class AppRouteObserver extends AutoRouteObserver { + @override + void didPush(Route route, Route? previousRoute) async { + debugPrint('New route pushed: ${route.settings.name}'); + // await FirebaseAnalytics.instance.logEvent( + // name: 'screen_view', + // parameters: { + // 'screen_name': route.settings.name?.replaceAll('Route', 'Page') ?? '', + // 'screen_class': route.settings.name ?? '', + // }, + // ); + } + + @override + void didPop(Route route, Route? previousRoute) { + debugPrint('Route popped: ${route.settings.name}'); + } + + @override + void didReplace({Route? newRoute, Route? oldRoute}) { + debugPrint( + 'Route replaced: ${oldRoute?.settings.name} -> ${newRoute?.settings.name}', + ); + } +} diff --git a/lib/src/routes/routes.dart b/lib/src/routes/routes.dart index a1c3bc0..59fc9c9 100644 --- a/lib/src/routes/routes.dart +++ b/lib/src/routes/routes.dart @@ -1,2 +1,3 @@ export 'app_router.dart'; export 'app_router.gr.dart'; +export 'app_route_observer.dart'; diff --git a/lib/src/shared/errors/async_error_logger.dart b/lib/src/shared/errors/async_error_logger.dart new file mode 100644 index 0000000..35f5a5c --- /dev/null +++ b/lib/src/shared/errors/async_error_logger.dart @@ -0,0 +1,34 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import 'error_logger.dart'; +import 'exceptions.dart'; + +class AsyncErrorLogger extends ProviderObserver { + @override + void didUpdateProvider( + ProviderBase provider, + Object? previousValue, + Object? newValue, + ProviderContainer container, + ) { + final errorLogger = container.read(errorLoggerProvider); + final error = _findError(newValue); + if (error != null) { + if (error.error is AppException) { + // only prints the AppException data + errorLogger.logException(error.error as AppException); + } else { + // prints everything including the stack trace + errorLogger.logError(error.error, error.stackTrace); + } + } + } + + AsyncError? _findError(Object? value) { + if (value is AsyncError) { + return value; + } else { + return null; + } + } +} diff --git a/lib/src/shared/errors/error_logger.dart b/lib/src/shared/errors/error_logger.dart new file mode 100644 index 0000000..2b2578e --- /dev/null +++ b/lib/src/shared/errors/error_logger.dart @@ -0,0 +1,34 @@ +import 'dart:developer' as developer; + +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import 'exceptions.dart'; + +part 'error_logger.g.dart'; + +abstract class ErrorLogger { + /// * This can be replaced with a call to a crash reporting tool of choice + void logError(Object error, StackTrace? stackTrace); + + /// * This can be replaced with a call to a crash reporting tool of choice + void logException(AppException exception); +} + +class AppErrorLogger implements ErrorLogger { + @override + void logError(Object error, StackTrace? stackTrace) { + // * This can be replaced with a call to a crash reporting tool of choice + developer.log(error.toString(), error: error, stackTrace: stackTrace); + } + + @override + void logException(AppException exception) { + // * This can be replaced with a call to a crash reporting tool of choice + developer.log(exception.toString()); + } +} + +@riverpod +ErrorLogger errorLogger(ErrorLoggerRef ref) { + return AppErrorLogger(); +} diff --git a/lib/src/shared/errors/exceptions.dart b/lib/src/shared/errors/exceptions.dart new file mode 100644 index 0000000..c578dd8 --- /dev/null +++ b/lib/src/shared/errors/exceptions.dart @@ -0,0 +1,17 @@ +/// Base class for all all client-side errors that can be generated by the app +sealed class AppException implements Exception { + AppException(this.code, this.message); + final String code; + final String message; + + @override + String toString() => message; +} + +class UnknownException extends AppException { + UnknownException([String? code, String? message]) + : super( + code ?? 'unknown', + message ?? 'Something went wrong! Please try again.', + ); +} diff --git a/pubspec.yaml b/pubspec.yaml index 60f5368..145d3ab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,6 +47,7 @@ flutter: assets: - assets/translations/ + - assets/images/logo.png # fonts: # - family: Schyler