feat: Notifications support for invitations

This commit is contained in:
sBubshait 2025-07-23 11:12:55 +03:00
parent a2523d603e
commit 13ad5a5bbe
6 changed files with 180 additions and 1 deletions

3
backend/.gitignore vendored
View File

@ -32,4 +32,5 @@ build/
### VS Code ###
.vscode/
.env
env.properties
env.properties
firebase-service-account.json

View File

@ -90,6 +90,11 @@
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.google.firebase</groupId>
<artifactId>firebase-admin</artifactId>
<version>9.2.0</version>
</dependency>
</dependencies>
<build>

View File

@ -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) {

View File

@ -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();
}
}

View File

@ -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<User> 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<String> fcmTokens, List<String> 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());
}
}
}
}

View File

@ -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<Attendee> attendees = attendeeRepository.findByInvitationId(invitationId);
List<User> allParticipants = attendees.stream()
.map(Attendee::getUser)
.collect(Collectors.toList());
allParticipants.add(invitation.getCreator());
// Extract FCM tokens and display names for async processing
List<String> fcmTokens = allParticipants.stream()
.map(User::getFcmToken)
.collect(Collectors.toList());
List<String> 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