Added Local Notification for Matches (Android Only)

master
Rafael 2024-07-12 13:14:09 +02:00
parent 7b82c1cbb8
commit 9ddfb5cfd5
9 changed files with 238 additions and 9 deletions

View File

@ -27,10 +27,13 @@ if (flutterVersionName == null) {
android { android {
namespace "com.example.cofounderella" namespace "com.example.cofounderella"
compileSdk flutter.compileSdkVersion compileSdk 34 // 34 required by flutter_local_notification; old value: flutter.compileSdkVersion
ndkVersion "26.1.10909125" ndkVersion "26.1.10909125"
compileOptions { compileOptions {
// Flag to enable support for the new language APIs
coreLibraryDesugaringEnabled true
// Sets Java compatibility to Java 8
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
@ -67,4 +70,10 @@ flutter {
source '../..' source '../..'
} }
dependencies {} dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
// There have been reports that enabling desugaring may result in a Flutter apps crashing on
// Android 12L and above. https://pub.dev/packages/flutter_local_notifications#-android-setup
implementation 'androidx.window:window:1.0.0'
implementation 'androidx.window:window-java:1.0.0'
}

View File

@ -1,3 +1,4 @@
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../constants.dart'; import '../constants.dart';
import '../services/auth/auth_gate.dart'; import '../services/auth/auth_gate.dart';
@ -10,6 +11,7 @@ import 'pages/conversations_page.dart';
import 'pages/liked_users_page.dart'; import 'pages/liked_users_page.dart';
import 'pages/user_matching_page.dart'; import 'pages/user_matching_page.dart';
import 'pages/user_profile_page.dart'; import 'pages/user_profile_page.dart';
import 'services/swipe_stream_service.dart';
void main() async { void main() async {
// Firebase stuff // Firebase stuff
@ -22,14 +24,35 @@ void main() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final isDarkMode = prefs.getBool(Constants.prefKeyThemeDarkMode) ?? false; final isDarkMode = prefs.getBool(Constants.prefKeyThemeDarkMode) ?? false;
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');
const InitializationSettings initializationSettings =
InitializationSettings(android: initializationSettingsAndroid);
await flutterLocalNotificationsPlugin.initialize(initializationSettings);
// Initialize the singleton instance with a dummy userId.
// The actual userId is set later in the HomePage.
SwipeStreamService()
.initialize('dummy_initial_user_id', flutterLocalNotificationsPlugin);
runApp( runApp(
ChangeNotifierProvider( MultiProvider(
create: (context) => ThemeProvider(initialIsDarkMode: isDarkMode), providers: [
ChangeNotifierProvider(
create: (context) => ThemeProvider(initialIsDarkMode: isDarkMode)),
Provider<FlutterLocalNotificationsPlugin>.value(
value: flutterLocalNotificationsPlugin),
],
child: const MyApp(), child: const MyApp(),
), ),
); );
} }
// Global key for navigation
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({super.key}); const MyApp({super.key});
@ -39,6 +62,7 @@ class MyApp extends StatelessWidget {
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
title: Constants.appTitle, title: Constants.appTitle,
theme: Provider.of<ThemeProvider>(context).themeData, theme: Provider.of<ThemeProvider>(context).themeData,
navigatorKey: navigatorKey,
home: const AuthGate(), home: const AuthGate(),
routes: { routes: {
'/discover': (context) => const UserMatchingPage(), '/discover': (context) => const UserMatchingPage(),

View File

@ -1,11 +1,23 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:provider/provider.dart';
import '../components/my_drawer.dart'; import '../components/my_drawer.dart';
import '../services/auth/auth_service.dart';
import '../services/swipe_stream_service.dart';
class HomePage extends StatelessWidget { class HomePage extends StatelessWidget {
const HomePage({super.key}); const HomePage({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final currentUserId = AuthService().getCurrentUser()!.uid;
final flutterLocalNotificationsPlugin =
Provider.of<FlutterLocalNotificationsPlugin>(context);
final swipeStreamService = SwipeStreamService();
swipeStreamService.initialize(
currentUserId, flutterLocalNotificationsPlugin);
swipeStreamService.listenToSwipes();
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Home'), title: const Text('Home'),

View File

@ -12,6 +12,7 @@ import '../constants.dart';
import '../forms/matched_screen.dart'; import '../forms/matched_screen.dart';
import '../models/user_profile.dart'; import '../models/user_profile.dart';
import '../services/auth/auth_service.dart'; import '../services/auth/auth_service.dart';
import '../services/swipe_stream_service.dart';
import '../services/user_service.dart'; import '../services/user_service.dart';
import '../utils/helper.dart'; import '../utils/helper.dart';
import '../utils/list_utils.dart'; import '../utils/list_utils.dart';
@ -173,6 +174,9 @@ class UserMatchingPageState extends State<UserMatchingPage> {
.doc(swipedUserId) .doc(swipedUserId)
.collection(Constants.dbCollectionMatches); .collection(Constants.dbCollectionMatches);
// current user swiped, so avoid local push notification
SwipeStreamService().addUser(swipedUserId);
await matchesCurrentUser.doc(swipedUserId).set({ await matchesCurrentUser.doc(swipedUserId).set({
'otherUserId': swipedUserId, 'otherUserId': swipedUserId,
'timestamp': FieldValue.serverTimestamp(), 'timestamp': FieldValue.serverTimestamp(),
@ -183,9 +187,6 @@ class UserMatchingPageState extends State<UserMatchingPage> {
'timestamp': FieldValue.serverTimestamp(), 'timestamp': FieldValue.serverTimestamp(),
}); });
//
// TODO Notify other user?
//
showMatchedScreen(currentUserId, swipedUserId); showMatchedScreen(currentUserId, swipedUserId);
// Remove matched user from the list of potential users // Remove matched user from the list of potential users
@ -237,9 +238,9 @@ class UserMatchingPageState extends State<UserMatchingPage> {
} else if (percentage >= 40) { } else if (percentage >= 40) {
return Colors.amber.shade200; // 54 - 40 return Colors.amber.shade200; // 54 - 40
} else if (percentage >= 20) { } else if (percentage >= 20) {
return Colors.orange.shade200; // 39 - 20 return Colors.orange; // 39 - 20
} else { } else {
return Colors.orange.shade300; // 19 - 0 return Colors.red.shade400; // 19 - 0
} }
} }

View File

@ -1,5 +1,7 @@
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import '../swipe_stream_service.dart';
class AuthService { class AuthService {
// instance of auth // instance of auth
final FirebaseAuth _auth = FirebaseAuth.instance; final FirebaseAuth _auth = FirebaseAuth.instance;
@ -45,6 +47,7 @@ class AuthService {
// sign out // sign out
Future<void> signOut() async { Future<void> signOut() async {
SwipeStreamService().stopListening();
await _auth.signOut(); await _auth.signOut();
} }
} }

View File

@ -0,0 +1,122 @@
import 'dart:async';
import 'package:flutter/foundation.dart' show debugPrint, kDebugMode;
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import '../constants.dart';
import '../main.dart';
import '../utils/helper_dialogs.dart';
import 'user_service.dart';
class SwipeStreamService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final List<String> _matchesInDB = [];
StreamSubscription<List<DocumentSnapshot>>? _subscription;
late String userId;
late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
late bool matchesRead = false;
// Singleton instance
static final SwipeStreamService _instance = SwipeStreamService._internal();
// Private constructor
SwipeStreamService._internal();
// Factory constructor
factory SwipeStreamService() {
return _instance;
}
// Initialization method
void initialize(String userId, FlutterLocalNotificationsPlugin plugin) {
_instance.userId = userId;
_instance.flutterLocalNotificationsPlugin = plugin;
// Reset old data
_matchesInDB.clear();
matchesRead = false;
}
Stream<List<DocumentSnapshot>> get swipesStream {
return _firestore
.collection(Constants.dbCollectionUsers)
.doc(userId)
.collection(Constants.dbCollectionMatches)
.snapshots()
.map((snapshot) => snapshot.docs);
}
void _showNotification(String swipeId) async {
String matchName = await UserService.getUserName(swipeId);
const AndroidNotificationDetails androidPlatformChannelSpecifics =
AndroidNotificationDetails(
'my_match_channel_id', 'my_match_channel_name',
channelShowBadge: true,
visibility: NotificationVisibility.private,
importance: Importance.max,
priority: Priority.high,
showWhen: true);
const NotificationDetails platformChannelSpecifics =
NotificationDetails(android: androidPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin.show(0, 'New Match',
'You have a new match with $matchName', platformChannelSpecifics);
}
void listenToSwipes() {
_subscription = swipesStream.listen(
(swipes) async {
List<String> userNames = [];
String tempUser = '';
if (!matchesRead) {
for (var swipe in swipes) {
debugPrint('Init Match Notify --> ${swipe.id}');
_matchesInDB.add(swipe.id);
tempUser = await UserService.getUserName(swipe.id);
userNames.add('INIT $tempUser');
}
if (kDebugMode) {
showErrorSnackBar(
navigatorKey.currentContext!,
userNames.join(', '),
);
}
matchesRead = true;
} else {
for (var swipe in swipes) {
tempUser = await UserService.getUserName(swipe.id);
if (!_matchesInDB.contains(swipe.id)) {
userNames.add('NEW! $tempUser');
debugPrint('NEW Match Notify --> ${swipe.id}');
_matchesInDB.add(swipe.id);
_showNotification(swipe.id);
} else {
userNames.add('OLD $tempUser');
debugPrint('Old Match Notify --> ${swipe.id}');
}
}
if (kDebugMode) {
showErrorSnackBar(
navigatorKey.currentContext!,
userNames.join(', '),
);
}
}
},
);
}
void stopListening() {
_subscription?.cancel();
_subscription = null;
}
void addUser(String userId) {
if (!_matchesInDB.contains(userId)) {
debugPrint('Notify: SKIP Match Notify for --> $userId');
_matchesInDB.add(userId);
}
}
}

View File

@ -346,4 +346,21 @@ class UserService {
throw Exception('Error fetching sectors from Firebase: $e'); throw Exception('Error fetching sectors from Firebase: $e');
} }
} }
static Future<String> getUserName(String userId) async {
try {
DocumentSnapshot userDoc = await FirebaseFirestore.instance
.collection(Constants.dbCollectionUsers)
.doc(userId)
.get();
if (userDoc.exists) {
return userDoc[Constants.dbFieldUsersName] ?? 'Unknown User';
} else {
return 'User not found';
}
} catch (e) {
return e.toString();
}
}
} }

View File

@ -129,6 +129,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.8" version: "1.0.8"
dbus:
dependency: transitive
description:
name: dbus
sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
expandable_text: expandable_text:
dependency: "direct main" dependency: "direct main"
description: description:
@ -294,6 +302,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.2" version: "3.0.2"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: "40e6fbd2da7dcc7ed78432c5cdab1559674b4af035fddbfb2f9a8f9c2112fcef"
url: "https://pub.dev"
source: hosted
version: "17.1.2"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03"
url: "https://pub.dev"
source: hosted
version: "4.0.0+1"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "340abf67df238f7f0ef58f4a26d2a83e1ab74c77ab03cd2b2d5018ac64db30b7"
url: "https://pub.dev"
source: hosted
version: "7.1.0"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@ -821,6 +853,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.0" version: "0.7.0"
timezone:
dependency: transitive
description:
name: timezone
sha256: a6ccda4a69a442098b602c44e61a1e2b4bf6f5516e875bbf0f427d5df14745d5
url: "https://pub.dev"
source: hosted
version: "0.9.3"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:

View File

@ -31,6 +31,7 @@ dependencies:
shared_preferences: ^2.2.3 shared_preferences: ^2.2.3
flutter_launcher_icons: ^0.13.1 flutter_launcher_icons: ^0.13.1
url_launcher: ^6.3.0 url_launcher: ^6.3.0
flutter_local_notifications: ^17.1.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: