mirror of
https://github.com/hanzoai/bootnode.git
synced 2026-10-25 02:31:30 +00:00
Remove the hardcoded 5-brand catalog from web/lib/brand.ts; resolve the active brand at runtime by hostname from an external catalog (window.__BRAND_CATALOG__ / /config.json), mirroring the @hanzo/id SPA. Ship one neutral built-in bootnode fallback. Add BrandProvider that loads the per-deploy catalog client-side after hydration. Co-authored-by: Hanzo Dev <[email protected]>
199 lines
6.7 KiB
TypeScript
199 lines
6.7 KiB
TypeScript
// White-label branding — runtime-resolved, brand-neutral source.
|
|
//
|
|
// This module knows ONE brand: bootnode's own neutral defaults (the built-in
|
|
// fallback). Every real brand (lux / zoo / pars / hanzo / …) is supplied at
|
|
// RUNTIME from an external catalog injected per-deploy — bootnode the app ships
|
|
// no brand names, domains, or links in source.
|
|
//
|
|
// Mechanism (mirrors the @hanzo/id SPA's runtime config loader):
|
|
// 1. The deploy serves `/config.json` and/or injects a `window.__BRAND_CATALOG__`
|
|
// global (a host->BrandConfig map, or a string of one) into the page.
|
|
// 2. `applyBrandCatalog()` ingests it; `getBrandKey()` resolves the active
|
|
// brand by `window.location.hostname` against the catalog.
|
|
// 3. `getBrand()` returns the resolved BrandConfig, falling back to the
|
|
// built-in neutral `bootnode` defaults when no catalog entry matches.
|
|
//
|
|
// The full multi-brand catalog lives in the DEPLOY config (universe / CF-Pages),
|
|
// NOT in this repo. See `public/config.json` for the contract + the README note
|
|
// in the PR for where the per-host catalog is provisioned.
|
|
|
|
export type BrandConfig = {
|
|
name: string
|
|
tagline: string
|
|
description: string
|
|
logo: string
|
|
logoIcon: string
|
|
logoWhite: string
|
|
favicon: string
|
|
domain: string
|
|
apiUrl: string
|
|
wsUrl: string
|
|
statusUrl: string
|
|
defaultTheme: "light" | "dark"
|
|
colors: {
|
|
primary: string
|
|
primaryForeground: string
|
|
accent: string // Hero gradient start
|
|
accentEnd: string // Hero gradient end
|
|
}
|
|
social: {
|
|
twitter?: string
|
|
github?: string
|
|
discord?: string
|
|
}
|
|
iam: {
|
|
url: string
|
|
clientId: string
|
|
domain: string // e.g., hanzo.id, zoo.id
|
|
}
|
|
footerLinks: {
|
|
name: string
|
|
href: string
|
|
external?: boolean
|
|
}[]
|
|
// Ecosystem apps — retail-facing services linked from dashboard
|
|
ecosystemApps: {
|
|
name: string
|
|
href: string
|
|
description: string
|
|
icon: string // lucide icon name
|
|
category: "explore" | "defi" | "tools" | "governance"
|
|
}[]
|
|
}
|
|
|
|
// Runtime catalog: host substring -> brand key, plus key -> BrandConfig.
|
|
// Both are empty in source. Populated at runtime by `applyBrandCatalog()` from
|
|
// the deploy-injected config (see public/config.json for the shape).
|
|
export type BrandCatalog = {
|
|
// Ordered host matchers. First whose `match` is a substring of the hostname wins.
|
|
hosts?: { match: string; key: string }[]
|
|
// Brand definitions, keyed by brand key (e.g. "lux", "zoo").
|
|
brands?: Record<string, BrandConfig>
|
|
// Default brand key for unknown hosts (defaults to "bootnode").
|
|
defaultKey?: string
|
|
}
|
|
|
|
// The ONE built-in brand: bootnode's neutral defaults. Used for dev, SSG, and
|
|
// any host with no catalog entry. Intentionally generic — no third-party brand.
|
|
const BOOTNODE_DEFAULT: BrandConfig = {
|
|
name: "Bootnode",
|
|
tagline: "Blockchain Infrastructure for Developers",
|
|
description:
|
|
"The complete blockchain development platform. Multi-chain RPC, Token APIs, NFT APIs, Smart Wallets, Webhooks, and more.",
|
|
logo: "/logo.svg",
|
|
logoIcon: "/logo.svg",
|
|
logoWhite: "/logo.svg",
|
|
favicon: "/logo.svg",
|
|
domain: "bootno.de",
|
|
apiUrl: "https://api.bootno.de",
|
|
wsUrl: "wss://ws.bootno.de",
|
|
statusUrl: "https://status.bootno.de",
|
|
defaultTheme: "light",
|
|
colors: {
|
|
primary: "#000000",
|
|
primaryForeground: "#ffffff",
|
|
accent: "#3b82f6",
|
|
accentEnd: "#06b6d4",
|
|
},
|
|
social: {},
|
|
iam: {
|
|
url: "https://hanzo.id",
|
|
clientId: "bootnode-web",
|
|
domain: "hanzo.id",
|
|
},
|
|
footerLinks: [],
|
|
ecosystemApps: [],
|
|
}
|
|
|
|
const DEFAULT_KEY = "bootnode"
|
|
|
|
// Active runtime catalog. Starts empty (source ships no brands). The deploy
|
|
// fills it via `applyBrandCatalog()` before/at hydration.
|
|
let catalog: BrandCatalog = {}
|
|
|
|
/**
|
|
* Parse a raw catalog value (object or JSON string) into a BrandCatalog.
|
|
* Returns {} on any error — never throws (mirrors id SPA's `parseCatalog`).
|
|
*/
|
|
export function parseBrandCatalog(raw: unknown): BrandCatalog {
|
|
if (!raw) return {}
|
|
try {
|
|
const obj = typeof raw === "string" ? JSON.parse(raw) : raw
|
|
if (!obj || typeof obj !== "object") return {}
|
|
return obj as BrandCatalog
|
|
} catch {
|
|
return {}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install the runtime brand catalog. Idempotent — safe to call repeatedly
|
|
* (e.g. once from the inline boot script, once after the /config.json fetch).
|
|
* Accepts either a parsed object or a raw JSON string.
|
|
*/
|
|
export function applyBrandCatalog(raw: unknown): void {
|
|
const next = parseBrandCatalog(raw)
|
|
if (Object.keys(next).length > 0) catalog = next
|
|
}
|
|
|
|
/**
|
|
* Read the catalog injected as a global by the deploy
|
|
* (`window.__BRAND_CATALOG__`). Synchronous — available before the first
|
|
* render when the deploy inlines it in the document head.
|
|
*/
|
|
function readInjectedCatalog(): BrandCatalog {
|
|
if (typeof window === "undefined") return {}
|
|
const w = window as unknown as { __BRAND_CATALOG__?: unknown }
|
|
return parseBrandCatalog(w.__BRAND_CATALOG__)
|
|
}
|
|
|
|
function activeCatalog(): BrandCatalog {
|
|
if (catalog.brands || catalog.hosts) return catalog
|
|
return readInjectedCatalog()
|
|
}
|
|
|
|
/**
|
|
* Resolve the active brand key from the hostname against the runtime catalog.
|
|
* No hardcoded host switches — the host->key table is data, supplied at deploy.
|
|
*/
|
|
function getBrandKey(): string {
|
|
const cat = activeCatalog()
|
|
const fallback = cat.defaultKey || DEFAULT_KEY
|
|
if (typeof window === "undefined") return fallback
|
|
const hostname = window.location.hostname.toLowerCase()
|
|
for (const rule of cat.hosts ?? []) {
|
|
if (rule.match && hostname.includes(rule.match.toLowerCase())) return rule.key
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
// Export the active brand configuration. Synchronous so module-level callers
|
|
// (`const brand = getBrand()`) keep working; resolves against the injected
|
|
// catalog when present, else the built-in neutral default.
|
|
export function getBrand(): BrandConfig {
|
|
const cat = activeCatalog()
|
|
const key = getBrandKey()
|
|
return cat.brands?.[key] ?? cat.brands?.[DEFAULT_KEY] ?? BOOTNODE_DEFAULT
|
|
}
|
|
|
|
// Export brand key for conditional rendering / per-brand local maps
|
|
// (SDK package names, region data, localStorage namespacing).
|
|
export function getBrandKey_(): string {
|
|
return getBrandKey()
|
|
}
|
|
|
|
// Check if the active brand is the Hanzo brand.
|
|
export function isHanzoBrand(): boolean {
|
|
return getBrandKey() === "hanzo"
|
|
}
|
|
|
|
// Get all brands currently known at runtime (for admin/testing). Empty until a
|
|
// catalog is injected — source ships no brands.
|
|
export function getAllBrands(): Record<string, BrandConfig> {
|
|
return activeCatalog().brands ?? { [DEFAULT_KEY]: BOOTNODE_DEFAULT }
|
|
}
|
|
|
|
// Default export for convenience (neutral default at module-eval / SSG time;
|
|
// the BrandProvider re-resolves client-side once the runtime catalog loads).
|
|
export const brand = getBrand()
|