import Cookies, { CookieAttributes } from 'js-cookie' import { v4 as uuid } from '@lukeed/uuid' import { isEqual } from 'es-toolkit' import { BootstrapData } from '../api/bootstrap' import { allowedCookieDomain, removeCookie, setCookie } from './cookies' import { topDomain } from './top-domain' const ANON_KEY = 'ko_id' const EMAIL_KEY = 'ko_e' const TRAITS_KEY = 'kl:traits' let domain: string | undefined = undefined try { domain = topDomain(new URL(window.location.href)) } catch (_) { domain = undefined } export interface Traits { referrer?: string utmParams?: Record email?: string utk?: string } export interface User { id?: string | null traits: Traits } export interface UserStoreOptions { cookies?: CookieAttributes } export function getUserId() { return window.localStorage.getItem(ANON_KEY) || Cookies.get(ANON_KEY) } export function getUserEmail() { return window.localStorage.getItem(EMAIL_KEY) || Cookies.get(EMAIL_KEY) } export function getTraits(): Traits { try { return JSON.parse(window.localStorage.getItem(TRAITS_KEY) || '{}') } catch (_err) { return {} } } export class UserStore { cookieDefaults: CookieAttributes = { expires: 365, // one year domain, path: '/', sameSite: 'lax' } constructor(options?: UserStoreOptions) { this.cookieDefaults = { ...this.cookieDefaults, ...options?.cookies } } cookiesAllowed() { return allowedCookieDomain(domain, this.cookieDefaults.domain) } // gets or sets a uuid id() { const id = getUserId() if (id) { return id } return this.setId(uuid()) } setId(id: string) { // do not set cookie if the domain is not at least the same top level domain if (this.cookiesAllowed()) { setCookie(ANON_KEY, id, this.cookieDefaults) } else { removeCookie(ANON_KEY, this.cookieDefaults) } window.localStorage.setItem(ANON_KEY, id) return id } setEmail(email?: string) { if (!email) return window.localStorage.setItem(EMAIL_KEY, email) if (this.cookiesAllowed()) { setCookie(EMAIL_KEY, email, this.cookieDefaults) } return email } email() { let e = getUserEmail() // backfill if it exists in traits if (!e && this.traits().email) { e = this.setEmail(this.traits().email) } return e } traits() { return getTraits() } upsertTraits(toUpsert: Traits): Traits { const existing = this.traits() const newTraits = { ...existing, ...toUpsert } // Store the identifier in both cookie and localStorage if it exists for cross-subdomain hydration if (newTraits.email) { this.setEmail(newTraits.email) } window.localStorage.setItem(TRAITS_KEY, JSON.stringify(newTraits)) return newTraits } netNewTraits(toDiff: Traits) { const existingTraits = this.traits() as Record const incomingTraits = { ...toDiff } as Record Object.keys(incomingTraits).forEach((key) => { if (isEqual(existingTraits[key], incomingTraits[key])) { delete incomingTraits[key] } }) return incomingTraits } userInfo(): User { return { id: this.id(), traits: this.traits() } } reset() { window.localStorage.removeItem(ANON_KEY) window.localStorage.removeItem(TRAITS_KEY) window.localStorage.removeItem(EMAIL_KEY) removeCookie(ANON_KEY, this.cookieDefaults) removeCookie(EMAIL_KEY, this.cookieDefaults) } } export const user = (options?: BootstrapData) => new UserStore({ cookies: options?.sdk_settings?.cookie_defaults })