From 03362955f04f0a7964f8e883713fe84cc8772d01 Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 24 Dec 2025 18:48:03 -0800 Subject: [PATCH] Implement free preview timer with localStorage persistence and UI indicator --- packages/web/src/routes/$username.tsx | 100 ++++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 6 deletions(-) diff --git a/packages/web/src/routes/$username.tsx b/packages/web/src/routes/$username.tsx index 47351c90..3ac7e968 100644 --- a/packages/web/src/routes/$username.tsx +++ b/packages/web/src/routes/$username.tsx @@ -47,6 +47,32 @@ const NIKIV_DATA: StreamPageData = { }, } +// Free preview duration in milliseconds (5 minutes) +const FREE_PREVIEW_MS = 5 * 60 * 1000 +const STORAGE_KEY = "linsa_stream_watch_time" + +function getWatchTime(): number { + if (typeof window === "undefined") return 0 + const stored = localStorage.getItem(STORAGE_KEY) + if (!stored) return 0 + try { + const data = JSON.parse(stored) + // Reset if older than 24 hours + if (Date.now() - data.startedAt > 24 * 60 * 60 * 1000) { + localStorage.removeItem(STORAGE_KEY) + return 0 + } + return data.watchTime || 0 + } catch { + return 0 + } +} + +function saveWatchTime(watchTime: number, startedAt: number) { + if (typeof window === "undefined") return + localStorage.setItem(STORAGE_KEY, JSON.stringify({ watchTime, startedAt })) +} + function StreamPage() { const { username } = Route.useParams() const { data: session, isPending: sessionLoading } = authClient.useSession() @@ -65,8 +91,48 @@ function StreamPage() { const [showReadyPulse, setShowReadyPulse] = useState(false) const readyPulseTimeoutRef = useRef | null>(null) + // Free preview tracking + const [watchTime, setWatchTime] = useState(0) + const [previewExpired, setPreviewExpired] = useState(false) + const watchStartRef = useRef(null) + const isAuthenticated = !sessionLoading && !!session?.user + // Track watch time for unauthenticated users + useEffect(() => { + if (isAuthenticated || sessionLoading) return + + // Initialize from localStorage + const savedTime = getWatchTime() + setWatchTime(savedTime) + if (savedTime >= FREE_PREVIEW_MS) { + setPreviewExpired(true) + return + } + + watchStartRef.current = Date.now() + const startedAt = Date.now() - savedTime + + const interval = setInterval(() => { + const elapsed = Date.now() - (watchStartRef.current || Date.now()) + savedTime + setWatchTime(elapsed) + saveWatchTime(elapsed, startedAt) + + if (elapsed >= FREE_PREVIEW_MS) { + setPreviewExpired(true) + clearInterval(interval) + } + }, 1000) + + return () => { + clearInterval(interval) + if (watchStartRef.current) { + const elapsed = Date.now() - watchStartRef.current + savedTime + saveWatchTime(elapsed, startedAt) + } + } + }, [isAuthenticated, sessionLoading]) + useEffect(() => { let isActive = true const setReadySafe = (ready: boolean) => { @@ -337,7 +403,13 @@ function StreamPage() { } }, [shouldFetchSpotify]) - // Auth gate - require login to view streams + // Format remaining time + const remainingMs = Math.max(0, FREE_PREVIEW_MS - watchTime) + const remainingMin = Math.floor(remainingMs / 60000) + const remainingSec = Math.floor((remainingMs % 60000) / 1000) + const remainingFormatted = `${remainingMin}:${remainingSec.toString().padStart(2, "0")}` + + // Auth gate - show preview for 5 min, then require login if (sessionLoading) { return (
@@ -346,19 +418,20 @@ function StreamPage() { ) } - if (!isAuthenticated) { + // Show auth wall when preview expires for unauthenticated users + if (!isAuthenticated && previewExpired) { return (
-

Sign in to watch

+

Free preview ended

- Create an account or sign in to view this stream + Sign in to continue watching this stream

- Sign in + Sign in to continue
@@ -416,8 +489,23 @@ function StreamPage() {
{/* Main content area */}
+ {/* Free preview countdown banner for unauthenticated users */} + {!isAuthenticated && !previewExpired && ( +
+ + Free preview: {remainingFormatted} remaining + + + Sign in for unlimited access + +
+ )} + {/* Viewer count overlay */} -
+