fix: resolve several issues

This commit is contained in:
Niels Gorter 2024-05-29 11:58:32 +02:00
parent 8aa34cfa41
commit a46a1d92d9
31 changed files with 589 additions and 536 deletions

17
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,17 @@
version: 2
updates:
- package-ecosystem: "pub"
directory: "/packages/flutter_notification_center"
schedule:
interval: "weekly"
- package-ecosystem: "pub"
directory: "/packages/flutter_notification_center_firebase"
schedule:
interval: "weekly"
- package-ecosystem: "pub"
directory: "/packages/flutter_notification_center/example"
schedule:
interval: "weekly"

View file

@ -0,0 +1,14 @@
name: Iconica Standard Component Documentation Workflow
# Workflow Caller version: 1.0.0
on:
release:
types: [published]
workflow_dispatch:
jobs:
call-iconica-component-documentation-workflow:
uses: Iconica-Development/.github/.github/workflows/component-documentation.yml@master
secrets: inherit
permissions: write-all

14
.github/workflows/melos-ci.yml vendored Normal file
View file

@ -0,0 +1,14 @@
name: Iconica Standard Melos CI Workflow
# Workflow Caller version: 1.0.0
on:
pull_request:
workflow_dispatch:
jobs:
call-global-iconica-workflow:
uses: Iconica-Development/.github/.github/workflows/melos-ci.yml@master
secrets: inherit
permissions: write-all
with:
subfolder: '.' # add optional subfolder to run workflow in

View file

