├── ios ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist └── .gitignore ├── app_gif.gif ├── assets └── cake.png ├── web ├── favicon.png ├── icons │ ├── Icon-192.png │ └── Icon-512.png ├── manifest.json └── index.html ├── l10n.yaml ├── android ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── drawable │ │ │ │ │ ├── app_icon.png │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── launcher_icon.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── launcher_icon.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── launcher_icon.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── launcher_icon.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ └── launcher_icon.png │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── tomerpacific │ │ │ │ │ └── birthday_calendar │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── build.gradle └── settings.gradle ├── lib ├── service │ ├── version_specific_service │ │ ├── VersionSpecificService.dart │ │ └── VersionSpecificServiceImpl.dart │ ├── notification_service │ │ ├── notificationCallbacks.dart │ │ ├── notification_service.dart │ │ └── notification_service_impl.dart │ ├── permission_service │ │ ├── permissions_service.dart │ │ └── permissions_service_impl.dart │ ├── update_service │ │ ├── update_service.dart │ │ └── update_service_impl.dart │ ├── contacts_service │ │ ├── contacts_service.dart │ │ └── contacts_service_impl.dart │ └── storage_service │ │ ├── storage_service.dart │ │ └── shared_preferences_storage.dart ├── BirthdayBloc │ ├── BirthdaysState.dart │ └── BirthdaysBloc.dart ├── VersionBloc │ └── VersionBloc.dart ├── ClearNotificationsBloc │ └── ClearNotificationsBloc.dart ├── ThemeBloc │ └── ThemeBloc.dart ├── model │ └── user_birthday.dart ├── constants.dart ├── UserNotificationStatusBloc │ └── UserNotificationStatusBloc.dart ├── ContactsPermissionStatusBloc │ └── ContactsPermissionStatusBloc.dart ├── widget │ ├── calendar.dart │ ├── calendar_day.dart │ ├── users_without_birthdays_dialogs.dart │ └── add_birthday_form.dart ├── utils.dart ├── l10n │ ├── app_de.arb │ ├── app_hi.arb │ ├── app_en.arb │ ├── app_localizations_hi.dart │ ├── app_localizations_en.dart │ ├── app_localizations_de.dart │ └── app_localizations.dart ├── BirthdayCalendarDateUtils.dart ├── main.dart └── page │ ├── birthdays_for_calendar_day_page │ └── birthdays_for_calendar_day.dart │ ├── settings_page │ └── settings_screen.dart │ ├── birthday │ └── birthday.dart │ └── main_page │ └── main_page.dart ├── README.md ├── .github ├── workflows │ └── flutter_build.yml └── copilot-instructions.md ├── .gitignore ├── pubspec.yaml ├── test ├── date_service_test.dart ├── shared_preferences_test.dart └── birthday_widget_test.dart └── docs └── index.html /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /app_gif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/app_gif.gif -------------------------------------------------------------------------------- /assets/cake.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/assets/cake.png -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/web/favicon.png -------------------------------------------------------------------------------- /l10n.yaml: -------------------------------------------------------------------------------- 1 | arb-dir: lib/l10n 2 | template-arb-file: app_en.arb 3 | output-localization-file: app_localizations.dart -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/web/icons/Icon-512.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/android/app/src/main/res/drawable/app_icon.png -------------------------------------------------------------------------------- /lib/service/version_specific_service/VersionSpecificService.dart: -------------------------------------------------------------------------------- 1 | 2 | abstract class VersionSpecificService { 3 | void migrateNotificationStatus(); 4 | } 5 | -------------------------------------------------------------------------------- /lib/service/notification_service/notificationCallbacks.dart: -------------------------------------------------------------------------------- 1 | 2 | abstract class NotificationCallbacks { 3 | Future onNotificationSelected(String? payload); 4 | } -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/android/app/src/main/res/mipmap-hdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/android/app/src/main/res/mipmap-mdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TomerPacific/BirthdayCalendar/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/tomerpacific/birthday_calendar/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.tomerpacific.birthday_calendar 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.defaults.buildfeatures.buildconfig=true 5 | android.nonTransitiveRClass=false 6 | android.nonFinalResIds=false 7 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip 6 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/service/permission_service/permissions_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:permission_handler/permission_handler.dart'; 2 | 3 | abstract class PermissionsService { 4 | Future getPermissionStatus(String permissionName); 5 | Future requestPermissionAndGetStatus(String permissionName); 6 | } -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /lib/BirthdayBloc/BirthdaysState.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/model/user_birthday.dart'; 2 | 3 | class BirthdaysState { 4 | final DateTime? date; 5 | final List? birthdays; 6 | final bool showAddBirthdayDialog; 7 | 8 | BirthdaysState( 9 | {this.date, this.birthdays, required this.showAddBirthdayDialog}); 10 | } 11 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = '../build' 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(':app') 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /lib/VersionBloc/VersionBloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc/flutter_bloc.dart'; 2 | import 'package:package_info_plus/package_info_plus.dart'; 3 | 4 | enum VersionEvent { versionUnknown } 5 | 6 | class VersionBloc extends Bloc { 7 | VersionBloc() : super("") { 8 | on((event, emit) async { 9 | PackageInfo packageInfo = await PackageInfo.fromPlatform(); 10 | emit(packageInfo.version); 11 | }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/service/update_service/update_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | abstract class UpdateService { 4 | void checkForInAppUpdate(Function onSuccess, Function onFailure, BuildContext context); 5 | bool isUpdateAvailable(); 6 | bool isImmediateUpdatePossible(); 7 | bool isFlexibleUpdatePossible(); 8 | Future applyImmediateUpdate(Function onSuccess, Function onFailure, BuildContext context); 9 | Future startFlexibleUpdate(); 10 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BirthdayCalendar 2 | 3 | An application written in Flutter that let's you store birthdays that are important to you. 4 | When you add a person's birthday, a notifcation is set that will remind you of that person's birthday when it comes. 5 | 6 |
7 | 8 | [Download From Google Play](https://play.google.com/store/apps/details?id=com.tomerpacific.birthday_calendar) 9 | 10 | ![Gif of Application](https://github.com/TomerPacific/BirthdayCalendar/blob/main/app_gif.gif?raw=true) 11 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | 13 | if #available(iOS 10.0, *) { 14 | UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate 15 | } 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/ClearNotificationsBloc/ClearNotificationsBloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/service/storage_service/storage_service.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | enum ClearNotificationsEvent { ClearedNotifications } 5 | 6 | class ClearNotificationsBloc extends Bloc { 7 | ClearNotificationsBloc(StorageService storageService) : super(false) { 8 | on((event, emit) async { 9 | if (event == ClearNotificationsEvent.ClearedNotifications) { 10 | storageService.clearAllBirthdays(); 11 | emit(true); 12 | } 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /lib/ThemeBloc/ThemeBloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/service/storage_service/storage_service.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | 5 | enum ThemeEvent { toggleDark, toggleLight } 6 | 7 | class ThemeBloc extends Bloc { 8 | ThemeBloc(StorageService storageService, bool isDarkMode) : super(isDarkMode ? ThemeMode.dark : ThemeMode.light) { 9 | on((event, emit) { 10 | ThemeMode themeMode = event == ThemeEvent.toggleDark ? ThemeMode.dark : ThemeMode.light; 11 | emit(themeMode); 12 | storageService.saveThemeModeSetting(themeMode == ThemeMode.dark ? true : false); 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/flutter_build.yml: -------------------------------------------------------------------------------- 1 | name: Flutter Build 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | build: 7 | name: flutter build 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-java@v2 12 | with: 13 | distribution: 'zulu' 14 | java-version: '12.x' 15 | - uses: subosito/flutter-action@v2 16 | with: 17 | flutter-version: '3.27.0' 18 | channel: 'stable' 19 | - uses: dart-lang/setup-dart@v1 20 | with: 21 | sdk: '3.6.0' 22 | 23 | - run: dart pub get 24 | 25 | - run: flutter pub get 26 | 27 | - run: flutter analyze 28 | 29 | - run: flutter test -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "birthday_calendar", 3 | "short_name": "birthday_calendar", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A birthday calendar application that lets you save and see when your contacts have birthdays", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version '8.9.1' apply false 22 | id "org.jetbrains.kotlin.android" version "2.2.10" apply false 23 | } 24 | 25 | include ":app" -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/service/contacts_service/contacts_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/model/user_birthday.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:flutter_contacts/flutter_contacts.dart'; 4 | import 'package:permission_handler/permission_handler.dart'; 5 | 6 | abstract class ContactsService { 7 | Future getContactsPermissionStatus(BuildContext context); 8 | Future requestContactsPermission(BuildContext context); 9 | void setContactsPermissionPermanentlyDenied(); 10 | Future isContactsPermissionsPermanentlyDenied(); 11 | Future> filterAlreadyImportedContacts(List contacts); 12 | void handleAddingBirthdaysToContacts(BuildContext context, List contactsWithoutBirthDates); 13 | Future> fetchContacts(bool withThumbnails); 14 | void addContactToCalendar(UserBirthday contact, BuildContext context); 15 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/model/user_birthday.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/constants.dart'; 2 | 3 | class UserBirthday { 4 | final String name; 5 | final DateTime birthdayDate; 6 | bool hasNotification; 7 | String phoneNumber; 8 | 9 | UserBirthday( 10 | this.name, this.birthdayDate, this.hasNotification, this.phoneNumber); 11 | 12 | void updateNotificationStatus(bool status) { 13 | this.hasNotification = status; 14 | } 15 | 16 | bool equals(UserBirthday otherBirthday) { 17 | return (this.name == otherBirthday.name && 18 | this.birthdayDate == otherBirthday.birthdayDate); 19 | } 20 | 21 | UserBirthday.fromJson(Map json) 22 | : name = json[userBirthdayNameKey], 23 | birthdayDate = 24 | DateTime.tryParse(json[userBirthdayDateKey]) ?? DateTime.now(), 25 | hasNotification = json[userBirthdayHasNotificationKey], 26 | phoneNumber = json[userBirthdayPhoneNumberKey]; 27 | 28 | Map toJson() => { 29 | userBirthdayNameKey: name, 30 | userBirthdayDateKey: birthdayDate.toIso8601String(), 31 | userBirthdayHasNotificationKey: hasNotification, 32 | userBirthdayPhoneNumberKey: phoneNumber 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /lib/service/notification_service/notification_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:birthday_calendar/model/user_birthday.dart'; 4 | import 'package:birthday_calendar/service/notification_service/notificationCallbacks.dart'; 5 | import 'package:flutter/cupertino.dart'; 6 | import 'package:flutter_local_notifications/flutter_local_notifications.dart'; 7 | import 'package:permission_handler/permission_handler.dart'; 8 | 9 | abstract class NotificationService { 10 | Future init(BuildContext context); 11 | 12 | Future isNotificationPermissionGranted(BuildContext context); 13 | 14 | Future requestNotificationPermission(BuildContext context); 15 | 16 | void scheduleNotificationForBirthday( 17 | UserBirthday userBirthday, String notificationMessage); 18 | 19 | void cancelNotificationForBirthday(UserBirthday birthday); 20 | 21 | void cancelAllNotifications(); 22 | 23 | Future> getAllScheduledNotifications(); 24 | 25 | void dispose(); 26 | 27 | void addListenerForSelectNotificationStream(NotificationCallbacks listener); 28 | 29 | void removeListenerForSelectNotificationStream( 30 | NotificationCallbacks listener); 31 | } 32 | -------------------------------------------------------------------------------- /lib/constants.dart: -------------------------------------------------------------------------------- 1 | const String applicationName = "Birthday Calendar"; 2 | 3 | const JANUARY_MONTH_NUMBER = 1; 4 | const FEBRUARY_MONTH_NUMBER = 2; 5 | const MARCH_MONTH_NUMBER = 3; 6 | const APRIL_MONTH_NUMBER = 4; 7 | const MAY_MONTH_NUMBER = 5; 8 | const JUNE_MONTH_NUMBER = 6; 9 | const JULY_MONTH_NUMBER = 7; 10 | const AUGUST_MONTH_NUMBER = 8; 11 | const SEPTEMBER_MONTH_NUMBER = 9; 12 | const OCTOBER_MONTH_NUMBER = 10; 13 | const NOVEMBER_MONTH_NUMBER = 11; 14 | const DECEMBER_MONTH_NUMBER = 12; 15 | 16 | const userBirthdayNameKey = "name"; 17 | const userBirthdayDateKey = "birthdayDate"; 18 | const userBirthdayHasNotificationKey = "hasNotification"; 19 | const userBirthdayPhoneNumberKey = "phoneNumber"; 20 | 21 | const darkModeKey = "darkMode"; 22 | const contactsPermissionKey = "contacts"; 23 | const notificationsPermissionKey = "notifications"; 24 | const contactsPermissionStatusKey = "contactsPermissionStatusKey"; 25 | const notificationsPermissionStatusKey = "notificationsPermissionStatusKey"; 26 | 27 | const versionToMigrateNotificationStatusFrom = "1.2.1"; 28 | const didAlreadyMigrateNotificationStatusFlag = "migrateNotificationStatus"; 29 | 30 | enum NotificationPermissionState { 31 | unknown, 32 | granted, 33 | deniedTemporary, 34 | deniedPermanently, 35 | } -------------------------------------------------------------------------------- /lib/service/permission_service/permissions_service_impl.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'permissions_service.dart'; 3 | import 'package:permission_handler/permission_handler.dart'; 4 | import 'package:birthday_calendar/constants.dart'; 5 | 6 | class PermissionsServiceImpl extends PermissionsService { 7 | @override 8 | Future getPermissionStatus(String permissionName) async { 9 | PermissionStatus status = PermissionStatus.denied; 10 | switch(permissionName) { 11 | case contactsPermissionKey: 12 | status = await Permission.contacts.status; 13 | break; 14 | case notificationsPermissionKey: 15 | status = await Permission.notification.status; 16 | break; 17 | } 18 | 19 | return status; 20 | } 21 | 22 | @override 23 | Future requestPermissionAndGetStatus(String permissionName) async { 24 | PermissionStatus status = PermissionStatus.denied; 25 | switch(permissionName) { 26 | case contactsPermissionKey: 27 | await Permission.contacts.shouldShowRequestRationale; 28 | status = await Permission.contacts.request(); 29 | break; 30 | case notificationsPermissionKey: 31 | await Permission.notification.shouldShowRequestRationale; 32 | status = await Permission.notification.request(); 33 | break; 34 | } 35 | 36 | return status; 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /lib/service/storage_service/storage_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/constants.dart'; 2 | import 'package:birthday_calendar/model/user_birthday.dart'; 3 | 4 | abstract class StorageService { 5 | Future> getBirthdaysForDate( 6 | DateTime dateTime, bool shouldGetBirthdaysFromSimilarDate); 7 | 8 | Future saveBirthdaysForDate( 9 | DateTime dateTime, List birthdays); 10 | 11 | void clearAllBirthdays(); 12 | 13 | Future updateNotificationStatusForBirthday( 14 | UserBirthday userBirthday, bool updatedStatus); 15 | 16 | Future getThemeModeSetting(); 17 | 18 | Future saveThemeModeSetting(bool isDarkModeEnabled); 19 | 20 | Stream> getBirthdaysStream(); 21 | 22 | void saveIsContactsPermissionPermanentlyDenied(bool isPermanentlyDenied); 23 | 24 | Future getIsContactPermissionPermanentlyDenied(); 25 | 26 | void saveDidAlreadyMigrateNotificationStatus(bool status); 27 | 28 | Future getAlreadyMigrateNotificationStatus(); 29 | 30 | Future> getAllBirthdays(); 31 | 32 | Future updatePhoneNumberForBirthday(UserBirthday birthday); 33 | 34 | Future setNotificationPermissionState( 35 | NotificationPermissionState state); 36 | 37 | Future getNotificationPermissionState(); 38 | 39 | void dispose(); 40 | } 41 | -------------------------------------------------------------------------------- /lib/UserNotificationStatusBloc/UserNotificationStatusBloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/model/user_birthday.dart'; 2 | import 'package:birthday_calendar/service/notification_service/notification_service.dart'; 3 | import 'package:birthday_calendar/service/storage_service/storage_service.dart'; 4 | import 'package:flutter_bloc/flutter_bloc.dart'; 5 | 6 | class UserNotificationStatusEvent { 7 | UserNotificationStatusEvent( 8 | {required this.userBirthday, 9 | required this.hasNotification, 10 | required this.notificationMsg}); 11 | 12 | final UserBirthday userBirthday; 13 | final bool hasNotification; 14 | final String notificationMsg; 15 | } 16 | 17 | class UserNotificationStatusBloc 18 | extends Bloc { 19 | UserNotificationStatusBloc( 20 | StorageService storageService, NotificationService notificationService) 21 | : super(false) { 22 | on((event, emit) async { 23 | bool notificationStatus = event.hasNotification; 24 | notificationStatus = !notificationStatus; 25 | UserBirthday birthday = event.userBirthday; 26 | birthday.hasNotification = notificationStatus; 27 | storageService.updateNotificationStatusForBirthday( 28 | birthday, notificationStatus); 29 | if (!notificationStatus) { 30 | notificationService.cancelNotificationForBirthday(birthday); 31 | } else { 32 | notificationService.scheduleNotificationForBirthday( 33 | birthday, event.notificationMsg); 34 | } 35 | emit(notificationStatus); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/ContactsPermissionStatusBloc/ContactsPermissionStatusBloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/service/contacts_service/contacts_service.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:permission_handler/permission_handler.dart'; 4 | 5 | enum ContactsPermissionStatusEvent { 6 | PermissionUnknown, 7 | PermissionDenied, 8 | PermissionGranted, 9 | PermissionPermanentlyDenied 10 | } 11 | 12 | class ContactsPermissionStatusBloc 13 | extends Bloc { 14 | ContactsPermissionStatusBloc(ContactsService contactsService) 15 | : super(PermissionStatus.denied) { 16 | on((event, emit) async { 17 | if (event == ContactsPermissionStatusEvent.PermissionUnknown) { 18 | bool permissionStatus = 19 | await contactsService.isContactsPermissionsPermanentlyDenied(); 20 | if (permissionStatus) { 21 | emit(PermissionStatus.permanentlyDenied); 22 | return; 23 | } 24 | } 25 | emit(_convertEventNameToPermissionStatus(event)); 26 | }); 27 | } 28 | 29 | PermissionStatus _convertEventNameToPermissionStatus( 30 | ContactsPermissionStatusEvent event) { 31 | switch (event) { 32 | case ContactsPermissionStatusEvent.PermissionDenied: 33 | return PermissionStatus.denied; 34 | case ContactsPermissionStatusEvent.PermissionGranted: 35 | return PermissionStatus.granted; 36 | case ContactsPermissionStatusEvent.PermissionPermanentlyDenied: 37 | return PermissionStatus.permanentlyDenied; 38 | default: 39 | return PermissionStatus.denied; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | birthday_calendar 30 | 31 | 32 | 33 | 36 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | Birthday Calendar 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | LSApplicationQueriesSchemes 45 | 46 | tel 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /lib/widget/calendar.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/BirthdayCalendarDateUtils.dart'; 2 | import 'package:birthday_calendar/service/notification_service/notification_service.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:birthday_calendar/widget/calendar_day.dart'; 5 | 6 | class CalendarWidget extends StatefulWidget { 7 | final int currentMonth; 8 | final NotificationService notificationService; 9 | 10 | const CalendarWidget( 11 | {required Key key, 12 | required this.currentMonth, 13 | required this.notificationService}) 14 | : super(key: key); 15 | 16 | @override 17 | _CalendarState createState() => _CalendarState(); 18 | } 19 | 20 | class _CalendarState extends State { 21 | @override 22 | void initState() { 23 | super.initState(); 24 | } 25 | 26 | @override 27 | void didUpdateWidget(CalendarWidget oldWidget) { 28 | super.didUpdateWidget(oldWidget); 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return new Center( 34 | child: new SizedBox( 35 | height: (MediaQuery.of(context).size.height), 36 | child: new GridView.builder( 37 | gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( 38 | crossAxisCount: 5), 39 | itemCount: BirthdayCalendarDateUtils.amountOfDaysInMonth( 40 | widget.currentMonth), 41 | shrinkWrap: true, 42 | itemBuilder: (BuildContext context, int index) { 43 | return new CalendarDayWidget( 44 | key: Key(widget.currentMonth.toString()), 45 | date: BirthdayCalendarDateUtils 46 | .constructDateTimeFromDayAndMonth( 47 | (index + 1), widget.currentMonth), 48 | notificationService: widget.notificationService); 49 | })), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: birthday_calendar 2 | description: A birthday calendar application that lets you save and see when your contacts have birthdays 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 2.1.0+26 19 | 20 | environment: 21 | flutter: ">=3.22.0 <4.0.0" 22 | sdk: ">=3.0.0 <4.0.0" 23 | 24 | dependencies: 25 | flutter: 26 | sdk: flutter 27 | flutter_localizations: 28 | sdk: flutter 29 | intl: any 30 | shared_preferences: 2.3.4 31 | flutter_local_notifications: 19.5.0 32 | flutter_launcher_icons: 0.9.0 33 | cupertino_icons: 1.0.2 34 | intl_phone_number_input: 0.7.3 35 | url_launcher: 6.3.2 36 | package_info_plus: 9.0.0 37 | permission_handler: 12.0.0 38 | flutter_contacts: 1.1.9+2 39 | provider: 6.0.2 40 | in_app_update: 4.2.5 41 | flutter_bloc: 8.1.5 42 | 43 | 44 | dev_dependencies: 45 | flutter_test: 46 | sdk: flutter 47 | 48 | # For information on the generic Dart part of this file, see the 49 | # following page: https://dart.dev/tools/pub/pubspec 50 | 51 | flutter_icons: 52 | android: "launcher_icon" 53 | ios: true 54 | image_path: "assets/cake.png" 55 | 56 | 57 | flutter: 58 | generate: true 59 | uses-material-design: true 60 | 61 | -------------------------------------------------------------------------------- /lib/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:developer'; 3 | import 'package:birthday_calendar/service/storage_service/storage_service.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_contacts/contact.dart'; 6 | import 'model/user_birthday.dart'; 7 | 8 | enum ElementType { background, icon, text } 9 | 10 | class Utils { 11 | static Future> filterAlreadyImportedContacts( 12 | StorageService _storageService, List contacts) async { 13 | List allStoredBirthdays = 14 | await _storageService.getAllBirthdays(); 15 | List names = allStoredBirthdays.map((e) => e.name).toList(); 16 | List filtered = contacts 17 | .where((contact) => !names.contains(contact.displayName)) 18 | .toList(); 19 | return filtered; 20 | } 21 | 22 | static UserBirthday? getUserBirthdayFromPayload(String? payload) { 23 | if (payload == null || payload.isEmpty) { 24 | return null; 25 | } 26 | 27 | UserBirthday? userBirthday; 28 | try { 29 | Map json = jsonDecode(payload); 30 | userBirthday = UserBirthday.fromJson(json); 31 | } on Exception catch (e) { 32 | log("Failed converting payload to UserBirthday object", error: e); 33 | } 34 | 35 | return userBirthday; 36 | } 37 | 38 | static void showSnackbarWithMessageAndAction( 39 | BuildContext context, String message, SnackBarAction action) { 40 | ScaffoldMessenger.of(context).showSnackBar(SnackBar( 41 | content: Text(message), 42 | action: action, 43 | )); 44 | } 45 | 46 | static void showSnackbarWithMessage(BuildContext context, String message) { 47 | ScaffoldMessenger.of(context).showSnackBar(SnackBar( 48 | content: Text(message), 49 | )); 50 | } 51 | 52 | static int correctMonthOverflow(int month) { 53 | if (month == 0) { 54 | month = 12; 55 | } else if (month == 13) { 56 | month = 1; 57 | } 58 | return month; 59 | } 60 | 61 | static Color getColorBasedOnPosition(int index, ElementType type) { 62 | if (type == ElementType.background) { 63 | return index.isEven ? Colors.indigoAccent : Colors.white24; 64 | } 65 | 66 | return index.isEven ? Colors.white : Colors.black; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/date_service_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/BirthdayCalendarDateUtils.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:birthday_calendar/l10n/app_localizations.dart'; 5 | 6 | void main() { 7 | final appLocalizations = lookupAppLocalizations(const Locale('en')); 8 | 9 | test("DateService convert month number 8 to August", () { 10 | final int monthNumber = 8; 11 | final String monthName = 12 | BirthdayCalendarDateUtils.convertAndTranslateMonthNumber( 13 | monthNumber, appLocalizations); 14 | expect(monthName, "August"); 15 | }); 16 | 17 | test('DateService invalid month number returns empty string', () { 18 | final int monthNumber = 14; 19 | final String monthName = 20 | BirthdayCalendarDateUtils.convertAndTranslateMonthNumber( 21 | monthNumber, appLocalizations); 22 | expect(monthName, ""); 23 | }); 24 | 25 | test("DateService get amount of days in month with 30 days", () { 26 | final int monthNumber = 9; 27 | final int amountOfDays = 28 | BirthdayCalendarDateUtils.amountOfDaysInMonth(monthNumber); 29 | expect(amountOfDays, 30); 30 | }); 31 | 32 | test("DateService get amount of days in invalid month will be equal to zero", 33 | () { 34 | final int monthNumber = 13; 35 | final int amountOfDays = 36 | BirthdayCalendarDateUtils.amountOfDaysInMonth(monthNumber); 37 | expect(amountOfDays, 0); 38 | }); 39 | 40 | test("DateService for the date of 5/12/21 we should get the day as Sunday", 41 | () { 42 | final DateTime dateTime = DateTime(2021, 12, 5); 43 | final String day = 44 | BirthdayCalendarDateUtils.getWeekdayNameFromDate(dateTime); 45 | expect(day, "Sunday"); 46 | }); 47 | 48 | test("DateService convert String representing actual date", () { 49 | final String date = "2020-01-04"; 50 | final bool isAValidDate = BirthdayCalendarDateUtils.isADate(date); 51 | expect(isAValidDate, true); 52 | }); 53 | 54 | test("DateService convert String NOT representing date", () { 55 | final String date = "Hello World!"; 56 | final bool isAValidDate = BirthdayCalendarDateUtils.isADate(date); 57 | expect(isAValidDate, false); 58 | }); 59 | 60 | test("DateService check current month number", () { 61 | final int currentMonthNumber = DateTime.now().month; 62 | final int monthNumber = BirthdayCalendarDateUtils.getCurrentMonthNumber(); 63 | expect(monthNumber, currentMonthNumber); 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | def keystoreProperties = new Properties() 26 | def keystorePropertiesFile = rootProject.file('key.properties') 27 | if (keystorePropertiesFile.exists()) { 28 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 29 | } 30 | 31 | android { 32 | compileSdkVersion 36 33 | 34 | sourceSets { 35 | main.java.srcDirs += 'src/main/kotlin' 36 | } 37 | 38 | defaultConfig { 39 | namespace "com.tomerpacific.birthday_calendar" 40 | minSdkVersion flutter.minSdkVersion 41 | targetSdkVersion 36 42 | versionCode flutterVersionCode.toInteger() 43 | versionName flutterVersionName 44 | multiDexEnabled true 45 | } 46 | 47 | signingConfigs { 48 | release { 49 | keyAlias keystoreProperties['keyAlias'] 50 | keyPassword keystoreProperties['keyPassword'] 51 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 52 | storePassword keystoreProperties['storePassword'] 53 | } 54 | } 55 | 56 | buildTypes { 57 | release { 58 | signingConfig signingConfigs.release 59 | } 60 | debug { 61 | minifyEnabled true 62 | signingConfig signingConfigs.debug 63 | } 64 | } 65 | 66 | compileOptions { 67 | coreLibraryDesugaringEnabled true 68 | sourceCompatibility JavaVersion.VERSION_17 69 | targetCompatibility JavaVersion.VERSION_17 70 | } 71 | 72 | kotlinOptions { 73 | jvmTarget = 17 74 | } 75 | 76 | namespace 'com.tomerpacific.birthday_calendar' 77 | } 78 | 79 | flutter { 80 | source '../..' 81 | } 82 | 83 | dependencies { 84 | coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4' 85 | implementation 'androidx.window:window:1.0.0' 86 | implementation 'androidx.window:window-java:1.0.0' 87 | } 88 | -------------------------------------------------------------------------------- /lib/service/update_service/update_service_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/service/update_service/update_service.dart'; 2 | import 'package:flutter/cupertino.dart'; 3 | import 'package:in_app_update/in_app_update.dart'; 4 | import 'package:birthday_calendar/l10n/app_localizations.dart'; 5 | 6 | class UpdateServiceImpl extends UpdateService { 7 | AppUpdateInfo? _appUpdateInfo; 8 | 9 | void checkForInAppUpdate( 10 | Function onSuccess, Function onFailure, BuildContext context) { 11 | InAppUpdate.checkForUpdate().then((value) { 12 | _appUpdateInfo = value; 13 | _checkForUpdateAvailability(onSuccess, onFailure, context); 14 | }).catchError((error) { 15 | onFailure(error.toString()); 16 | }); 17 | } 18 | 19 | @override 20 | bool isUpdateAvailable() { 21 | if (_appUpdateInfo != null) { 22 | return _appUpdateInfo!.updateAvailability == 23 | UpdateAvailability.updateAvailable; 24 | } 25 | return false; 26 | } 27 | 28 | @override 29 | bool isImmediateUpdatePossible() { 30 | if (_appUpdateInfo != null) { 31 | return _appUpdateInfo!.immediateUpdateAllowed; 32 | } 33 | 34 | return false; 35 | } 36 | 37 | @override 38 | bool isFlexibleUpdatePossible() { 39 | if (_appUpdateInfo != null) { 40 | return _appUpdateInfo!.flexibleUpdateAllowed; 41 | } 42 | 43 | return false; 44 | } 45 | 46 | @override 47 | Future applyImmediateUpdate( 48 | Function onSuccess, Function onFailure, BuildContext context) async { 49 | InAppUpdate.performImmediateUpdate() 50 | .then((appUpdateResult) => { 51 | if (appUpdateResult == AppUpdateResult.userDeniedUpdate) 52 | {onFailure(AppLocalizations.of(context)!.userDeniedUpdate)} 53 | else if (appUpdateResult == AppUpdateResult.inAppUpdateFailed) 54 | {onFailure(AppLocalizations.of(context)!.appUpdateFailed)} 55 | else 56 | {onSuccess()} 57 | }) 58 | .catchError((onError) { 59 | return onFailure(onError); 60 | }); 61 | } 62 | 63 | @override 64 | Future startFlexibleUpdate() async { 65 | AppUpdateResult appUpdateResult = await InAppUpdate.startFlexibleUpdate(); 66 | if (appUpdateResult == AppUpdateResult.success) { 67 | InAppUpdate.completeFlexibleUpdate(); 68 | } 69 | } 70 | 71 | void _checkForUpdateAvailability( 72 | Function onSuccess, Function onFailure, BuildContext context) { 73 | bool needToUpdate = isUpdateAvailable(); 74 | if (needToUpdate) { 75 | bool isImmediateUpdateAvailable = isImmediateUpdatePossible(); 76 | if (isImmediateUpdateAvailable) { 77 | applyImmediateUpdate(onSuccess, onFailure, context); 78 | } else { 79 | bool isFlexibleUpdateAvailable = isFlexibleUpdatePossible(); 80 | if (isFlexibleUpdateAvailable) { 81 | startFlexibleUpdate(); 82 | } 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/l10n/app_de.arb: -------------------------------------------------------------------------------- 1 | { 2 | "appTitle": "Geburtstagskalender", 3 | "settings": "Einstellungen", 4 | "addBirthday": "Geburtstag hinzufügen", 5 | "contactsImportedSuccessfully": "Kontakte erfolgreich importiert", 6 | "noContactsFoundMsg": "Es gibt keine Kontakte auf Ihrem Gerät", 7 | "alreadyAddedContactsMsg": "Alle aktuellen Kontakte wurden bereits hinzugefügt", 8 | "unableToMakeCallMsg": "Wir können den Anruf nicht tätigen", 9 | "january": "Januar", 10 | "february": "Februar", 11 | "march": "März", 12 | "april": "April", 13 | "may": "Mai", 14 | "june": "Juni", 15 | "july": "Juli", 16 | "august": "August", 17 | "september": "September", 18 | "october": "Oktober", 19 | "november": "November", 20 | "december": "Dezember", 21 | "ok": "Ok", 22 | "updateSuccessfullyInstalledTitle": "Update erfolgreich installiert", 23 | "updateSuccessfullyInstalledDescription": "Geburtstagskalender wurde erfolgreich aktualisiert! 🎂", 24 | "tryAgain": "Erneut versuchen?", 25 | "dismiss": "Schließen", 26 | "updateFailedToInstallTitle": "Update konnte nicht installiert werden ❌", 27 | "updateFailedToInstallDescription": "Geburtstagskalender konnte nicht aktualisiert werden, weil: {error}", 28 | "userDeniedUpdate": "Benutzer hat das Update abgelehnt", 29 | "appUpdateFailed": "App-Update fehlgeschlagen", 30 | "darkMode": "Dunkelmodus", 31 | "importContacts": "Kontakte importieren", 32 | "clearNotifications": "Benachrichtigungen löschen", 33 | "clearNotificationsAlertTitle": "Sind Sie sicher?", 34 | "clearNotificationsAlertDescription": "Möchten Sie alle Benachrichtigungen entfernen?", 35 | "no": "Nein", 36 | "yes": "Ja", 37 | "addPhoneNumber": "Telefonnummer hinzufügen", 38 | "add": "Hinzufügen", 39 | "cancel": "Abbrechen", 40 | "back": "Zurück", 41 | "proceed": "Fortfahren", 42 | "hintTextForNameInputField": "Name?", 43 | "notValidName": "Bitte geben Sie einen gültigen Namen ein", 44 | "nameAlreadyExists": "Ein Geburtstag mit diesem Namen existiert bereits", 45 | "birthdaysForDayAndMonth": "Geburtstage am {day}. {month}", 46 | "helpTextChooseBirthdateForImportedContact": "Wählen Sie das Geburtsdatum für {contactName}", 47 | "fieldLabelTextChooseBirthdateForImportedContact": "Geburtsdatum von {contactName}", 48 | "notificationForBirthdayMessage": "{contactName} hat bald Geburtstag!", 49 | "addBirthdaysToContactsAlertDialogTitle": "Geburtstage zu Kontakten hinzufügen", 50 | "addBirthdaysToContactsAlertDialogDescription": "Möchten Sie Geburtsdaten für Ihre importierten Kontakte hinzufügen?", 51 | "peopleWithoutBirthdaysAlertDialogTitle": "Personen ohne Geburtstage", 52 | "notificationPermissionDenied": "Um Benachrichtigungen zu Geburtstagen zu erhalten, müssen Sie BirthdayCalendar die Berechtigung erteilen, Ihnen Benachrichtigungen zu senden", 53 | "notificationPermissionPermanentlyDenied": "Sie müssen die Benachrichtigungsberechtigung in den App-Einstellungen aktivieren, um Benachrichtigungen planen zu können.", 54 | "openSettings": "Einstellungen öffnen" 55 | } -------------------------------------------------------------------------------- /lib/l10n/app_hi.arb: -------------------------------------------------------------------------------- 1 | { 2 | "appTitle": "जन्मदिन कैलेंडर", 3 | "settings": "सेटिंग्स", 4 | "addBirthday": "जन्मदिन जोड़ें", 5 | "contactsImportedSuccessfully": "संपर्क सफलतापूर्वक आयात किए गए", 6 | "noContactsFoundMsg": "आपके डिवाइस पर कोई संपर्क नहीं मिला", 7 | "alreadyAddedContactsMsg": "आपके सभी वर्तमान संपर्क पहले ही जोड़े जा चुके हैं", 8 | "unableToMakeCallMsg": "हम कॉल नहीं कर सकते", 9 | "january": "जनवरी", 10 | "february": "फ़रवरी", 11 | "march": "मार्च", 12 | "april": "अप्रैल", 13 | "may": "मई", 14 | "june": "जून", 15 | "july": "जुलाई", 16 | "august": "अगस्त", 17 | "september": "सितंबर", 18 | "october": "अक्टूबर", 19 | "november": "नवंबर", 20 | "december": "दिसंबर", 21 | "ok": "ठीक है", 22 | "updateSuccessfullyInstalledTitle": "अपडेट सफलतापूर्वक इंस्टॉल किया गया", 23 | "updateSuccessfullyInstalledDescription": "बर्थडे कैलेंडर को सफलतापूर्वक अपडेट कर दिया गया है! 🎂", 24 | "tryAgain": "फिर से प्रयास करें?", 25 | "dismiss": "खारिज करें", 26 | "updateFailedToInstallTitle": "अपडेट इंस्टॉल करने में विफल ❌", 27 | "updateFailedToInstallDescription": "बर्थडे कैलेंडर अपडेट करने में विफल रहा क्योंकि: {error}", 28 | "userDeniedUpdate": "उपयोगकर्ता ने अपडेट अस्वीकार कर दिया", 29 | "appUpdateFailed": "ऐप अपडेट असफल रहा", 30 | "darkMode": "डार्क मोड", 31 | "importContacts": "संपर्क आयात करें", 32 | "clearNotifications": "सूचनाएँ साफ़ करें", 33 | "clearNotificationsAlertTitle": "क्या आप सुनिश्चित हैं?", 34 | "clearNotificationsAlertDescription": "क्या आप सभी सूचनाएं हटाना चाहते हैं?", 35 | "no": "नहीं", 36 | "yes": "हाँ", 37 | "addPhoneNumber": "फोन नंबर जोड़ें", 38 | "add": "जोड़ें", 39 | "cancel": "रद्द करें", 40 | "back": "वापस", 41 | "proceed": "आगे बढ़ें", 42 | "hintTextForNameInputField": "नाम?", 43 | "notValidName": "कृपया एक मान्य नाम दर्ज करें", 44 | "nameAlreadyExists": "इस नाम से एक जन्मदिन पहले से मौजूद है", 45 | "birthdaysForDayAndMonth": "{day} {month} के लिए जन्मदिन", 46 | "helpTextChooseBirthdateForImportedContact": "{contactName} के लिए जन्मतिथि चुनें", 47 | "fieldLabelTextChooseBirthdateForImportedContact": "{contactName} की जन्मतिथि", 48 | "contactsImportedSuccessfully": "संपर्क सफलतापूर्वक आयात किए गए", 49 | "notificationForBirthdayMessage": "{contactName} का जन्मदिन आने वाला है!", 50 | "unableToMakeCallMsg": "हम कॉल करने में असमर्थ हैं", 51 | "addBirthdaysToContactsAlertDialogTitle": "Add Birthdays To Contacts", 52 | "addBirthdaysToContactsAlertDialogDescription": "Would you like to add birth dates for your imported contacts?", 53 | "peopleWithoutBirthdaysAlertDialogTitle": "People Without Birthdays", 54 | "notificationPermissionDenied": "जन्मदिन की सूचनाएँ प्राप्त करने के लिए, आपको BirthdayCalendar को आपको सूचनाएँ भेजने की अनुमति देनी होगी", 55 | "notificationPermissionPermanentlyDenied": "सूचनाएँ निर्धारित करने के लिए आपको ऐप की सेटिंग्स में अधिसूचना अनुमति चालू करनी होगी।", 56 | "openSettings": "सेटिंग्स खोलें" 57 | } -------------------------------------------------------------------------------- /lib/l10n/app_en.arb: -------------------------------------------------------------------------------- 1 | { 2 | "appTitle": "Birthday Calendar", 3 | "settings": "Settings", 4 | "addBirthday": "Add Birthday", 5 | "contactsImportedSuccessfully": "Contacts Imported Successfully", 6 | "noContactsFoundMsg": "There are no contacts on your device", 7 | "alreadyAddedContactsMsg": "All of your current contacts have already been added", 8 | "unableToMakeCallMsg": "We are unable to make the call", 9 | "january": "January", 10 | "february": "February", 11 | "march": "March", 12 | "april": "April", 13 | "may": "May", 14 | "june": "June", 15 | "july": "July", 16 | "august": "August", 17 | "september": "September", 18 | "october": "October", 19 | "november": "November", 20 | "december": "December", 21 | "ok": "Ok", 22 | "updateSuccessfullyInstalledTitle": "Update Successfully Installed", 23 | "updateSuccessfullyInstalledDescription": "Birthday Calendar has been updated successfully! 🎂", 24 | "tryAgain": "Try Again?", 25 | "dismiss": "Dismiss", 26 | "updateFailedToInstallTitle": "Update Failed To Install ❌", 27 | "updateFailedToInstallDescription": "Birthday Calendar has failed to update because: {error}", 28 | "userDeniedUpdate": "User denied update", 29 | "appUpdateFailed": "App Update Failed", 30 | "darkMode": "Dark Mode", 31 | "importContacts": "Import Contacts", 32 | "clearNotifications": "Clear Notifications", 33 | "clearNotificationsAlertTitle": "Are You Sure?", 34 | "clearNotificationsAlertDescription": "Do you want to remove all notifications?", 35 | "no": "No", 36 | "yes": "Yes", 37 | "addPhoneNumber": "Add Phone Number", 38 | "add": "Add", 39 | "cancel": "Cancel", 40 | "back": "Back", 41 | "proceed": "Proceed", 42 | "hintTextForNameInputField": "Name?", 43 | "notValidName": "Please enter a valid name", 44 | "nameAlreadyExists": "A birthday with this name already exists", 45 | "birthdaysForDayAndMonth": "Birthdays for {day} {month}", 46 | "helpTextChooseBirthdateForImportedContact": "Choose birth date for {contactName}", 47 | "fieldLabelTextChooseBirthdateForImportedContact": "{contactName}'s birth date", 48 | "contactsImportedSuccessfully": "Contacts Imported Successfully", 49 | "notificationForBirthdayMessage": "{contactName} has an upcoming birthday!", 50 | "unableToMakeCallMsg": "We are unable to make the call", 51 | "addBirthdaysToContactsAlertDialogTitle": "Add Birthdays To Contacts", 52 | "addBirthdaysToContactsAlertDialogDescription": "Would you like to add birth dates for your imported contacts?", 53 | "peopleWithoutBirthdaysAlertDialogTitle": "People Without Birthdays", 54 | "notificationPermissionDenied": "In order to get notifications for birthdays, you will need to authorize BirthdayCalendar to send you notifications", 55 | "notificationPermissionPermanentlyDenied": "You will need to turn on the notification permission in the application's settings in order to schedule notifications", 56 | "openSettings": "Open Settings" 57 | } -------------------------------------------------------------------------------- /lib/service/version_specific_service/VersionSpecificServiceImpl.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'package:birthday_calendar/model/user_birthday.dart'; 3 | import 'VersionSpecificService.dart'; 4 | import 'package:birthday_calendar/service/notification_service/notification_service.dart'; 5 | import 'package:birthday_calendar/service/storage_service/storage_service.dart'; 6 | import 'package:package_info_plus/package_info_plus.dart'; 7 | import 'package:birthday_calendar/constants.dart'; 8 | import 'package:flutter_local_notifications/flutter_local_notifications.dart'; 9 | import 'dart:convert'; 10 | import 'package:collection/collection.dart'; 11 | 12 | class VersionSpecificServiceImpl extends VersionSpecificService { 13 | 14 | VersionSpecificServiceImpl({ 15 | required this.storageService, 16 | required this.notificationService 17 | }) { 18 | migrateNotificationStatus(); 19 | } 20 | 21 | final StorageService storageService; 22 | final NotificationService notificationService; 23 | 24 | 25 | @override 26 | void migrateNotificationStatus() async { 27 | 28 | bool didAlreadyMigrateNotificationStatus = await storageService.getAlreadyMigrateNotificationStatus(); 29 | if (didAlreadyMigrateNotificationStatus) { 30 | return; 31 | } 32 | 33 | PackageInfo packageInfo = await PackageInfo.fromPlatform(); 34 | if (_isVersionGreaterThan(packageInfo.version, versionToMigrateNotificationStatusFrom)) { 35 | List pendingNotifications = await notificationService.getAllScheduledNotifications(); 36 | for(PendingNotificationRequest request in pendingNotifications) { 37 | if (request.payload != null) { 38 | String payload = request.payload!; 39 | UserBirthday userBirthday = UserBirthday.fromJson(jsonDecode(payload)); 40 | if (!userBirthday.hasNotification) { 41 | List birthdays = await storageService.getBirthdaysForDate(userBirthday.birthdayDate, false); 42 | UserBirthday? found = birthdays.firstWhereOrNull((element) => element.equals(userBirthday)); 43 | if (found != null) { 44 | birthdays.remove(found); 45 | userBirthday.updateNotificationStatus(true); 46 | birthdays.add(userBirthday); 47 | storageService.saveBirthdaysForDate(userBirthday.birthdayDate, birthdays); 48 | } 49 | } 50 | } 51 | } 52 | storageService.saveDidAlreadyMigrateNotificationStatus(true); 53 | } 54 | } 55 | 56 | bool _isVersionGreaterThan(String newVersion, String currentVersion){ 57 | List currentVersionSplit = currentVersion.split("."); 58 | List newVersionSplit = newVersion.split("."); 59 | bool isNewVersionGreaterThanCurrentVersion = false; 60 | for (var i = 0 ; i < currentVersionSplit.length; i++){ 61 | isNewVersionGreaterThanCurrentVersion = int.parse(newVersionSplit[i]) > int.parse(currentVersionSplit[i]); 62 | if(int.parse(newVersionSplit[i]) != int.parse(currentVersionSplit[i])) { 63 | break; 64 | } 65 | } 66 | return isNewVersionGreaterThanCurrentVersion; 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /test/shared_preferences_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/model/user_birthday.dart'; 2 | import 'package:birthday_calendar/service/storage_service/shared_preferences_storage.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:shared_preferences/shared_preferences.dart'; 6 | import 'package:birthday_calendar/service/storage_service/storage_service.dart'; 7 | 8 | void main() { 9 | 10 | StorageService _storageService = StorageServiceSharedPreferences(); 11 | 12 | setUp(() { 13 | return Future(() async { 14 | WidgetsFlutterBinding.ensureInitialized(); 15 | SharedPreferences.setMockInitialValues({}); 16 | _storageService.clearAllBirthdays(); 17 | }); 18 | }); 19 | 20 | test("SharedPreferences get empty birthday array for date", () async { 21 | final DateTime dateTime = DateTime(2021, 12, 5); 22 | final birthdays = await _storageService.getBirthdaysForDate(dateTime, false); 23 | expect(birthdays.length, 0); 24 | }); 25 | 26 | test("SharedPreferences set birthday for date", () async { 27 | final DateTime dateTime = DateTime(2021, 12, 5); 28 | final String phoneNumber = '+234 500 500 5005'; 29 | final UserBirthday userBirthday = new UserBirthday("Someone", DateTime.now(), false, phoneNumber); 30 | List birthdays = []; 31 | birthdays.add(userBirthday); 32 | _storageService.saveBirthdaysForDate(dateTime, birthdays); 33 | 34 | final storedBirthdays = await _storageService.getBirthdaysForDate(dateTime, false); 35 | expect(storedBirthdays.length, 1); 36 | expect(storedBirthdays[0].name, userBirthday.name); 37 | expect(storedBirthdays[0].birthdayDate, userBirthday.birthdayDate); 38 | expect(storedBirthdays[0].hasNotification, userBirthday.hasNotification); 39 | 40 | }); 41 | 42 | test("SharedPreferences clear all birthday for date", () async { 43 | final DateTime dateTime = DateTime(2021, 12, 5); 44 | final String phoneNumber = '+234 500 500 5005'; 45 | 46 | final UserBirthday userBirthday = new UserBirthday("Someone", DateTime.now(), false, phoneNumber); 47 | List birthdays = []; 48 | birthdays.add(userBirthday); 49 | _storageService.saveBirthdaysForDate(dateTime, birthdays); 50 | 51 | List storedBirthdays = await _storageService.getBirthdaysForDate(dateTime, false); 52 | expect(storedBirthdays.length, 1); 53 | 54 | _storageService.clearAllBirthdays(); 55 | 56 | storedBirthdays = await _storageService.getBirthdaysForDate(dateTime, false); 57 | 58 | expect(storedBirthdays.length, 0); 59 | 60 | }); 61 | 62 | test("SharedPreferences set ThemeMode to dark mode", () async { 63 | await _storageService.saveThemeModeSetting(true); 64 | bool isDarkModeEnabled = await _storageService.getThemeModeSetting(); 65 | expect(isDarkModeEnabled, true); 66 | }); 67 | 68 | test("SharedPreferences default contact permission status is not permanently denied", () async { 69 | bool isContactsPermissionStatusPermanentlyDenied = await _storageService.getIsContactPermissionPermanentlyDenied(); 70 | expect(isContactsPermissionStatusPermanentlyDenied, false); 71 | }); 72 | 73 | } -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 18 | 22 | 26 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 52 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /lib/BirthdayCalendarDateUtils.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/constants.dart'; 2 | import 'package:intl/intl.dart'; 3 | import 'package:birthday_calendar/l10n/app_localizations.dart'; 4 | 5 | class BirthdayCalendarDateUtils { 6 | static int getCurrentMonthNumber() { 7 | DateTime now = new DateTime.now(); 8 | return now.month; 9 | } 10 | 11 | static int amountOfDaysInMonth(int month) { 12 | int days = 0; 13 | switch (month) { 14 | case JANUARY_MONTH_NUMBER: 15 | case MARCH_MONTH_NUMBER: 16 | case MAY_MONTH_NUMBER: 17 | case JULY_MONTH_NUMBER: 18 | case AUGUST_MONTH_NUMBER: 19 | case OCTOBER_MONTH_NUMBER: 20 | case DECEMBER_MONTH_NUMBER: 21 | { 22 | days = 31; 23 | break; 24 | } 25 | case APRIL_MONTH_NUMBER: 26 | case JUNE_MONTH_NUMBER: 27 | case SEPTEMBER_MONTH_NUMBER: 28 | case NOVEMBER_MONTH_NUMBER: 29 | { 30 | days = 30; 31 | break; 32 | } 33 | case FEBRUARY_MONTH_NUMBER: 34 | { 35 | days = isLeapYear() ? 29 : 28; 36 | break; 37 | } 38 | } 39 | 40 | return days; 41 | } 42 | 43 | static bool isLeapYear() { 44 | DateTime now = new DateTime.now(); 45 | int year = now.year; 46 | if (year % 4 == 0 && year % 100 != 0) { 47 | return true; 48 | } else if (year % 4 == 0 && year % 100 == 0 && year % 400 == 0) { 49 | return true; 50 | } 51 | return false; 52 | } 53 | 54 | static String getWeekdayNameFromDate(DateTime date) { 55 | return DateFormat('EEEE').format(date); 56 | } 57 | 58 | static DateTime constructDateTimeFromDayAndMonth(int day, int month) { 59 | int year = new DateTime.now().year; 60 | String paddedMonth = month < 10 ? "0" + month.toString() : month.toString(); 61 | String paddedDay = day < 10 ? "0" + day.toString() : day.toString(); 62 | String wholeDate = year.toString() + "-$paddedMonth-$paddedDay"; 63 | return DateTime.parse(wholeDate); 64 | } 65 | 66 | static String formatDateForSharedPrefs(DateTime date) { 67 | DateFormat dateFormat = DateFormat("yyyy-MM-dd"); 68 | return dateFormat.format(date); 69 | } 70 | 71 | static bool isADate(String date) { 72 | bool isValidDate = true; 73 | try { 74 | DateTime.parse(date); 75 | } catch (exception) { 76 | isValidDate = false; 77 | } 78 | 79 | return isValidDate; 80 | } 81 | 82 | static String convertAndTranslateMonthNumber( 83 | int month, AppLocalizations appLocalizations) { 84 | switch (month) { 85 | case JANUARY_MONTH_NUMBER: 86 | return appLocalizations.january; 87 | case FEBRUARY_MONTH_NUMBER: 88 | return appLocalizations.february; 89 | case MARCH_MONTH_NUMBER: 90 | return appLocalizations.march; 91 | case APRIL_MONTH_NUMBER: 92 | return appLocalizations.april; 93 | case MAY_MONTH_NUMBER: 94 | return appLocalizations.may; 95 | case JUNE_MONTH_NUMBER: 96 | return appLocalizations.june; 97 | case JULY_MONTH_NUMBER: 98 | return appLocalizations.july; 99 | case AUGUST_MONTH_NUMBER: 100 | return appLocalizations.august; 101 | case SEPTEMBER_MONTH_NUMBER: 102 | return appLocalizations.september; 103 | case OCTOBER_MONTH_NUMBER: 104 | return appLocalizations.october; 105 | case NOVEMBER_MONTH_NUMBER: 106 | return appLocalizations.november; 107 | case DECEMBER_MONTH_NUMBER: 108 | return appLocalizations.december; 109 | default: 110 | return ""; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/BirthdayBloc/BirthdaysBloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/BirthdayBloc/BirthdaysState.dart'; 2 | import 'package:birthday_calendar/model/user_birthday.dart'; 3 | import 'package:birthday_calendar/service/notification_service/notification_service.dart'; 4 | import 'package:birthday_calendar/service/storage_service/storage_service.dart'; 5 | import 'package:flutter_bloc/flutter_bloc.dart'; 6 | 7 | enum BirthdayEvent { AddBirthday, RemoveBirthday, ShowAddBirthdayDialog } 8 | 9 | class BirthdaysEvent { 10 | final BirthdayEvent eventName; 11 | final UserBirthday? birthday; 12 | final bool? shouldShowAddBirthdayDialog; 13 | final List? birthdays; 14 | final DateTime? date; 15 | final String? notificationMsg; 16 | 17 | BirthdaysEvent( 18 | {required this.eventName, 19 | this.birthday, 20 | this.shouldShowAddBirthdayDialog, 21 | this.birthdays, 22 | this.date, 23 | this.notificationMsg}); 24 | } 25 | 26 | class BirthdaysBloc extends Bloc { 27 | BirthdaysBloc(NotificationService notificationService, 28 | StorageService storageService, List birthdaysForDate) 29 | : super(BirthdaysState( 30 | date: DateTime.now(), 31 | birthdays: birthdaysForDate, 32 | showAddBirthdayDialog: false)) { 33 | on((event, emit) async { 34 | switch (event.eventName) { 35 | case BirthdayEvent.AddBirthday: 36 | _handleAddEvent(event, emit, storageService, notificationService); 37 | break; 38 | case BirthdayEvent.RemoveBirthday: 39 | await _handleRemoveEvent( 40 | event, emit, storageService, notificationService); 41 | break; 42 | case BirthdayEvent.ShowAddBirthdayDialog: 43 | emit(new BirthdaysState(showAddBirthdayDialog: true)); 44 | break; 45 | } 46 | }); 47 | } 48 | 49 | void _handleAddEvent( 50 | BirthdaysEvent event, 51 | Emitter emit, 52 | StorageService storageService, 53 | NotificationService notificationService) async { 54 | UserBirthday? userBirthday = event.birthday; 55 | 56 | if (userBirthday == null) { 57 | return; 58 | } 59 | 60 | DateTime birthdayDate = userBirthday.birthdayDate; 61 | List birthdaysMatchingDate = await storageService 62 | .getBirthdaysForDate(userBirthday.birthdayDate, false); 63 | birthdaysMatchingDate.add(userBirthday); 64 | storageService.saveBirthdaysForDate(birthdayDate, birthdaysMatchingDate); 65 | 66 | String notificationMsg = event.notificationMsg ?? ""; 67 | notificationService.scheduleNotificationForBirthday( 68 | userBirthday, notificationMsg); 69 | 70 | emit(new BirthdaysState( 71 | date: birthdayDate, 72 | birthdays: birthdaysMatchingDate, 73 | showAddBirthdayDialog: false)); 74 | } 75 | 76 | Future _handleRemoveEvent( 77 | BirthdaysEvent event, 78 | Emitter emit, 79 | StorageService storageService, 80 | NotificationService notificationService) async { 81 | UserBirthday? userBirthday = event.birthday; 82 | 83 | if (userBirthday == null) { 84 | return; 85 | } 86 | 87 | DateTime birthdayDate = userBirthday.birthdayDate; 88 | 89 | List birthdaysForDateDeleted = 90 | await storageService.getBirthdaysForDate(birthdayDate, false); 91 | 92 | List filteredBirthdays = birthdaysForDateDeleted 93 | .where((element) => !element.equals(userBirthday)) 94 | .toList(); 95 | 96 | await storageService.saveBirthdaysForDate(birthdayDate, filteredBirthdays); 97 | emit(new BirthdaysState( 98 | date: birthdayDate, 99 | birthdays: filteredBirthdays, 100 | showAddBirthdayDialog: false)); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/widget/calendar_day.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/service/notification_service/notification_service.dart'; 2 | import 'package:birthday_calendar/service/storage_service/shared_preferences_storage.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'dart:async'; 5 | import 'package:birthday_calendar/page/birthdays_for_calendar_day_page/birthdays_for_calendar_day.dart'; 6 | import 'package:birthday_calendar/model/user_birthday.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | class CalendarDayWidget extends StatefulWidget { 10 | final DateTime date; 11 | final NotificationService notificationService; 12 | 13 | const CalendarDayWidget( 14 | {required Key key, required this.date, required this.notificationService}) 15 | : super(key: key); 16 | 17 | @override 18 | _CalendarDayState createState() => _CalendarDayState(); 19 | } 20 | 21 | class _CalendarDayState extends State { 22 | List _birthdays = []; 23 | late StreamSubscription> _streamSubscription; 24 | 25 | @override 26 | void initState() { 27 | _fetchBirthdaysFromStorage(); 28 | Stream> stream = 29 | context.read().getBirthdaysStream(); 30 | _streamSubscription = stream.listen(_handleEventFromStorageService); 31 | super.initState(); 32 | } 33 | 34 | void _handleEventFromStorageService(List event) { 35 | List currentBirthdays = _birthdays; 36 | for (UserBirthday birthday in event) { 37 | DateTime firstDateWithoutYear = 38 | new DateTime(birthday.birthdayDate.month, birthday.birthdayDate.day); 39 | DateTime secondDateWithoutYear = 40 | new DateTime(widget.date.month, widget.date.day); 41 | 42 | if (firstDateWithoutYear == secondDateWithoutYear && 43 | !currentBirthdays.contains(birthday)) { 44 | currentBirthdays.add(birthday); 45 | } 46 | } 47 | 48 | if (currentBirthdays.length > 0) { 49 | setState(() { 50 | _birthdays = currentBirthdays; 51 | }); 52 | } 53 | } 54 | 55 | @override 56 | void didUpdateWidget(CalendarDayWidget oldWidget) { 57 | super.didUpdateWidget(oldWidget); 58 | _fetchBirthdaysFromStorage(); 59 | } 60 | 61 | void _fetchBirthdaysFromStorage() async { 62 | List storedBirthdays = await context 63 | .read() 64 | .getBirthdaysForDate(widget.date, true); 65 | setState(() { 66 | _birthdays = storedBirthdays; 67 | }); 68 | } 69 | 70 | Widget _showBirthdayIcon() { 71 | return Icon( 72 | Icons.cake_outlined, 73 | color: Colors.pink, 74 | size: 24.0, 75 | ); 76 | } 77 | 78 | @override 79 | Widget build(BuildContext context) { 80 | return TextButton( 81 | onPressed: () { 82 | Navigator.push( 83 | context, 84 | MaterialPageRoute( 85 | builder: (context) => BirthdaysForCalendarDayWidget( 86 | key: Key(widget.date.toString()), 87 | dateOfDay: widget.date, 88 | birthdays: _birthdays, 89 | notificationService: widget.notificationService), 90 | )).then((value) => _fetchBirthdaysFromStorage()); 91 | }, 92 | child: FittedBox( 93 | fit: BoxFit.fitWidth, 94 | child: Column(children: [ 95 | Text(widget.date.day.toString(), 96 | style: new TextStyle( 97 | fontSize: 15.0, fontWeight: FontWeight.bold)), 98 | if (_birthdays.length > 0) _showBirthdayIcon() 99 | ]))); 100 | } 101 | 102 | @override 103 | void dispose() { 104 | _streamSubscription.cancel(); 105 | super.dispose(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/ContactsPermissionStatusBloc/ContactsPermissionStatusBloc.dart'; 2 | import 'package:birthday_calendar/BirthdayCalendarDateUtils.dart'; 3 | import 'package:birthday_calendar/ThemeBloc/ThemeBloc.dart'; 4 | import 'package:birthday_calendar/VersionBloc/VersionBloc.dart'; 5 | import 'package:birthday_calendar/service/contacts_service/contacts_service.dart'; 6 | import 'package:birthday_calendar/service/contacts_service/contacts_service_impl.dart'; 7 | import 'package:birthday_calendar/service/notification_service/notification_service.dart'; 8 | import 'package:birthday_calendar/service/notification_service/notification_service_impl.dart'; 9 | import 'package:birthday_calendar/service/permission_service/permissions_service.dart'; 10 | import 'package:birthday_calendar/service/permission_service/permissions_service_impl.dart'; 11 | import 'package:birthday_calendar/service/storage_service/shared_preferences_storage.dart'; 12 | import 'package:birthday_calendar/service/storage_service/storage_service.dart'; 13 | import 'package:flutter/material.dart'; 14 | import 'constants.dart'; 15 | import 'package:birthday_calendar/page/main_page/main_page.dart'; 16 | import 'package:flutter_bloc/flutter_bloc.dart'; 17 | import 'package:flutter_localizations/flutter_localizations.dart'; 18 | import 'package:birthday_calendar/l10n/app_localizations.dart'; 19 | 20 | Future main() async { 21 | WidgetsFlutterBinding.ensureInitialized(); 22 | 23 | PermissionsService permissionsService = PermissionsServiceImpl(); 24 | StorageService storageService = StorageServiceSharedPreferences(); 25 | 26 | NotificationService notificationService = 27 | NotificationServiceImpl(permissionsService: permissionsService, storageService: storageService); 28 | ContactsService contactsService = ContactsServiceImpl( 29 | storageService: storageService, 30 | notificationService: notificationService, 31 | permissionsService: permissionsService); 32 | 33 | bool isDarkMode = await storageService.getThemeModeSetting(); 34 | 35 | runApp(MyApp( 36 | notificationService: notificationService, 37 | contactsService: contactsService, 38 | isDarkMode: isDarkMode, 39 | )); 40 | } 41 | 42 | class MyApp extends StatelessWidget { 43 | MyApp( 44 | {required this.notificationService, 45 | required this.contactsService, 46 | required this.isDarkMode}); 47 | 48 | final NotificationService notificationService; 49 | final ContactsService contactsService; 50 | final bool isDarkMode; 51 | 52 | @override 53 | Widget build(BuildContext context) { 54 | return RepositoryProvider( 55 | create: (context) => StorageServiceSharedPreferences(), 56 | child: MultiBlocProvider( 57 | providers: [ 58 | BlocProvider( 59 | create: (context) => ThemeBloc( 60 | context.read(), 61 | isDarkMode)), 62 | BlocProvider( 63 | create: (context) => 64 | ContactsPermissionStatusBloc(contactsService)), 65 | BlocProvider(create: (context) => VersionBloc()) 66 | ], 67 | child: BlocBuilder( 68 | builder: (context, state) { 69 | return MaterialApp( 70 | title: applicationName, 71 | localizationsDelegates: [ 72 | AppLocalizations.delegate, 73 | GlobalMaterialLocalizations.delegate, 74 | GlobalWidgetsLocalizations.delegate, 75 | GlobalCupertinoLocalizations.delegate, 76 | ], 77 | supportedLocales: [ 78 | Locale('en'), 79 | Locale('hi'), 80 | Locale('de'), 81 | ], 82 | theme: ThemeData.light(), 83 | themeMode: state, 84 | darkTheme: ThemeData.dark(), 85 | home: MainPage( 86 | key: Key("BirthdayCalendar"), 87 | notificationService: notificationService, 88 | contactsService: contactsService, 89 | title: applicationName, 90 | currentMonth: 91 | BirthdayCalendarDateUtils.getCurrentMonthNumber())); 92 | }, 93 | ), 94 | )); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/page/birthdays_for_calendar_day_page/birthdays_for_calendar_day.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/BirthdayBloc/BirthdaysBloc.dart'; 2 | import 'package:birthday_calendar/BirthdayBloc/BirthdaysState.dart'; 3 | import 'package:birthday_calendar/BirthdayCalendarDateUtils.dart'; 4 | import 'package:birthday_calendar/l10n/app_localizations.dart'; 5 | import 'package:birthday_calendar/service/notification_service/notification_service.dart'; 6 | import 'package:birthday_calendar/service/storage_service/shared_preferences_storage.dart'; 7 | import 'package:birthday_calendar/widget/add_birthday_form.dart'; 8 | import 'package:flutter/material.dart'; 9 | import 'package:birthday_calendar/page/birthday/birthday.dart'; 10 | import 'package:birthday_calendar/model/user_birthday.dart'; 11 | import 'package:flutter_bloc/flutter_bloc.dart'; 12 | 13 | class BirthdaysForCalendarDayWidget extends StatelessWidget { 14 | final DateTime dateOfDay; 15 | final List birthdays; 16 | final NotificationService notificationService; 17 | 18 | BirthdaysForCalendarDayWidget( 19 | {required Key key, 20 | required this.dateOfDay, 21 | required this.birthdays, 22 | required this.notificationService}) 23 | : super(key: key); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return BlocProvider( 28 | create: (context) => BirthdaysBloc(notificationService, 29 | context.read(), birthdays), 30 | child: BlocBuilder( 31 | builder: (context, state) { 32 | return Scaffold( 33 | appBar: AppBar( 34 | title: FittedBox( 35 | fit: BoxFit.fitWidth, 36 | child: Text(AppLocalizations.of(context)! 37 | .birthdaysForDayAndMonth( 38 | BirthdayCalendarDateUtils 39 | .convertAndTranslateMonthNumber( 40 | this.dateOfDay.month, 41 | AppLocalizations.of(context)!), 42 | this.dateOfDay.day)))), 43 | body: Center( 44 | child: Column( 45 | children: [ 46 | (state.birthdays == null || state.birthdays!.length == 0) 47 | ? Spacer() 48 | : Expanded( 49 | child: ListView.builder( 50 | itemCount: state.birthdays != null 51 | ? state.birthdays!.length 52 | : 0, 53 | itemBuilder: (BuildContext context, int index) { 54 | return BlocProvider.value( 55 | value: BlocProvider.of(context), 56 | child: BirthdayWidget( 57 | key: Key(state.birthdays![index].name), 58 | birthdayOfPerson: state.birthdays![index], 59 | indexOfBirthday: index, 60 | notificationService: notificationService)); 61 | }, 62 | ), 63 | ), 64 | BlocListener( 65 | listener: (context, state) { 66 | if (state.showAddBirthdayDialog) { 67 | showDialog( 68 | context: context, 69 | builder: (_) => BlocProvider.value( 70 | value: BlocProvider.of(context), 71 | child: AddBirthdayForm( 72 | dateOfDay: dateOfDay, 73 | notificationService: notificationService))); 74 | } 75 | }, 76 | child: Spacer(), 77 | ) 78 | ], 79 | )), 80 | floatingActionButton: FloatingActionButton( 81 | onPressed: () { 82 | BlocProvider.of(context).add(BirthdaysEvent( 83 | eventName: BirthdayEvent.ShowAddBirthdayDialog, 84 | shouldShowAddBirthdayDialog: true, 85 | birthdays: birthdays)); 86 | }, 87 | child: Icon(Icons.add)), 88 | ); 89 | })); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/widget/users_without_birthdays_dialogs.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_contacts/contact.dart'; 3 | import 'package:birthday_calendar/l10n/app_localizations.dart'; 4 | 5 | class UsersWithoutBirthdaysDialogs { 6 | UsersWithoutBirthdaysDialogs(this.usersWithoutBirthdays); 7 | 8 | final List usersWithoutBirthdays; 9 | 10 | Future> showConfirmationDialog(BuildContext context) async { 11 | AlertDialog alert = AlertDialog( 12 | title: Text(AppLocalizations.of(context)!.addBirthdaysToContactsAlertDialogTitle), 13 | content: Text( 14 | AppLocalizations.of(context)!.addBirthdaysToContactsAlertDialogDescription), 15 | actions: [ 16 | TextButton( 17 | onPressed: () { 18 | Navigator.pop(context); 19 | }, 20 | child: Text(AppLocalizations.of(context)!.no), 21 | ), 22 | TextButton( 23 | onPressed: () async { 24 | var result = await showDialog( 25 | context: context, 26 | builder: (BuildContext context) { 27 | return _showUsersDialog(context); 28 | }); 29 | Navigator.pop(context, result); 30 | }, 31 | child: Text(AppLocalizations.of(context)!.proceed), 32 | ), 33 | ]); 34 | var result = await showDialog( 35 | context: context, 36 | builder: (BuildContext context) { 37 | return alert; 38 | }); 39 | 40 | return result == null ? [] : result; 41 | } 42 | 43 | Widget _showUsersDialog(BuildContext context) { 44 | List _usersSelectedToAddBirthdaysFor = 45 | List.filled(usersWithoutBirthdays.length, false); 46 | bool _haveAnyContactsBeenSelected = false; 47 | 48 | AlertDialog alert = AlertDialog( 49 | title: Text(AppLocalizations.of(context)!.peopleWithoutBirthdaysAlertDialogTitle), 50 | content: StatefulBuilder( 51 | builder: (BuildContext context, StateSetter setState) { 52 | return Container( 53 | height: MediaQuery.of(context).size.height, 54 | width: MediaQuery.of(context).size.width, 55 | child: ListView( 56 | children: [ 57 | ListView.builder( 58 | shrinkWrap: true, 59 | physics: NeverScrollableScrollPhysics(), 60 | itemCount: usersWithoutBirthdays.length, 61 | itemBuilder: (BuildContext context, int index) { 62 | return CheckboxListTile( 63 | title: Text(usersWithoutBirthdays[index].displayName), 64 | value: _usersSelectedToAddBirthdaysFor[index], 65 | onChanged: (bool? value) { 66 | if (value != null) { 67 | setState(() { 68 | _usersSelectedToAddBirthdaysFor[index] = value; 69 | _haveAnyContactsBeenSelected = 70 | _haveUsersBeenSelected( 71 | _usersSelectedToAddBirthdaysFor); 72 | }); 73 | } 74 | }); 75 | }, 76 | ), 77 | Row( 78 | mainAxisAlignment: MainAxisAlignment.spaceAround, 79 | children: [ 80 | TextButton( 81 | child: Text(AppLocalizations.of(context)!.cancel), 82 | onPressed: () { 83 | Navigator.pop(context); 84 | }, 85 | ), 86 | TextButton( 87 | child: Text(AppLocalizations.of(context)!.ok), 88 | onPressed: !_haveAnyContactsBeenSelected 89 | ? null 90 | : () => _collectUsersToAddBirthdaysTo( 91 | context, _usersSelectedToAddBirthdaysFor)), 92 | ], 93 | ) 94 | ], 95 | )); 96 | })); 97 | 98 | return alert; 99 | } 100 | 101 | bool _haveUsersBeenSelected(List usersSelectedToAddBirthdaysFor) { 102 | return usersSelectedToAddBirthdaysFor 103 | .firstWhere((element) => element == true, orElse: () => false); 104 | } 105 | 106 | void _collectUsersToAddBirthdaysTo( 107 | BuildContext context, List usersSelectedToAddBirthdaysFor) { 108 | List usersToSetBirthDatesTo = []; 109 | usersWithoutBirthdays.asMap().forEach((index, value) { 110 | if (usersSelectedToAddBirthdaysFor[index]) { 111 | usersToSetBirthDatesTo.add(value); 112 | } 113 | }); 114 | Navigator.pop(context, usersToSetBirthDatesTo); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/service/contacts_service/contacts_service_impl.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/constants.dart'; 2 | import 'package:birthday_calendar/service/contacts_service/contacts_service.dart'; 3 | import 'package:birthday_calendar/service/permission_service/permissions_service.dart'; 4 | import 'package:birthday_calendar/service/storage_service/storage_service.dart'; 5 | import 'package:birthday_calendar/service/notification_service/notification_service.dart'; 6 | import 'package:birthday_calendar/model/user_birthday.dart'; 7 | import 'package:birthday_calendar/utils.dart'; 8 | import 'package:birthday_calendar/widget/users_without_birthdays_dialogs.dart'; 9 | import 'package:collection/collection.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter_contacts/flutter_contacts.dart'; 12 | import 'package:permission_handler/permission_handler.dart'; 13 | import 'package:birthday_calendar/l10n/app_localizations.dart'; 14 | 15 | class ContactsServiceImpl extends ContactsService { 16 | ContactsServiceImpl( 17 | {required this.storageService, 18 | required this.notificationService, 19 | required this.permissionsService}); 20 | 21 | final StorageService storageService; 22 | final NotificationService notificationService; 23 | final PermissionsService permissionsService; 24 | 25 | @override 26 | Future getContactsPermissionStatus( 27 | BuildContext context) async { 28 | return await permissionsService.getPermissionStatus(contactsPermissionKey); 29 | } 30 | 31 | @override 32 | Future requestContactsPermission( 33 | BuildContext context) async { 34 | return await permissionsService 35 | .requestPermissionAndGetStatus(contactsPermissionKey); 36 | } 37 | 38 | @override 39 | void setContactsPermissionPermanentlyDenied() { 40 | storageService.saveIsContactsPermissionPermanentlyDenied(true); 41 | } 42 | 43 | @override 44 | Future isContactsPermissionsPermanentlyDenied() async { 45 | return storageService.getIsContactPermissionPermanentlyDenied(); 46 | } 47 | 48 | @override 49 | Future> filterAlreadyImportedContacts( 50 | List contacts) async { 51 | return Utils.filterAlreadyImportedContacts(storageService, contacts); 52 | } 53 | 54 | void handleAddingBirthdaysToContacts( 55 | BuildContext context, List contactsWithoutBirthDates) async { 56 | UsersWithoutBirthdaysDialogs assignBirthdaysToUsers = 57 | UsersWithoutBirthdaysDialogs(contactsWithoutBirthDates); 58 | List users = 59 | await assignBirthdaysToUsers.showConfirmationDialog(context); 60 | if (users.isNotEmpty) { 61 | _gatherBirthdaysForUsers(context, users); 62 | } 63 | } 64 | 65 | void _gatherBirthdaysForUsers( 66 | BuildContext context, List users) async { 67 | int amountOfBirthdaysSet = 0; 68 | 69 | for (Contact contact in users) { 70 | DateTime? chosenBirthDate = await showDatePicker( 71 | context: context, 72 | initialDate: DateTime(1970, 1, 1), 73 | firstDate: DateTime(1970, 1, 1), 74 | lastDate: DateTime.now(), 75 | initialEntryMode: DatePickerEntryMode.input, 76 | helpText: AppLocalizations.of(context)! 77 | .helpTextChooseBirthdateForImportedContact(contact.displayName), 78 | fieldLabelText: AppLocalizations.of(context)! 79 | .fieldLabelTextChooseBirthdateForImportedContact( 80 | contact.displayName)); 81 | 82 | if (chosenBirthDate != null) { 83 | UserBirthday userBirthday = new UserBirthday( 84 | contact.displayName, 85 | chosenBirthDate, 86 | true, 87 | contact.phones.isNotEmpty ? contact.phones.first.number : ""); 88 | 89 | addContactToCalendar(userBirthday, context); 90 | amountOfBirthdaysSet++; 91 | } 92 | } 93 | 94 | if (amountOfBirthdaysSet > 0) { 95 | Utils.showSnackbarWithMessage( 96 | context, AppLocalizations.of(context)!.contactsImportedSuccessfully); 97 | } 98 | } 99 | 100 | @override 101 | Future> fetchContacts(bool withThumbnails) async { 102 | return await FlutterContacts.getContacts(withProperties: true); 103 | } 104 | 105 | @override 106 | void addContactToCalendar(UserBirthday contact, BuildContext context) async { 107 | List birthdays = 108 | await storageService.getBirthdaysForDate(contact.birthdayDate, false); 109 | String contactName = contact.name; 110 | 111 | UserBirthday? birthdayWithSameName = 112 | birthdays.firstWhereOrNull((element) => element.name == contactName); 113 | 114 | if (birthdayWithSameName != null) { 115 | return; 116 | } 117 | 118 | notificationService.scheduleNotificationForBirthday( 119 | contact, 120 | AppLocalizations.of(context)! 121 | .notificationForBirthdayMessage(contact.name)); 122 | 123 | birthdays.add(contact); 124 | 125 | storageService.saveBirthdaysForDate(contact.birthdayDate, birthdays); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /lib/l10n/app_localizations_hi.dart: -------------------------------------------------------------------------------- 1 | // ignore: unused_import 2 | import 'package:intl/intl.dart' as intl; 3 | import 'app_localizations.dart'; 4 | 5 | // ignore_for_file: type=lint 6 | 7 | /// The translations for Hindi (`hi`). 8 | class AppLocalizationsHi extends AppLocalizations { 9 | AppLocalizationsHi([String locale = 'hi']) : super(locale); 10 | 11 | @override 12 | String get appTitle => 'जन्मदिन कैलेंडर'; 13 | 14 | @override 15 | String get settings => 'सेटिंग्स'; 16 | 17 | @override 18 | String get addBirthday => 'जन्मदिन जोड़ें'; 19 | 20 | @override 21 | String get contactsImportedSuccessfully => 'संपर्क सफलतापूर्वक आयात किए गए'; 22 | 23 | @override 24 | String get noContactsFoundMsg => 'आपके डिवाइस पर कोई संपर्क नहीं मिला'; 25 | 26 | @override 27 | String get alreadyAddedContactsMsg => 28 | 'आपके सभी वर्तमान संपर्क पहले ही जोड़े जा चुके हैं'; 29 | 30 | @override 31 | String get unableToMakeCallMsg => 'हम कॉल करने में असमर्थ हैं'; 32 | 33 | @override 34 | String get january => 'जनवरी'; 35 | 36 | @override 37 | String get february => 'फ़रवरी'; 38 | 39 | @override 40 | String get march => 'मार्च'; 41 | 42 | @override 43 | String get april => 'अप्रैल'; 44 | 45 | @override 46 | String get may => 'मई'; 47 | 48 | @override 49 | String get june => 'जून'; 50 | 51 | @override 52 | String get july => 'जुलाई'; 53 | 54 | @override 55 | String get august => 'अगस्त'; 56 | 57 | @override 58 | String get september => 'सितंबर'; 59 | 60 | @override 61 | String get october => 'अक्टूबर'; 62 | 63 | @override 64 | String get november => 'नवंबर'; 65 | 66 | @override 67 | String get december => 'दिसंबर'; 68 | 69 | @override 70 | String get ok => 'ठीक है'; 71 | 72 | @override 73 | String get updateSuccessfullyInstalledTitle => 74 | 'अपडेट सफलतापूर्वक इंस्टॉल किया गया'; 75 | 76 | @override 77 | String get updateSuccessfullyInstalledDescription => 78 | 'बर्थडे कैलेंडर को सफलतापूर्वक अपडेट कर दिया गया है! 🎂'; 79 | 80 | @override 81 | String get tryAgain => 'फिर से प्रयास करें?'; 82 | 83 | @override 84 | String get dismiss => 'खारिज करें'; 85 | 86 | @override 87 | String get updateFailedToInstallTitle => 'अपडेट इंस्टॉल करने में विफल ❌'; 88 | 89 | @override 90 | String updateFailedToInstallDescription(Object error) { 91 | return 'बर्थडे कैलेंडर अपडेट करने में विफल रहा क्योंकि: $error'; 92 | } 93 | 94 | @override 95 | String get userDeniedUpdate => 'उपयोगकर्ता ने अपडेट अस्वीकार कर दिया'; 96 | 97 | @override 98 | String get appUpdateFailed => 'ऐप अपडेट असफल रहा'; 99 | 100 | @override 101 | String get darkMode => 'डार्क मोड'; 102 | 103 | @override 104 | String get importContacts => 'संपर्क आयात करें'; 105 | 106 | @override 107 | String get clearNotifications => 'सूचनाएँ साफ़ करें'; 108 | 109 | @override 110 | String get clearNotificationsAlertTitle => 'क्या आप सुनिश्चित हैं?'; 111 | 112 | @override 113 | String get clearNotificationsAlertDescription => 114 | 'क्या आप सभी सूचनाएं हटाना चाहते हैं?'; 115 | 116 | @override 117 | String get no => 'नहीं'; 118 | 119 | @override 120 | String get yes => 'हाँ'; 121 | 122 | @override 123 | String get addPhoneNumber => 'फोन नंबर जोड़ें'; 124 | 125 | @override 126 | String get add => 'जोड़ें'; 127 | 128 | @override 129 | String get cancel => 'रद्द करें'; 130 | 131 | @override 132 | String get back => 'वापस'; 133 | 134 | @override 135 | String get proceed => 'आगे बढ़ें'; 136 | 137 | @override 138 | String get hintTextForNameInputField => 'नाम?'; 139 | 140 | @override 141 | String get notValidName => 'कृपया एक मान्य नाम दर्ज करें'; 142 | 143 | @override 144 | String get nameAlreadyExists => 'इस नाम से एक जन्मदिन पहले से मौजूद है'; 145 | 146 | @override 147 | String birthdaysForDayAndMonth(Object day, Object month) { 148 | return '$day $month के लिए जन्मदिन'; 149 | } 150 | 151 | @override 152 | String helpTextChooseBirthdateForImportedContact(Object contactName) { 153 | return '$contactName के लिए जन्मतिथि चुनें'; 154 | } 155 | 156 | @override 157 | String fieldLabelTextChooseBirthdateForImportedContact(Object contactName) { 158 | return '$contactName की जन्मतिथि'; 159 | } 160 | 161 | @override 162 | String notificationForBirthdayMessage(Object contactName) { 163 | return '$contactName का जन्मदिन आने वाला है!'; 164 | } 165 | 166 | @override 167 | String get addBirthdaysToContactsAlertDialogTitle => 168 | 'Add Birthdays To Contacts'; 169 | 170 | @override 171 | String get addBirthdaysToContactsAlertDialogDescription => 172 | 'Would you like to add birth dates for your imported contacts?'; 173 | 174 | @override 175 | String get peopleWithoutBirthdaysAlertDialogTitle => 176 | 'People Without Birthdays'; 177 | 178 | @override 179 | String get notificationPermissionDenied => 180 | 'जन्मदिन की सूचनाएँ प्राप्त करने के लिए, आपको BirthdayCalendar को आपको सूचनाएँ भेजने की अनुमति देनी होगी'; 181 | 182 | @override 183 | String get notificationPermissionPermanentlyDenied => 184 | 'सूचनाएँ निर्धारित करने के लिए आपको ऐप की सेटिंग्स में अधिसूचना अनुमति चालू करनी होगी।'; 185 | 186 | @override 187 | String get openSettings => 'सेटिंग्स खोलें'; 188 | } 189 | -------------------------------------------------------------------------------- /lib/l10n/app_localizations_en.dart: -------------------------------------------------------------------------------- 1 | // ignore: unused_import 2 | import 'package:intl/intl.dart' as intl; 3 | import 'app_localizations.dart'; 4 | 5 | // ignore_for_file: type=lint 6 | 7 | /// The translations for English (`en`). 8 | class AppLocalizationsEn extends AppLocalizations { 9 | AppLocalizationsEn([String locale = 'en']) : super(locale); 10 | 11 | @override 12 | String get appTitle => 'Birthday Calendar'; 13 | 14 | @override 15 | String get settings => 'Settings'; 16 | 17 | @override 18 | String get addBirthday => 'Add Birthday'; 19 | 20 | @override 21 | String get contactsImportedSuccessfully => 'Contacts Imported Successfully'; 22 | 23 | @override 24 | String get noContactsFoundMsg => 'There are no contacts on your device'; 25 | 26 | @override 27 | String get alreadyAddedContactsMsg => 28 | 'All of your current contacts have already been added'; 29 | 30 | @override 31 | String get unableToMakeCallMsg => 'We are unable to make the call'; 32 | 33 | @override 34 | String get january => 'January'; 35 | 36 | @override 37 | String get february => 'February'; 38 | 39 | @override 40 | String get march => 'March'; 41 | 42 | @override 43 | String get april => 'April'; 44 | 45 | @override 46 | String get may => 'May'; 47 | 48 | @override 49 | String get june => 'June'; 50 | 51 | @override 52 | String get july => 'July'; 53 | 54 | @override 55 | String get august => 'August'; 56 | 57 | @override 58 | String get september => 'September'; 59 | 60 | @override 61 | String get october => 'October'; 62 | 63 | @override 64 | String get november => 'November'; 65 | 66 | @override 67 | String get december => 'December'; 68 | 69 | @override 70 | String get ok => 'Ok'; 71 | 72 | @override 73 | String get updateSuccessfullyInstalledTitle => 74 | 'Update Successfully Installed'; 75 | 76 | @override 77 | String get updateSuccessfullyInstalledDescription => 78 | 'Birthday Calendar has been updated successfully! 🎂'; 79 | 80 | @override 81 | String get tryAgain => 'Try Again?'; 82 | 83 | @override 84 | String get dismiss => 'Dismiss'; 85 | 86 | @override 87 | String get updateFailedToInstallTitle => 'Update Failed To Install ❌'; 88 | 89 | @override 90 | String updateFailedToInstallDescription(Object error) { 91 | return 'Birthday Calendar has failed to update because: $error'; 92 | } 93 | 94 | @override 95 | String get userDeniedUpdate => 'User denied update'; 96 | 97 | @override 98 | String get appUpdateFailed => 'App Update Failed'; 99 | 100 | @override 101 | String get darkMode => 'Dark Mode'; 102 | 103 | @override 104 | String get importContacts => 'Import Contacts'; 105 | 106 | @override 107 | String get clearNotifications => 'Clear Notifications'; 108 | 109 | @override 110 | String get clearNotificationsAlertTitle => 'Are You Sure?'; 111 | 112 | @override 113 | String get clearNotificationsAlertDescription => 114 | 'Do you want to remove all notifications?'; 115 | 116 | @override 117 | String get no => 'No'; 118 | 119 | @override 120 | String get yes => 'Yes'; 121 | 122 | @override 123 | String get addPhoneNumber => 'Add Phone Number'; 124 | 125 | @override 126 | String get add => 'Add'; 127 | 128 | @override 129 | String get cancel => 'Cancel'; 130 | 131 | @override 132 | String get back => 'Back'; 133 | 134 | @override 135 | String get proceed => 'Proceed'; 136 | 137 | @override 138 | String get hintTextForNameInputField => 'Name?'; 139 | 140 | @override 141 | String get notValidName => 'Please enter a valid name'; 142 | 143 | @override 144 | String get nameAlreadyExists => 'A birthday with this name already exists'; 145 | 146 | @override 147 | String birthdaysForDayAndMonth(Object day, Object month) { 148 | return 'Birthdays for $day $month'; 149 | } 150 | 151 | @override 152 | String helpTextChooseBirthdateForImportedContact(Object contactName) { 153 | return 'Choose birth date for $contactName'; 154 | } 155 | 156 | @override 157 | String fieldLabelTextChooseBirthdateForImportedContact(Object contactName) { 158 | return '$contactName\'s birth date'; 159 | } 160 | 161 | @override 162 | String notificationForBirthdayMessage(Object contactName) { 163 | return '$contactName has an upcoming birthday!'; 164 | } 165 | 166 | @override 167 | String get addBirthdaysToContactsAlertDialogTitle => 168 | 'Add Birthdays To Contacts'; 169 | 170 | @override 171 | String get addBirthdaysToContactsAlertDialogDescription => 172 | 'Would you like to add birth dates for your imported contacts?'; 173 | 174 | @override 175 | String get peopleWithoutBirthdaysAlertDialogTitle => 176 | 'People Without Birthdays'; 177 | 178 | @override 179 | String get notificationPermissionDenied => 180 | 'In order to get notifications for birthdays, you will need to authorize BirthdayCalendar to send you notifications'; 181 | 182 | @override 183 | String get notificationPermissionPermanentlyDenied => 184 | 'You will need to turn on the notification permission in the application\'s settings in order to schedule notifications'; 185 | 186 | @override 187 | String get openSettings => 'Open Settings'; 188 | } 189 | -------------------------------------------------------------------------------- /lib/l10n/app_localizations_de.dart: -------------------------------------------------------------------------------- 1 | // ignore: unused_import 2 | import 'package:intl/intl.dart' as intl; 3 | import 'app_localizations.dart'; 4 | 5 | // ignore_for_file: type=lint 6 | 7 | /// The translations for German (`de`). 8 | class AppLocalizationsDe extends AppLocalizations { 9 | AppLocalizationsDe([String locale = 'de']) : super(locale); 10 | 11 | @override 12 | String get appTitle => 'Geburtstagskalender'; 13 | 14 | @override 15 | String get settings => 'Einstellungen'; 16 | 17 | @override 18 | String get addBirthday => 'Geburtstag hinzufügen'; 19 | 20 | @override 21 | String get contactsImportedSuccessfully => 'Kontakte erfolgreich importiert'; 22 | 23 | @override 24 | String get noContactsFoundMsg => 'Es gibt keine Kontakte auf Ihrem Gerät'; 25 | 26 | @override 27 | String get alreadyAddedContactsMsg => 28 | 'Alle aktuellen Kontakte wurden bereits hinzugefügt'; 29 | 30 | @override 31 | String get unableToMakeCallMsg => 'Wir können den Anruf nicht tätigen'; 32 | 33 | @override 34 | String get january => 'Januar'; 35 | 36 | @override 37 | String get february => 'Februar'; 38 | 39 | @override 40 | String get march => 'März'; 41 | 42 | @override 43 | String get april => 'April'; 44 | 45 | @override 46 | String get may => 'Mai'; 47 | 48 | @override 49 | String get june => 'Juni'; 50 | 51 | @override 52 | String get july => 'Juli'; 53 | 54 | @override 55 | String get august => 'August'; 56 | 57 | @override 58 | String get september => 'September'; 59 | 60 | @override 61 | String get october => 'Oktober'; 62 | 63 | @override 64 | String get november => 'November'; 65 | 66 | @override 67 | String get december => 'Dezember'; 68 | 69 | @override 70 | String get ok => 'Ok'; 71 | 72 | @override 73 | String get updateSuccessfullyInstalledTitle => 74 | 'Update erfolgreich installiert'; 75 | 76 | @override 77 | String get updateSuccessfullyInstalledDescription => 78 | 'Geburtstagskalender wurde erfolgreich aktualisiert! 🎂'; 79 | 80 | @override 81 | String get tryAgain => 'Erneut versuchen?'; 82 | 83 | @override 84 | String get dismiss => 'Schließen'; 85 | 86 | @override 87 | String get updateFailedToInstallTitle => 88 | 'Update konnte nicht installiert werden ❌'; 89 | 90 | @override 91 | String updateFailedToInstallDescription(Object error) { 92 | return 'Geburtstagskalender konnte nicht aktualisiert werden, weil: $error'; 93 | } 94 | 95 | @override 96 | String get userDeniedUpdate => 'Benutzer hat das Update abgelehnt'; 97 | 98 | @override 99 | String get appUpdateFailed => 'App-Update fehlgeschlagen'; 100 | 101 | @override 102 | String get darkMode => 'Dunkelmodus'; 103 | 104 | @override 105 | String get importContacts => 'Kontakte importieren'; 106 | 107 | @override 108 | String get clearNotifications => 'Benachrichtigungen löschen'; 109 | 110 | @override 111 | String get clearNotificationsAlertTitle => 'Sind Sie sicher?'; 112 | 113 | @override 114 | String get clearNotificationsAlertDescription => 115 | 'Möchten Sie alle Benachrichtigungen entfernen?'; 116 | 117 | @override 118 | String get no => 'Nein'; 119 | 120 | @override 121 | String get yes => 'Ja'; 122 | 123 | @override 124 | String get addPhoneNumber => 'Telefonnummer hinzufügen'; 125 | 126 | @override 127 | String get add => 'Hinzufügen'; 128 | 129 | @override 130 | String get cancel => 'Abbrechen'; 131 | 132 | @override 133 | String get back => 'Zurück'; 134 | 135 | @override 136 | String get proceed => 'Fortfahren'; 137 | 138 | @override 139 | String get hintTextForNameInputField => 'Name?'; 140 | 141 | @override 142 | String get notValidName => 'Bitte geben Sie einen gültigen Namen ein'; 143 | 144 | @override 145 | String get nameAlreadyExists => 146 | 'Ein Geburtstag mit diesem Namen existiert bereits'; 147 | 148 | @override 149 | String birthdaysForDayAndMonth(Object day, Object month) { 150 | return 'Geburtstage am $day. $month'; 151 | } 152 | 153 | @override 154 | String helpTextChooseBirthdateForImportedContact(Object contactName) { 155 | return 'Wählen Sie das Geburtsdatum für $contactName'; 156 | } 157 | 158 | @override 159 | String fieldLabelTextChooseBirthdateForImportedContact(Object contactName) { 160 | return 'Geburtsdatum von $contactName'; 161 | } 162 | 163 | @override 164 | String notificationForBirthdayMessage(Object contactName) { 165 | return '$contactName hat bald Geburtstag!'; 166 | } 167 | 168 | @override 169 | String get addBirthdaysToContactsAlertDialogTitle => 170 | 'Geburtstage zu Kontakten hinzufügen'; 171 | 172 | @override 173 | String get addBirthdaysToContactsAlertDialogDescription => 174 | 'Möchten Sie Geburtsdaten für Ihre importierten Kontakte hinzufügen?'; 175 | 176 | @override 177 | String get peopleWithoutBirthdaysAlertDialogTitle => 178 | 'Personen ohne Geburtstage'; 179 | 180 | @override 181 | String get notificationPermissionDenied => 182 | 'Um Benachrichtigungen zu Geburtstagen zu erhalten, müssen Sie BirthdayCalendar die Berechtigung erteilen, Ihnen Benachrichtigungen zu senden'; 183 | 184 | @override 185 | String get notificationPermissionPermanentlyDenied => 186 | 'Sie müssen die Benachrichtigungsberechtigung in den App-Einstellungen aktivieren, um Benachrichtigungen planen zu können.'; 187 | 188 | @override 189 | String get openSettings => 'Einstellungen öffnen'; 190 | } 191 | -------------------------------------------------------------------------------- /.github/copilot-instructions.md: -------------------------------------------------------------------------------- 1 | # Birthday Calendar - Copilot Instructions 2 | 3 | ## Project Summary 4 | Birthday Calendar is a Flutter mobile app (3.1MB, ~1,694 lines Dart code across 40 files) that stores birthdays and sends notification reminders. Uses BLoC pattern for state management with flutter_bloc, shared_preferences for storage, flutter_local_notifications, and supports English/Hindi/German localization. 5 | 6 | **Tech Stack**: Flutter 3.24.0+, Dart 3.5.0+, Android (API 35), iOS, Web. Key dependencies: flutter_bloc (8.1.5), shared_preferences (2.3.4), flutter_contacts (1.1.9+2), provider (6.0.2) 7 | 8 | ## Build and Validation (CRITICAL - Run in This Exact Order) 9 | 10 | **Prerequisites**: Flutter 3.24.0+, Dart 3.5.0+, Java 12.x (Zulu), Android SDK API 35+, Gradle with `-Xmx1536M` 11 | 12 | **ALWAYS run commands in this sequence** (matches CI pipeline in `.github/workflows/flutter_build.yml`): 13 | ```bash 14 | dart pub get # 10-20s - MUST run first 15 | flutter pub get # 10-30s - MUST run after dart pub get 16 | flutter analyze # 20-40s - Must pass 17 | flutter test # 30-60s - Must pass 18 | ``` 19 | 20 | **Build commands**: 21 | - Android: `flutter build apk --release` (2-5 min, outputs to `build/app/outputs/flutter-apk/`) 22 | - iOS: `flutter build ios` (macOS only, requires Xcode) 23 | - Clean: `flutter clean` (then re-run `dart pub get` && `flutter pub get`) 24 | 25 | **Common Issues**: 26 | 1. Gradle failures → Check Java 12.x, Gradle heap in `android/gradle.properties` 27 | 2. Package errors → `flutter clean` then `dart pub get` && `flutter pub get` 28 | 3. Platform errors → Notifications/contacts require platform-specific setup 29 | 30 | **Tests** (3 files in `test/`): Widget tests, date utilities, storage. Use `WidgetsFlutterBinding.ensureInitialized()` and mock SharedPreferences with `SharedPreferences.setMockInitialValues({})` 31 | 32 | ## Project Architecture & Key Files 33 | 34 | **Root Structure**: 35 | - `.github/workflows/flutter_build.yml` - CI pipeline (runs on PRs: dart pub get → flutter pub get → analyze → test) 36 | - `pubspec.yaml` - Dependencies (run `dart pub get` && `flutter pub get` after modifying) 37 | - `l10n.yaml` - Localization config (arb files in `lib/l10n/`, run `flutter gen-l10n` after editing .arb) 38 | - `android/app/build.gradle` - Android config (compileSdk 35, targetSdk 35, Java 17, Kotlin 2.0.20) 39 | - `lib/main.dart` - App entry point 40 | - `lib/constants.dart` - App-wide constants 41 | - `test/` - 3 test files (widget tests, date utils, storage) 42 | 43 | **lib/ Directory** (BLoC pattern with service layer): 44 | ``` 45 | lib/ 46 | ├── main.dart # Entry point, dependency injection 47 | ├── constants.dart # Month numbers, storage keys 48 | ├── BirthdayCalendarDateUtils.dart # Date utilities 49 | ├── model/user_birthday.dart # Core data: name, date, hasNotification, phoneNumber 50 | ├── [Feature]Bloc/ # State management (BirthdaysBloc, ThemeBloc, etc.) 51 | ├── page/ # UI screens (main_page/, birthday/, settings_page/) 52 | ├── widget/ # Reusable components (calendar.dart, add_birthday_form.dart) 53 | ├── service/ # Business logic (interface + *_impl.dart) 54 | │ ├── contacts_service/ # Contact management 55 | │ ├── notification_service/ # Local notifications 56 | │ ├── storage_service/ # SharedPreferences wrapper 57 | │ └── permission_service/ # Runtime permissions 58 | └── l10n/ # app_en.arb, app_hi.arb, app_de.arb + generated files 59 | ``` 60 | 61 | **BLoC Pattern**: Each feature has BLoC for state (e.g., `BirthdaysBloc/BirthdaysBloc.dart` + `BirthdaysState.dart`). Services use interface + implementation pattern (`*_service.dart` + `*_service_impl.dart`). Data stored via SharedPreferences (no database). 62 | 63 | **Android-specific**: Requires notification channels, contacts/notification permissions. Config in `android/app/build.gradle` (multiDex enabled, desugar enabled for Java 8+ APIs) 64 | 65 | ## Code Conventions & Making Changes 66 | 67 | **Style**: snake_case files, PascalCase classes. Each BLoC has own directory with Bloc+State files. Services use interface + `_impl` suffix. Import order: Flutter, packages, relative. 68 | 69 | **When modifying**: 70 | - Run `dart pub get` && `flutter pub get` if modifying `pubspec.yaml` 71 | - Run `flutter analyze` frequently to catch issues early 72 | - Run `flutter test` before committing 73 | - Update all .arb files (en, hi, de) for user-facing strings, then `flutter gen-l10n` 74 | - Follow BLoC pattern for state - don't manage state in widgets 75 | - Use existing services - don't duplicate service layer logic 76 | 77 | **When adding features**: 78 | - Create new BLoC if managing new state (Bloc + State files in directory) 79 | - Add interface + implementation for services (`*_service.dart` + `*_service_impl.dart`) 80 | - Add tests in `test/` following existing patterns (mock SharedPreferences) 81 | - Update all 3 localization files (en, hi, de) 82 | 83 | **Platform-specific**: Native code in `android/app/src/main/kotlin/` or `ios/Runner/` requires full rebuild 84 | 85 | ## Trust These Instructions 86 | These instructions match the CI/CD pipeline and codebase. **Only search/explore if**: (1) info here is incomplete for your task, (2) you encounter errors contradicting these instructions, or (3) you need implementation details not covered. Always validate with: `dart pub get` → `flutter pub get` → `flutter analyze` → `flutter test` 87 | -------------------------------------------------------------------------------- /lib/page/settings_page/settings_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/ClearNotificationsBloc/ClearNotificationsBloc.dart'; 2 | import 'package:birthday_calendar/ContactsPermissionStatusBloc/ContactsPermissionStatusBloc.dart'; 3 | import 'package:birthday_calendar/ThemeBloc/ThemeBloc.dart'; 4 | import 'package:birthday_calendar/VersionBloc/VersionBloc.dart'; 5 | import 'package:birthday_calendar/service/contacts_service/contacts_service.dart'; 6 | import 'package:birthday_calendar/utils.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:flutter_bloc/flutter_bloc.dart'; 9 | import 'package:permission_handler/permission_handler.dart'; 10 | import 'package:flutter_contacts/flutter_contacts.dart'; 11 | import 'package:birthday_calendar/l10n/app_localizations.dart'; 12 | 13 | class SettingsScreen extends StatelessWidget { 14 | SettingsScreen({ 15 | required this.contactsService, 16 | }); 17 | 18 | final ContactsService contactsService; 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return Scaffold( 23 | appBar: AppBar( 24 | title: new Text(AppLocalizations.of(context)!.settings), 25 | ), 26 | body: Column( 27 | mainAxisAlignment: MainAxisAlignment.start, 28 | children: [ 29 | SwitchListTile( 30 | title: Text(AppLocalizations.of(context)!.darkMode), 31 | value: context.read().state == ThemeMode.dark 32 | ? true 33 | : false, 34 | secondary: new Icon(Icons.dark_mode, 35 | color: context.read().state == ThemeMode.dark 36 | ? Color.fromARGB(200, 243, 231, 106) 37 | : Color(0xFF642ef3)), 38 | onChanged: (bool newValue) { 39 | ThemeEvent event = 40 | context.read().state == ThemeMode.dark 41 | ? ThemeEvent.toggleLight 42 | : ThemeEvent.toggleDark; 43 | BlocProvider.of(context).add(event); 44 | }), 45 | BlocBuilder( 46 | builder: (context, state) { 47 | return ListTile( 48 | title: Text(AppLocalizations.of(context)!.importContacts), 49 | leading: Icon(Icons.contacts, color: Colors.blue), 50 | onTap: () { 51 | _handleImportingContacts(context); 52 | }, 53 | enabled: state.isPermanentlyDenied ? false : true); 54 | }), 55 | ListTile( 56 | title: Text(AppLocalizations.of(context)!.clearNotifications), 57 | leading: const Icon(Icons.clear, color: Colors.redAccent), 58 | onTap: () { 59 | _showClearBirthdaysConfirmationDialog(context); 60 | }), 61 | Spacer(), 62 | Row( 63 | mainAxisAlignment: MainAxisAlignment.end, 64 | children: [ 65 | Align( 66 | alignment: Alignment.bottomRight, 67 | child: BlocBuilder( 68 | builder: (context, state) { 69 | return Text("v $state"); 70 | })) 71 | ], 72 | ) 73 | ], 74 | ), 75 | ); 76 | } 77 | 78 | void _showClearBirthdaysConfirmationDialog(BuildContext context) { 79 | AlertDialog alert = AlertDialog( 80 | title: Text(AppLocalizations.of(context)!.clearNotificationsAlertTitle), 81 | content: Text( 82 | AppLocalizations.of(context)!.clearNotificationsAlertDescription), 83 | actions: [ 84 | TextButton( 85 | onPressed: () { 86 | Navigator.pop(context); 87 | }, 88 | child: Text(AppLocalizations.of(context)!.no), 89 | ), 90 | TextButton( 91 | onPressed: () { 92 | BlocProvider.of(context) 93 | .add(ClearNotificationsEvent.ClearedNotifications); 94 | Navigator.pop(context); 95 | }, 96 | child: Text(AppLocalizations.of(context)!.yes), 97 | ) 98 | ], 99 | ); 100 | showDialog( 101 | context: context, 102 | builder: (BuildContext context) { 103 | return alert; 104 | }); 105 | } 106 | 107 | void _handleImportingContacts(BuildContext context) async { 108 | PermissionStatus status = 109 | await contactsService.getContactsPermissionStatus(context); 110 | 111 | if (status == PermissionStatus.denied) { 112 | status = await contactsService.requestContactsPermission(context); 113 | } 114 | 115 | if (status == PermissionStatus.permanentlyDenied) { 116 | contactsService.setContactsPermissionPermanentlyDenied(); 117 | BlocProvider.of(context) 118 | .add(ContactsPermissionStatusEvent.PermissionPermanentlyDenied); 119 | return; 120 | } 121 | 122 | if (status == PermissionStatus.granted) { 123 | BlocProvider.of(context) 124 | .add(ContactsPermissionStatusEvent.PermissionGranted); 125 | List contacts = await contactsService.fetchContacts(false); 126 | 127 | if (contacts.isEmpty) { 128 | Utils.showSnackbarWithMessage( 129 | context, AppLocalizations.of(context)!.noContactsFoundMsg); 130 | return; 131 | } 132 | 133 | contacts = await contactsService.filterAlreadyImportedContacts(contacts); 134 | 135 | if (contacts.isEmpty) { 136 | Utils.showSnackbarWithMessage( 137 | context, AppLocalizations.of(context)!.alreadyAddedContactsMsg); 138 | return; 139 | } 140 | 141 | contactsService.handleAddingBirthdaysToContacts(context, contacts); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Privacy Policy 7 | 27 | 28 | 29 | 30 |

Privacy Policy

31 | 32 |

tomerpacific built the BirthdayCalendar app as a Free app. This SERVICE is provided by tomerpacific at no cost and is intended for use as is.

33 | 34 |

This page is used to inform visitors regarding my policies with the collection, use, and disclosure of Personal Information if anyone decided to use my Service.

35 | 36 |

If you choose to use my Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that I collect is used for providing and improving the Service. I will not use or share your information with anyone except as described in this Privacy Policy.

37 | 38 |

The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which are accessible at BirthdayCalendar unless otherwise defined in this Privacy Policy.

39 | 40 |

Information Collection and Use

41 | 42 |

For a better experience, while using our Service, I may require you to provide us with certain personally identifiable information. The information that I request will be retained on your device and is not collected by me in any way.

43 | 44 |

The app does use third-party services that may collect information used to identify you.

45 | 46 |

Link to the privacy policy of third-party service providers used by the app:

47 | 50 | 51 |

Log Data

52 | 53 |

I want to inform you that whenever you use my Service, in a case of an error in the app I collect data and information (through third-party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing my Service, the time and date of your use of the Service, and other statistics.

54 | 55 |

Cookies

56 | 57 |

Cookies are files with a small amount of data that are commonly used as anonymous unique identifiers. These are sent to your browser from the websites that you visit and are stored on your device's internal memory.

58 | 59 |

This Service does not use these “cookies” explicitly. However, the app may use third-party code and libraries that use “cookies” to collect information and improve their services. You have the option to either accept or refuse these cookies and know when a cookie is being sent to your device. If you choose to refuse our cookies, you may not be able to use some portions of this Service.

60 | 61 |

Service Providers

62 | 63 |

I may employ third-party companies and individuals due to the following reasons:

64 |
    65 |
  • To facilitate our Service;
  • 66 |
  • To provide the Service on our behalf;
  • 67 |
  • To perform Service-related services; or
  • 68 |
  • To assist us in analyzing how our Service is used.
  • 69 |
70 | 71 |

I want to inform users of this Service that these third parties have access to their Personal Information. The reason is to perform the tasks assigned to them on our behalf. However, they are obligated not to disclose or use the information for any other purpose.

72 | 73 |

Security

74 | 75 |

I value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and I cannot guarantee its absolute security.

76 | 77 |

Links to Other Sites

78 | 79 |

This Service may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by me. Therefore, I strongly advise you to review the Privacy Policy of these websites. I have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services.

80 | 81 |

Children’s Privacy

82 | 83 |

These Services do not address anyone under the age of 13. I do not knowingly collect personally identifiable information from children under 13 years of age. In the case I discover that a child under 13 has provided me with personal information, I immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact me so that I will be able to do the necessary actions.

84 | 85 |

Changes to This Privacy Policy

86 | 87 |

I may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new Privacy Policy on this page.

88 | 89 |

This policy is effective as of 2022-03-22

90 | 91 |

Contact Us

92 | 93 |

If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact me at tomerpacific@gmail.com.

94 | 95 |

This privacy policy page was created at privacypolicytemplate.net and modified/generated by App Privacy Policy Generator.

96 | 97 | 98 | -------------------------------------------------------------------------------- /test/birthday_widget_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/BirthdayBloc/BirthdaysBloc.dart'; 2 | import 'package:birthday_calendar/BirthdayBloc/BirthdaysState.dart'; 3 | import 'package:birthday_calendar/ThemeBloc/ThemeBloc.dart'; 4 | import 'package:birthday_calendar/VersionBloc/VersionBloc.dart'; 5 | import 'package:birthday_calendar/model/user_birthday.dart'; 6 | import 'package:birthday_calendar/service/notification_service/notification_service.dart'; 7 | import 'package:birthday_calendar/service/notification_service/notification_service_impl.dart'; 8 | import 'package:birthday_calendar/service/permission_service/permissions_service.dart'; 9 | import 'package:birthday_calendar/service/permission_service/permissions_service_impl.dart'; 10 | import 'package:birthday_calendar/service/storage_service/shared_preferences_storage.dart'; 11 | import 'package:birthday_calendar/service/storage_service/storage_service.dart'; 12 | import 'package:flutter_bloc/flutter_bloc.dart'; 13 | import 'package:flutter_test/flutter_test.dart'; 14 | import 'package:birthday_calendar/page/birthday/birthday.dart'; 15 | import 'package:flutter/material.dart'; 16 | import 'package:shared_preferences/shared_preferences.dart'; 17 | 18 | var printLog = []; 19 | 20 | void print(String s) => printLog.add(s); 21 | 22 | void main() { 23 | StorageService storageService = StorageServiceSharedPreferences(); 24 | PermissionsService permissionsService = PermissionsServiceImpl(); 25 | NotificationService notificationService = NotificationServiceImpl( 26 | permissionsService: permissionsService, storageService: storageService); 27 | List birthdays = []; 28 | 29 | setUp(() { 30 | return Future(() async { 31 | WidgetsFlutterBinding.ensureInitialized(); 32 | SharedPreferences.setMockInitialValues({}); 33 | }); 34 | }); 35 | 36 | Widget base = RepositoryProvider( 37 | create: (context) => StorageServiceSharedPreferences(), 38 | child: MultiBlocProvider( 39 | providers: [ 40 | BlocProvider(create: (context) => ThemeBloc(storageService, false)), 41 | BlocProvider(create: (context) => VersionBloc()) 42 | ], 43 | child: BlocBuilder( 44 | builder: (context, state) { 45 | return MaterialApp( 46 | title: '', 47 | theme: ThemeData.light(), 48 | themeMode: state, 49 | darkTheme: ThemeData.dark(), 50 | home: Material( 51 | child: new SizedBox( 52 | height: 40, 53 | child: BlocProvider( 54 | create: (context) => BirthdaysBloc( 55 | notificationService, storageService, birthdays), 56 | child: BlocBuilder( 57 | builder: (context, state) { 58 | return Column(children: [ 59 | (state.birthdays == null || 60 | state.birthdays!.length == 0) 61 | ? Spacer() 62 | : Expanded( 63 | child: ListView.builder( 64 | itemCount: state.birthdays != null 65 | ? state.birthdays!.length 66 | : 0, 67 | itemBuilder: (BuildContext context, 68 | int index) { 69 | return BlocProvider.value( 70 | value: BlocProvider.of< 71 | BirthdaysBloc>(context), 72 | child: BirthdayWidget( 73 | key: Key(state 74 | .birthdays![index] 75 | .name), 76 | birthdayOfPerson: 77 | state.birthdays![index], 78 | indexOfBirthday: index, 79 | notificationService: 80 | notificationService)); 81 | }, 82 | ), 83 | ), 84 | ]); 85 | }))))); 86 | }, 87 | ), 88 | )); 89 | 90 | testWidgets("BirthdayWidget show birthday for Someone", 91 | (WidgetTester tester) async { 92 | final String phoneNumber = '+234 500 500 5005'; 93 | UserBirthday userBirthday = 94 | new UserBirthday("Someone", DateTime.now(), false, phoneNumber); 95 | birthdays = [userBirthday]; 96 | await tester.pumpWidget(base); 97 | 98 | final nameFinder = find.text('Someone'); 99 | expect(nameFinder, findsOneWidget); 100 | }); 101 | 102 | testWidgets("BirthdayWidget click on remove birthday icon", 103 | (WidgetTester tester) async { 104 | final String phoneNumber = '+234 500 500 5005'; 105 | UserBirthday userBirthday = 106 | new UserBirthday("Someone", DateTime.now(), false, phoneNumber); 107 | birthdays = [userBirthday]; 108 | await tester.pumpWidget(base); 109 | 110 | await tester.tap(find.descendant( 111 | of: find.byType(IconButton), matching: find.byIcon(Icons.clear))); 112 | 113 | await tester.pumpAndSettle(); 114 | 115 | final nameFinder = find.text('Someone'); 116 | expect(nameFinder, findsNothing); 117 | }); 118 | 119 | testWidgets("BirthdayWidget press on call button", 120 | (WidgetTester tester) async { 121 | final String phoneNumber = '+234 500 500 5005'; 122 | UserBirthday userBirthday = 123 | new UserBirthday("Someone", DateTime.now(), false, phoneNumber); 124 | 125 | birthdays = [userBirthday]; 126 | 127 | await tester.pumpWidget(base); 128 | 129 | await tester.tap(find.descendant( 130 | of: find.byType(IconButton), matching: find.byIcon(Icons.call))); 131 | await tester.pump(); 132 | 133 | final callButtonIcon = find.byIcon(Icons.call); 134 | expect(callButtonIcon, findsOneWidget); 135 | }); 136 | } 137 | -------------------------------------------------------------------------------- /lib/widget/add_birthday_form.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/BirthdayBloc/BirthdaysBloc.dart'; 2 | import 'package:birthday_calendar/model/user_birthday.dart'; 3 | import 'package:birthday_calendar/service/notification_service/notification_service.dart'; 4 | import 'package:birthday_calendar/service/storage_service/shared_preferences_storage.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_bloc/flutter_bloc.dart'; 7 | import 'package:intl_phone_number_input/intl_phone_number_input.dart'; 8 | import 'package:collection/collection.dart'; 9 | import 'package:birthday_calendar/l10n/app_localizations.dart'; 10 | 11 | class AddBirthdayForm extends StatefulWidget { 12 | final DateTime dateOfDay; 13 | final NotificationService notificationService; 14 | 15 | AddBirthdayForm( 16 | {Key? key, required this.dateOfDay, required this.notificationService}) 17 | : super(key: key); 18 | 19 | @override 20 | State createState() { 21 | return AddBirthdayFormState(); 22 | } 23 | } 24 | 25 | class AddBirthdayFormState extends State { 26 | final _addBirthdayFormKey = GlobalKey(); 27 | final _birthdayNameKey = GlobalKey(); 28 | final _phoneNumberKey = GlobalKey(); 29 | 30 | TextEditingController _birthdayPersonController = new TextEditingController(); 31 | TextEditingController _phoneNumberController = new TextEditingController(); 32 | PhoneNumber _birthdayPhoneNumber = PhoneNumber(isoCode: 'US'); 33 | List birthdaysForDate = []; 34 | bool doesWantToAddPhoneNumber = false; 35 | late FocusNode addTelephoneButtonFocusNode; 36 | 37 | bool _isUniqueName(String name) { 38 | UserBirthday? birthday = 39 | birthdaysForDate.firstWhereOrNull((element) => element.name == name); 40 | return birthday == null; 41 | } 42 | 43 | @override 44 | void initState() { 45 | super.initState(); 46 | addTelephoneButtonFocusNode = FocusNode(); 47 | _getBirthdaysForDate(); 48 | } 49 | 50 | void _getBirthdaysForDate() async { 51 | birthdaysForDate = await context 52 | .read() 53 | .getBirthdaysForDate(widget.dateOfDay, true); 54 | } 55 | 56 | Widget _phoneNumberInputField() { 57 | return doesWantToAddPhoneNumber == true 58 | ? new InternationalPhoneNumberInput( 59 | focusNode: addTelephoneButtonFocusNode, 60 | key: _phoneNumberKey, 61 | onInputChanged: (PhoneNumber number) { 62 | _birthdayPhoneNumber = number; 63 | }, 64 | onInputValidated: (bool value) {}, 65 | selectorConfig: SelectorConfig( 66 | selectorType: PhoneInputSelectorType.BOTTOM_SHEET, 67 | ), 68 | ignoreBlank: false, 69 | autoValidateMode: AutovalidateMode.disabled, 70 | initialValue: _birthdayPhoneNumber, 71 | textFieldController: _phoneNumberController, 72 | formatInput: false, 73 | keyboardType: 74 | TextInputType.numberWithOptions(signed: true, decimal: true), 75 | inputBorder: OutlineInputBorder(), 76 | onSaved: (PhoneNumber number) { 77 | _birthdayPhoneNumber = number; 78 | }, 79 | ) 80 | : Spacer(); 81 | } 82 | 83 | @override 84 | Widget build(BuildContext context) { 85 | return AlertDialog( 86 | title: Text(AppLocalizations.of(context)!.addBirthday), 87 | content: Form( 88 | key: _addBirthdayFormKey, 89 | child: new Column( 90 | mainAxisAlignment: MainAxisAlignment.center, 91 | children: [ 92 | new Row( 93 | children: [ 94 | Expanded( 95 | child: new TextFormField( 96 | autofocus: true, 97 | controller: _birthdayPersonController, 98 | decoration: InputDecoration( 99 | hintText: AppLocalizations.of(context)! 100 | .hintTextForNameInputField), 101 | key: _birthdayNameKey, 102 | validator: (value) { 103 | if (value == null || value.isEmpty) { 104 | return AppLocalizations.of(context)!.notValidName; 105 | } 106 | if (!_isUniqueName(value)) { 107 | return AppLocalizations.of(context)! 108 | .nameAlreadyExists; 109 | } 110 | return null; 111 | }, 112 | ), 113 | ), 114 | Expanded( 115 | child: IconButton( 116 | icon: doesWantToAddPhoneNumber == false 117 | ? Icon(Icons.phone) 118 | : Icon(Icons.phone, color: Colors.blueAccent), 119 | onPressed: () { 120 | setState(() { 121 | doesWantToAddPhoneNumber = !doesWantToAddPhoneNumber; 122 | }); 123 | addTelephoneButtonFocusNode.requestFocus(); 124 | }, 125 | )) 126 | ], 127 | ), 128 | _phoneNumberInputField() 129 | ])), 130 | actions: [ 131 | TextButton( 132 | style: TextButton.styleFrom(foregroundColor: Colors.green), 133 | onPressed: () async { 134 | if (_addBirthdayFormKey.currentState != null && 135 | _addBirthdayFormKey.currentState!.validate()) { 136 | _addBirthdayFormKey.currentState!.save(); 137 | 138 | bool hasUserGrantedNotificationPermission = await widget 139 | .notificationService 140 | .isNotificationPermissionGranted(context); 141 | 142 | UserBirthday userBirthday = new UserBirthday( 143 | _birthdayPersonController.text, 144 | widget.dateOfDay, 145 | hasUserGrantedNotificationPermission, 146 | _birthdayPhoneNumber.phoneNumber != null 147 | ? _birthdayPhoneNumber.parseNumber() 148 | : ""); 149 | BlocProvider.of(context).add(new BirthdaysEvent( 150 | eventName: BirthdayEvent.AddBirthday, 151 | birthdays: birthdaysForDate, 152 | birthday: userBirthday, 153 | shouldShowAddBirthdayDialog: false, 154 | notificationMsg: AppLocalizations.of(context)! 155 | .notificationForBirthdayMessage(userBirthday.name))); 156 | 157 | Navigator.pop(context); 158 | } else { 159 | if (_birthdayNameKey.currentState != null && 160 | !_birthdayNameKey.currentState!.isValid) { 161 | _birthdayPersonController.clear(); 162 | } 163 | if (_phoneNumberKey.currentState != null && 164 | !_phoneNumberKey.currentState!.isValid) { 165 | _birthdayPersonController.clear(); 166 | } 167 | } 168 | }, 169 | child: Text(AppLocalizations.of(context)!.ok)), 170 | TextButton( 171 | style: TextButton.styleFrom(foregroundColor: Colors.red), 172 | onPressed: () { 173 | _birthdayPersonController.clear(); 174 | _phoneNumberController.clear(); 175 | Navigator.pop(context); 176 | }, 177 | child: Text(AppLocalizations.of(context)!.back)) 178 | ], 179 | ); 180 | } 181 | 182 | @override 183 | dispose() { 184 | _birthdayPersonController.dispose(); 185 | _phoneNumberController.dispose(); 186 | super.dispose(); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /lib/service/storage_service/shared_preferences_storage.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:async'; 3 | import 'package:birthday_calendar/BirthdayCalendarDateUtils.dart'; 4 | import 'package:birthday_calendar/constants.dart'; 5 | import 'package:birthday_calendar/model/user_birthday.dart'; 6 | import 'package:intl/intl.dart'; 7 | import 'storage_service.dart'; 8 | import 'package:shared_preferences/shared_preferences.dart'; 9 | import 'package:collection/collection.dart'; 10 | 11 | class StorageServiceSharedPreferences extends StorageService { 12 | StreamController> streamController = 13 | StreamController>.broadcast(); 14 | 15 | @override 16 | void clearAllBirthdays() async { 17 | final sharedPreferences = await SharedPreferences.getInstance(); 18 | Set keys = sharedPreferences.getKeys(); 19 | DateFormat format = DateFormat('yyyy-MM-dd'); 20 | for (String key in keys) { 21 | try { 22 | format.parse(key); 23 | sharedPreferences.remove(key); 24 | } catch (error) {} 25 | } 26 | } 27 | 28 | @override 29 | Future> getBirthdaysForDate( 30 | DateTime dateTime, bool shouldGetBirthdaysFromSimilarDate) async { 31 | if (shouldGetBirthdaysFromSimilarDate) { 32 | List birthdays = []; 33 | List birthdaysWithSimilarDates = 34 | await _getBirthdaysWithSimilarDate(dateTime); 35 | for (DateTime dateTime in birthdaysWithSimilarDates) { 36 | List decodedBirthdays = 37 | await _decodeBirthdaysFromDate(dateTime); 38 | birthdays.addAll(decodedBirthdays); 39 | } 40 | 41 | return birthdays; 42 | } 43 | 44 | List decodedBirthdays = 45 | await _decodeBirthdaysFromDate(dateTime); 46 | return decodedBirthdays; 47 | } 48 | 49 | Future> _decodeBirthdaysFromDate(DateTime dateTime) async { 50 | final sharedPreferences = await SharedPreferences.getInstance(); 51 | 52 | String formattedDate = 53 | BirthdayCalendarDateUtils.formatDateForSharedPrefs(dateTime); 54 | String? birthdaysJSON = sharedPreferences.getString(formattedDate); 55 | if (birthdaysJSON != null) { 56 | List decodedBirthdaysForDate = jsonDecode(birthdaysJSON); 57 | List decodedBirthdays = decodedBirthdaysForDate 58 | .map((decodedBirthday) => UserBirthday.fromJson(decodedBirthday)) 59 | .toList(); 60 | return decodedBirthdays; 61 | } 62 | 63 | return []; 64 | } 65 | 66 | Future> _getBirthdaysWithSimilarDate(DateTime dateTime) async { 67 | List matchingBirthdays = []; 68 | final sharedPreferences = await SharedPreferences.getInstance(); 69 | Set dates = sharedPreferences.getKeys(); 70 | 71 | for (String date in dates) { 72 | if (!BirthdayCalendarDateUtils.isADate(date)) { 73 | continue; 74 | } 75 | 76 | DateTime converted = DateTime.parse(date); 77 | if (dateTime.month == converted.month && dateTime.day == converted.day) { 78 | matchingBirthdays.add(converted); 79 | } 80 | } 81 | 82 | return matchingBirthdays; 83 | } 84 | 85 | @override 86 | Future getThemeModeSetting() async { 87 | final sharedPreferences = await SharedPreferences.getInstance(); 88 | bool? isDarkModeEnabled = sharedPreferences.getBool(darkModeKey); 89 | return isDarkModeEnabled != null ? isDarkModeEnabled : false; 90 | } 91 | 92 | @override 93 | Future saveBirthdaysForDate( 94 | DateTime dateTime, List birthdays) async { 95 | final sharedPreferences = await SharedPreferences.getInstance(); 96 | String encoded = jsonEncode(birthdays); 97 | String formattedDate = 98 | BirthdayCalendarDateUtils.formatDateForSharedPrefs(dateTime); 99 | sharedPreferences.setString(formattedDate, encoded); 100 | 101 | streamController.sink.add(birthdays); 102 | } 103 | 104 | @override 105 | Future saveThemeModeSetting(bool isDarkModeEnabled) async { 106 | final sharedPreferences = await SharedPreferences.getInstance(); 107 | sharedPreferences.setBool(darkModeKey, isDarkModeEnabled); 108 | } 109 | 110 | @override 111 | Future updateNotificationStatusForBirthday( 112 | UserBirthday userBirthday, bool updatedStatus) async { 113 | List birthdays = 114 | await getBirthdaysForDate(userBirthday.birthdayDate, false); 115 | for (int i = 0; i < birthdays.length; i++) { 116 | UserBirthday savedBirthday = birthdays[i]; 117 | if (savedBirthday.equals(userBirthday)) { 118 | savedBirthday.updateNotificationStatus(updatedStatus); 119 | } 120 | } 121 | 122 | saveBirthdaysForDate(userBirthday.birthdayDate, birthdays); 123 | } 124 | 125 | @override 126 | Stream> getBirthdaysStream() { 127 | return streamController.stream; 128 | } 129 | 130 | @override 131 | void saveIsContactsPermissionPermanentlyDenied( 132 | bool isPermanentlyDenied) async { 133 | final sharedPreferences = await SharedPreferences.getInstance(); 134 | sharedPreferences.setBool(contactsPermissionStatusKey, isPermanentlyDenied); 135 | } 136 | 137 | @override 138 | Future getIsContactPermissionPermanentlyDenied() async { 139 | final sharedPreferences = await SharedPreferences.getInstance(); 140 | bool? isPermanentlyDenied = 141 | sharedPreferences.getBool(contactsPermissionStatusKey); 142 | return isPermanentlyDenied != null ? isPermanentlyDenied : false; 143 | } 144 | 145 | @override 146 | void saveDidAlreadyMigrateNotificationStatus(bool status) async { 147 | final sharedPreferences = await SharedPreferences.getInstance(); 148 | sharedPreferences.setBool(didAlreadyMigrateNotificationStatusFlag, status); 149 | } 150 | 151 | @override 152 | Future getAlreadyMigrateNotificationStatus() async { 153 | final sharedPreferences = await SharedPreferences.getInstance(); 154 | bool? hasAlreadyMigratedNotificationStatus = 155 | sharedPreferences.getBool(didAlreadyMigrateNotificationStatusFlag); 156 | return hasAlreadyMigratedNotificationStatus != null 157 | ? hasAlreadyMigratedNotificationStatus 158 | : false; 159 | } 160 | 161 | @override 162 | Future> getAllBirthdays() async { 163 | List birthdays = []; 164 | final sharedPreferences = await SharedPreferences.getInstance(); 165 | Set dates = sharedPreferences.getKeys(); 166 | 167 | for (String date in dates) { 168 | if (!BirthdayCalendarDateUtils.isADate(date)) { 169 | continue; 170 | } 171 | 172 | String? birthdaysJSON = sharedPreferences.getString(date); 173 | if (birthdaysJSON != null) { 174 | List decodedBirthdaysForDate = jsonDecode(birthdaysJSON); 175 | List userBirthdays = decodedBirthdaysForDate 176 | .map((decodedBirthday) => UserBirthday.fromJson(decodedBirthday)) 177 | .toList(); 178 | birthdays = birthdays + userBirthdays; 179 | } 180 | } 181 | 182 | return birthdays; 183 | } 184 | 185 | @override 186 | Future updatePhoneNumberForBirthday(UserBirthday birthday) async { 187 | List birthdays = 188 | await getBirthdaysForDate(birthday.birthdayDate, false); 189 | UserBirthday? storedBirthday = 190 | birthdays.firstWhereOrNull((element) => element.name == birthday.name); 191 | if (storedBirthday != null) { 192 | storedBirthday.phoneNumber = birthday.phoneNumber; 193 | saveBirthdaysForDate(storedBirthday.birthdayDate, birthdays); 194 | } 195 | } 196 | 197 | @override 198 | Future setNotificationPermissionState( 199 | NotificationPermissionState state) async { 200 | final prefs = await SharedPreferences.getInstance(); 201 | await prefs.setInt(notificationsPermissionStatusKey, state.index); 202 | } 203 | 204 | @override 205 | Future getNotificationPermissionState() async { 206 | final prefs = await SharedPreferences.getInstance(); 207 | final index = prefs.getInt(notificationsPermissionStatusKey); 208 | if (index == null) return NotificationPermissionState.unknown; 209 | return NotificationPermissionState.values[index]; 210 | } 211 | 212 | void dispose() { 213 | streamController.close(); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /lib/page/birthday/birthday.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/BirthdayBloc/BirthdaysBloc.dart'; 2 | import 'package:birthday_calendar/UserNotificationStatusBloc/UserNotificationStatusBloc.dart'; 3 | import 'package:birthday_calendar/l10n/app_localizations.dart'; 4 | import 'package:birthday_calendar/service/notification_service/notification_service.dart'; 5 | import 'package:birthday_calendar/service/storage_service/shared_preferences_storage.dart'; 6 | import 'package:birthday_calendar/utils.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:birthday_calendar/model/user_birthday.dart'; 9 | import 'package:flutter_bloc/flutter_bloc.dart'; 10 | import 'package:intl_phone_number_input/intl_phone_number_input.dart'; 11 | import 'package:permission_handler/permission_handler.dart'; 12 | import 'package:url_launcher/url_launcher.dart'; 13 | 14 | class BirthdayWidget extends StatefulWidget { 15 | final UserBirthday birthdayOfPerson; 16 | final int indexOfBirthday; 17 | final NotificationService notificationService; 18 | 19 | BirthdayWidget( 20 | {required Key key, 21 | required this.birthdayOfPerson, 22 | required this.indexOfBirthday, 23 | required this.notificationService}) 24 | : super(key: key); 25 | 26 | @override 27 | _BirthdayWidgetState createState() => _BirthdayWidgetState( 28 | notificationService, birthdayOfPerson, indexOfBirthday); 29 | } 30 | 31 | class _BirthdayWidgetState extends State { 32 | _BirthdayWidgetState( 33 | this.notificationService, this.birthdayOfPerson, this.indexOfBirthday); 34 | 35 | NotificationService notificationService; 36 | UserBirthday birthdayOfPerson; 37 | int indexOfBirthday; 38 | 39 | void _handleCallButtonPressed( 40 | BuildContext context, String phoneNumber) async { 41 | Uri phoneUri = Uri.parse('tel:$phoneNumber'); 42 | if (await canLaunchUrl(phoneUri)) { 43 | launchUrl(phoneUri); 44 | } else { 45 | Utils.showSnackbarWithMessage( 46 | context, AppLocalizations.of(context)!.unableToMakeCallMsg); 47 | } 48 | } 49 | 50 | void _handleAddingPhoneNumber(BuildContext context) async { 51 | PhoneNumber _birthdayPhoneNumber = PhoneNumber(isoCode: 'US'); 52 | final _phoneNumberKey = GlobalKey(); 53 | TextEditingController _phoneNumberController = new TextEditingController(); 54 | 55 | AlertDialog addPhoneNumberAlert = AlertDialog( 56 | title: Text(AppLocalizations.of(context)!.addPhoneNumber), 57 | content: InternationalPhoneNumberInput( 58 | key: _phoneNumberKey, 59 | onInputChanged: (PhoneNumber number) { 60 | _birthdayPhoneNumber = number; 61 | }, 62 | onInputValidated: (bool value) {}, 63 | selectorConfig: SelectorConfig( 64 | selectorType: PhoneInputSelectorType.BOTTOM_SHEET, 65 | ), 66 | ignoreBlank: false, 67 | autoValidateMode: AutovalidateMode.disabled, 68 | initialValue: _birthdayPhoneNumber, 69 | textFieldController: _phoneNumberController, 70 | formatInput: false, 71 | keyboardType: 72 | TextInputType.numberWithOptions(signed: true, decimal: true), 73 | inputBorder: OutlineInputBorder(), 74 | onSaved: (PhoneNumber number) { 75 | _birthdayPhoneNumber = number; 76 | }, 77 | ), 78 | actions: [ 79 | TextButton( 80 | style: TextButton.styleFrom(foregroundColor: Colors.green), 81 | onPressed: () { 82 | if (_birthdayPhoneNumber.phoneNumber != null) { 83 | String phone = _birthdayPhoneNumber.parseNumber(); 84 | birthdayOfPerson.phoneNumber = phone; 85 | context 86 | .read() 87 | .updatePhoneNumberForBirthday(birthdayOfPerson); 88 | setState(() {}); 89 | _phoneNumberController.clear(); 90 | Navigator.pop(context); 91 | } else { 92 | return null; 93 | } 94 | }, 95 | child: Text(AppLocalizations.of(context)!.add), 96 | ), 97 | TextButton( 98 | style: TextButton.styleFrom(foregroundColor: Colors.red), 99 | onPressed: () { 100 | _phoneNumberController.clear(); 101 | Navigator.pop(context); 102 | }, 103 | child: Text(AppLocalizations.of(context)!.cancel)), 104 | ]); 105 | 106 | showDialog( 107 | context: context, 108 | builder: (BuildContext context) { 109 | return addPhoneNumberAlert; 110 | }); 111 | } 112 | 113 | Widget callIconButton(BuildContext context) { 114 | return birthdayOfPerson.phoneNumber.isNotEmpty 115 | ? new IconButton( 116 | icon: Icon(Icons.call, 117 | color: Utils.getColorBasedOnPosition( 118 | indexOfBirthday, ElementType.icon)), 119 | onPressed: () { 120 | _handleCallButtonPressed(context, birthdayOfPerson.phoneNumber); 121 | }) 122 | : new IconButton( 123 | icon: Icon(Icons.add_ic_call_outlined, 124 | color: Utils.getColorBasedOnPosition( 125 | indexOfBirthday, ElementType.icon)), 126 | onPressed: () { 127 | _handleAddingPhoneNumber(context); 128 | }); 129 | } 130 | 131 | @override 132 | Widget build(BuildContext context) { 133 | return Container( 134 | height: 40, 135 | color: Utils.getColorBasedOnPosition( 136 | indexOfBirthday, ElementType.background), 137 | child: Row( 138 | children: [ 139 | new Padding( 140 | padding: const EdgeInsets.all(8.0), 141 | child: Text( 142 | birthdayOfPerson.name, 143 | textDirection: TextDirection.ltr, 144 | style: new TextStyle( 145 | fontSize: 20.0, 146 | color: Utils.getColorBasedOnPosition( 147 | indexOfBirthday, ElementType.text)), 148 | ), 149 | ), 150 | new Spacer(), 151 | BlocProvider( 152 | create: (context) => UserNotificationStatusBloc( 153 | context.read(), 154 | notificationService), 155 | child: BlocBuilder( 156 | builder: (context, state) { 157 | return new IconButton( 158 | icon: Icon( 159 | !birthdayOfPerson.hasNotification 160 | ? Icons.notifications_off_outlined 161 | : Icons.notifications_active_outlined, 162 | color: Utils.getColorBasedOnPosition( 163 | indexOfBirthday, ElementType.icon)), 164 | onPressed: () async { 165 | if (!birthdayOfPerson.hasNotification) { 166 | PermissionStatus status = await notificationService 167 | .requestNotificationPermission(context); 168 | 169 | if (status.isGranted) { 170 | BlocProvider.of(context) 171 | .add(UserNotificationStatusEvent( 172 | userBirthday: birthdayOfPerson, 173 | hasNotification: birthdayOfPerson.hasNotification, 174 | notificationMsg: AppLocalizations.of(context)! 175 | .notificationForBirthdayMessage( 176 | birthdayOfPerson.name), 177 | )); 178 | return; 179 | } 180 | 181 | if (status.isPermanentlyDenied) { 182 | Utils.showSnackbarWithMessageAndAction( 183 | context, 184 | AppLocalizations.of(context)! 185 | .notificationPermissionPermanentlyDenied, 186 | SnackBarAction( 187 | label: AppLocalizations.of(context)! 188 | .openSettings, 189 | onPressed: openAppSettings)); 190 | return; 191 | } 192 | 193 | Utils.showSnackbarWithMessage( 194 | context, 195 | AppLocalizations.of(context)! 196 | .notificationPermissionDenied); 197 | } else { 198 | BlocProvider.of(context) 199 | .add(UserNotificationStatusEvent( 200 | userBirthday: birthdayOfPerson, 201 | hasNotification: birthdayOfPerson.hasNotification, 202 | notificationMsg: AppLocalizations.of(context)! 203 | .notificationForBirthdayMessage( 204 | birthdayOfPerson.name), 205 | )); 206 | } 207 | }); 208 | })), 209 | callIconButton(context), 210 | new IconButton( 211 | icon: Icon(Icons.clear, 212 | color: Utils.getColorBasedOnPosition( 213 | indexOfBirthday, ElementType.icon)), 214 | onPressed: () { 215 | BlocProvider.of(context).add(new BirthdaysEvent( 216 | eventName: BirthdayEvent.RemoveBirthday, 217 | birthday: birthdayOfPerson)); 218 | }), 219 | ], 220 | ), 221 | ); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /lib/page/main_page/main_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:birthday_calendar/BirthdayCalendarDateUtils.dart'; 2 | import 'package:birthday_calendar/ClearNotificationsBloc/ClearNotificationsBloc.dart'; 3 | import 'package:birthday_calendar/ContactsPermissionStatusBloc/ContactsPermissionStatusBloc.dart'; 4 | import 'package:birthday_calendar/VersionBloc/VersionBloc.dart'; 5 | import 'package:birthday_calendar/model/user_birthday.dart'; 6 | import 'package:birthday_calendar/page/birthdays_for_calendar_day_page/birthdays_for_calendar_day.dart'; 7 | import 'package:birthday_calendar/service/contacts_service/contacts_service.dart'; 8 | import 'package:birthday_calendar/service/notification_service/notificationCallbacks.dart'; 9 | import 'package:birthday_calendar/service/notification_service/notification_service.dart'; 10 | import 'package:birthday_calendar/service/storage_service/shared_preferences_storage.dart'; 11 | import 'package:birthday_calendar/service/update_service/update_service.dart'; 12 | import 'package:birthday_calendar/service/update_service/update_service_impl.dart'; 13 | import 'package:birthday_calendar/service/version_specific_service/VersionSpecificService.dart'; 14 | import 'package:birthday_calendar/service/version_specific_service/VersionSpecificServiceImpl.dart'; 15 | import 'package:birthday_calendar/utils.dart'; 16 | import 'package:flutter/material.dart'; 17 | import 'package:birthday_calendar/page/settings_page/settings_screen.dart'; 18 | import 'package:birthday_calendar/widget/calendar.dart'; 19 | import 'package:flutter_bloc/flutter_bloc.dart'; 20 | import 'package:birthday_calendar/l10n/app_localizations.dart'; 21 | 22 | class MainPage extends StatefulWidget { 23 | MainPage( 24 | {required Key key, 25 | required this.notificationService, 26 | required this.contactsService, 27 | required this.title, 28 | required this.currentMonth}) 29 | : super(key: key); 30 | 31 | final String title; 32 | final int currentMonth; 33 | final NotificationService notificationService; 34 | final ContactsService contactsService; 35 | 36 | @override 37 | _MainPageState createState() => _MainPageState(notificationService); 38 | } 39 | 40 | class _MainPageState extends State implements NotificationCallbacks { 41 | _MainPageState(this.notificationService); 42 | 43 | int monthToPresent = -1; 44 | NotificationService notificationService; 45 | UpdateService _updateService = UpdateServiceImpl(); 46 | late VersionSpecificService versionSpecificService; 47 | 48 | void _calculateNextMonthToShow(AxisDirection direction) { 49 | setState(() { 50 | monthToPresent = direction == AxisDirection.left 51 | ? monthToPresent + 1 52 | : monthToPresent - 1; 53 | monthToPresent = Utils.correctMonthOverflow(monthToPresent); 54 | }); 55 | } 56 | 57 | void _decideOnNextMonthToShow(DragUpdateDetails details) { 58 | details.delta.dx > 0 59 | ? _calculateNextMonthToShow(AxisDirection.right) 60 | : _calculateNextMonthToShow(AxisDirection.left); 61 | } 62 | 63 | void _onUpdateSuccess() { 64 | Widget alertDialogOkButton = TextButton( 65 | onPressed: () { 66 | Navigator.pop(context); 67 | }, 68 | child: Text(AppLocalizations.of(context)!.ok)); 69 | AlertDialog alertDialog = AlertDialog( 70 | title: 71 | Text(AppLocalizations.of(context)!.updateSuccessfullyInstalledTitle), 72 | content: Text( 73 | AppLocalizations.of(context)!.updateSuccessfullyInstalledDescription), 74 | actions: [alertDialogOkButton], 75 | ); 76 | showDialog( 77 | context: context, 78 | builder: (BuildContext context) { 79 | return alertDialog; 80 | }); 81 | } 82 | 83 | void _onUpdateFailure(String error) { 84 | Widget alertDialogTryAgainButton = TextButton( 85 | onPressed: () { 86 | _updateService.checkForInAppUpdate( 87 | _onUpdateSuccess, _onUpdateFailure, context); 88 | Navigator.pop(context); 89 | }, 90 | child: Text(AppLocalizations.of(context)!.tryAgain)); 91 | Widget alertDialogCancelButton = TextButton( 92 | onPressed: () { 93 | Navigator.pop(context); 94 | }, 95 | child: Text(AppLocalizations.of(context)!.dismiss), 96 | ); 97 | AlertDialog alertDialog = AlertDialog( 98 | title: Text(AppLocalizations.of(context)!.updateFailedToInstallTitle), 99 | content: Text(AppLocalizations.of(context)! 100 | .updateFailedToInstallDescription(error)), 101 | actions: [alertDialogTryAgainButton, alertDialogCancelButton], 102 | ); 103 | showDialog( 104 | context: context, 105 | builder: (BuildContext context) { 106 | return alertDialog; 107 | }); 108 | } 109 | 110 | @override 111 | void initState() { 112 | super.initState(); 113 | versionSpecificService = new VersionSpecificServiceImpl( 114 | storageService: context.read(), 115 | notificationService: notificationService); 116 | monthToPresent = widget.currentMonth; 117 | widget.notificationService.init(context); 118 | 119 | widget.notificationService.addListenerForSelectNotificationStream(this); 120 | _updateService.checkForInAppUpdate( 121 | _onUpdateSuccess, _onUpdateFailure, context); 122 | BlocProvider.of(context) 123 | .add(ContactsPermissionStatusEvent.PermissionUnknown); 124 | BlocProvider.of(context).add(VersionEvent.versionUnknown); 125 | } 126 | 127 | @override 128 | void didUpdateWidget(covariant MainPage oldWidget) { 129 | super.didUpdateWidget(oldWidget); 130 | monthToPresent = widget.currentMonth; 131 | } 132 | 133 | @override 134 | Widget build(BuildContext context) { 135 | return BlocProvider( 136 | create: (context) => ClearNotificationsBloc( 137 | context.read()), 138 | child: BlocBuilder( 139 | builder: (context, state) { 140 | return Scaffold( 141 | appBar: AppBar( 142 | actions: [ 143 | IconButton( 144 | icon: Icon( 145 | Icons.settings, 146 | ), 147 | onPressed: () { 148 | Navigator.push(context, MaterialPageRoute(builder: (_) { 149 | return BlocProvider.value( 150 | value: BlocProvider.of( 151 | context), 152 | child: SettingsScreen( 153 | contactsService: widget.contactsService)); 154 | })).then((result) {}); 155 | }, 156 | ) 157 | ], 158 | ), 159 | body: BlocListener( 160 | listener: (context, state) { 161 | if (state) { 162 | setState(() {}); 163 | } 164 | }, 165 | child: new GestureDetector( 166 | onHorizontalDragUpdate: _decideOnNextMonthToShow, 167 | child: Column( 168 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 169 | crossAxisAlignment: CrossAxisAlignment.center, 170 | children: [ 171 | new Padding( 172 | padding: const EdgeInsets.only(bottom: 50, top: 50), 173 | child: Row( 174 | mainAxisAlignment: MainAxisAlignment.center, 175 | children: [ 176 | new Text( 177 | BirthdayCalendarDateUtils 178 | .convertAndTranslateMonthNumber( 179 | monthToPresent, 180 | AppLocalizations.of(context)!), 181 | style: new TextStyle( 182 | fontSize: 25.0, 183 | fontWeight: FontWeight.bold)) 184 | ], 185 | ), 186 | ), 187 | new Expanded( 188 | child: new Row( 189 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 190 | children: [ 191 | new IconButton( 192 | icon: new Icon(Icons.chevron_left), 193 | onPressed: () { 194 | _calculateNextMonthToShow( 195 | AxisDirection.right); 196 | }), 197 | new Expanded( 198 | child: new CalendarWidget( 199 | key: Key(monthToPresent.toString()), 200 | currentMonth: monthToPresent, 201 | notificationService: 202 | widget.notificationService), 203 | ), 204 | new IconButton( 205 | icon: new Icon(Icons.chevron_right), 206 | onPressed: () { 207 | _calculateNextMonthToShow(AxisDirection.left); 208 | }), 209 | ], 210 | )) 211 | ], 212 | )), 213 | )); 214 | })); 215 | } 216 | 217 | @override 218 | void dispose() { 219 | context.read().dispose(); 220 | widget.notificationService.removeListenerForSelectNotificationStream(this); 221 | super.dispose(); 222 | } 223 | 224 | @override 225 | Future onNotificationSelected(String? payload) async { 226 | if (payload != null) { 227 | UserBirthday? birthday = Utils.getUserBirthdayFromPayload(payload); 228 | if (birthday != null) { 229 | List birthdays = await context 230 | .read() 231 | .getBirthdaysForDate(birthday.birthdayDate, true); 232 | Navigator.push( 233 | context, 234 | MaterialPageRoute( 235 | builder: (context) => BirthdaysForCalendarDayWidget( 236 | key: Key(birthday.birthdayDate.toString()), 237 | dateOfDay: birthday.birthdayDate, 238 | birthdays: birthdays, 239 | notificationService: widget.notificationService), 240 | )); 241 | } 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /lib/service/notification_service/notification_service_impl.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'package:birthday_calendar/service/notification_service/notificationCallbacks.dart'; 4 | import 'package:birthday_calendar/service/permission_service/permissions_service.dart'; 5 | import 'package:birthday_calendar/service/storage_service/storage_service.dart'; 6 | import 'package:birthday_calendar/utils.dart'; 7 | import 'package:flutter/cupertino.dart'; 8 | import 'package:permission_handler/permission_handler.dart'; 9 | 10 | import 'notification_service.dart'; 11 | import 'package:flutter_local_notifications/flutter_local_notifications.dart'; 12 | import 'package:timezone/data/latest.dart' as tz; 13 | import 'package:timezone/timezone.dart' as tz; 14 | import 'package:birthday_calendar/constants.dart'; 15 | import 'package:birthday_calendar/model/user_birthday.dart'; 16 | import 'package:birthday_calendar/l10n/app_localizations.dart'; 17 | 18 | const String channel_id = "123"; 19 | const String channel_name = "birthday_notification"; 20 | const String navigationActionId = 'id_1'; 21 | 22 | class NotificationServiceImpl extends NotificationService { 23 | NotificationServiceImpl({ 24 | required this.permissionsService, 25 | required this.storageService, 26 | }); 27 | 28 | StreamSubscription? _selectSubscription; 29 | 30 | final PermissionsService permissionsService; 31 | final StorageService storageService; 32 | 33 | final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = 34 | FlutterLocalNotificationsPlugin(); 35 | final StreamController selectNotificationStream = 36 | StreamController.broadcast(); 37 | List selectNotificationStreamListeners = []; 38 | 39 | Future init(BuildContext context) async { 40 | tz.initializeTimeZones(); 41 | 42 | final AndroidInitializationSettings initializationSettingsAndroid = 43 | AndroidInitializationSettings('app_icon'); 44 | 45 | final InitializationSettings initializationSettings = 46 | InitializationSettings(android: initializationSettingsAndroid); 47 | 48 | _initializeLocalNotificationsPlugin(initializationSettings, context); 49 | 50 | AndroidFlutterLocalNotificationsPlugin? 51 | androidFlutterLocalNotificationsPlugin = 52 | flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation< 53 | AndroidFlutterLocalNotificationsPlugin>(); 54 | 55 | final channel = AndroidNotificationChannel( 56 | channel_id, 57 | channel_name, 58 | description: 'To remind you about upcoming birthdays', 59 | importance: Importance.max, 60 | ); 61 | await androidFlutterLocalNotificationsPlugin 62 | ?.createNotificationChannel(channel); 63 | 64 | bool permissionGranted = await isNotificationPermissionGranted(context); 65 | if (permissionGranted) { 66 | await _setupSubscription(context); 67 | } 68 | } 69 | 70 | Future isNotificationPermissionGranted(BuildContext context) async { 71 | PermissionStatus permissionStatus = await permissionsService 72 | .getPermissionStatus(notificationsPermissionKey); 73 | 74 | if (permissionStatus.isGranted) { 75 | await storageService 76 | .setNotificationPermissionState(NotificationPermissionState.granted); 77 | return true; 78 | } 79 | 80 | if (permissionStatus.isPermanentlyDenied) { 81 | await storageService.setNotificationPermissionState( 82 | NotificationPermissionState.deniedPermanently); 83 | return false; 84 | } 85 | 86 | await storageService.setNotificationPermissionState( 87 | NotificationPermissionState.deniedTemporary); 88 | return false; 89 | } 90 | 91 | Future requestNotificationPermission( 92 | BuildContext context) async { 93 | PermissionStatus notificationPermissionStatus = await permissionsService 94 | .getPermissionStatus(notificationsPermissionKey); 95 | 96 | if (notificationPermissionStatus.isGranted) { 97 | await storageService 98 | .setNotificationPermissionState(NotificationPermissionState.granted); 99 | return PermissionStatus.granted; 100 | } 101 | 102 | notificationPermissionStatus = await permissionsService 103 | .requestPermissionAndGetStatus(notificationsPermissionKey); 104 | 105 | if (notificationPermissionStatus.isGranted) { 106 | await storageService 107 | .setNotificationPermissionState(NotificationPermissionState.granted); 108 | await _setupSubscription(context); 109 | } else if (notificationPermissionStatus.isPermanentlyDenied) { 110 | await storageService.setNotificationPermissionState( 111 | NotificationPermissionState.deniedPermanently); 112 | } else { 113 | await storageService.setNotificationPermissionState( 114 | NotificationPermissionState.deniedTemporary); 115 | } 116 | 117 | return notificationPermissionStatus; 118 | } 119 | 120 | void _initializeLocalNotificationsPlugin( 121 | InitializationSettings initializationSettings, 122 | BuildContext context) async { 123 | await flutterLocalNotificationsPlugin.initialize(initializationSettings, 124 | onDidReceiveNotificationResponse: 125 | (NotificationResponse notificationResponse) { 126 | switch (notificationResponse.notificationResponseType) { 127 | case NotificationResponseType.selectedNotification: 128 | selectNotificationStream.add(notificationResponse.payload); 129 | break; 130 | case NotificationResponseType.selectedNotificationAction: 131 | if (notificationResponse.actionId == navigationActionId) { 132 | selectNotificationStream.add(notificationResponse.payload); 133 | } 134 | break; 135 | } 136 | }); 137 | _handleApplicationWasLaunchedFromNotification(context); 138 | } 139 | 140 | void _showNotification( 141 | UserBirthday userBirthday, String notificationMessage) async { 142 | await flutterLocalNotificationsPlugin.show( 143 | userBirthday.hashCode, 144 | applicationName, 145 | notificationMessage, 146 | NotificationDetails(android: _createAndroidNotificationDetails()), 147 | payload: jsonEncode(userBirthday)); 148 | } 149 | 150 | void scheduleNotificationForBirthday( 151 | UserBirthday userBirthday, String notificationMessage) async { 152 | DateTime now = DateTime.now(); 153 | DateTime birthdayDate = userBirthday.birthdayDate; 154 | DateTime correctedBirthdayDate = birthdayDate; 155 | 156 | if (birthdayDate.year < now.year) { 157 | correctedBirthdayDate = 158 | new DateTime(now.year, birthdayDate.month, birthdayDate.day); 159 | } 160 | 161 | Duration difference = now.isAfter(correctedBirthdayDate) 162 | ? now.difference(correctedBirthdayDate) 163 | : correctedBirthdayDate.difference(now); 164 | 165 | bool didApplicationLaunchFromNotification = 166 | await _wasApplicationLaunchedFromNotification(); 167 | if (didApplicationLaunchFromNotification && difference.inDays == 0) { 168 | _scheduleNotificationForNextYear(userBirthday, notificationMessage); 169 | return; 170 | } else if (!didApplicationLaunchFromNotification && 171 | difference.inDays == 0) { 172 | _showNotification(userBirthday, notificationMessage); 173 | return; 174 | } 175 | 176 | await flutterLocalNotificationsPlugin.zonedSchedule( 177 | userBirthday.hashCode, 178 | applicationName, 179 | notificationMessage, 180 | tz.TZDateTime.now(tz.local).add(difference), 181 | NotificationDetails(android: _createAndroidNotificationDetails()), 182 | payload: jsonEncode(userBirthday), 183 | androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle); 184 | } 185 | 186 | void _scheduleNotificationForNextYear( 187 | UserBirthday userBirthday, String notificationMessage) async { 188 | await flutterLocalNotificationsPlugin.zonedSchedule( 189 | userBirthday.hashCode, 190 | applicationName, 191 | notificationMessage, 192 | tz.TZDateTime.now(tz.local).add(new Duration(days: 365)), 193 | NotificationDetails(android: _createAndroidNotificationDetails()), 194 | payload: jsonEncode(userBirthday), 195 | androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle); 196 | } 197 | 198 | void cancelNotificationForBirthday(UserBirthday birthday) async { 199 | await flutterLocalNotificationsPlugin.cancel(birthday.hashCode); 200 | } 201 | 202 | void cancelAllNotifications() async { 203 | await flutterLocalNotificationsPlugin.cancelAll(); 204 | } 205 | 206 | void _handleApplicationWasLaunchedFromNotification( 207 | BuildContext context) async { 208 | final NotificationAppLaunchDetails? notificationAppLaunchDetails = 209 | await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); 210 | if (notificationAppLaunchDetails != null && 211 | notificationAppLaunchDetails.didNotificationLaunchApp) { 212 | NotificationResponse? notificationResponse = 213 | notificationAppLaunchDetails.notificationResponse; 214 | if (notificationResponse != null) { 215 | String? payload = notificationResponse.payload; 216 | selectNotificationStream.add(payload); 217 | _rescheduleNotificationFromPayload(payload, context); 218 | } 219 | } 220 | } 221 | 222 | Future _wasApplicationLaunchedFromNotification() async { 223 | NotificationAppLaunchDetails? notificationAppLaunchDetails = 224 | await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); 225 | 226 | if (notificationAppLaunchDetails != null) { 227 | return notificationAppLaunchDetails.didNotificationLaunchApp; 228 | } 229 | 230 | return false; 231 | } 232 | 233 | void _rescheduleNotificationFromPayload( 234 | String? payload, BuildContext context) { 235 | UserBirthday? userBirthday = Utils.getUserBirthdayFromPayload(payload); 236 | if (userBirthday != null) { 237 | cancelNotificationForBirthday(userBirthday); 238 | scheduleNotificationForBirthday( 239 | userBirthday, 240 | AppLocalizations.of(context)! 241 | .notificationForBirthdayMessage(userBirthday.name)); 242 | } 243 | } 244 | 245 | Future> 246 | getAllScheduledNotifications() async { 247 | List pendingNotifications = 248 | await flutterLocalNotificationsPlugin.pendingNotificationRequests(); 249 | return pendingNotifications; 250 | } 251 | 252 | @override 253 | void dispose() { 254 | _selectSubscription?.cancel(); 255 | selectNotificationStream.close(); 256 | selectNotificationStreamListeners.clear(); 257 | } 258 | 259 | @override 260 | void addListenerForSelectNotificationStream(NotificationCallbacks listener) { 261 | selectNotificationStreamListeners.add(listener); 262 | } 263 | 264 | @override 265 | void removeListenerForSelectNotificationStream( 266 | NotificationCallbacks listener) { 267 | if (selectNotificationStreamListeners.contains(listener)) { 268 | selectNotificationStreamListeners.remove(listener); 269 | } 270 | } 271 | 272 | AndroidNotificationDetails _createAndroidNotificationDetails() { 273 | return AndroidNotificationDetails(channel_id, channel_name, 274 | channelDescription: 'To remind you about upcoming birthdays', 275 | importance: Importance.max, 276 | priority: Priority.high, 277 | ticker: "ticker"); 278 | } 279 | 280 | Future _setupSubscription(BuildContext context) async { 281 | await _selectSubscription?.cancel(); 282 | _selectSubscription = selectNotificationStream.stream.listen((payload) { 283 | _rescheduleNotificationFromPayload(payload, context); 284 | for (var listener in selectNotificationStreamListeners) { 285 | listener.onNotificationSelected(payload); 286 | } 287 | }); 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /lib/l10n/app_localizations.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/widgets.dart'; 5 | import 'package:flutter_localizations/flutter_localizations.dart'; 6 | import 'package:intl/intl.dart' as intl; 7 | 8 | import 'app_localizations_de.dart'; 9 | import 'app_localizations_en.dart'; 10 | import 'app_localizations_hi.dart'; 11 | 12 | // ignore_for_file: type=lint 13 | 14 | /// Callers can lookup localized strings with an instance of AppLocalizations 15 | /// returned by `AppLocalizations.of(context)`. 16 | /// 17 | /// Applications need to include `AppLocalizations.delegate()` in their app's 18 | /// `localizationDelegates` list, and the locales they support in the app's 19 | /// `supportedLocales` list. For example: 20 | /// 21 | /// ```dart 22 | /// import 'l10n/app_localizations.dart'; 23 | /// 24 | /// return MaterialApp( 25 | /// localizationsDelegates: AppLocalizations.localizationsDelegates, 26 | /// supportedLocales: AppLocalizations.supportedLocales, 27 | /// home: MyApplicationHome(), 28 | /// ); 29 | /// ``` 30 | /// 31 | /// ## Update pubspec.yaml 32 | /// 33 | /// Please make sure to update your pubspec.yaml to include the following 34 | /// packages: 35 | /// 36 | /// ```yaml 37 | /// dependencies: 38 | /// # Internationalization support. 39 | /// flutter_localizations: 40 | /// sdk: flutter 41 | /// intl: any # Use the pinned version from flutter_localizations 42 | /// 43 | /// # Rest of dependencies 44 | /// ``` 45 | /// 46 | /// ## iOS Applications 47 | /// 48 | /// iOS applications define key application metadata, including supported 49 | /// locales, in an Info.plist file that is built into the application bundle. 50 | /// To configure the locales supported by your app, you’ll need to edit this 51 | /// file. 52 | /// 53 | /// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. 54 | /// Then, in the Project Navigator, open the Info.plist file under the Runner 55 | /// project’s Runner folder. 56 | /// 57 | /// Next, select the Information Property List item, select Add Item from the 58 | /// Editor menu, then select Localizations from the pop-up menu. 59 | /// 60 | /// Select and expand the newly-created Localizations item then, for each 61 | /// locale your application supports, add a new item and select the locale 62 | /// you wish to add from the pop-up menu in the Value field. This list should 63 | /// be consistent with the languages listed in the AppLocalizations.supportedLocales 64 | /// property. 65 | abstract class AppLocalizations { 66 | AppLocalizations(String locale) 67 | : localeName = intl.Intl.canonicalizedLocale(locale.toString()); 68 | 69 | final String localeName; 70 | 71 | static AppLocalizations? of(BuildContext context) { 72 | return Localizations.of(context, AppLocalizations); 73 | } 74 | 75 | static const LocalizationsDelegate delegate = 76 | _AppLocalizationsDelegate(); 77 | 78 | /// A list of this localizations delegate along with the default localizations 79 | /// delegates. 80 | /// 81 | /// Returns a list of localizations delegates containing this delegate along with 82 | /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, 83 | /// and GlobalWidgetsLocalizations.delegate. 84 | /// 85 | /// Additional delegates can be added by appending to this list in 86 | /// MaterialApp. This list does not have to be used at all if a custom list 87 | /// of delegates is preferred or required. 88 | static const List> localizationsDelegates = 89 | >[ 90 | delegate, 91 | GlobalMaterialLocalizations.delegate, 92 | GlobalCupertinoLocalizations.delegate, 93 | GlobalWidgetsLocalizations.delegate, 94 | ]; 95 | 96 | /// A list of this localizations delegate's supported locales. 97 | static const List supportedLocales = [ 98 | Locale('de'), 99 | Locale('en'), 100 | Locale('hi') 101 | ]; 102 | 103 | /// No description provided for @appTitle. 104 | /// 105 | /// In en, this message translates to: 106 | /// **'Birthday Calendar'** 107 | String get appTitle; 108 | 109 | /// No description provided for @settings. 110 | /// 111 | /// In en, this message translates to: 112 | /// **'Settings'** 113 | String get settings; 114 | 115 | /// No description provided for @addBirthday. 116 | /// 117 | /// In en, this message translates to: 118 | /// **'Add Birthday'** 119 | String get addBirthday; 120 | 121 | /// No description provided for @contactsImportedSuccessfully. 122 | /// 123 | /// In en, this message translates to: 124 | /// **'Contacts Imported Successfully'** 125 | String get contactsImportedSuccessfully; 126 | 127 | /// No description provided for @noContactsFoundMsg. 128 | /// 129 | /// In en, this message translates to: 130 | /// **'There are no contacts on your device'** 131 | String get noContactsFoundMsg; 132 | 133 | /// No description provided for @alreadyAddedContactsMsg. 134 | /// 135 | /// In en, this message translates to: 136 | /// **'All of your current contacts have already been added'** 137 | String get alreadyAddedContactsMsg; 138 | 139 | /// No description provided for @unableToMakeCallMsg. 140 | /// 141 | /// In en, this message translates to: 142 | /// **'We are unable to make the call'** 143 | String get unableToMakeCallMsg; 144 | 145 | /// No description provided for @january. 146 | /// 147 | /// In en, this message translates to: 148 | /// **'January'** 149 | String get january; 150 | 151 | /// No description provided for @february. 152 | /// 153 | /// In en, this message translates to: 154 | /// **'February'** 155 | String get february; 156 | 157 | /// No description provided for @march. 158 | /// 159 | /// In en, this message translates to: 160 | /// **'March'** 161 | String get march; 162 | 163 | /// No description provided for @april. 164 | /// 165 | /// In en, this message translates to: 166 | /// **'April'** 167 | String get april; 168 | 169 | /// No description provided for @may. 170 | /// 171 | /// In en, this message translates to: 172 | /// **'May'** 173 | String get may; 174 | 175 | /// No description provided for @june. 176 | /// 177 | /// In en, this message translates to: 178 | /// **'June'** 179 | String get june; 180 | 181 | /// No description provided for @july. 182 | /// 183 | /// In en, this message translates to: 184 | /// **'July'** 185 | String get july; 186 | 187 | /// No description provided for @august. 188 | /// 189 | /// In en, this message translates to: 190 | /// **'August'** 191 | String get august; 192 | 193 | /// No description provided for @september. 194 | /// 195 | /// In en, this message translates to: 196 | /// **'September'** 197 | String get september; 198 | 199 | /// No description provided for @october. 200 | /// 201 | /// In en, this message translates to: 202 | /// **'October'** 203 | String get october; 204 | 205 | /// No description provided for @november. 206 | /// 207 | /// In en, this message translates to: 208 | /// **'November'** 209 | String get november; 210 | 211 | /// No description provided for @december. 212 | /// 213 | /// In en, this message translates to: 214 | /// **'December'** 215 | String get december; 216 | 217 | /// No description provided for @ok. 218 | /// 219 | /// In en, this message translates to: 220 | /// **'Ok'** 221 | String get ok; 222 | 223 | /// No description provided for @updateSuccessfullyInstalledTitle. 224 | /// 225 | /// In en, this message translates to: 226 | /// **'Update Successfully Installed'** 227 | String get updateSuccessfullyInstalledTitle; 228 | 229 | /// No description provided for @updateSuccessfullyInstalledDescription. 230 | /// 231 | /// In en, this message translates to: 232 | /// **'Birthday Calendar has been updated successfully! 🎂'** 233 | String get updateSuccessfullyInstalledDescription; 234 | 235 | /// No description provided for @tryAgain. 236 | /// 237 | /// In en, this message translates to: 238 | /// **'Try Again?'** 239 | String get tryAgain; 240 | 241 | /// No description provided for @dismiss. 242 | /// 243 | /// In en, this message translates to: 244 | /// **'Dismiss'** 245 | String get dismiss; 246 | 247 | /// No description provided for @updateFailedToInstallTitle. 248 | /// 249 | /// In en, this message translates to: 250 | /// **'Update Failed To Install ❌'** 251 | String get updateFailedToInstallTitle; 252 | 253 | /// No description provided for @updateFailedToInstallDescription. 254 | /// 255 | /// In en, this message translates to: 256 | /// **'Birthday Calendar has failed to update because: {error}'** 257 | String updateFailedToInstallDescription(Object error); 258 | 259 | /// No description provided for @userDeniedUpdate. 260 | /// 261 | /// In en, this message translates to: 262 | /// **'User denied update'** 263 | String get userDeniedUpdate; 264 | 265 | /// No description provided for @appUpdateFailed. 266 | /// 267 | /// In en, this message translates to: 268 | /// **'App Update Failed'** 269 | String get appUpdateFailed; 270 | 271 | /// No description provided for @darkMode. 272 | /// 273 | /// In en, this message translates to: 274 | /// **'Dark Mode'** 275 | String get darkMode; 276 | 277 | /// No description provided for @importContacts. 278 | /// 279 | /// In en, this message translates to: 280 | /// **'Import Contacts'** 281 | String get importContacts; 282 | 283 | /// No description provided for @clearNotifications. 284 | /// 285 | /// In en, this message translates to: 286 | /// **'Clear Notifications'** 287 | String get clearNotifications; 288 | 289 | /// No description provided for @clearNotificationsAlertTitle. 290 | /// 291 | /// In en, this message translates to: 292 | /// **'Are You Sure?'** 293 | String get clearNotificationsAlertTitle; 294 | 295 | /// No description provided for @clearNotificationsAlertDescription. 296 | /// 297 | /// In en, this message translates to: 298 | /// **'Do you want to remove all notifications?'** 299 | String get clearNotificationsAlertDescription; 300 | 301 | /// No description provided for @no. 302 | /// 303 | /// In en, this message translates to: 304 | /// **'No'** 305 | String get no; 306 | 307 | /// No description provided for @yes. 308 | /// 309 | /// In en, this message translates to: 310 | /// **'Yes'** 311 | String get yes; 312 | 313 | /// No description provided for @addPhoneNumber. 314 | /// 315 | /// In en, this message translates to: 316 | /// **'Add Phone Number'** 317 | String get addPhoneNumber; 318 | 319 | /// No description provided for @add. 320 | /// 321 | /// In en, this message translates to: 322 | /// **'Add'** 323 | String get add; 324 | 325 | /// No description provided for @cancel. 326 | /// 327 | /// In en, this message translates to: 328 | /// **'Cancel'** 329 | String get cancel; 330 | 331 | /// No description provided for @back. 332 | /// 333 | /// In en, this message translates to: 334 | /// **'Back'** 335 | String get back; 336 | 337 | /// No description provided for @proceed. 338 | /// 339 | /// In en, this message translates to: 340 | /// **'Proceed'** 341 | String get proceed; 342 | 343 | /// No description provided for @hintTextForNameInputField. 344 | /// 345 | /// In en, this message translates to: 346 | /// **'Name?'** 347 | String get hintTextForNameInputField; 348 | 349 | /// No description provided for @notValidName. 350 | /// 351 | /// In en, this message translates to: 352 | /// **'Please enter a valid name'** 353 | String get notValidName; 354 | 355 | /// No description provided for @nameAlreadyExists. 356 | /// 357 | /// In en, this message translates to: 358 | /// **'A birthday with this name already exists'** 359 | String get nameAlreadyExists; 360 | 361 | /// No description provided for @birthdaysForDayAndMonth. 362 | /// 363 | /// In en, this message translates to: 364 | /// **'Birthdays for {day} {month}'** 365 | String birthdaysForDayAndMonth(Object day, Object month); 366 | 367 | /// No description provided for @helpTextChooseBirthdateForImportedContact. 368 | /// 369 | /// In en, this message translates to: 370 | /// **'Choose birth date for {contactName}'** 371 | String helpTextChooseBirthdateForImportedContact(Object contactName); 372 | 373 | /// No description provided for @fieldLabelTextChooseBirthdateForImportedContact. 374 | /// 375 | /// In en, this message translates to: 376 | /// **'{contactName}\'s birth date'** 377 | String fieldLabelTextChooseBirthdateForImportedContact(Object contactName); 378 | 379 | /// No description provided for @notificationForBirthdayMessage. 380 | /// 381 | /// In en, this message translates to: 382 | /// **'{contactName} has an upcoming birthday!'** 383 | String notificationForBirthdayMessage(Object contactName); 384 | 385 | /// No description provided for @addBirthdaysToContactsAlertDialogTitle. 386 | /// 387 | /// In en, this message translates to: 388 | /// **'Add Birthdays To Contacts'** 389 | String get addBirthdaysToContactsAlertDialogTitle; 390 | 391 | /// No description provided for @addBirthdaysToContactsAlertDialogDescription. 392 | /// 393 | /// In en, this message translates to: 394 | /// **'Would you like to add birth dates for your imported contacts?'** 395 | String get addBirthdaysToContactsAlertDialogDescription; 396 | 397 | /// No description provided for @peopleWithoutBirthdaysAlertDialogTitle. 398 | /// 399 | /// In en, this message translates to: 400 | /// **'People Without Birthdays'** 401 | String get peopleWithoutBirthdaysAlertDialogTitle; 402 | 403 | /// No description provided for @notificationPermissionDenied. 404 | /// 405 | /// In en, this message translates to: 406 | /// **'In order to get notifications for birthdays, you will need to authorize BirthdayCalendar to send you notifications'** 407 | String get notificationPermissionDenied; 408 | 409 | /// No description provided for @notificationPermissionPermanentlyDenied. 410 | /// 411 | /// In en, this message translates to: 412 | /// **'You will need to turn on the notification permission in the application\'s settings in order to schedule notifications'** 413 | String get notificationPermissionPermanentlyDenied; 414 | 415 | /// No description provided for @openSettings. 416 | /// 417 | /// In en, this message translates to: 418 | /// **'Open Settings'** 419 | String get openSettings; 420 | } 421 | 422 | class _AppLocalizationsDelegate 423 | extends LocalizationsDelegate { 424 | const _AppLocalizationsDelegate(); 425 | 426 | @override 427 | Future load(Locale locale) { 428 | return SynchronousFuture(lookupAppLocalizations(locale)); 429 | } 430 | 431 | @override 432 | bool isSupported(Locale locale) => 433 | ['de', 'en', 'hi'].contains(locale.languageCode); 434 | 435 | @override 436 | bool shouldReload(_AppLocalizationsDelegate old) => false; 437 | } 438 | 439 | AppLocalizations lookupAppLocalizations(Locale locale) { 440 | // Lookup logic when only language code is specified. 441 | switch (locale.languageCode) { 442 | case 'de': 443 | return AppLocalizationsDe(); 444 | case 'en': 445 | return AppLocalizationsEn(); 446 | case 'hi': 447 | return AppLocalizationsHi(); 448 | } 449 | 450 | throw FlutterError( 451 | 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' 452 | 'an issue with the localizations generation tool. Please file an issue ' 453 | 'on GitHub with a reproducible sample app and the gen-l10n configuration ' 454 | 'that was used.'); 455 | } 456 | --------------------------------------------------------------------------------