mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
141 lines
4.4 KiB
TypeScript
141 lines
4.4 KiB
TypeScript
import { api } from '@hanzo/app/lib/api'
|
|
import { scope } from '@hanzo/app/lib/session'
|
|
import type { Program } from '@/components/beta/program'
|
|
|
|
/**
|
|
* Beta records and ecosystem beta requests on hanzo-inc/cloud apps/marketing/beta.go:
|
|
*
|
|
* GET /v1/marketing/beta the person's record and the betas they asked for
|
|
* PUT /v1/marketing/beta save program, questionnaire answers, and requested betas
|
|
* POST /v1/marketing/beta/request request access to specific feature-flagged ecosystem betas (or alpha)
|
|
*
|
|
* The person's points and place are the rewards ledger's (lib/hanzo/rewards):
|
|
* the record never holds points.
|
|
*/
|
|
|
|
export interface BetaAnswer {
|
|
id: string
|
|
question: string
|
|
answer: string[]
|
|
}
|
|
|
|
export interface BetaAction {
|
|
action: 'star' | 'follow'
|
|
target: string
|
|
at: string
|
|
}
|
|
|
|
export interface BetaStanding {
|
|
applied: boolean
|
|
program: Program
|
|
answers: BetaAnswer[]
|
|
betas: string[]
|
|
requested: Record<string, string>
|
|
github: string
|
|
actions: BetaAction[]
|
|
lead: string
|
|
since: string
|
|
}
|
|
|
|
/**
|
|
* Stored with `hz_` prefix (like `hz_ref` in referrals.ts) so `own(subject())`
|
|
* does not clear them when an anonymous visitor signs up or logs in.
|
|
*/
|
|
const LOCAL_BETAS_KEY = 'hz_beta_requested'
|
|
|
|
export function getLocalRequestedBetas(): string[] {
|
|
if (typeof window === 'undefined') return []
|
|
try {
|
|
const raw = JSON.parse(window.localStorage.getItem(LOCAL_BETAS_KEY) ?? '[]')
|
|
return Array.isArray(raw) ? raw.filter((x): x is string => typeof x === 'string') : []
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
export function setLocalRequestedBetas(ids: string[]): void {
|
|
if (typeof window === 'undefined') return
|
|
try {
|
|
const unique = Array.from(new Set(ids))
|
|
window.localStorage.setItem(LOCAL_BETAS_KEY, JSON.stringify(unique))
|
|
} catch {
|
|
/* ignore storage errors */
|
|
}
|
|
}
|
|
|
|
function normalizeStanding(raw: Partial<BetaStanding>): BetaStanding {
|
|
const prog = raw.program === 'research' || raw.program === 'partner' ? raw.program : 'beta'
|
|
return {
|
|
applied: Boolean(raw.applied || raw.program),
|
|
program: prog,
|
|
answers: Array.isArray(raw.answers) ? raw.answers : [],
|
|
betas: Array.isArray(raw.betas) ? raw.betas : [],
|
|
requested: raw.requested && typeof raw.requested === 'object' ? raw.requested : {},
|
|
github: typeof raw.github === 'string' ? raw.github : '',
|
|
actions: Array.isArray(raw.actions) ? raw.actions : [],
|
|
lead: typeof raw.lead === 'string' ? raw.lead : '',
|
|
since: typeof raw.since === 'string' ? raw.since : '',
|
|
}
|
|
}
|
|
|
|
/** Read the authenticated user's beta record and waitlist standing. */
|
|
export async function standing(): Promise<BetaStanding | null> {
|
|
const headers = scope()
|
|
if (!headers.Authorization) return null
|
|
try {
|
|
const res = await fetch(`${api()}/v1/marketing/beta`, { headers })
|
|
if (!res.ok) return null
|
|
const body = (await res.json()) as Partial<BetaStanding>
|
|
return normalizeStanding(body)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/** Save program, answers, and requested betas on the user's record. */
|
|
export async function keep(payload: {
|
|
program: Program
|
|
answers?: BetaAnswer[]
|
|
betas?: string[]
|
|
lead?: string
|
|
}): Promise<BetaStanding | null> {
|
|
const headers = scope()
|
|
if (!headers.Authorization) return null
|
|
try {
|
|
const res = await fetch(`${api()}/v1/marketing/beta`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json', ...headers },
|
|
body: JSON.stringify(payload),
|
|
})
|
|
if (!res.ok) return null
|
|
const body = (await res.json()) as Partial<BetaStanding>
|
|
return normalizeStanding(body)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/** Request access to one or more ecosystem betas (or 'alpha'). Also persists locally when signed out. */
|
|
export async function ask(betas: string[]): Promise<BetaStanding | null> {
|
|
const clean = betas.map((b) => b.trim().toLowerCase()).filter(Boolean)
|
|
if (!clean.length) return null
|
|
|
|
const existingLocal = getLocalRequestedBetas()
|
|
setLocalRequestedBetas([...existingLocal, ...clean])
|
|
|
|
const headers = scope()
|
|
if (!headers.Authorization) return null
|
|
try {
|
|
const res = await fetch(`${api()}/v1/marketing/beta/request`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', ...headers },
|
|
body: JSON.stringify({ betas: clean }),
|
|
})
|
|
if (!res.ok) return null
|
|
const body = (await res.json()) as Partial<BetaStanding>
|
|
return normalizeStanding(body)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|