feat: Notifications support for invitations
This commit is contained in:
parent
a2523d603e
commit
13ad5a5bbe
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
@ -33,3 +33,4 @@ build/
|
||||
.vscode/
|
||||
.env
|
||||
env.properties
|
||||
firebase-service-account.json
|
||||
@ -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>
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user