diff --git a/backend/.gitignore b/backend/.gitignore
index 239f1c2..f2b0a9f 100644
--- a/backend/.gitignore
+++ b/backend/.gitignore
@@ -32,4 +32,5 @@ build/
### VS Code ###
.vscode/
.env
-env.properties
\ No newline at end of file
+env.properties
+firebase-service-account.json
\ No newline at end of file
diff --git a/backend/pom.xml b/backend/pom.xml
index c085247..1af2abd 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -90,6 +90,11 @@
springdoc-openapi-starter-webmvc-ui
2.3.0
+
+ com.google.firebase
+ firebase-admin
+ 9.2.0
+
diff --git a/backend/src/main/java/online/wesal/wesal/WesalApplication.java b/backend/src/main/java/online/wesal/wesal/WesalApplication.java
index 5b2a4b8..f25819c 100644
--- a/backend/src/main/java/online/wesal/wesal/WesalApplication.java
+++ b/backend/src/main/java/online/wesal/wesal/WesalApplication.java
@@ -2,8 +2,10 @@ package online.wesal.wesal;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
+@EnableAsync
public class WesalApplication {
public static void main(String[] args) {
diff --git a/backend/src/main/java/online/wesal/wesal/config/FirebaseConfig.java b/backend/src/main/java/online/wesal/wesal/config/FirebaseConfig.java
new file mode 100644
index 0000000..48ca0b3
--- /dev/null
+++ b/backend/src/main/java/online/wesal/wesal/config/FirebaseConfig.java
@@ -0,0 +1,29 @@
+package online.wesal.wesal.config;
+
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.FirebaseOptions;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.ClassPathResource;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+@Configuration
+public class FirebaseConfig {
+
+ @Bean
+ public FirebaseApp firebaseApp() throws IOException {
+ if (FirebaseApp.getApps().isEmpty()) {
+ InputStream serviceAccount = new ClassPathResource("firebase-service-account.json").getInputStream();
+
+ FirebaseOptions options = FirebaseOptions.builder()
+ .setCredentials(GoogleCredentials.fromStream(serviceAccount))
+ .build();
+
+ return FirebaseApp.initializeApp(options);
+ }
+ return FirebaseApp.getInstance();
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/online/wesal/wesal/service/FCMNotificationService.java b/backend/src/main/java/online/wesal/wesal/service/FCMNotificationService.java
new file mode 100644
index 0000000..9fd9f90
--- /dev/null
+++ b/backend/src/main/java/online/wesal/wesal/service/FCMNotificationService.java
@@ -0,0 +1,105 @@
+package online.wesal.wesal.service;
+
+import com.google.firebase.messaging.FirebaseMessaging;
+import com.google.firebase.messaging.Message;
+import com.google.firebase.messaging.Notification;
+import online.wesal.wesal.entity.User;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class FCMNotificationService {
+
+ private static final Logger logger = LoggerFactory.getLogger(FCMNotificationService.class);
+
+ @Async
+ public void sendNotificationToUser(User user, String title, String body) {
+ if (user.getFcmToken() == null || user.getFcmToken().trim().isEmpty()) {
+ logger.warn("User {} has no FCM token, skipping notification", user.getDisplayName());
+ return;
+ }
+
+ try {
+ Message message = Message.builder()
+ .setToken(user.getFcmToken())
+ .setNotification(Notification.builder()
+ .setTitle(title)
+ .setBody(body)
+ .build())
+ .build();
+
+
+ String response = FirebaseMessaging.getInstance().send(message);
+ logger.info("Successfully sent notification to {}: {}", user.getDisplayName(), response);
+ } catch (Exception e) {
+ logger.error("Failed to send notification to {}: {}", user.getDisplayName(), e.getMessage());
+ }
+ }
+
+ @Async
+ public void sendNotificationToUsers(List users, String title, String body) {
+ users.forEach(user -> sendNotificationToUser(user, title, body));
+ }
+
+ @Async
+ public void sendInvitationAcceptedNotification(String organizerToken, String organizerName, String accepterName, String tagName) {
+ String title = "New Attendee! 🎉";
+ String body = String.format("%s accepted your invite for %s", accepterName, tagName);
+
+ if (organizerToken == null || organizerToken.trim().isEmpty()) {
+ logger.warn("User {} has no FCM token, skipping notification", organizerName);
+ return;
+ }
+
+ try {
+ Message message = Message.builder()
+ .setToken(organizerToken)
+ .setNotification(Notification.builder()
+ .setTitle(title)
+ .setBody(body)
+ .build())
+ .build();
+
+ String response = FirebaseMessaging.getInstance().send(message);
+ logger.info("Successfully sent notification to {}: {}", organizerName, response);
+ } catch (Exception e) {
+ logger.error("Failed to send notification to {}: {}", organizerName, e.getMessage());
+ }
+ }
+
+ @Async
+ public void sendInvitationFullNotification(List fcmTokens, List displayNames, String tagName, int totalAttendees) {
+ String title = String.format("You're set for %s! 🚀", tagName);
+ String body = String.format("%d people have accepted the invitation. Click to see who they are!", totalAttendees);
+
+ for (int i = 0; i < fcmTokens.size(); i++) {
+ String token = fcmTokens.get(i);
+ String name = displayNames.get(i);
+
+ if (token == null || token.trim().isEmpty()) {
+ logger.warn("User {} has no FCM token, skipping notification", name);
+ continue;
+ }
+
+ try {
+ Message message = Message.builder()
+ .setToken(token)
+ .setNotification(Notification.builder()
+ .setTitle(title)
+ .setBody(body)
+ .build())
+ .build();
+
+ String response = FirebaseMessaging.getInstance().send(message);
+ logger.info("Successfully sent notification to {}: {}", name, response);
+ } catch (Exception e) {
+ logger.error("Failed to send notification to {}: {}", name, e.getMessage());
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/backend/src/main/java/online/wesal/wesal/service/InvitationService.java b/backend/src/main/java/online/wesal/wesal/service/InvitationService.java
index 57fbbdb..ac9638c 100644
--- a/backend/src/main/java/online/wesal/wesal/service/InvitationService.java
+++ b/backend/src/main/java/online/wesal/wesal/service/InvitationService.java
@@ -35,6 +35,9 @@ public class InvitationService {
@Autowired
private AttendeeRepository attendeeRepository;
+ @Autowired
+ private FCMNotificationService fcmNotificationService;
+
@Transactional
public InvitationResponse createInvitation(CreateInvitationRequest request, String userEmail) {
User creator = userRepository.findByEmail(userEmail)
@@ -118,7 +121,41 @@ public class InvitationService {
attendeeRepository.save(attendee);
invitation.setCurrentAttendees(invitation.getCurrentAttendees() + 1);
+ int newAttendeeCount = invitation.getCurrentAttendees();
invitationRepository.save(invitation);
+
+ // If invitation is now full, notify everyone
+ if (newAttendeeCount == invitation.getMaxParticipants()) {
+ // Get all participants (attendees + organizer)
+ List attendees = attendeeRepository.findByInvitationId(invitationId);
+ List allParticipants = attendees.stream()
+ .map(Attendee::getUser)
+ .collect(Collectors.toList());
+ allParticipants.add(invitation.getCreator());
+
+ // Extract FCM tokens and display names for async processing
+ List fcmTokens = allParticipants.stream()
+ .map(User::getFcmToken)
+ .collect(Collectors.toList());
+ List displayNames = allParticipants.stream()
+ .map(User::getDisplayName)
+ .collect(Collectors.toList());
+
+ fcmNotificationService.sendInvitationFullNotification(
+ fcmTokens,
+ displayNames,
+ invitation.getTag().getName(),
+ newAttendeeCount
+ );
+ } else {
+ // Send notification to organizer about new attendee (only if not full)
+ fcmNotificationService.sendInvitationAcceptedNotification(
+ invitation.getCreator().getFcmToken(),
+ invitation.getCreator().getDisplayName(),
+ user.getDisplayName(),
+ invitation.getTag().getName()
+ );
+ }
}
@Transactional