Files
zeekay 6c65e3e941 @hanzo/app 0.2.0: the app SDK, renamed from @hanzo/rooms
Every import, the catalog pin and the docs name @hanzo/app and
github.com/hanzoai/app. lib/rooms.ts is lib/app.ts and
components/rooms.tsx is components/app.tsx.
2026-10-22 19:07:20 -07:00

70 lines
2.5 KiB
TypeScript

import { scope } from '@hanzo/app/lib/session'
import { api } from '@hanzo/app/lib/api'
/**
* The landing half of an affiliate link.
*
* hanzoai/cloud apps/affiliate mints `https://hanzo.ai/?aff=<code>` (linkBase),
* so this origin is where a referred visitor arrives. `keep` holds the code
* across the sign-in round trip and counts the click (POST /v1/affiliate/click,
* public). `bind` names the visitor's organization to the affiliate once the
* session has one (POST /v1/affiliate/attribute, which takes the referred org
* from the validated caller). The server keeps first touch, so a repeat is a
* no-op there.
*/
/**
* The console's name for the same code (console lib/affiliates/claim.ts). Not
* `hanzo`-prefixed on purpose: `own()` empties those keys when the person
* changes, and signing up is exactly that change.
*/
const KEY = 'hz_aff'
/** Take `?aff=` off the address into storage and count the click once per tab. */
export function keep(): void {
const code = (new URLSearchParams(window.location.search).get('aff') ?? '').trim()
if (!code) return
try {
window.localStorage.setItem(KEY, code)
if (window.sessionStorage.getItem(`${KEY}:${code}`)) return
window.sessionStorage.setItem(`${KEY}:${code}`, '1')
} catch {
/* no storage: the click still counts, but nothing is left to bind */
}
void fetch(`${api()}/v1/affiliate/click`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
}).catch(() => {})
}
/**
* Attribute the kept code to the organization this browser works in.
*
* Nothing is sent without an organization: with none named, the caller would
* be whatever the token's owner claim says, which is the application's org and
* not the visitor's. The code is dropped on any final answer (bound, already
* bound, self, unknown) and kept for the next load on 401 or a server fault.
*/
export async function bind(): Promise<void> {
let code = ''
try {
code = (window.localStorage.getItem(KEY) ?? '').trim()
} catch {
return
}
const headers = scope()
if (!code || !headers.Authorization || !headers['X-Org-Id']) return
try {
const res = await fetch(`${api()}/v1/affiliate/attribute`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...headers },
body: JSON.stringify({ code }),
})
if (res.status === 401 || res.status >= 500) return
window.localStorage.removeItem(KEY)
} catch {
/* offline or refused: the code stays for the next load */
}
}