package services import ( "context" "fmt" "os" firebase "firebase.google.com/go/v4" "firebase.google.com/go/v4/messaging" "github.com/patbritton/sojorn-backend/internal/repository" "github.com/rs/zerolog/log" "google.golang.org/api/option" ) type PushService struct { client *messaging.Client userRepo *repository.UserRepository } func NewPushService(userRepo *repository.UserRepository, credentialsFile string) (*PushService, error) { ctx := context.Background() var opt option.ClientOption if credentialsFile != "" { if _, err := os.Stat(credentialsFile); err == nil { opt = option.WithCredentialsFile(credentialsFile) } else { log.Warn().Msg("Firebase credentials file not found, using default credentials") opt = option.WithoutAuthentication() // Or handle differently } } else { // Attempt to use logic suitable for Cloud Run/GCP or emulator opt = option.WithCredentialsFile("firebase-service-account.json") // Default fallback } app, err := firebase.NewApp(ctx, nil, opt) if err != nil { return nil, fmt.Errorf("error initializing app: %v", err) } client, err := app.Messaging(ctx) if err != nil { return nil, fmt.Errorf("error getting Messaging client: %v", err) } return &PushService{ client: client, userRepo: userRepo, }, nil } func (s *PushService) SendPush(ctx context.Context, userID, title, body string, data map[string]string) error { tokens, err := s.userRepo.GetFCMTokens(ctx, userID) if err != nil { return fmt.Errorf("failed to get FCM tokens: %w", err) } if len(tokens) == 0 { return nil // No tokens, no push } // Multicast message message := &messaging.MulticastMessage{ Tokens: tokens, Notification: &messaging.Notification{ Title: title, Body: body, }, Data: data, Android: &messaging.AndroidConfig{ Priority: "high", Notification: &messaging.AndroidNotification{ Sound: "default", ClickAction: "FLUTTER_NOTIFICATION_CLICK", }, }, APNS: &messaging.APNSConfig{ Payload: &messaging.APNSPayload{ Aps: &messaging.Aps{ Sound: "default", }, }, }, } br, err := s.client.SendMulticast(ctx, message) if err != nil { return fmt.Errorf("error sending multicast message: %w", err) } if br.FailureCount > 0 { var failedTokens []string for idx, resp := range br.Responses { if !resp.Success { if resp.Error != nil && messaging.IsRegistrationTokenNotRegistered(resp.Error) { if err := s.userRepo.DeleteFCMToken(ctx, userID, tokens[idx]); err != nil { log.Warn().Err(err).Str("user_id", userID).Msg("Failed to delete invalid FCM token") } continue } failedTokens = append(failedTokens, tokens[idx]) } } log.Warn().Int("failure_count", br.FailureCount).Strs("failed_tokens", failedTokens).Msg("Some push notifications failed") } return nil }