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/
|
.vscode/
|
||||||
.env
|
.env
|
||||||
env.properties
|
env.properties
|
||||||
|
firebase-service-account.json
|
||||||
@ -90,6 +90,11 @@
|
|||||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
<version>2.3.0</version>
|
<version>2.3.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.firebase</groupId>
|
||||||
|
<artifactId>firebase-admin</artifactId>
|
||||||
|
<version>9.2.0</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@ -2,8 +2,10 @@ package online.wesal.wesal;
|
|||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
|
@EnableAsync
|
||||||
public class WesalApplication {
|
public class WesalApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
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
|
@Autowired
|
||||||
private AttendeeRepository attendeeRepository;
|
private AttendeeRepository attendeeRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FCMNotificationService fcmNotificationService;
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public InvitationResponse createInvitation(CreateInvitationRequest request, String userEmail) {
|
public InvitationResponse createInvitation(CreateInvitationRequest request, String userEmail) {
|
||||||
User creator = userRepository.findByEmail(userEmail)
|
User creator = userRepository.findByEmail(userEmail)
|
||||||
@ -118,7 +121,41 @@ public class InvitationService {
|
|||||||
attendeeRepository.save(attendee);
|
attendeeRepository.save(attendee);
|
||||||
|
|
||||||
invitation.setCurrentAttendees(invitation.getCurrentAttendees() + 1);
|
invitation.setCurrentAttendees(invitation.getCurrentAttendees() + 1);
|
||||||
|
int newAttendeeCount = invitation.getCurrentAttendees();
|
||||||
invitationRepository.save(invitation);
|
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
|
@Transactional
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user