d262f734f0
* mobile: wire notification registration and listener
* implement backend components for push notifications
* refactor: agentic comment cleanup
* docs: use proper module name for particle processor
* set required env variables for push notifications
* bump version
* fix: always upsert push token on mobile start
* Revert "fix: always upsert push token on mobile start"
This reverts commit 90ff18a788.
* send push notifications regardless of online status
129 lines
3.4 KiB
Go
129 lines
3.4 KiB
Go
package waitlist
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
pbaero "github.com/flowy-live/llink/genproto/aero"
|
|
"github.com/flowy-live/llink/internal/utils"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
type WaitlistEntry struct {
|
|
Id int `db:"id"`
|
|
Email string `db:"email"`
|
|
Metadata map[string]string `db:"metadata"`
|
|
CreatedAt time.Time `db:"created_at"`
|
|
InvitedAt *time.Time `db:"invited_at"`
|
|
}
|
|
|
|
var (
|
|
AlreadyInWaitlistError = errors.New("already in the waitlist")
|
|
EntryNotFoundError = errors.New("not found")
|
|
)
|
|
|
|
type GetWaitlistFilter string
|
|
|
|
const (
|
|
GetWaitlistFilterAll GetWaitlistFilter = "all"
|
|
GetWaitlistFilterInvitedOnly GetWaitlistFilter = "invited-only"
|
|
GetWaitlistFilterUninvitedOnly GetWaitlistFilter = "uninvited-only"
|
|
)
|
|
|
|
type Service interface {
|
|
// AddToWaitlist returns AlreadyInWaitlistError if the email is already present.
|
|
AddToWaitlist(ctx context.Context, email string, metadata map[string]string) error
|
|
GetWaitlist(ctx context.Context, filter GetWaitlistFilter) ([]*WaitlistEntry, error)
|
|
// GetWaitlistEntryByEmail returns EntryNotFoundError if missing.
|
|
GetWaitlistEntryByEmail(ctx context.Context, email string) (*WaitlistEntry, error)
|
|
MarkWaitlistEntryInvited(ctx context.Context, email string) error
|
|
}
|
|
|
|
type serviceImpl struct {
|
|
repo repository
|
|
aeroSvc pbaero.PrimaryClient
|
|
}
|
|
|
|
func NewService(pool *pgxpool.Pool, aeroSvc pbaero.PrimaryClient) Service {
|
|
return &serviceImpl{
|
|
repo: newRepository(pool),
|
|
aeroSvc: aeroSvc,
|
|
}
|
|
}
|
|
|
|
func (s *serviceImpl) AddToWaitlist(ctx context.Context, email string, metadata map[string]string) error {
|
|
normalizedEmail, err := utils.NormalizeEmail(email)
|
|
if err != nil {
|
|
return errors.New("invalid email")
|
|
}
|
|
|
|
if metadata == nil {
|
|
metadata = map[string]string{}
|
|
}
|
|
|
|
_, err = s.repo.create(ctx, normalizedEmail, metadata)
|
|
if err != nil {
|
|
if errors.Is(err, errAlreadyExists) {
|
|
return AlreadyInWaitlistError
|
|
}
|
|
return err
|
|
}
|
|
|
|
message := fmt.Sprintf("New human added to the llink waitlist: %s", email)
|
|
_, err = s.aeroSvc.ShootEmail(context.Background(), &pbaero.ShootEmailRequest{
|
|
ToEmails: []string{"team@flowylabs.ai"},
|
|
Subject: "Flowy Llink - New Waitlist Subscriber",
|
|
TemplateData: &pbaero.ShootEmailRequest_GenericFlowyAdminAlertData{
|
|
GenericFlowyAdminAlertData: &pbaero.GenericFlowyAdminAlertData{
|
|
Message: message,
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
slog.Error("failed to add to waitlist", "error", err, "email", email)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *serviceImpl) GetWaitlist(ctx context.Context, filter GetWaitlistFilter) ([]*WaitlistEntry, error) {
|
|
return s.repo.getAll(ctx, filter)
|
|
}
|
|
|
|
func (s *serviceImpl) GetWaitlistEntryByEmail(ctx context.Context, email string) (*WaitlistEntry, error) {
|
|
normalizedEmail, err := utils.NormalizeEmail(email)
|
|
if err != nil {
|
|
return nil, errors.New("invalid email")
|
|
}
|
|
|
|
entry, err := s.repo.getByEmail(ctx, normalizedEmail)
|
|
if err != nil {
|
|
if errors.Is(err, errNotFound) {
|
|
return nil, EntryNotFoundError
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
return entry, nil
|
|
}
|
|
|
|
func (s *serviceImpl) MarkWaitlistEntryInvited(ctx context.Context, email string) error {
|
|
normalizedEmail, err := utils.NormalizeEmail(email)
|
|
if err != nil {
|
|
return errors.New("invalid email")
|
|
}
|
|
|
|
err = s.repo.markInvited(ctx, normalizedEmail)
|
|
if err != nil {
|
|
if errors.Is(err, errNotFound) {
|
|
return EntryNotFoundError
|
|
}
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|