@enterprisestandard/react 0.0.20-beta.20260620.3 → 0.0.20-beta.20260623.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +18 -1
- package/dist/index.js +712 -1
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export * from "@enterprisestandard/core";
|
|
2
|
+
import { generateULID } from "@enterprisestandard/core";
|
|
2
3
|
import { Customer, Logger } from "@enterprisestandard/core";
|
|
3
4
|
import { ReactNode as ReactNode2 } from "react";
|
|
4
5
|
type StorageType = "local" | "session" | "memory";
|
|
@@ -130,4 +131,20 @@ type TenantProviderProps = {
|
|
|
130
131
|
declare function createTenantUrls(id: string, strategy?: TenantRoutingStrategy): TenantUrls;
|
|
131
132
|
declare function TenantProvider({ id, strategy, children }: TenantProviderProps): React.ReactNode;
|
|
132
133
|
declare function useTenant(): TenantContextValue;
|
|
133
|
-
|
|
134
|
+
import { TenantDirectoryResponse, TenantResponse } from "@enterprisestandard/core";
|
|
135
|
+
type UseTenantDirectoryOptions = {
|
|
136
|
+
tenantManagerBaseUrl?: string;
|
|
137
|
+
start?: number;
|
|
138
|
+
limit?: number;
|
|
139
|
+
order?: string;
|
|
140
|
+
};
|
|
141
|
+
declare function useTenantDirectory<TTenant extends TenantResponse = TenantResponse>(options?: UseTenantDirectoryOptions): {
|
|
142
|
+
directory: TenantDirectoryResponse<TTenant> | null;
|
|
143
|
+
tenants: Record<string, TTenant>;
|
|
144
|
+
ids: string[];
|
|
145
|
+
items: TTenant[];
|
|
146
|
+
isLoading: boolean;
|
|
147
|
+
error: Error | null;
|
|
148
|
+
refresh: () => Promise<void>;
|
|
149
|
+
};
|
|
150
|
+
export { useTenantDirectory, useTenant, useSSOContext, useOptionalSSOContext, useOptionalCIAMContext, useCIAMProviders, useCIAMContext, logoutCustomer, logout, getUser, getCustomer, generateULID, createTenantUrls, UseTenantDirectoryOptions, UseCIAMProvidersResult, TenantProvider, SignedOut, SignedIn, SignInLoading, SSOProvider, CIAMProviderListProps, CIAMProviderList, CIAMProvider };
|
package/dist/index.js
CHANGED
|
@@ -1 +1,712 @@
|
|
|
1
|
-
|
|
1
|
+
// packages/react/src/index.ts
|
|
2
|
+
export * from "@enterprisestandard/core";
|
|
3
|
+
import { generateULID } from "@enterprisestandard/core";
|
|
4
|
+
|
|
5
|
+
// packages/react/src/ciam-provider.tsx
|
|
6
|
+
import { silentLogger } from "@enterprisestandard/core";
|
|
7
|
+
import { createContext, useCallback, useContext, useEffect, useState } from "react";
|
|
8
|
+
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
9
|
+
var CTX = createContext(undefined);
|
|
10
|
+
var VALID_STORAGE_KEY = /^[a-zA-Z0-9._-]{1,1024}$/;
|
|
11
|
+
function validateStorageKey(key) {
|
|
12
|
+
if (!key.trim()) {
|
|
13
|
+
throw new Error("CIAMProvider storageKey must be a non-empty string.");
|
|
14
|
+
}
|
|
15
|
+
if (!VALID_STORAGE_KEY.test(key)) {
|
|
16
|
+
throw new Error("CIAMProvider storageKey must be 1-1024 characters and contain only letters, numbers, dots, underscores, and hyphens.");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
var getStorageKey = (storageKey) => {
|
|
20
|
+
if (storageKey === undefined || storageKey === "") {
|
|
21
|
+
return "es.ciam.customer";
|
|
22
|
+
}
|
|
23
|
+
validateStorageKey(storageKey);
|
|
24
|
+
return storageKey;
|
|
25
|
+
};
|
|
26
|
+
function loadCustomerFromStorageForKey(storage, storageKey, log = silentLogger) {
|
|
27
|
+
if (typeof window === "undefined")
|
|
28
|
+
return null;
|
|
29
|
+
try {
|
|
30
|
+
const storageObject = storage === "local" ? localStorage : sessionStorage;
|
|
31
|
+
const customerData = storageObject.getItem(storageKey);
|
|
32
|
+
if (!customerData)
|
|
33
|
+
return null;
|
|
34
|
+
const parsedCustomer = JSON.parse(customerData);
|
|
35
|
+
if (parsedCustomer.ciam?.expires) {
|
|
36
|
+
parsedCustomer.ciam.expires = new Date(parsedCustomer.ciam.expires);
|
|
37
|
+
}
|
|
38
|
+
return parsedCustomer;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
log.error("Error loading customer from storage:", error);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function fetchCustomerFromServer(customerUrl, log = silentLogger) {
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch(customerUrl);
|
|
47
|
+
if (response.status === 401) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
throw new Error(`Failed to fetch customer: ${response.status} ${response.statusText}`);
|
|
52
|
+
}
|
|
53
|
+
const customerData = await response.json();
|
|
54
|
+
if (customerData.ciam?.expires && typeof customerData.ciam.expires === "string") {
|
|
55
|
+
customerData.ciam.expires = new Date(customerData.ciam.expires);
|
|
56
|
+
}
|
|
57
|
+
return customerData;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
log.error("Error fetching customer from URL:", error);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function CIAMProvider({
|
|
64
|
+
storageKey: storageKeyProp,
|
|
65
|
+
customerUrl,
|
|
66
|
+
storage = "memory",
|
|
67
|
+
disableListener = false,
|
|
68
|
+
log = silentLogger,
|
|
69
|
+
children
|
|
70
|
+
}) {
|
|
71
|
+
const [customer, setCustomerState] = useState(null);
|
|
72
|
+
const [isLoading, setIsLoading] = useState(!!customerUrl);
|
|
73
|
+
const storageKey = getStorageKey(storageKeyProp);
|
|
74
|
+
const loadCustomerFromStorage = useCallback(() => {
|
|
75
|
+
if (storage === "memory")
|
|
76
|
+
return null;
|
|
77
|
+
return loadCustomerFromStorageForKey(storage, storageKey, log);
|
|
78
|
+
}, [storage, storageKey, log]);
|
|
79
|
+
const saveCustomerToStorage = useCallback((customer2) => {
|
|
80
|
+
if (storage === "memory" || typeof window === "undefined")
|
|
81
|
+
return;
|
|
82
|
+
try {
|
|
83
|
+
const storageObject = storage === "local" ? localStorage : sessionStorage;
|
|
84
|
+
if (customer2 === null) {
|
|
85
|
+
storageObject.removeItem(storageKey);
|
|
86
|
+
} else {
|
|
87
|
+
storageObject.setItem(storageKey, JSON.stringify(customer2));
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
log.error("Error saving customer to storage:", error);
|
|
91
|
+
}
|
|
92
|
+
}, [storage, storageKey, log]);
|
|
93
|
+
const setCustomer = useCallback((newCustomer) => {
|
|
94
|
+
setCustomerState(newCustomer);
|
|
95
|
+
saveCustomerToStorage(newCustomer);
|
|
96
|
+
}, [saveCustomerToStorage]);
|
|
97
|
+
const fetchCustomerFromUrl = useCallback(async () => {
|
|
98
|
+
if (!customerUrl)
|
|
99
|
+
return;
|
|
100
|
+
setIsLoading(true);
|
|
101
|
+
try {
|
|
102
|
+
const customerData = await fetchCustomerFromServer(customerUrl, log);
|
|
103
|
+
setCustomerState(customerData);
|
|
104
|
+
saveCustomerToStorage(customerData);
|
|
105
|
+
} finally {
|
|
106
|
+
setIsLoading(false);
|
|
107
|
+
}
|
|
108
|
+
}, [customerUrl, saveCustomerToStorage, log]);
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const storedCustomer = loadCustomerFromStorage();
|
|
111
|
+
if (storedCustomer) {
|
|
112
|
+
setCustomerState(storedCustomer);
|
|
113
|
+
}
|
|
114
|
+
if (customerUrl) {
|
|
115
|
+
fetchCustomerFromUrl();
|
|
116
|
+
} else {
|
|
117
|
+
setIsLoading(false);
|
|
118
|
+
}
|
|
119
|
+
}, [loadCustomerFromStorage, customerUrl, fetchCustomerFromUrl]);
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (disableListener || storage === "memory")
|
|
122
|
+
return;
|
|
123
|
+
const handleStorageChange = (event) => {
|
|
124
|
+
if (event.key !== storageKey)
|
|
125
|
+
return;
|
|
126
|
+
if (event.newValue === null) {
|
|
127
|
+
setCustomerState(null);
|
|
128
|
+
} else {
|
|
129
|
+
try {
|
|
130
|
+
const parsedCustomer = JSON.parse(event.newValue);
|
|
131
|
+
if (parsedCustomer.ciam?.expires) {
|
|
132
|
+
parsedCustomer.ciam.expires = new Date(parsedCustomer.ciam.expires);
|
|
133
|
+
}
|
|
134
|
+
setCustomerState(parsedCustomer);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
log.error("Error parsing customer from storage event:", error);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const handleLogout = () => {
|
|
141
|
+
setCustomerState(null);
|
|
142
|
+
};
|
|
143
|
+
window.addEventListener("storage", handleStorageChange);
|
|
144
|
+
window.addEventListener("es-ciam-logout", handleLogout);
|
|
145
|
+
return () => {
|
|
146
|
+
window.removeEventListener("storage", handleStorageChange);
|
|
147
|
+
window.removeEventListener("es-ciam-logout", handleLogout);
|
|
148
|
+
};
|
|
149
|
+
}, [disableListener, storage, storageKey, log]);
|
|
150
|
+
const contextValue = {
|
|
151
|
+
customer,
|
|
152
|
+
setCustomer,
|
|
153
|
+
isLoading
|
|
154
|
+
};
|
|
155
|
+
return /* @__PURE__ */ jsxDEV(CTX.Provider, {
|
|
156
|
+
value: contextValue,
|
|
157
|
+
children
|
|
158
|
+
}, undefined, false, undefined, this);
|
|
159
|
+
}
|
|
160
|
+
function useCIAMContext() {
|
|
161
|
+
const context = useContext(CTX);
|
|
162
|
+
if (context === undefined) {
|
|
163
|
+
throw new Error("useCIAMContext must be used within a CIAMProvider");
|
|
164
|
+
}
|
|
165
|
+
return context;
|
|
166
|
+
}
|
|
167
|
+
function useOptionalCIAMContext() {
|
|
168
|
+
return useContext(CTX);
|
|
169
|
+
}
|
|
170
|
+
async function getCustomer(customerUrl, storageKey) {
|
|
171
|
+
const key = getStorageKey(storageKey);
|
|
172
|
+
const fromLocal = loadCustomerFromStorageForKey("local", key);
|
|
173
|
+
if (fromLocal)
|
|
174
|
+
return fromLocal;
|
|
175
|
+
const fromSession = loadCustomerFromStorageForKey("session", key);
|
|
176
|
+
if (fromSession)
|
|
177
|
+
return fromSession;
|
|
178
|
+
if (customerUrl) {
|
|
179
|
+
const fromServer = await fetchCustomerFromServer(customerUrl);
|
|
180
|
+
if (fromServer)
|
|
181
|
+
return fromServer;
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
async function logoutCustomer(logoutUrl) {
|
|
186
|
+
try {
|
|
187
|
+
const response = await fetch(logoutUrl, {
|
|
188
|
+
headers: { Accept: "application/json" }
|
|
189
|
+
});
|
|
190
|
+
if (!response.ok) {
|
|
191
|
+
return { success: false, error: `HTTP ${response.status}` };
|
|
192
|
+
}
|
|
193
|
+
const data = await response.json();
|
|
194
|
+
if (!data.success) {
|
|
195
|
+
return { success: false, error: data.message || "Logout failed" };
|
|
196
|
+
}
|
|
197
|
+
if (typeof window !== "undefined") {
|
|
198
|
+
localStorage.removeItem("es.ciam.customer");
|
|
199
|
+
sessionStorage.removeItem("es.ciam.customer");
|
|
200
|
+
window.dispatchEvent(new CustomEvent("es-ciam-logout"));
|
|
201
|
+
}
|
|
202
|
+
return { success: true };
|
|
203
|
+
} catch (error) {
|
|
204
|
+
return {
|
|
205
|
+
success: false,
|
|
206
|
+
error: error instanceof Error ? error.message : "Network error"
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// packages/react/src/ciam-providers.tsx
|
|
211
|
+
import { silentLogger as silentLogger2 } from "@enterprisestandard/core";
|
|
212
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useState as useState2 } from "react";
|
|
213
|
+
import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
|
|
214
|
+
function useCIAMProviders(providersUrl, log = silentLogger2) {
|
|
215
|
+
const [providers, setProviders] = useState2([]);
|
|
216
|
+
const [isLoading, setIsLoading] = useState2(Boolean(providersUrl));
|
|
217
|
+
const [error, setError] = useState2(null);
|
|
218
|
+
const refresh = useCallback2(() => {
|
|
219
|
+
if (!providersUrl)
|
|
220
|
+
return;
|
|
221
|
+
let cancelled = false;
|
|
222
|
+
setIsLoading(true);
|
|
223
|
+
setError(null);
|
|
224
|
+
fetch(providersUrl, { headers: { Accept: "application/json" } }).then(async (response) => {
|
|
225
|
+
if (!response.ok) {
|
|
226
|
+
throw new Error(`Failed to fetch CIAM providers: ${response.status} ${response.statusText}`);
|
|
227
|
+
}
|
|
228
|
+
const data = await response.json();
|
|
229
|
+
if (!cancelled)
|
|
230
|
+
setProviders(Array.isArray(data.providers) ? data.providers : []);
|
|
231
|
+
}).catch((err) => {
|
|
232
|
+
const normalized = err instanceof Error ? err : new Error(String(err));
|
|
233
|
+
log.error("Error fetching CIAM providers:", normalized);
|
|
234
|
+
if (!cancelled)
|
|
235
|
+
setError(normalized);
|
|
236
|
+
}).finally(() => {
|
|
237
|
+
if (!cancelled)
|
|
238
|
+
setIsLoading(false);
|
|
239
|
+
});
|
|
240
|
+
return () => {
|
|
241
|
+
cancelled = true;
|
|
242
|
+
};
|
|
243
|
+
}, [providersUrl, log]);
|
|
244
|
+
useEffect2(() => {
|
|
245
|
+
const cleanup = refresh();
|
|
246
|
+
return cleanup;
|
|
247
|
+
}, [refresh]);
|
|
248
|
+
return { providers, isLoading, error, refresh };
|
|
249
|
+
}
|
|
250
|
+
function providerButtonStyle(provider, theme) {
|
|
251
|
+
const background = theme === "dark" ? provider.colors?.dark : provider.colors?.light;
|
|
252
|
+
return {
|
|
253
|
+
display: "flex",
|
|
254
|
+
alignItems: "center",
|
|
255
|
+
gap: "0.5rem",
|
|
256
|
+
width: "100%",
|
|
257
|
+
padding: "0.625rem 0.875rem",
|
|
258
|
+
border: "1px solid rgba(0,0,0,0.15)",
|
|
259
|
+
borderRadius: "0.5rem",
|
|
260
|
+
background: background ?? (theme === "dark" ? "#1f1f1f" : "#ffffff"),
|
|
261
|
+
color: theme === "dark" ? "#ffffff" : "#111111",
|
|
262
|
+
textDecoration: "none",
|
|
263
|
+
font: "inherit",
|
|
264
|
+
cursor: "pointer"
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function CIAMProviderList({
|
|
268
|
+
providersUrl,
|
|
269
|
+
limit,
|
|
270
|
+
theme = "light",
|
|
271
|
+
renderProvider,
|
|
272
|
+
loadingFallback,
|
|
273
|
+
emptyFallback,
|
|
274
|
+
moreLabel = "Show more",
|
|
275
|
+
className,
|
|
276
|
+
log = silentLogger2
|
|
277
|
+
}) {
|
|
278
|
+
const { providers, isLoading } = useCIAMProviders(providersUrl, log);
|
|
279
|
+
const [expanded, setExpanded] = useState2(false);
|
|
280
|
+
if (isLoading) {
|
|
281
|
+
return loadingFallback ?? null;
|
|
282
|
+
}
|
|
283
|
+
if (providers.length === 0) {
|
|
284
|
+
return emptyFallback ?? null;
|
|
285
|
+
}
|
|
286
|
+
const visible = !expanded && typeof limit === "number" && limit > 0 ? providers.slice(0, limit) : providers;
|
|
287
|
+
const hasMore = visible.length < providers.length;
|
|
288
|
+
return /* @__PURE__ */ jsxDEV2("div", {
|
|
289
|
+
className,
|
|
290
|
+
style: { display: "flex", flexDirection: "column", gap: "0.5rem" },
|
|
291
|
+
"data-es-ciam-providers": "",
|
|
292
|
+
children: [
|
|
293
|
+
visible.map((provider) => renderProvider ? /* @__PURE__ */ jsxDEV2("span", {
|
|
294
|
+
children: renderProvider(provider)
|
|
295
|
+
}, provider.name, false, undefined, this) : /* @__PURE__ */ jsxDEV2("a", {
|
|
296
|
+
href: provider.loginUrl,
|
|
297
|
+
style: providerButtonStyle(provider, theme),
|
|
298
|
+
"data-es-ciam-provider": provider.name,
|
|
299
|
+
children: [
|
|
300
|
+
provider.iconUrl ? /* @__PURE__ */ jsxDEV2("img", {
|
|
301
|
+
src: provider.iconUrl,
|
|
302
|
+
alt: "",
|
|
303
|
+
width: 20,
|
|
304
|
+
height: 20,
|
|
305
|
+
"aria-hidden": "true"
|
|
306
|
+
}, undefined, false, undefined, this) : null,
|
|
307
|
+
/* @__PURE__ */ jsxDEV2("span", {
|
|
308
|
+
children: [
|
|
309
|
+
"Continue with ",
|
|
310
|
+
provider.displayName
|
|
311
|
+
]
|
|
312
|
+
}, undefined, true, undefined, this)
|
|
313
|
+
]
|
|
314
|
+
}, provider.name, true, undefined, this)),
|
|
315
|
+
hasMore ? /* @__PURE__ */ jsxDEV2("button", {
|
|
316
|
+
type: "button",
|
|
317
|
+
onClick: () => setExpanded(true),
|
|
318
|
+
"data-es-ciam-show-more": "",
|
|
319
|
+
children: moreLabel
|
|
320
|
+
}, undefined, false, undefined, this) : null
|
|
321
|
+
]
|
|
322
|
+
}, undefined, true, undefined, this);
|
|
323
|
+
}
|
|
324
|
+
// packages/react/src/sso-provider.tsx
|
|
325
|
+
import { silentLogger as silentLogger3 } from "@enterprisestandard/core";
|
|
326
|
+
import { createContext as createContext2, useCallback as useCallback3, useContext as useContext2, useEffect as useEffect3, useState as useState3 } from "react";
|
|
327
|
+
import { jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime";
|
|
328
|
+
var CTX2 = createContext2(undefined);
|
|
329
|
+
var VALID_STORAGE_KEY2 = /^[a-zA-Z0-9._-]{1,1024}$/;
|
|
330
|
+
function validateStorageKey2(key) {
|
|
331
|
+
if (!key.trim()) {
|
|
332
|
+
throw new Error("SSOProvider storageKey must be a non-empty string.");
|
|
333
|
+
}
|
|
334
|
+
if (!VALID_STORAGE_KEY2.test(key)) {
|
|
335
|
+
throw new Error("SSOProvider storageKey must be 1-1024 characters and contain only letters, numbers, dots, underscores, and hyphens.");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
var getStorageKey2 = (storageKey) => {
|
|
339
|
+
if (storageKey === undefined || storageKey === "") {
|
|
340
|
+
return "es.sso.user";
|
|
341
|
+
}
|
|
342
|
+
validateStorageKey2(storageKey);
|
|
343
|
+
return storageKey;
|
|
344
|
+
};
|
|
345
|
+
function loadUserFromStorageForKey(storage, storageKey, log = silentLogger3) {
|
|
346
|
+
if (typeof window === "undefined")
|
|
347
|
+
return null;
|
|
348
|
+
try {
|
|
349
|
+
const storageObject = storage === "local" ? localStorage : sessionStorage;
|
|
350
|
+
const userData = storageObject.getItem(storageKey);
|
|
351
|
+
if (!userData)
|
|
352
|
+
return null;
|
|
353
|
+
const parsedUser = JSON.parse(userData);
|
|
354
|
+
if (parsedUser.sso?.expires) {
|
|
355
|
+
parsedUser.sso.expires = new Date(parsedUser.sso.expires);
|
|
356
|
+
}
|
|
357
|
+
return parsedUser;
|
|
358
|
+
} catch (error) {
|
|
359
|
+
log.error("Error loading user from storage:", error);
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async function fetchUserFromServer(userUrl, log = silentLogger3) {
|
|
364
|
+
try {
|
|
365
|
+
const response = await fetch(userUrl);
|
|
366
|
+
if (response.status === 401) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
if (!response.ok) {
|
|
370
|
+
throw new Error(`Failed to fetch user: ${response.status} ${response.statusText}`);
|
|
371
|
+
}
|
|
372
|
+
const userData = await response.json();
|
|
373
|
+
if (userData.sso?.expires && typeof userData.sso.expires === "string") {
|
|
374
|
+
userData.sso.expires = new Date(userData.sso.expires);
|
|
375
|
+
}
|
|
376
|
+
return userData;
|
|
377
|
+
} catch (error) {
|
|
378
|
+
log.error("Error fetching user from URL:", error);
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function SSOProvider({
|
|
383
|
+
storageKey: storageKeyProp,
|
|
384
|
+
userUrl,
|
|
385
|
+
storage = "memory",
|
|
386
|
+
disableListener = false,
|
|
387
|
+
log = silentLogger3,
|
|
388
|
+
children
|
|
389
|
+
}) {
|
|
390
|
+
const [user, setUserState] = useState3(null);
|
|
391
|
+
const [isLoading, setIsLoading] = useState3(!!userUrl);
|
|
392
|
+
const storageKey = getStorageKey2(storageKeyProp);
|
|
393
|
+
const loadUserFromStorage = useCallback3(() => {
|
|
394
|
+
if (storage === "memory")
|
|
395
|
+
return null;
|
|
396
|
+
return loadUserFromStorageForKey(storage, storageKey, log);
|
|
397
|
+
}, [storage, storageKey, log]);
|
|
398
|
+
const saveUserToStorage = useCallback3((user2) => {
|
|
399
|
+
if (storage === "memory" || typeof window === "undefined")
|
|
400
|
+
return;
|
|
401
|
+
try {
|
|
402
|
+
const storageObject = storage === "local" ? localStorage : sessionStorage;
|
|
403
|
+
if (user2 === null) {
|
|
404
|
+
storageObject.removeItem(storageKey);
|
|
405
|
+
} else {
|
|
406
|
+
storageObject.setItem(storageKey, JSON.stringify(user2));
|
|
407
|
+
}
|
|
408
|
+
} catch (error) {
|
|
409
|
+
log.error("Error saving user to storage:", error);
|
|
410
|
+
}
|
|
411
|
+
}, [storage, storageKey, log]);
|
|
412
|
+
const setUser = useCallback3((newUser) => {
|
|
413
|
+
setUserState(newUser);
|
|
414
|
+
saveUserToStorage(newUser);
|
|
415
|
+
}, [saveUserToStorage]);
|
|
416
|
+
const fetchUserFromUrl = useCallback3(async () => {
|
|
417
|
+
if (!userUrl)
|
|
418
|
+
return;
|
|
419
|
+
setIsLoading(true);
|
|
420
|
+
try {
|
|
421
|
+
const userData = await fetchUserFromServer(userUrl, log);
|
|
422
|
+
setUserState(userData);
|
|
423
|
+
saveUserToStorage(userData);
|
|
424
|
+
} finally {
|
|
425
|
+
setIsLoading(false);
|
|
426
|
+
}
|
|
427
|
+
}, [userUrl, saveUserToStorage, log]);
|
|
428
|
+
useEffect3(() => {
|
|
429
|
+
const storedUser = loadUserFromStorage();
|
|
430
|
+
if (storedUser) {
|
|
431
|
+
setUserState(storedUser);
|
|
432
|
+
}
|
|
433
|
+
if (userUrl) {
|
|
434
|
+
fetchUserFromUrl();
|
|
435
|
+
} else {
|
|
436
|
+
setIsLoading(false);
|
|
437
|
+
}
|
|
438
|
+
}, [loadUserFromStorage, userUrl, fetchUserFromUrl]);
|
|
439
|
+
useEffect3(() => {
|
|
440
|
+
if (disableListener || storage === "memory")
|
|
441
|
+
return;
|
|
442
|
+
const handleStorageChange = (event) => {
|
|
443
|
+
if (event.key !== storageKey)
|
|
444
|
+
return;
|
|
445
|
+
if (event.newValue === null) {
|
|
446
|
+
setUserState(null);
|
|
447
|
+
} else {
|
|
448
|
+
try {
|
|
449
|
+
const parsedUser = JSON.parse(event.newValue);
|
|
450
|
+
if (parsedUser.sso?.expires) {
|
|
451
|
+
parsedUser.sso.expires = new Date(parsedUser.sso.expires);
|
|
452
|
+
}
|
|
453
|
+
setUserState(parsedUser);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
log.error("Error parsing user from storage event:", error);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
const handleLogout = () => {
|
|
460
|
+
setUserState(null);
|
|
461
|
+
};
|
|
462
|
+
window.addEventListener("storage", handleStorageChange);
|
|
463
|
+
window.addEventListener("es-sso-logout", handleLogout);
|
|
464
|
+
return () => {
|
|
465
|
+
window.removeEventListener("storage", handleStorageChange);
|
|
466
|
+
window.removeEventListener("es-sso-logout", handleLogout);
|
|
467
|
+
};
|
|
468
|
+
}, [disableListener, storage, storageKey, log]);
|
|
469
|
+
const contextValue = {
|
|
470
|
+
user,
|
|
471
|
+
setUser,
|
|
472
|
+
isLoading
|
|
473
|
+
};
|
|
474
|
+
return /* @__PURE__ */ jsxDEV3(CTX2.Provider, {
|
|
475
|
+
value: contextValue,
|
|
476
|
+
children
|
|
477
|
+
}, undefined, false, undefined, this);
|
|
478
|
+
}
|
|
479
|
+
function useSSOContext() {
|
|
480
|
+
const context = useContext2(CTX2);
|
|
481
|
+
if (context === undefined) {
|
|
482
|
+
throw new Error("useSSOContext must be used within a SSOProvider");
|
|
483
|
+
}
|
|
484
|
+
return context;
|
|
485
|
+
}
|
|
486
|
+
function useOptionalSSOContext() {
|
|
487
|
+
return useContext2(CTX2);
|
|
488
|
+
}
|
|
489
|
+
async function getUser(userUrl, storageKey) {
|
|
490
|
+
const key = getStorageKey2(storageKey);
|
|
491
|
+
const fromLocal = loadUserFromStorageForKey("local", key);
|
|
492
|
+
if (fromLocal)
|
|
493
|
+
return fromLocal;
|
|
494
|
+
const fromSession = loadUserFromStorageForKey("session", key);
|
|
495
|
+
if (fromSession)
|
|
496
|
+
return fromSession;
|
|
497
|
+
if (userUrl) {
|
|
498
|
+
const fromServer = await fetchUserFromServer(userUrl);
|
|
499
|
+
if (fromServer)
|
|
500
|
+
return fromServer;
|
|
501
|
+
}
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
async function logout(logoutUrl) {
|
|
505
|
+
try {
|
|
506
|
+
const response = await fetch(logoutUrl, {
|
|
507
|
+
headers: { Accept: "application/json" }
|
|
508
|
+
});
|
|
509
|
+
if (!response.ok) {
|
|
510
|
+
return { success: false, error: `HTTP ${response.status}` };
|
|
511
|
+
}
|
|
512
|
+
const data = await response.json();
|
|
513
|
+
if (!data.success) {
|
|
514
|
+
return { success: false, error: data.message || "Logout failed" };
|
|
515
|
+
}
|
|
516
|
+
if (typeof window !== "undefined") {
|
|
517
|
+
localStorage.removeItem("es.sso.user");
|
|
518
|
+
sessionStorage.removeItem("es.sso.user");
|
|
519
|
+
window.dispatchEvent(new CustomEvent("es-sso-logout"));
|
|
520
|
+
}
|
|
521
|
+
return { success: true };
|
|
522
|
+
} catch (error) {
|
|
523
|
+
return {
|
|
524
|
+
success: false,
|
|
525
|
+
error: error instanceof Error ? error.message : "Network error"
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// packages/react/src/sign-in-loading.tsx
|
|
531
|
+
import { jsxDEV as jsxDEV4, Fragment } from "react/jsx-dev-runtime";
|
|
532
|
+
function SignInLoading({
|
|
533
|
+
complete = false,
|
|
534
|
+
audience = "workforce",
|
|
535
|
+
children
|
|
536
|
+
}) {
|
|
537
|
+
const sso = useOptionalSSOContext();
|
|
538
|
+
const ciam = useOptionalCIAMContext();
|
|
539
|
+
const isLoading = audience === "customer" ? !!ciam?.isLoading : !!sso?.isLoading;
|
|
540
|
+
if (isLoading && !complete)
|
|
541
|
+
return /* @__PURE__ */ jsxDEV4(Fragment, {
|
|
542
|
+
children
|
|
543
|
+
}, undefined, false, undefined, this);
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
// packages/react/src/signed-in.tsx
|
|
547
|
+
import { jsxDEV as jsxDEV5, Fragment as Fragment2 } from "react/jsx-dev-runtime";
|
|
548
|
+
function SignedIn({
|
|
549
|
+
audience = "workforce",
|
|
550
|
+
children
|
|
551
|
+
}) {
|
|
552
|
+
const sso = useOptionalSSOContext();
|
|
553
|
+
const ciam = useOptionalCIAMContext();
|
|
554
|
+
const present = audience === "customer" ? !!ciam?.customer : !!sso?.user;
|
|
555
|
+
if (present)
|
|
556
|
+
return /* @__PURE__ */ jsxDEV5(Fragment2, {
|
|
557
|
+
children
|
|
558
|
+
}, undefined, false, undefined, this);
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
// packages/react/src/signed-out.tsx
|
|
562
|
+
import { jsxDEV as jsxDEV6, Fragment as Fragment3 } from "react/jsx-dev-runtime";
|
|
563
|
+
function SignedOut({
|
|
564
|
+
audience = "workforce",
|
|
565
|
+
children
|
|
566
|
+
}) {
|
|
567
|
+
const sso = useOptionalSSOContext();
|
|
568
|
+
const ciam = useOptionalCIAMContext();
|
|
569
|
+
const present = audience === "customer" ? !!ciam?.customer : !!sso?.user;
|
|
570
|
+
const isLoading = audience === "customer" ? !!ciam?.isLoading : !!sso?.isLoading;
|
|
571
|
+
if (present || isLoading)
|
|
572
|
+
return null;
|
|
573
|
+
return /* @__PURE__ */ jsxDEV6(Fragment3, {
|
|
574
|
+
children
|
|
575
|
+
}, undefined, false, undefined, this);
|
|
576
|
+
}
|
|
577
|
+
// packages/react/src/tenant-context.tsx
|
|
578
|
+
import {
|
|
579
|
+
buildTenantPath,
|
|
580
|
+
DEFAULT_TENANT_API_NAMESPACE,
|
|
581
|
+
DEFAULT_TENANT_UI_NAMESPACE,
|
|
582
|
+
normalizeTenantRoutingStrategy
|
|
583
|
+
} from "@enterprisestandard/core";
|
|
584
|
+
import { createContext as createContext3, useContext as useContext3, useMemo } from "react";
|
|
585
|
+
import { jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime";
|
|
586
|
+
var TenantContext = createContext3(null);
|
|
587
|
+
function createTenantUrls(id, strategy) {
|
|
588
|
+
const normalized = normalizeTenantRoutingStrategy(strategy);
|
|
589
|
+
if (normalized.type === "jwt") {
|
|
590
|
+
const uiSegments = normalized.ui?.segments ?? [];
|
|
591
|
+
const apiSegments = normalized.api?.segments ?? ["api"];
|
|
592
|
+
return {
|
|
593
|
+
ui: (restPath = "/") => buildStaticPath(uiSegments, restPath),
|
|
594
|
+
api: (restPath = "/") => buildStaticPath(apiSegments, restPath)
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
const uiNamespace = normalized.ui ?? DEFAULT_TENANT_UI_NAMESPACE;
|
|
598
|
+
const apiNamespace = normalized.api ?? DEFAULT_TENANT_API_NAMESPACE;
|
|
599
|
+
return {
|
|
600
|
+
ui: (restPath = "/") => buildTenantPath(id, restPath, uiNamespace),
|
|
601
|
+
api: (restPath = "/") => buildTenantPath(id, restPath, apiNamespace)
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function buildStaticPath(prefixSegments, restPath = "/") {
|
|
605
|
+
const prefix = prefixSegments.join("/");
|
|
606
|
+
const rest = restPath.trim().replace(/^\/+/, "");
|
|
607
|
+
if (!prefix && !rest)
|
|
608
|
+
return "/";
|
|
609
|
+
if (!prefix)
|
|
610
|
+
return `/${rest}`;
|
|
611
|
+
if (!rest)
|
|
612
|
+
return `/${prefix}`;
|
|
613
|
+
return `/${prefix}/${rest}`;
|
|
614
|
+
}
|
|
615
|
+
function TenantProvider({ id, strategy, children }) {
|
|
616
|
+
const value = useMemo(() => {
|
|
617
|
+
const normalized = normalizeTenantRoutingStrategy(strategy);
|
|
618
|
+
return {
|
|
619
|
+
id,
|
|
620
|
+
strategy: normalized,
|
|
621
|
+
urls: createTenantUrls(id, normalized)
|
|
622
|
+
};
|
|
623
|
+
}, [strategy, id]);
|
|
624
|
+
return /* @__PURE__ */ jsxDEV7(TenantContext.Provider, {
|
|
625
|
+
value,
|
|
626
|
+
children
|
|
627
|
+
}, undefined, false, undefined, this);
|
|
628
|
+
}
|
|
629
|
+
function useTenant() {
|
|
630
|
+
const value = useContext3(TenantContext);
|
|
631
|
+
if (!value) {
|
|
632
|
+
throw new Error("useTenant() must be used within a TenantProvider");
|
|
633
|
+
}
|
|
634
|
+
return value;
|
|
635
|
+
}
|
|
636
|
+
// packages/react/src/use-tenant-directory.ts
|
|
637
|
+
import { useCallback as useCallback4, useEffect as useEffect4, useMemo as useMemo2, useState as useState4 } from "react";
|
|
638
|
+
function useTenantDirectory(options = {}) {
|
|
639
|
+
const tenantManagerBaseUrl = options.tenantManagerBaseUrl ?? "/api/tenants/mine";
|
|
640
|
+
const [directory, setDirectory] = useState4(null);
|
|
641
|
+
const [isLoading, setIsLoading] = useState4(true);
|
|
642
|
+
const [error, setError] = useState4(null);
|
|
643
|
+
const requestUrl = useMemo2(() => {
|
|
644
|
+
const url = new URL(tenantManagerBaseUrl, "http://localhost");
|
|
645
|
+
if (options.start != null) {
|
|
646
|
+
url.searchParams.set("start", String(options.start));
|
|
647
|
+
}
|
|
648
|
+
if (options.limit != null) {
|
|
649
|
+
url.searchParams.set("limit", String(options.limit));
|
|
650
|
+
}
|
|
651
|
+
if (options.order) {
|
|
652
|
+
url.searchParams.set("order", options.order);
|
|
653
|
+
}
|
|
654
|
+
return `${url.pathname}${url.search}`;
|
|
655
|
+
}, [options.limit, options.order, options.start, tenantManagerBaseUrl]);
|
|
656
|
+
const refresh = useCallback4(async () => {
|
|
657
|
+
setIsLoading(true);
|
|
658
|
+
setError(null);
|
|
659
|
+
try {
|
|
660
|
+
const response = await fetch(requestUrl);
|
|
661
|
+
if (!response.ok) {
|
|
662
|
+
setDirectory(null);
|
|
663
|
+
setError(new Error(`Tenant directory request failed with status ${response.status}`));
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const data = await response.json();
|
|
667
|
+
setDirectory(data);
|
|
668
|
+
} catch (cause) {
|
|
669
|
+
setDirectory(null);
|
|
670
|
+
setError(cause instanceof Error ? cause : new Error("Failed to load tenant directory"));
|
|
671
|
+
} finally {
|
|
672
|
+
setIsLoading(false);
|
|
673
|
+
}
|
|
674
|
+
}, [requestUrl]);
|
|
675
|
+
useEffect4(() => {
|
|
676
|
+
refresh();
|
|
677
|
+
}, [refresh]);
|
|
678
|
+
const tenants = directory?.tenants ?? {};
|
|
679
|
+
const ids = directory?.ids ?? [];
|
|
680
|
+
const items = ids.map((id) => tenants[id]).filter((tenant) => !!tenant);
|
|
681
|
+
return {
|
|
682
|
+
directory,
|
|
683
|
+
tenants,
|
|
684
|
+
ids,
|
|
685
|
+
items,
|
|
686
|
+
isLoading,
|
|
687
|
+
error,
|
|
688
|
+
refresh
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
export {
|
|
692
|
+
useTenantDirectory,
|
|
693
|
+
useTenant,
|
|
694
|
+
useSSOContext,
|
|
695
|
+
useOptionalSSOContext,
|
|
696
|
+
useOptionalCIAMContext,
|
|
697
|
+
useCIAMProviders,
|
|
698
|
+
useCIAMContext,
|
|
699
|
+
logoutCustomer,
|
|
700
|
+
logout,
|
|
701
|
+
getUser,
|
|
702
|
+
getCustomer,
|
|
703
|
+
generateULID,
|
|
704
|
+
createTenantUrls,
|
|
705
|
+
TenantProvider,
|
|
706
|
+
SignedOut,
|
|
707
|
+
SignedIn,
|
|
708
|
+
SignInLoading,
|
|
709
|
+
SSOProvider,
|
|
710
|
+
CIAMProviderList,
|
|
711
|
+
CIAMProvider
|
|
712
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enterprisestandard/react",
|
|
3
|
-
"version": "0.0.20-beta.
|
|
3
|
+
"version": "0.0.20-beta.20260623.1",
|
|
4
4
|
"description": "Enterprise Standard React Components",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "enterprisestandard",
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"./package.json": "./package.json"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@enterprisestandard/core": "0.0.20-beta.
|
|
25
|
+
"@enterprisestandard/core": "0.0.20-beta.20260623.1"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"react": "^19.
|
|
29
|
-
"react-dom": "^19.
|
|
28
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
29
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
30
30
|
}
|
|
31
31
|
}
|