@ -1,3 +1,14 @@
## [1.4.0] - 29 May 2024
* Make notifications user bound and not app bound (https://github.com/Iconica-Development/flutter_notification_center/issues/8)
* Fix indicator amount of bell icon (https://github.com/Iconica-Development/flutter_notification_center/issues/10)
* Add unpin functionality (https://github.com/Iconica-Development/flutter_notification_center/issues/15)
* Change color of bin icon (https://github.com/Iconica-Development/flutter_notification_center/issues/19)
* Fix multiple dialog and snackbars stacking on top
* Fix notification center updating when new notifications come in and the screen is open
* Fix sorting of all notifications
* Fix pinned notifications should remain on top
## [1.3.1] - 30 April 2024
* Fix Animationcontroller not disposing correctly.

9
LICENSE Normal file
View file

@ -0,0 +1,9 @@
Copyright (c) 2024 Iconica, All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -1,9 +0,0 @@
include: package:flutter_iconica_analysis/components_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

39
melos.yaml Normal file
View file

@ -0,0 +1,39 @@
name: flutter_notification_center
packages:
- packages/**
command:
version:
branch: master
scripts:
lint:all:
run: dart run melos run analyze && dart run melos run format-check
description: Run all static analysis checks.
get:
run: |
melos exec -c 1 -- "flutter pub get"
melos exec --scope="*example*" -c 1 -- "flutter pub get"
upgrade:
run: melos exec -c 1 -- "flutter pub upgrade"
create:
# run create in the example folder of flutter_chat_view
run: melos exec --scope="*example*" -c 1 -- "flutter create ."
analyze:
run: |
dart run melos exec -c 1 -- \
flutter analyze --fatal-infos
description: Run `flutter analyze` for all packages.
format:
run: dart run melos exec dart format .
description: Run `dart format` for all packages.
format-check:
run: dart run melos exec dart format . --set-exit-if-changed
description: Run `dart format` checks for all packages.

View file

@ -49,4 +49,4 @@ app.*.map.json
lib/config/
pubspec.lock
dotenv
firebase_options.dart

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_notification_center/flutter_notification_center.dart';
import 'package:flutter_notification_center_firebase/flutter_notification_center_firebase.dart';
@ -22,7 +24,35 @@ class CustomNotificationWidget extends StatelessWidget {
Widget build(BuildContext context) {
return notification.isPinned
//Pinned notification
? GestureDetector(
? Dismissible(
key: Key('${notification.id}_pinned'),
onDismissed: (direction) async {
await unPinNotification(notificationService, notification,
notificationTranslations, context);
},
background: Container(
color: const Color.fromRGBO(59, 213, 111, 1),
alignment: Alignment.centerLeft,
child: const Padding(
padding: EdgeInsets.only(left: 16.0),
child: Icon(
Icons.push_pin,
color: Colors.white,
),
),
),
secondaryBackground: Container(
color: const Color.fromRGBO(59, 213, 111, 1),
alignment: Alignment.centerLeft,
child: const Padding(
padding: EdgeInsets.only(left: 16.0),
child: Icon(
Icons.push_pin,
color: Colors.white,
),
),
),
child: GestureDetector(
onTap: () async =>
_navigateToNotificationDetail(context, notification),
child: ListTile(
@ -51,16 +81,18 @@ class CustomNotificationWidget extends StatelessWidget {
padding: const EdgeInsets.only(left: 60.0),
),
),
),
)
//Dismissable notification
: Dismissible(
key: Key(notification.id),
onDismissed: (direction) async {
if (direction == DismissDirection.endToStart) {
await dismissNotification(notificationService, notification);
await dismissNotification(notificationService, notification,
notificationTranslations, context);
} else if (direction == DismissDirection.startToEnd) {
await pinNotification(
notificationService, notification, context);
await pinNotification(notificationService, notification,
notificationTranslations, context);
}
},
background: Container(
@ -81,7 +113,7 @@ class CustomNotificationWidget extends StatelessWidget {
padding: EdgeInsets.only(right: 16.0),
child: Icon(
Icons.delete,
color: Colors.white,
color: Colors.black,
),
),
),
@ -130,7 +162,7 @@ class CustomNotificationWidget extends StatelessWidget {
BuildContext context,
NotificationModel notification,
) async {
await markNotificationAsRead(notificationService, notification);
unawaited(markNotificationAsRead(notificationService, notification));
if (context.mounted) {
await Navigator.push(
context,
@ -144,25 +176,4 @@ class CustomNotificationWidget extends StatelessWidget {
);
}
}
Future<void> dismissNotification(
FirebaseNotificationService notificationService,
NotificationModel notification,
) async {
await notificationService.dismissActiveNotification(notification);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Notification dismissed"),
),
);
}
}
Future<void> markNotificationAsRead(
FirebaseNotificationService notificationService,
NotificationModel notification,
) async {
await notificationService.markNotificationAsRead(notification);
}
}

View file

@ -1,8 +1,9 @@
import 'package:example/custom_notification.dart';
// import 'package:firebase_auth/firebase_auth.dart';
// import 'package:example/firebase_options.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:firebase_core/firebase_core.dart';
// import 'package:firebase_core/firebase_core.dart';
import 'package:flutter_notification_center_firebase/flutter_notification_center_firebase.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:flutter_notification_center/flutter_notification_center.dart';
@ -21,14 +22,9 @@ void main() async {
}
Future<void> _configureApp() async {
try {
await dotenv.load(fileName: 'dotenv');
} catch (e) {
debugPrint('Failed to load dotenv file: $e');
}
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// await Firebase.initializeApp(
// options: DefaultFirebaseOptions.currentPlatform,
// );
await SystemChrome.setPreferredOrientations(
[
DeviceOrientation.portraitDown,
@ -55,14 +51,15 @@ class _NotificationCenterDemoState extends State<NotificationCenterDemo> {
@override
void initState() {
super.initState();
var service =
FirebaseNotificationService(newNotificationCallback: (notification) {
var service = FirebaseNotificationService(
newNotificationCallback: (notification) {
popupHandler.handleNotificationPopup(notification);
});
},
);
config = NotificationConfig(
service: service,
enableNotificationPopups: true,
showAsSnackBar: false,
showAsSnackBar: true,
notificationWidgetBuilder: (notification, context) =>
CustomNotificationWidget(
notification: notification,
@ -82,7 +79,7 @@ class _NotificationCenterDemoState extends State<NotificationCenterDemo> {
showNotificationIcon: true,
),
notificationService: service,
notificationTranslations: const NotificationTranslations(),
notificationTranslations: const NotificationTranslations.empty(),
context: context,
),
seperateNotificationsWithDivider: true,

View file

@ -14,24 +14,17 @@ dependencies:
sdk: flutter
intl: ^0.17.0
flutter_animated_widgets:
git:
url: https://github.com/Iconica-Development/flutter_animated_widgets
ref: 0.0.1
cloud_firestore: ^4.16.1
flutter_dotenv: ^5.0.2
firebase_auth: ^4.2.6
firebase_core: ^2.5.0
firebase_storage: ^11.0.14
flutter_notification_center:
git:
url: https://github.com/Iconica-Development/flutter_notification_center
ref: 1.3.0
ref: 1.4.0
path: packages/flutter_notification_center
flutter_notification_center_firebase:
path: ../../flutter_notification_center_firebase
git:
url: https://github.com/Iconica-Development/flutter_notification_center
ref: 1.4.0
path: packages/flutter_notification_center_firebase
@ -46,6 +39,4 @@ dev_dependencies:
# The following section is specific to Flutter packages.
flutter:
uses-material-design: true
assets:
- dotenv

View file

@ -13,7 +13,7 @@ class NotificationConfig {
const NotificationConfig({
required this.service,
this.seperateNotificationsWithDivider = true,
this.translations = const NotificationTranslations(),
this.translations = const NotificationTranslations.empty(),
this.notificationWidgetBuilder,
this.showAsSnackBar = true,
this.enableNotificationPopups = true,

View file

@ -1,17 +1,28 @@
/// Defines translations for notification messages.
class NotificationTranslations {
/// Creates a new [NotificationTranslations] instance.
///
/// The [appBarTitle] parameter specifies the title to be displayed in the
/// app bar of the notification center. The default value
/// is 'Notification Center'.
///
/// The [noNotifications] parameter specifies the message to be displayed when
/// there are no unread notifications available. The default value is
/// 'No unread notifications available.'.
const NotificationTranslations({
required this.appBarTitle,
required this.noNotifications,
required this.notificationDismissed,
required this.notificationPinned,
required this.notificationUnpinned,
required this.errorMessage,
required this.datePrefix,
required this.notAvailable,
required this.dissmissDialog,
});
const NotificationTranslations.empty({
this.appBarTitle = "Notification Center",
this.noNotifications = "No unread notifications available.",
this.notificationDismissed = "Notification dismissed.",
this.notificationPinned = "Notification pinned.",
this.notificationUnpinned = "Notification unpinned.",
this.errorMessage = "An error occurred. Please try again later.",
this.datePrefix = "Date:",
this.notAvailable = "N/A",
this.dissmissDialog = "Dismiss",
});
/// The title to be displayed in the app bar of the notification center.
@ -20,4 +31,50 @@ class NotificationTranslations {
/// The message to be displayed when there are no unread
/// notifications available.
final String noNotifications;
/// The message to be displayed when a notification is dismissed.
final String notificationDismissed;
/// The message to be displayed when a notification is pinned.
final String notificationPinned;
/// The message to be displayed when a notification is unpinned.
final String notificationUnpinned;
/// The message to be displayed when an error occurs.
final String errorMessage;
/// The message to be displayed before the date of a notification.
final String datePrefix;
/// The message to be displayed when parsing of the date fails
final String notAvailable;
/// The message to be displayed on the dismiss dialog / snackbar
final String dissmissDialog;
NotificationTranslations copyWith({
String? appBarTitle,
String? noNotifications,
String? notificationDismissed,
String? notificationPinned,
String? notificationUnpinned,
String? errorMessage,
String? datePrefix,
String? notAvailable,
String? dissmissDialog,
}) {
return NotificationTranslations(
appBarTitle: appBarTitle ?? this.appBarTitle,
noNotifications: noNotifications ?? this.noNotifications,
notificationDismissed:
notificationDismissed ?? this.notificationDismissed,
notificationPinned: notificationPinned ?? this.notificationPinned,
notificationUnpinned: notificationUnpinned ?? this.notificationUnpinned,
errorMessage: errorMessage ?? this.errorMessage,
datePrefix: datePrefix ?? this.datePrefix,
notAvailable: notAvailable ?? this.notAvailable,
dissmissDialog: dissmissDialog ?? this.dissmissDialog,
);
}
}

View file

@ -37,13 +37,9 @@ class _NotificationBellState extends State<NotificationBell> {
@override
void initState() {
super.initState();
// Fetch active notifications and update the notification count
WidgetsBinding.instance.addPostFrameCallback((_) async {
var amount = await widget.config.service.getActiveNotifications();
widget.config.service.getActiveAmountStream().listen((amount) {
setState(() {
notificationAmount = amount.length;
notificationAmount = amount;
});
});
}

View file

@ -27,6 +27,9 @@ class NotificationCenterState extends State<NotificationCenter> {
super.initState();
// ignore: discarded_futures
_notificationsFuture = widget.config.service.getActiveNotifications();
widget.config.service.getActiveAmountStream().listen((amount) {
_notificationsFuture = widget.config.service.getActiveNotifications();
});
widget.config.service.addListener(_listener);
}
@ -60,10 +63,12 @@ class NotificationCenterState extends State<NotificationCenter> {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text("Error: ${snapshot.error}"));
debugPrint("Error: ${snapshot.error}");
return Center(
child: Text(widget.config.translations.errorMessage));
} else if (snapshot.data == null || snapshot.data!.isEmpty) {
return const Center(
child: Text("No unread notifications available."),
return Center(
child: Text(widget.config.translations.noNotifications),
);
} else {
return ListView.builder(
@ -88,7 +93,40 @@ class NotificationCenterState extends State<NotificationCenter> {
notification, context)
: notification.isPinned
//Pinned notification
? GestureDetector(
? Dismissible(
key: Key('${notification.id}_pinned'),
onDismissed: (direction) async {
await unPinNotification(
widget.config.service,
notification,
widget.config.translations,
context);
},
background: Container(
color:
const Color.fromRGBO(59, 213, 111, 1),
alignment: Alignment.centerLeft,
child: const Padding(
padding: EdgeInsets.only(left: 16.0),
child: Icon(
Icons.push_pin,
color: Colors.white,
),
),
),
secondaryBackground: Container(
color:
const Color.fromRGBO(59, 213, 111, 1),
alignment: Alignment.centerLeft,
child: const Padding(
padding: EdgeInsets.only(left: 16.0),
child: Icon(
Icons.push_pin,
color: Colors.white,
),
),
),
child: GestureDetector(
onTap: () async =>
_navigateToNotificationDetail(
context,
@ -131,6 +169,7 @@ class NotificationCenterState extends State<NotificationCenter> {
const EdgeInsets.only(left: 60.0),
),
),
),
)
//Dismissable notification
: Dismissible(
@ -141,12 +180,14 @@ class NotificationCenterState extends State<NotificationCenter> {
await dismissNotification(
widget.config.service,
notification,
widget.config.translations,
context);
} else if (direction ==
DismissDirection.startToEnd) {
await pinNotification(
widget.config.service,
notification,
widget.config.translations,
context);
}
},
@ -170,7 +211,7 @@ class NotificationCenterState extends State<NotificationCenter> {
padding: EdgeInsets.only(right: 16.0),
child: Icon(
Icons.delete,
color: Colors.white,
color: Colors.black,
),
),
),
@ -251,13 +292,15 @@ Future<void> _navigateToNotificationDetail(
Future<void> dismissNotification(
NotificationService notificationService,
NotificationModel notification,
NotificationTranslations notificationTranslations,
BuildContext context,
) async {
await notificationService.dismissActiveNotification(notification);
if (context.mounted) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Notification dismissed"),
SnackBar(
content: Text(notificationTranslations.notificationDismissed),
),
);
}
@ -266,13 +309,32 @@ Future<void> dismissNotification(
Future<void> pinNotification(
NotificationService notificationService,
NotificationModel notification,
NotificationTranslations notificationTranslations,
BuildContext context,
) async {
await notificationService.pinActiveNotification(notification);
if (context.mounted) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Notification pinned"),
SnackBar(
content: Text(notificationTranslations.notificationPinned),
),
);
}
}
Future<void> unPinNotification(
NotificationService notificationService,
NotificationModel notification,
NotificationTranslations notificationTranslations,
BuildContext context,
) async {
await notificationService.unPinActiveNotification(notification);
if (context.mounted) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(notificationTranslations.notificationUnpinned),
),
);
}

View file

@ -53,7 +53,7 @@ class NotificationDetailPage extends StatelessWidget {
),
const SizedBox(height: 10),
Text(
'Date: ${DateFormat('yyyy-MM-dd HH:mm').format(
'${translations.datePrefix} ${DateFormat('yyyy-MM-dd HH:mm').format(
notification.dateTimePushed ?? DateTime.now(),
)}',
style: const TextStyle(

View file

@ -1,15 +1,18 @@
import 'package:flutter/material.dart';
import 'package:flutter_notification_center/flutter_notification_center.dart';
import 'package:intl/intl.dart';
class NotificationDialog extends StatelessWidget {
final String title;
final String body;
final DateTime? datetimePublished;
final NotificationTranslations translations;
const NotificationDialog({
super.key,
required this.title,
required this.body,
required this.translations,
this.datetimePublished,
});
@ -17,7 +20,7 @@ class NotificationDialog extends StatelessWidget {
Widget build(BuildContext context) {
String formattedDateTime = datetimePublished != null
? DateFormat('dd MMM HH:mm').format(datetimePublished!)
: 'N/A';
: translations.notAvailable;
return AlertDialog(
title: Text(
@ -55,9 +58,9 @@ class NotificationDialog extends StatelessWidget {
onPressed: () {
Navigator.of(context).pop();
},
child: const Text(
'Dismiss',
style: TextStyle(
child: Text(
translations.dissmissDialog,
style: const TextStyle(
color: Colors.red,
),
),

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_notification_center/flutter_notification_center.dart';
import 'package:intl/intl.dart';
class NotificationSnackbar extends SnackBar {
@ -6,6 +7,8 @@ class NotificationSnackbar extends SnackBar {
super.key,
required String title,
required String body,
required NotificationTranslations translations,
required VoidCallback onDismiss,
DateTime? datetimePublished,
}) : super(
content: Column(
@ -31,7 +34,7 @@ class NotificationSnackbar extends SnackBar {
Text(
datetimePublished != null
? DateFormat('dd MMM HH:mm').format(datetimePublished)
: 'N/A',
: translations.notAvailable,
style: const TextStyle(
fontSize: 12.0,
color: Colors.white,
@ -39,10 +42,10 @@ class NotificationSnackbar extends SnackBar {
),
],
),
duration: const Duration(seconds: 8),
duration: const Duration(seconds: 50),
action: SnackBarAction(
label: 'Dismiss',
onPressed: () {},
label: translations.dissmissDialog,
onPressed: onDismiss,
textColor: Colors.white,
),
);

View file

@ -16,17 +16,25 @@ class PopupHandler {
if (!config.enableNotificationPopups) return;
if (config.showAsSnackBar) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
NotificationSnackbar(
title: notification.title,
body: notification.body,
translations: config.translations,
onDismiss: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
},
datetimePublished: DateTime.now(),
),
);
} else {
if (ModalRoute.of(context)?.isCurrent != true) return;
showDialog(
context: context,
builder: (context) => NotificationDialog(
translations: config.translations,
title: notification.title,
body: notification.body,
datetimePublished: notification.dateTimePushed,

View file

@ -47,9 +47,15 @@ abstract class NotificationService with ChangeNotifier {
/// Pin an active notification.
Future pinActiveNotification(NotificationModel notification);
/// Unpin an active notification.
Future unPinActiveNotification(NotificationModel notification);
/// Marks a notification as read.
Future markNotificationAsRead(NotificationModel notification);
/// Checks for scheduled notifications.
Future checkForScheduledNotifications();
/// Returns a stream of the number of active notifications.
Stream<int> getActiveAmountStream();
}

View file

@ -2,7 +2,7 @@ name: flutter_notification_center
description: "A Flutter package for displaying notifications in a notification center."
publish_to: 'none'
version: 1.3.1
version: 1.4.0
environment:
sdk: '>=3.3.2 <4.0.0'

View file

@ -3,7 +3,3 @@
// SPDX-License-Identifier: BSD-3-Clause
export "src/services/firebase_notification_service.dart";
export "src/config/firebase_collections.dart";
export "src/config/environment.dart";
export "src/config/firebase_options.dart";
export "src/config/firebase.dart";

View file

@ -1,25 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
const _errorMessage = 'Unable to fetch dotenv, did you make sure to generate '
'your build environment config?\nUse the command: '
'dart pub run environment_config:generate\n'
'For more information, look at the readme\n'
'Using default now...';
/// This environment config is used for the features inside the package
/// The project that uses this package should have their own environment config
/// the values in the dotenv should atleast include the following:
mixin SharedEnvironmentConfig {}
/// This environment config is used only for the firebase configuration
mixin SharedFirebaseEnvironmentConfig {
static String get firebaseAppName {
var firebaseAppName = dotenv.env['FIREBASE_APP_NAME'];
if (firebaseAppName == null) {
debugPrint(_errorMessage);
throw Exception(_errorMessage);
}
return firebaseAppName;
}
}

View file

@ -1,136 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
const _errorMessage = 'Unable to fetch dotenv, did you make sure to generate '
'your build environment config?\nUse the command: '
'flutter pub run environment_config:generate\n'
'For more information, look at the readme\n'
'Using default now...';
class EnvironmentConfig {
String get firebaseProjectId {
var firebaseProjectId = dotenv.env['FIREBASE_PROJECT_ID'];
if (firebaseProjectId == null) {
debugPrint(_errorMessage);
throw Exception(_errorMessage);
}
return firebaseProjectId;
}
String get firebaseMessageId {
var firebaseMessageId = dotenv.env['FIREBASE_MESSAGE_ID'];
if (firebaseMessageId == null) {
debugPrint(_errorMessage);
throw Exception(_errorMessage);
}
return firebaseMessageId;
}
String get firebaseAuthDomain {
var firebaseAuthDomain = dotenv.env['FIREBASE_AUTH_DOMAIN'];
if (firebaseAuthDomain == null) {
debugPrint(_errorMessage);
throw Exception(_errorMessage);
}
return firebaseAuthDomain;
}
String get firebaseStorageUrl {
var firebaseStorageUrl = dotenv.env['FIREBASE_STORAGE_URL'];
if (firebaseStorageUrl == null) {
debugPrint(_errorMessage);
throw Exception(_errorMessage);
}
return firebaseStorageUrl;
}
String get firebaseDatabaseUrl {
var firebaseDatabaseUrl = dotenv.env['FIREBASE_DATABASE_URL'];
if (firebaseDatabaseUrl == null) {
debugPrint(_errorMessage);
throw Exception(_errorMessage);
}
return firebaseDatabaseUrl;
}
String get firebaseWebApiKey {
var firebaseKey = dotenv.env['FIREBASE_WEB_API_KEY'];
if (firebaseKey == null) {
debugPrint(_errorMessage);
throw Exception(_errorMessage);
}
return firebaseKey;
}
String get firebaseIosApiKey {
var firebaseKey = dotenv.env['FIREBASE_IOS_API_KEY'];
if (firebaseKey == null) {
debugPrint(_errorMessage);
throw Exception(_errorMessage);
}
return firebaseKey;
}
String get firebaseAndroidApiKey {
var firebaseKey = dotenv.env['FIREBASE_ANDROID_API_KEY'];
if (firebaseKey == null) {
debugPrint(_errorMessage);
throw Exception(_errorMessage);
}
return firebaseKey;
}
String get firebaseAppIdAndroid {
var firebaseAppIdAndroid = dotenv.env['FIREBASE_APP_ID_ANDROID'];
if (firebaseAppIdAndroid == null) {
debugPrint(_errorMessage);
throw Exception(_errorMessage);
}
return firebaseAppIdAndroid;
}
String get firebaseAppIdIos {
var firebaseAppIdIos = dotenv.env['FIREBASE_APP_ID_IOS'];
if (firebaseAppIdIos == null) {
debugPrint(_errorMessage);
throw Exception(_errorMessage);
}
return firebaseAppIdIos;
}
String get firebaseAppIdMacos {
var firebaseAppIdMacos = dotenv.env['FIREBASE_APP_ID_MACOS'];
if (firebaseAppIdMacos == null) {
debugPrint(_errorMessage);
throw Exception(_errorMessage);
}
return firebaseAppIdMacos;
}
String get firebaseAppIdWeb {
var firebaseAppIdWeb = dotenv.env['FIREBASE_APP_ID_WEB'];
if (firebaseAppIdWeb == null) {
debugPrint(_errorMessage);
throw Exception(_errorMessage);
}
return firebaseAppIdWeb;
}
String get firebaseClientIdIos {
var firebaseClientIdIos = dotenv.env['FIREBASE_CLIENT_ID_IOS'];
if (firebaseClientIdIos == null) {
debugPrint(_errorMessage);
throw Exception(_errorMessage);
}
return firebaseClientIdIos;
}
String get firebaseClientIdMacos {
var firebaseClientIdMacos = dotenv.env['FIREBASE_CLIENT_ID_MACOS'];
if (firebaseClientIdMacos == null) {
debugPrint(_errorMessage);
throw Exception(_errorMessage);
}
return firebaseClientIdMacos;
}
}

View file

@ -1,22 +0,0 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'environment.dart';
mixin FirebaseInstance {
static FirebaseApp instance() =>
SharedFirebaseEnvironmentConfig.firebaseAppName.isEmpty
? Firebase.app()
: Firebase.app(SharedFirebaseEnvironmentConfig.firebaseAppName);
}
mixin Database {
static FirebaseFirestore ref() => FirebaseFirestore.instanceFor(
app: FirebaseInstance.instance(),
);
}
mixin Storage {
static Reference ref({bool prefixed = true}) =>
FirebaseStorage.instanceFor(app: FirebaseInstance.instance()).ref();
}

View file

@ -1,4 +0,0 @@
mixin FirebaseCollectionNames {
static const String activeNotifications = 'active_notifications';
static const String plannedNotifications = 'planned_notifications';
}

View file

@ -1,84 +0,0 @@
// File generated by FlutterFire CLI.
// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members
import 'environment_config.dart';
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show TargetPlatform, defaultTargetPlatform, kIsWeb;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
return macos;
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static FirebaseOptions web = FirebaseOptions(
apiKey: EnvironmentConfig().firebaseWebApiKey,
appId: EnvironmentConfig().firebaseAppIdWeb,
messagingSenderId: EnvironmentConfig().firebaseMessageId,
projectId: EnvironmentConfig().firebaseProjectId,
authDomain: EnvironmentConfig().firebaseAuthDomain,
storageBucket: EnvironmentConfig().firebaseStorageUrl,
);
static FirebaseOptions android = FirebaseOptions(
apiKey: EnvironmentConfig().firebaseAndroidApiKey,
appId: EnvironmentConfig().firebaseAppIdAndroid,
messagingSenderId: EnvironmentConfig().firebaseMessageId,
projectId: EnvironmentConfig().firebaseProjectId,
storageBucket: EnvironmentConfig().firebaseStorageUrl,
);
static FirebaseOptions ios = FirebaseOptions(
apiKey: EnvironmentConfig().firebaseIosApiKey,
appId: EnvironmentConfig().firebaseAppIdIos,
messagingSenderId: EnvironmentConfig().firebaseMessageId,
projectId: EnvironmentConfig().firebaseProjectId,
storageBucket: EnvironmentConfig().firebaseStorageUrl,
iosClientId: EnvironmentConfig().firebaseClientIdIos,
iosBundleId: 'nl.iconica.appshellDemo',
);
static FirebaseOptions macos = FirebaseOptions(
apiKey: EnvironmentConfig().firebaseIosApiKey,
appId: EnvironmentConfig().firebaseAppIdMacos,
messagingSenderId: EnvironmentConfig().firebaseMessageId,
projectId: EnvironmentConfig().firebaseProjectId,
storageBucket: EnvironmentConfig().firebaseStorageUrl,
iosClientId: EnvironmentConfig().firebaseClientIdMacos,
iosBundleId: 'nl.iconica.appshellDemo.RunnerTests',
);
}

View file

@ -1,14 +1,19 @@
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_notification_center/flutter_notification_center.dart';
import '../config/firebase_collections.dart';
class FirebaseNotificationService
with ChangeNotifier
implements NotificationService {
final Function(NotificationModel) newNotificationCallback;
final FirebaseApp? firebaseApp;
final String activeNotificationsCollection;
final String plannedNotificationsCollection;
late FirebaseApp _firebaseApp;
@override
List<NotificationModel> listOfActiveNotifications;
@ -18,10 +23,15 @@ class FirebaseNotificationService
// ignore: unused_field
late Timer _timer;
FirebaseNotificationService(
{required this.newNotificationCallback,
FirebaseNotificationService({
required this.newNotificationCallback,
this.firebaseApp,
this.activeNotificationsCollection = 'active_notifications',
this.plannedNotificationsCollection = 'planned_notifications',
this.listOfActiveNotifications = const [],
this.listOfPlannedNotifications = const []}) {
this.listOfPlannedNotifications = const [],
}) {
_firebaseApp = firebaseApp ?? Firebase.app();
_startTimer();
}
@ -36,15 +46,25 @@ class FirebaseNotificationService
Future<void> pushNotification(NotificationModel notification,
[Function(NotificationModel model)? onNewNotification]) async {
try {
CollectionReference notifications = FirebaseFirestore.instance
.collection(FirebaseCollectionNames.activeNotifications);
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint('User is not authenticated');
return;
}
CollectionReference notifications =
FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(activeNotificationsCollection)
.doc(userId)
.collection(activeNotificationsCollection);
DateTime currentDateTime = DateTime.now();
notification.dateTimePushed = currentDateTime;
Map<String, dynamic> notificationMap = notification.toMap();
await notifications.doc(notification.id).set(notificationMap);
listOfActiveNotifications.add(notification);
listOfActiveNotifications = [...listOfActiveNotifications, notification];
//Show popup with notification conte
if (onNewNotification != null) {
@ -62,11 +82,20 @@ class FirebaseNotificationService
@override
Future<List<NotificationModel>> getActiveNotifications() async {
try {
CollectionReference activeNotificationsCollection = FirebaseFirestore
.instance
.collection(FirebaseCollectionNames.activeNotifications);
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
QuerySnapshot querySnapshot = await activeNotificationsCollection.get();
if (userId == null) {
debugPrint('User is not authenticated');
return [];
}
CollectionReference activeNotificationsResult =
FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(activeNotificationsCollection)
.doc(userId)
.collection(activeNotificationsCollection);
QuerySnapshot querySnapshot = await activeNotificationsResult.get();
List<NotificationModel> activeNotifications =
querySnapshot.docs.map((doc) {
@ -75,7 +104,18 @@ class FirebaseNotificationService
return NotificationModel.fromJson(data);
}).toList();
listOfActiveNotifications = activeNotifications;
listOfActiveNotifications = List.from(activeNotifications);
listOfActiveNotifications.removeWhere((element) => element.isPinned);
activeNotifications
.sort((a, b) => b.dateTimePushed!.compareTo(a.dateTimePushed!));
listOfActiveNotifications
.sort((a, b) => b.dateTimePushed!.compareTo(a.dateTimePushed!));
listOfActiveNotifications.insertAll(
0, activeNotifications.where((element) => element.isPinned));
notifyListeners();
return listOfActiveNotifications;
} catch (e) {
@ -115,8 +155,19 @@ class FirebaseNotificationService
Future<void> createScheduledNotification(
NotificationModel notification) async {
try {
CollectionReference plannedNotifications = FirebaseFirestore.instance
.collection(FirebaseCollectionNames.plannedNotifications);
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint('User is not authenticated');
return;
}
CollectionReference plannedNotifications =
FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(plannedNotificationsCollection)
.doc(userId)
.collection(plannedNotificationsCollection);
Map<String, dynamic> notificationMap = notification.toMap();
await plannedNotifications.doc(notification.id).set(notificationMap);
} catch (e) {
@ -128,13 +179,26 @@ class FirebaseNotificationService
Future<void> deletePlannedNotification(
NotificationModel notificationModel) async {
try {
DocumentReference documentReference = FirebaseFirestore.instance
.collection(FirebaseCollectionNames.plannedNotifications)
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint('User is not authenticated');
return;
}
DocumentReference documentReference =
FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(plannedNotificationsCollection)
.doc(userId)
.collection(plannedNotificationsCollection)
.doc(notificationModel.id);
await documentReference.delete();
QuerySnapshot querySnapshot = await FirebaseFirestore.instance
.collection(FirebaseCollectionNames.plannedNotifications)
QuerySnapshot querySnapshot =
await FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(plannedNotificationsCollection)
.doc(userId)
.collection(plannedNotificationsCollection)
.get();
if (querySnapshot.docs.isEmpty) {
@ -152,8 +216,18 @@ class FirebaseNotificationService
Future<void> dismissActiveNotification(
NotificationModel notificationModel) async {
try {
DocumentReference documentReference = FirebaseFirestore.instance
.collection(FirebaseCollectionNames.activeNotifications)
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint('User is not authenticated');
return;
}
DocumentReference documentReference =
FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(activeNotificationsCollection)
.doc(userId)
.collection(activeNotificationsCollection)
.doc(notificationModel.id);
await documentReference.delete();
listOfActiveNotifications
@ -168,11 +242,57 @@ class FirebaseNotificationService
Future<void> pinActiveNotification(
NotificationModel notificationModel) async {
try {
DocumentReference documentReference = FirebaseFirestore.instance
.collection(FirebaseCollectionNames.activeNotifications)
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint('User is not authenticated');
return;
}
DocumentReference documentReference =
FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(activeNotificationsCollection)
.doc(userId)
.collection(activeNotificationsCollection)
.doc(notificationModel.id);
await documentReference.update({'isPinned': true});
notificationModel.isPinned = true;
listOfActiveNotifications
.removeAt(listOfActiveNotifications.indexOf(notificationModel));
listOfActiveNotifications.insert(0, notificationModel);
notifyListeners();
} catch (e) {
debugPrint('Error updating document: $e');
}
}
@override
Future<void> unPinActiveNotification(
NotificationModel notificationModel) async {
try {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint('User is not authenticated');
return;
}
DocumentReference documentReference =
FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(activeNotificationsCollection)
.doc(userId)
.collection(activeNotificationsCollection)
.doc(notificationModel.id);
await documentReference.update({'isPinned': false});
notificationModel.isPinned = false;
listOfActiveNotifications
.removeAt(listOfActiveNotifications.indexOf(notificationModel));
listOfActiveNotifications.add(notificationModel);
notifyListeners();
} catch (e) {
debugPrint('Error updating document: $e');
@ -183,8 +303,18 @@ class FirebaseNotificationService
Future<void> markNotificationAsRead(
NotificationModel notificationModel) async {
try {
DocumentReference documentReference = FirebaseFirestore.instance
.collection(FirebaseCollectionNames.activeNotifications)
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint('User is not authenticated');
return;
}
DocumentReference documentReference =
FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(activeNotificationsCollection)
.doc(userId)
.collection(activeNotificationsCollection)
.doc(notificationModel.id);
await documentReference.update({'isRead': true});
notificationModel.isRead = true;
@ -198,11 +328,20 @@ class FirebaseNotificationService
Future<void> checkForScheduledNotifications() async {
DateTime currentTime = DateTime.now();
try {
CollectionReference plannedNotificationsCollection = FirebaseFirestore
.instance
.collection(FirebaseCollectionNames.plannedNotifications);
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
QuerySnapshot querySnapshot = await plannedNotificationsCollection.get();
if (userId == null) {
debugPrint('User is not authenticated');
return;
}
CollectionReference plannedNotificationsResult =
FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(plannedNotificationsCollection)
.doc(userId)
.collection(plannedNotificationsCollection);
QuerySnapshot querySnapshot = await plannedNotificationsResult.get();
if (querySnapshot.docs.isEmpty) {
debugPrint('No scheduled notifications to be pushed');
@ -241,4 +380,22 @@ class FirebaseNotificationService
return;
}
}
@override
Stream<int> getActiveAmountStream() async* {
var userId = FirebaseAuth.instanceFor(app: _firebaseApp).currentUser?.uid;
if (userId == null) {
debugPrint('User is not authenticated');
yield 0;
}
var amount = FirebaseFirestore.instanceFor(app: _firebaseApp)
.collection(activeNotificationsCollection)
.doc(userId)
.collection(activeNotificationsCollection)
.snapshots()
.map((e) => e.docs.length);
yield* amount;
}
}

View file

@ -2,7 +2,7 @@ name: flutter_notification_center_firebase
description: "A new Flutter project."
publish_to: "none"
version: 1.3.1
version: 1.4.0
environment:
sdk: ">=2.18.0 <3.0.0"
@ -10,21 +10,19 @@ environment:
dependencies:
flutter:
sdk: flutter
flutter_dotenv: ^5.0.2
intl: any
# Firebase
cloud_firestore: ^4.16.0
firebase_auth: ^4.2.6
firebase_core: ^2.5.0
firebase_storage: ^11.0.14
cupertino_icons: ^1.0.2
flutter_notification_center:
git:
url: https://github.com/Iconica-Development/flutter_notification_center
ref: 1.3.1
ref: 1.4.0
path: packages/flutter_notification_center
dev_dependencies:

View file

@ -1,32 +1,6 @@
name: flutter_notification_center
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.2.0
name: flutter_nofication_center_workspace
environment:
sdk: '>=3.3.2 <4.0.0'
dependencies:
flutter:
sdk: flutter
intl: any
flutter_animated_widgets:
git:
url: https://github.com/Iconica-Development/flutter_animated_widgets
ref: 0.1.1
sdk: ">=3.1.0 <4.0.0"
dev_dependencies:
flutter_test:
sdk: flutter
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0
# The following section is specific to Flutter packages.
flutter:
uses-material-design: true
melos: ^3.0.1

View file

@ -1,30 +0,0 @@
// // This is a basic Flutter widget test.
// //
// // To perform an interaction with a widget in your test, use the WidgetTester
// // utility in the flutter_test package. For example, you can send tap and scroll
// // gestures. You can also use WidgetTester to find child widgets in the widget
// // tree, read text, and verify that the values of widget properties are correct.
// import 'package:flutter/material.dart';
// import 'package:flutter_test/flutter_test.dart';
// import 'package:flutter_notification_center/main.dart';
// void main() {
// testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// // Build our app and trigger a frame.
// await tester.pumpWidget(const MyApp());
// // Verify that our counter starts at 0.
// expect(find.text('0'), findsOneWidget);
// expect(find.text('1'), findsNothing);
// // Tap the '+' icon and trigger a frame.
// await tester.tap(find.byIcon(Icons.add));
// await tester.pump();
// // Verify that our counter has incremented.
// expect(find.text('0'), findsNothing);
// expect(find.text('1'), findsOneWidget);
// });
// }