implement backend components for push notifications

This commit is contained in:
talksik
2026-05-18 10:31:16 -07:00
parent 1618cb58cb
commit 6f476fe773
16 changed files with 904 additions and 60 deletions
+4
View File
@@ -15,6 +15,7 @@ import (
"github.com/flowy-live/llink/internal/billing"
"github.com/flowy-live/llink/internal/depot"
"github.com/flowy-live/llink/internal/human"
"github.com/flowy-live/llink/internal/human/pushnotify"
"github.com/flowy-live/llink/internal/livekit"
"github.com/flowy-live/llink/internal/middleware"
"github.com/flowy-live/llink/internal/network"
@@ -32,6 +33,7 @@ type Handler struct {
depotSvc depot.Service
waitlistSvc waitlist.Service
billingSvc billing.Service
pushTokenSvc pushnotify.Service
livekitClient livekit.Client
firestoreClient *firestore.Client
}
@@ -44,6 +46,7 @@ func NewHandler(
depotSvc depot.Service,
waitlistSvc waitlist.Service,
billingSvc billing.Service,
pushTokenSvc pushnotify.Service,
livekitClient livekit.Client,
firestoreClient *firestore.Client,
) *Handler {
@@ -55,6 +58,7 @@ func NewHandler(
depotSvc: depotSvc,
waitlistSvc: waitlistSvc,
billingSvc: billingSvc,
pushTokenSvc: pushTokenSvc,
livekitClient: livekitClient,
firestoreClient: firestoreClient,
}
+85
View File
@@ -0,0 +1,85 @@
package handler
import (
"encoding/json"
"errors"
"log/slog"
"net/http"
"github.com/flowy-live/llink/internal/human/pushnotify"
"github.com/flowy-live/llink/internal/middleware"
)
type RegisterPushTokenRequest struct {
Token string `json:"token"`
Platform string `json:"platform"`
AppVersion string `json:"app_version"`
}
type UnregisterPushTokenRequest struct {
Token string `json:"token"`
}
// RegisterPushToken upserts an Expo push token against the authenticated human.
// Re-binding a token to a new human (e.g., after a device-level account switch)
// happens transparently via ON CONFLICT.
func (h *Handler) RegisterPushToken(w http.ResponseWriter, r *http.Request) {
humanId, ok := middleware.HumanIdFromContext(r.Context())
if !ok {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
var req RegisterPushTokenRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
err := h.pushTokenSvc.Register(r.Context(), humanId, pushnotify.RegisterInput{
Token: req.Token,
Platform: pushnotify.Platform(req.Platform),
AppVersion: req.AppVersion,
})
if err != nil {
if errors.Is(err, pushnotify.ErrInvalidPlatform) || errors.Is(err, pushnotify.ErrInvalidToken) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
slog.Error("failed to register push token", "error", err, "humanId", humanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
// UnregisterPushToken removes a push token belonging to the authenticated human.
// Returns 204 even if the token wasn't found — idempotent from the client's POV.
func (h *Handler) UnregisterPushToken(w http.ResponseWriter, r *http.Request) {
humanId, ok := middleware.HumanIdFromContext(r.Context())
if !ok {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
var req UnregisterPushTokenRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
if req.Token == "" {
http.Error(w, "token is required", http.StatusBadRequest)
return
}
err := h.pushTokenSvc.Unregister(r.Context(), humanId, req.Token)
if err != nil && !errors.Is(err, pushnotify.ErrNotFound) {
slog.Error("failed to unregister push token", "error", err, "humanId", humanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}