Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
'use client';
|
||||
|
||||
import posthog from 'posthog-js';
|
||||
|
||||
let initialized = false;
|
||||
let posthogReady = false;
|
||||
|
||||
export function initAnalytics(): void {
|
||||
if (initialized || typeof window === 'undefined') return;
|
||||
initialized = true;
|
||||
const key = process.env.NEXT_PUBLIC_POSTHOG_KEY;
|
||||
if (!key) return;
|
||||
posthog.init(key, {
|
||||
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://us.i.posthog.com',
|
||||
capture_pageview: false,
|
||||
persistence: 'localStorage+cookie',
|
||||
autocapture: false,
|
||||
loaded: (ph) => {
|
||||
posthogReady = true;
|
||||
if (process.env.NODE_ENV === 'development') ph.debug();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function safeCapture(event: string, properties: Record<string, unknown> = {}) {
|
||||
if (!posthogReady) return;
|
||||
try {
|
||||
posthog.capture(event, properties);
|
||||
} catch {
|
||||
// analytics failures should never break the app
|
||||
}
|
||||
}
|
||||
|
||||
export function identifyUser(userId: string, properties: Record<string, unknown> = {}) {
|
||||
if (!posthogReady) return;
|
||||
try {
|
||||
posthog.identify(userId, properties);
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
}
|
||||
|
||||
export function resetIdentity() {
|
||||
if (!posthogReady) return;
|
||||
try {
|
||||
posthog.reset();
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
}
|
||||
|
||||
export function trackPageView(path: string) {
|
||||
safeCapture('page_viewed', { path });
|
||||
}
|
||||
|
||||
export function trackScanCompleted(data: {
|
||||
sport: string;
|
||||
player: string;
|
||||
stat: string;
|
||||
line: number;
|
||||
grade: string;
|
||||
tier: string;
|
||||
}) {
|
||||
safeCapture('scan_completed', data);
|
||||
}
|
||||
|
||||
export function trackParlayBuilt(data: { legs: number; sports: string[]; grade: string }) {
|
||||
safeCapture('parlay_built', data);
|
||||
}
|
||||
|
||||
export function trackUpgradeClicked(data: {
|
||||
current_tier: string;
|
||||
target_tier: string;
|
||||
trigger_location: string;
|
||||
}) {
|
||||
safeCapture('upgrade_clicked', data);
|
||||
}
|
||||
|
||||
export function trackShareCardGenerated(data: { sport: string; grade: string }) {
|
||||
safeCapture('share_card_generated', data);
|
||||
}
|
||||
|
||||
export function trackScanLimitHit(data: { current_scan_count: number; tier: string }) {
|
||||
safeCapture('scan_limit_hit', data);
|
||||
}
|
||||
|
||||
export function trackSignup(data: { method: string }) {
|
||||
safeCapture('signup', data);
|
||||
}
|
||||
|
||||
export function trackLogin(data: { method: string }) {
|
||||
safeCapture('login', data);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
import { getServerSupabase } from './supabase';
|
||||
|
||||
export interface AuthedUser {
|
||||
id: string;
|
||||
email: string | null;
|
||||
tier: 'free' | 'analyst' | 'desk';
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a bearer token from the Authorization header against Supabase.
|
||||
* Returns null when missing/invalid — callers decide whether to 401.
|
||||
*/
|
||||
export async function getUserFromRequest(req: NextRequest): Promise<AuthedUser | null> {
|
||||
const auth = req.headers.get('authorization');
|
||||
if (!auth || !auth.toLowerCase().startsWith('bearer ')) return null;
|
||||
|
||||
const sb = getServerSupabase(auth);
|
||||
if (!sb) return null;
|
||||
|
||||
const { data, error } = await sb.auth.getUser();
|
||||
if (error || !data.user) return null;
|
||||
|
||||
const { data: profile } = await sb
|
||||
.from('user_profiles')
|
||||
.select('tier')
|
||||
.eq('id', data.user.id)
|
||||
.maybeSingle();
|
||||
|
||||
return {
|
||||
id: data.user.id,
|
||||
email: data.user.email ?? null,
|
||||
tier: ((profile?.tier as AuthedUser['tier']) ?? 'free'),
|
||||
};
|
||||
}
|
||||
|
||||
export function jsonError(status: number, message: string) {
|
||||
return Response.json({ error: message }, { status });
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Tracks Reads completed in localStorage. Call markReadComplete() once a user
|
||||
// has actually viewed a grade card or scan result — *not* on page load.
|
||||
// InstallPrompt and PushPrompt use this counter to gate when they appear.
|
||||
|
||||
const READS_KEY = 'vyndr_reads_completed';
|
||||
|
||||
export function markReadComplete(): number {
|
||||
if (typeof window === 'undefined') return 0;
|
||||
const next = readsCompleted() + 1;
|
||||
window.localStorage.setItem(READS_KEY, String(next));
|
||||
return next;
|
||||
}
|
||||
|
||||
export function readsCompleted(): number {
|
||||
if (typeof window === 'undefined') return 0;
|
||||
const raw = window.localStorage.getItem(READS_KEY);
|
||||
return raw ? parseInt(raw, 10) || 0 : 0;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { createClient, type SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
let browserClient: SupabaseClient | null = null;
|
||||
|
||||
export function getBrowserSupabase(): SupabaseClient | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
if (!url || !anonKey) return null;
|
||||
if (browserClient) return browserClient;
|
||||
browserClient = createClient(url, anonKey, {
|
||||
auth: {
|
||||
persistSession: true,
|
||||
autoRefreshToken: true,
|
||||
detectSessionInUrl: true,
|
||||
},
|
||||
});
|
||||
return browserClient;
|
||||
}
|
||||
|
||||
export function getServerSupabase(authHeader?: string | null): SupabaseClient | null {
|
||||
if (!url || !anonKey) return null;
|
||||
return createClient(url, anonKey, {
|
||||
auth: { persistSession: false, autoRefreshToken: false },
|
||||
global: authHeader ? { headers: { Authorization: authHeader } } : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export function getServiceRoleSupabase(): SupabaseClient | null {
|
||||
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
if (!url || !serviceKey) return null;
|
||||
return createClient(url, serviceKey, {
|
||||
auth: { persistSession: false, autoRefreshToken: false },
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user