Skip to content
File

Blob: src/client/lib/config-provider.tsx

typescript73 lines
1import { type ReactNode, useEffect, useState } from "react";
2 
3import {
4 ClientConfigContext,
5 type ClientConfig,
6 type ClientConfigState,
7 type SocialProvider,
8} from "@/client/lib/config";
9import { apiRequest } from "@/client/lib/http";
10 
11const isSocialProvider = (value: unknown): value is SocialProvider => value === "github" || value === "google";
12 
13const fetchClientConfig = async (): Promise<ClientConfig> => {
14 try {
15 const res = await apiRequest("/api/config");
16 if (!res.ok) return { status: "unavailable" };
17 const body = (await res.json()) as {
18 turnstileSiteKey?: unknown;
19 issuer?: unknown;
20 socialProviders?: unknown;
21 operatorName?: unknown;
22 operatorContactEmail?: unknown;
23 };
24 if (typeof body.turnstileSiteKey !== "string" || body.turnstileSiteKey.length === 0) {
25 return { status: "unavailable" };
26 }
27 if (typeof body.issuer !== "string" || body.issuer.length === 0) {
28 return { status: "unavailable" };
29 }
30 if (typeof body.operatorName !== "string" || body.operatorName.length === 0) {
31 return { status: "unavailable" };
32 }
33 if (typeof body.operatorContactEmail !== "string" || body.operatorContactEmail.length === 0) {
34 return { status: "unavailable" };
35 }
36 const socialProviders = Array.isArray(body.socialProviders) ? body.socialProviders.filter(isSocialProvider) : [];
37 return {
38 status: "ok",
39 turnstileSiteKey: body.turnstileSiteKey,
40 issuer: body.issuer,
41 socialProviders,
42 operatorName: body.operatorName,
43 operatorContactEmail: body.operatorContactEmail,
44 };
45 } catch {
46 return { status: "unavailable" };
47 }
48};
49 
50interface ClientConfigProviderProps {
51 children: ReactNode;
52}
53 
54// Single source for /api/config across the app. One round-trip per
55// page load is shared across every consumer that calls useClientConfig,
56// so each component reads the same resolved value without duplicating
57// the fetch effect.
58export const ClientConfigProvider = ({ children }: ClientConfigProviderProps) => {
59 const [config, setConfig] = useState<ClientConfigState>(null);
60 
61 useEffect(() => {
62 let cancelled = false;
63 fetchClientConfig().then((value) => {
64 if (!cancelled) setConfig(value);
65 });
66 return () => {
67 cancelled = true;
68 };
69 }, []);
70 
71 return <ClientConfigContext.Provider value={config}>{children}</ClientConfigContext.Provider>;
72};