Added Local Notification for Matches (Android Only)
parent
7b82c1cbb8
commit
9ddfb5cfd5
|
@ -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'
|
||||||
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
MultiProvider(
|
||||||
|
providers: [
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
create: (context) => ThemeProvider(initialIsDarkMode: isDarkMode),
|
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(),
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
40
pubspec.lock
40
pubspec.lock
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue