Files
llink/go/internal/waitlist/service.go
T
Arjun Patel d262f734f0 Mobile notifications for iOS (#210)
* 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
2026-05-18 12:44:31 -07:00

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
}