package services import ( "context" "fmt" "sync" firebase "firebase.google.com/go/v4" "firebase.google.com/go/v4/messaging" "google.golang.org/api/option" ) type FCMService struct { credentials *CredentialsService client *messaging.Client currentKey string mu sync.RWMutex } func NewFCMService(c *CredentialsService) *FCMService { return &FCMService{credentials: c} } func (s *FCMService) getClient(ctx context.Context) (*messaging.Client, error) { s.mu.Lock() defer s.mu.Unlock() // Get Service Account JSON from DB jsonKey, err := s.credentials.GetDecryptedKey(ctx, "fcm_service_account") if err != nil { return nil, fmt.Errorf("fcm credentials missing: %w", err) } // If initialized and key hasn't changed, return cached client if s.client != nil && jsonKey == s.currentKey { return s.client, nil } // Initialize new client opt := option.WithCredentialsJSON([]byte(jsonKey)) app, err := firebase.NewApp(ctx, nil, opt) if err != nil { return nil, fmt.Errorf("error initializing firebase app: %w", err) } client, err := app.Messaging(ctx) if err != nil { return nil, fmt.Errorf("error getting messaging client: %w", err) } s.client = client s.currentKey = jsonKey return client, nil } // SendPush sends a push notification to a specific token func (s *FCMService) SendPush(ctx context.Context, token, title, body string, data map[string]string) error { client, err := s.getClient(ctx) if err != nil { return err } msg := &messaging.Message{ Token: token, Notification: &messaging.Notification{ Title: title, Body: body, }, Data: data, } _, err = client.Send(ctx, msg) return err } // SubscribeToTopic subscribes a token to a topic func (s *FCMService) SubscribeToTopic(ctx context.Context, tokens []string, topic string) error { client, err := s.getClient(ctx) if err != nil { return err } _, err = client.SubscribeToTopic(ctx, tokens, topic) return err }