@ilivemylife/react-sdk 0.1.0

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.js ADDED
@@ -0,0 +1,631 @@
1
+ // src/ssoHelpers.ts
2
+ function getSsoLoginUrl(ssoUrl, options = {}) {
3
+ const { addTimestamp = true, route = "", clientId } = options;
4
+ const baseUrl = route ? `${ssoUrl}/${route}` : ssoUrl;
5
+ const params = new URLSearchParams();
6
+ if (addTimestamp) {
7
+ params.append("timestamp", Date.now().toString());
8
+ }
9
+ if (clientId) {
10
+ params.append("client_id", clientId);
11
+ }
12
+ const queryString = params.toString();
13
+ return queryString ? `${baseUrl}?${queryString}` : baseUrl;
14
+ }
15
+ function popupCenterParams(width, height) {
16
+ return {
17
+ left: window.screen.width / 2 - width / 2,
18
+ top: window.screen.height / 2 - height / 2
19
+ };
20
+ }
21
+ function isValidSsoOrigin(event, ssoUrl) {
22
+ try {
23
+ const expectedOrigin = new URL(ssoUrl).origin;
24
+ return event.origin === expectedOrigin;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ // src/useIlmlSso.ts
31
+ import { useCallback, useEffect, useRef } from "react";
32
+ var MESSENGER_TYPES = ["messengerReady", "messengerInitialized"];
33
+ function useIlmlSso(opts) {
34
+ const {
35
+ ssoUrl,
36
+ ssoRoute,
37
+ clientId,
38
+ onSsoData,
39
+ onError,
40
+ onClose,
41
+ responseType = "loginSuccess",
42
+ windowName = "ilmlSsoLogin",
43
+ width = 500,
44
+ height = 700
45
+ } = opts;
46
+ const loginWindowRef = useRef(null);
47
+ const processingRef = useRef(false);
48
+ const openLogin = useCallback(() => {
49
+ if (loginWindowRef.current && !loginWindowRef.current.closed) {
50
+ loginWindowRef.current.focus();
51
+ return;
52
+ }
53
+ processingRef.current = false;
54
+ const url = getSsoLoginUrl(ssoUrl, { route: ssoRoute, clientId });
55
+ const pos = popupCenterParams(width, height);
56
+ const popup = window.open(
57
+ url,
58
+ windowName,
59
+ `width=${width},height=${height},toolbar=no,location=no,directories=no,status=no,menubar=no,copyhistory=no,top=${pos.top},left=${pos.left}`
60
+ );
61
+ loginWindowRef.current = popup;
62
+ if (!onClose) return;
63
+ if (!popup) {
64
+ onClose();
65
+ return;
66
+ }
67
+ const poll = window.setInterval(() => {
68
+ if (popup.closed) {
69
+ window.clearInterval(poll);
70
+ if (!processingRef.current) onClose();
71
+ }
72
+ }, 300);
73
+ }, [ssoUrl, ssoRoute, clientId, windowName, width, height, onClose]);
74
+ const onMessage = useCallback(
75
+ async (event) => {
76
+ if (!isValidSsoOrigin(event, ssoUrl)) return;
77
+ if (MESSENGER_TYPES.includes(event.data?.type)) return;
78
+ let parsed;
79
+ try {
80
+ parsed = JSON.parse(event.data?.type);
81
+ } catch {
82
+ return;
83
+ }
84
+ if (parsed?.name === "loginError") {
85
+ onError?.("loginError");
86
+ return;
87
+ }
88
+ if (parsed?.name === "signupError") {
89
+ onError?.("signupError");
90
+ return;
91
+ }
92
+ if (parsed?.name === responseType) {
93
+ if (processingRef.current) return;
94
+ processingRef.current = true;
95
+ const data = event.data?.data;
96
+ if (data?.accessToken) {
97
+ try {
98
+ localStorage.setItem("walletAccessToken", data.accessToken);
99
+ } catch {
100
+ }
101
+ }
102
+ if (loginWindowRef.current && !loginWindowRef.current.closed) {
103
+ loginWindowRef.current.close();
104
+ }
105
+ await onSsoData(data ?? {});
106
+ }
107
+ },
108
+ [ssoUrl, responseType, onSsoData, onError]
109
+ );
110
+ useEffect(() => {
111
+ window.addEventListener("message", onMessage);
112
+ return () => window.removeEventListener("message", onMessage);
113
+ }, [onMessage]);
114
+ return { openLogin };
115
+ }
116
+
117
+ // src/IlmlChat.tsx
118
+ import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState } from "react";
119
+ import { jsx, jsxs } from "react/jsx-runtime";
120
+ function IlmlChat({ ssoUrl, nodeId, token, isNodeReady, className, style }) {
121
+ const iframeRef = useRef2(null);
122
+ const mountedRef = useRef2(true);
123
+ const [loading, setLoading] = useState(true);
124
+ useEffect2(() => {
125
+ mountedRef.current = true;
126
+ return () => {
127
+ mountedRef.current = false;
128
+ };
129
+ }, []);
130
+ useEffect2(() => {
131
+ setLoading(true);
132
+ }, [nodeId]);
133
+ const expectedOrigin = (() => {
134
+ try {
135
+ return new URL(ssoUrl).origin;
136
+ } catch {
137
+ return "";
138
+ }
139
+ })();
140
+ const resolveToken = useCallback2(() => {
141
+ if (token != null) return token;
142
+ try {
143
+ return localStorage.getItem("walletAccessToken");
144
+ } catch {
145
+ return null;
146
+ }
147
+ }, [token]);
148
+ const handshake = useCallback2(async () => {
149
+ if (isNodeReady) {
150
+ for (let attempt = 0; attempt < 15; attempt++) {
151
+ if (!mountedRef.current) return;
152
+ if (await isNodeReady(nodeId)) break;
153
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
154
+ }
155
+ }
156
+ const win = iframeRef.current?.contentWindow;
157
+ if (!mountedRef.current || !win) return;
158
+ win.postMessage({ type: "initMessenger", token: resolveToken() }, expectedOrigin);
159
+ setLoading(false);
160
+ }, [isNodeReady, nodeId, resolveToken, expectedOrigin]);
161
+ const handleMessage = useCallback2(
162
+ (event) => {
163
+ if (!expectedOrigin || event.origin !== expectedOrigin) return;
164
+ const { type } = event.data || {};
165
+ if (type === "messengerReady") void handshake();
166
+ else if (type === "messengerInitialized") setLoading(false);
167
+ },
168
+ [expectedOrigin, handshake]
169
+ );
170
+ useEffect2(() => {
171
+ window.addEventListener("message", handleMessage);
172
+ return () => window.removeEventListener("message", handleMessage);
173
+ }, [handleMessage]);
174
+ const iframeUrl = `${ssoUrl}/embed/app?node=${encodeURIComponent(nodeId)}`;
175
+ return /* @__PURE__ */ jsxs("div", { className, style: { position: "relative", ...style }, children: [
176
+ /* @__PURE__ */ jsx(
177
+ "iframe",
178
+ {
179
+ ref: iframeRef,
180
+ src: iframeUrl,
181
+ title: "iLiveMyLife Chat",
182
+ style: { width: "100%", height: "100%", border: "none" },
183
+ allow: "microphone; camera",
184
+ sandbox: "allow-scripts allow-same-origin allow-forms allow-popups"
185
+ }
186
+ ),
187
+ loading ? /* @__PURE__ */ jsxs(
188
+ "div",
189
+ {
190
+ style: {
191
+ position: "absolute",
192
+ inset: 0,
193
+ display: "flex",
194
+ flexDirection: "column",
195
+ alignItems: "center",
196
+ justifyContent: "center",
197
+ gap: 10,
198
+ background: "#fff",
199
+ color: "#64748b",
200
+ fontSize: 13
201
+ },
202
+ children: [
203
+ /* @__PURE__ */ jsx("style", { children: "@keyframes ilml-chat-spin{to{transform:rotate(360deg)}}" }),
204
+ /* @__PURE__ */ jsx(
205
+ "span",
206
+ {
207
+ style: {
208
+ width: 22,
209
+ height: 22,
210
+ border: "2px solid #e2e8f0",
211
+ borderTopColor: "#0178d4",
212
+ borderRadius: "50%",
213
+ animation: "ilml-chat-spin 0.8s linear infinite"
214
+ }
215
+ }
216
+ ),
217
+ /* @__PURE__ */ jsx("span", { children: "Loading chat\u2026" })
218
+ ]
219
+ }
220
+ ) : null
221
+ ] });
222
+ }
223
+
224
+ // src/IlmlChatWidget.tsx
225
+ import { useState as useState2 } from "react";
226
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
227
+ var iconBtn = {
228
+ background: "none",
229
+ border: "none",
230
+ cursor: "pointer",
231
+ color: "rgba(255,255,255,0.82)",
232
+ fontSize: 14,
233
+ lineHeight: 1,
234
+ padding: "2px 6px",
235
+ borderRadius: 4,
236
+ textDecoration: "none",
237
+ display: "inline-flex",
238
+ alignItems: "center"
239
+ };
240
+ function IlmlChatWidget({
241
+ ssoUrl,
242
+ nodeId,
243
+ token,
244
+ title,
245
+ onClose,
246
+ onMinimize,
247
+ isNodeReady,
248
+ allowFullscreen = true,
249
+ allowTall = true,
250
+ accentColor = "#0178d4",
251
+ className,
252
+ style
253
+ }) {
254
+ const [expanded, setExpanded] = useState2(false);
255
+ const [tall, setTall] = useState2(false);
256
+ const openInAppUrl = `${ssoUrl}/item/${encodeURIComponent(nodeId)}`;
257
+ const dockStyle = {
258
+ display: "flex",
259
+ flexDirection: "column",
260
+ overflow: "hidden",
261
+ height: "70vh",
262
+ maxHeight: 520,
263
+ width: "92vw",
264
+ maxWidth: 360,
265
+ borderRadius: 16,
266
+ background: "#fff",
267
+ boxShadow: "0 20px 50px rgba(0,0,0,0.25)",
268
+ ...style,
269
+ // Tall mode wins over any consumer height in `style`: fill (almost) the whole viewport height,
270
+ // leaving a small margin, at the docked width.
271
+ ...tall ? { height: "94vh", maxHeight: "94vh" } : {}
272
+ };
273
+ const fullStyle = {
274
+ display: "flex",
275
+ flexDirection: "column",
276
+ overflow: "hidden",
277
+ position: "fixed",
278
+ inset: 0,
279
+ zIndex: 1400,
280
+ background: "#fff"
281
+ };
282
+ return /* @__PURE__ */ jsxs2("div", { className: expanded ? void 0 : className, style: expanded ? fullStyle : dockStyle, children: [
283
+ /* @__PURE__ */ jsxs2(
284
+ "div",
285
+ {
286
+ style: {
287
+ display: "flex",
288
+ alignItems: "center",
289
+ gap: 4,
290
+ padding: "8px 12px",
291
+ background: accentColor,
292
+ color: "#fff",
293
+ flexShrink: 0
294
+ },
295
+ children: [
296
+ /* @__PURE__ */ jsx2(
297
+ "button",
298
+ {
299
+ onClick: onMinimize,
300
+ disabled: !onMinimize,
301
+ title: onMinimize ? "Minimize" : void 0,
302
+ style: {
303
+ ...iconBtn,
304
+ flex: 1,
305
+ minWidth: 0,
306
+ textAlign: "left",
307
+ color: "#fff",
308
+ fontWeight: 600,
309
+ fontSize: 14,
310
+ overflow: "hidden",
311
+ textOverflow: "ellipsis",
312
+ whiteSpace: "nowrap",
313
+ display: "block",
314
+ cursor: onMinimize ? "pointer" : "default"
315
+ },
316
+ children: title || "Chat"
317
+ }
318
+ ),
319
+ /* @__PURE__ */ jsx2("a", { href: openInAppUrl, target: "_blank", rel: "noopener", title: "Open in iLiveMyLife", style: iconBtn, children: "\u2197" }),
320
+ allowTall && !expanded ? /* @__PURE__ */ jsx2("button", { onClick: () => setTall((v) => !v), title: tall ? "Restore height" : "Full height", style: iconBtn, children: "\u2195" }) : null,
321
+ allowFullscreen ? /* @__PURE__ */ jsx2("button", { onClick: () => setExpanded((v) => !v), title: expanded ? "Restore" : "Full screen", style: iconBtn, children: expanded ? "\u2750" : "\u26F6" }) : null,
322
+ onMinimize ? /* @__PURE__ */ jsx2("button", { onClick: onMinimize, title: "Minimize", style: iconBtn, children: "\u2014" }) : null,
323
+ onClose ? /* @__PURE__ */ jsx2("button", { onClick: onClose, title: "Close", style: { ...iconBtn, fontSize: 18 }, children: "\xD7" }) : null
324
+ ]
325
+ }
326
+ ),
327
+ /* @__PURE__ */ jsx2(IlmlChat, { ssoUrl, nodeId, token, isNodeReady, style: { flex: 1, minHeight: 0 } })
328
+ ] });
329
+ }
330
+
331
+ // src/IlmlButton.tsx
332
+ import { jsx as jsx3 } from "react/jsx-runtime";
333
+ function IlmlButton({
334
+ href,
335
+ onClick,
336
+ disabled,
337
+ target,
338
+ rel,
339
+ type = "button",
340
+ className,
341
+ style,
342
+ children
343
+ }) {
344
+ const cls = `ilml-btn${className ? ` ${className}` : ""}`;
345
+ if (href) {
346
+ return /* @__PURE__ */ jsx3("a", { href, onClick, target, rel, className: cls, style, "aria-disabled": disabled || void 0, children });
347
+ }
348
+ return /* @__PURE__ */ jsx3("button", { type, onClick, disabled, className: cls, style, children });
349
+ }
350
+
351
+ // src/useIlmlProfile.ts
352
+ import { useEffect as useEffect3, useState as useState3 } from "react";
353
+ var DEFAULT_GRAPHQL_URL = "https://api.ilivemylife.io/graphql/v1";
354
+ var ME_QUERY = "{ me { id displayName fullName email currentAvatar { uri } } }";
355
+ function useIlmlProfile(token, opts = {}) {
356
+ const graphqlUrl = opts.graphqlUrl || DEFAULT_GRAPHQL_URL;
357
+ const [state, setState] = useState3({
358
+ profile: null,
359
+ loading: false,
360
+ invalid: false
361
+ });
362
+ useEffect3(() => {
363
+ if (!token) {
364
+ setState({ profile: null, loading: false, invalid: false });
365
+ return;
366
+ }
367
+ let alive = true;
368
+ setState((s) => ({ ...s, loading: true, invalid: false }));
369
+ (async () => {
370
+ try {
371
+ const res = await fetch(graphqlUrl, {
372
+ method: "POST",
373
+ headers: { "content-type": "application/json", "access-token": token },
374
+ body: JSON.stringify({ query: ME_QUERY })
375
+ });
376
+ if (!alive) return;
377
+ const json = await res.json().catch(() => null);
378
+ const me = json?.data?.me;
379
+ if (me?.id) {
380
+ setState({
381
+ profile: {
382
+ id: me.id,
383
+ name: me.displayName || me.fullName || me.email || "Account",
384
+ avatar: me.currentAvatar?.uri || void 0,
385
+ email: me.email
386
+ },
387
+ loading: false,
388
+ invalid: false
389
+ });
390
+ return;
391
+ }
392
+ const code = json?.errors?.[0]?.extensions?.code;
393
+ const authFailed = code === "UNAUTHENTICATED" || res.status === 401 || res.status === 403;
394
+ setState({ profile: null, loading: false, invalid: authFailed });
395
+ } catch {
396
+ if (alive) setState({ profile: null, loading: false, invalid: false });
397
+ }
398
+ })();
399
+ return () => {
400
+ alive = false;
401
+ };
402
+ }, [token, graphqlUrl]);
403
+ return state;
404
+ }
405
+
406
+ // src/useIlmlSession.ts
407
+ import { useCallback as useCallback3, useEffect as useEffect4, useSyncExternalStore } from "react";
408
+ var ME_QUERY2 = "{ me { id displayName fullName email currentAvatar { uri } } }";
409
+ var ACCESS_TOKEN_HEADER = "access-token";
410
+ var cfg = null;
411
+ function configure(opts) {
412
+ if (cfg) return;
413
+ cfg = {
414
+ graphqlUrl: opts.graphqlUrl,
415
+ onNewToken: opts.onNewToken,
416
+ tokenKey: opts.tokenKey || "walletAccessToken",
417
+ profileKey: opts.profileKey || "ilmlProfile"
418
+ };
419
+ }
420
+ function getItem(key) {
421
+ try {
422
+ return typeof window !== "undefined" ? window.localStorage.getItem(key) : null;
423
+ } catch {
424
+ return null;
425
+ }
426
+ }
427
+ function setItem(key, value) {
428
+ try {
429
+ window.localStorage.setItem(key, value);
430
+ } catch {
431
+ }
432
+ }
433
+ function removeItem(key) {
434
+ try {
435
+ window.localStorage.removeItem(key);
436
+ } catch {
437
+ }
438
+ }
439
+ function readToken() {
440
+ return cfg ? getItem(cfg.tokenKey) : null;
441
+ }
442
+ function readCachedProfile() {
443
+ if (!cfg) return null;
444
+ const raw = getItem(cfg.profileKey);
445
+ if (!raw) return null;
446
+ try {
447
+ return JSON.parse(raw);
448
+ } catch {
449
+ return null;
450
+ }
451
+ }
452
+ var sessionCache = { token: null, profile: null };
453
+ var firedTokens = /* @__PURE__ */ new Set();
454
+ function fireNewToken(token) {
455
+ if (!cfg?.onNewToken || firedTokens.has(token)) return;
456
+ firedTokens.add(token);
457
+ try {
458
+ const r = cfg.onNewToken(token);
459
+ if (r && typeof r.then === "function") {
460
+ r.then(void 0, () => firedTokens.delete(token));
461
+ }
462
+ } catch {
463
+ firedTokens.delete(token);
464
+ }
465
+ }
466
+ var sessionListeners = /* @__PURE__ */ new Set();
467
+ var sessionReady = false;
468
+ var sessionInitialized = false;
469
+ function notifySession() {
470
+ sessionListeners.forEach((cb) => cb());
471
+ }
472
+ function subscribeSession(cb) {
473
+ sessionListeners.add(cb);
474
+ return () => {
475
+ sessionListeners.delete(cb);
476
+ };
477
+ }
478
+ function setSessionToken(token) {
479
+ const tokenKey = cfg?.tokenKey || "walletAccessToken";
480
+ const profileKey = cfg?.profileKey || "ilmlProfile";
481
+ if (token) {
482
+ setItem(tokenKey, token);
483
+ fireNewToken(token);
484
+ } else {
485
+ removeItem(tokenKey);
486
+ removeItem(profileKey);
487
+ sessionCache.profile = null;
488
+ }
489
+ sessionCache.token = token;
490
+ notifySession();
491
+ }
492
+ function signOutIlmlSession() {
493
+ setSessionToken(null);
494
+ }
495
+ function getIlmlToken() {
496
+ return sessionCache.token ?? readToken();
497
+ }
498
+ function initSessionOnce() {
499
+ if (sessionInitialized || typeof window === "undefined" || !cfg) return;
500
+ sessionInitialized = true;
501
+ const t = readToken();
502
+ sessionCache.token = t;
503
+ if (t && !sessionCache.profile) sessionCache.profile = readCachedProfile();
504
+ if (t) fireNewToken(t);
505
+ sessionReady = true;
506
+ window.addEventListener("storage", (e) => {
507
+ if (!cfg || e.key !== cfg.tokenKey) return;
508
+ const next = readToken();
509
+ if (next === sessionCache.token) return;
510
+ sessionCache.token = next;
511
+ if (!next) sessionCache.profile = null;
512
+ else fireNewToken(next);
513
+ notifySession();
514
+ });
515
+ notifySession();
516
+ }
517
+ var profileState = { token: null, profile: null, loading: false, invalid: false };
518
+ var profileListeners = /* @__PURE__ */ new Set();
519
+ var profileLatestToken = null;
520
+ function subscribeProfile(cb) {
521
+ profileListeners.add(cb);
522
+ return () => {
523
+ profileListeners.delete(cb);
524
+ };
525
+ }
526
+ function notifyProfile() {
527
+ profileListeners.forEach((cb) => cb());
528
+ }
529
+ async function ensureProfile(token) {
530
+ if (!cfg) return;
531
+ const t = token || null;
532
+ if (!t) {
533
+ profileLatestToken = null;
534
+ if (profileState.token !== null || profileState.profile) {
535
+ profileState = { token: null, profile: null, loading: false, invalid: false };
536
+ notifyProfile();
537
+ }
538
+ return;
539
+ }
540
+ if (profileState.token === t && profileState.profile && !profileState.loading) return;
541
+ if (profileState.token === t && profileState.invalid) return;
542
+ if (profileLatestToken === t && profileState.loading) return;
543
+ profileLatestToken = t;
544
+ profileState = { token: t, profile: profileState.token === t ? profileState.profile : null, loading: true, invalid: false };
545
+ notifyProfile();
546
+ try {
547
+ const res = await fetch(cfg.graphqlUrl, {
548
+ method: "POST",
549
+ headers: { "content-type": "application/json", [ACCESS_TOKEN_HEADER]: t },
550
+ body: JSON.stringify({ query: ME_QUERY2 })
551
+ });
552
+ const json = await res.json().catch(() => null);
553
+ if (profileLatestToken !== t) return;
554
+ const me = json?.data?.me;
555
+ if (me?.id) {
556
+ const profile = {
557
+ id: me.id,
558
+ name: me.displayName || me.fullName || me.email || "Account",
559
+ avatar: me.currentAvatar?.uri || void 0,
560
+ email: me.email
561
+ };
562
+ sessionCache.profile = profile;
563
+ setItem(cfg.profileKey, JSON.stringify(profile));
564
+ profileState = { token: t, profile, loading: false, invalid: false };
565
+ } else {
566
+ const code = json?.errors?.[0]?.extensions?.code;
567
+ const authFailed = code === "UNAUTHENTICATED" || res.status === 401 || res.status === 403;
568
+ profileState = { token: t, profile: null, loading: false, invalid: authFailed };
569
+ if (authFailed) setSessionToken(null);
570
+ }
571
+ } catch {
572
+ if (profileLatestToken !== t) return;
573
+ profileState = { token: t, profile: profileState.profile, loading: false, invalid: false };
574
+ } finally {
575
+ if (profileLatestToken === t) notifyProfile();
576
+ }
577
+ }
578
+ function useIlmlSessionState() {
579
+ const token = useSyncExternalStore(subscribeSession, () => sessionCache.token, () => null);
580
+ const ready = useSyncExternalStore(subscribeSession, () => sessionReady, () => false);
581
+ const profileSnap = useSyncExternalStore(subscribeProfile, () => profileState, () => profileState);
582
+ useEffect4(() => {
583
+ initSessionOnce();
584
+ }, []);
585
+ useEffect4(() => {
586
+ ensureProfile(token);
587
+ }, [token]);
588
+ useEffect4(() => {
589
+ if (sessionCache.token) fireNewToken(sessionCache.token);
590
+ }, []);
591
+ const t = token || null;
592
+ const profile = profileSnap.token === t ? profileSnap.profile : null;
593
+ return {
594
+ token,
595
+ isLoggedIn: !!token,
596
+ ready,
597
+ // Fresh `me` wins; the cached profile fills the gap while the first fetch is in flight.
598
+ profile: profile || (t ? sessionCache.profile : null),
599
+ profileLoading: profileSnap.loading,
600
+ profileInvalid: profileSnap.invalid
601
+ };
602
+ }
603
+ function useIlmlSession(opts) {
604
+ configure(opts);
605
+ const state = useIlmlSessionState();
606
+ const { openLogin } = useIlmlSso({
607
+ ssoUrl: opts.ssoUrl,
608
+ ssoRoute: opts.ssoRoute,
609
+ clientId: opts.clientId,
610
+ onSsoData: (data) => {
611
+ setSessionToken(data?.accessToken || readToken());
612
+ },
613
+ onClose: opts.onLoginClose
614
+ });
615
+ const logout = useCallback3(() => setSessionToken(null), []);
616
+ return { ...state, login: openLogin, logout };
617
+ }
618
+ export {
619
+ IlmlButton,
620
+ IlmlChat,
621
+ IlmlChatWidget,
622
+ getIlmlToken,
623
+ getSsoLoginUrl,
624
+ isValidSsoOrigin,
625
+ popupCenterParams,
626
+ signOutIlmlSession,
627
+ useIlmlProfile,
628
+ useIlmlSession,
629
+ useIlmlSessionState,
630
+ useIlmlSso
631
+ };
@@ -0,0 +1,44 @@
1
+ /*
2
+ * The official iLiveMyLife brand button — THE single source of the brand gradient. Two ways to use it:
3
+ * - React: <IlmlButton> (wraps this class)
4
+ * - Anything: include this file, then <button class="ilml-btn">…</button>
5
+ * Nothing else redefines these colours (#F5626B → #D3FF76 → #FFF8B6 → #2bb673). No copying, no duplication.
6
+ */
7
+ .ilml-btn {
8
+ display: inline-flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ gap: 8px;
12
+ padding: 10px 22px;
13
+ border: none;
14
+ border-radius: 9999px;
15
+ font-weight: 600;
16
+ font-size: 14px;
17
+ line-height: 1;
18
+ cursor: pointer;
19
+ text-decoration: none;
20
+ -webkit-tap-highlight-color: transparent;
21
+ color: #fff;
22
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
23
+ background-image: linear-gradient(45deg, #f5626b, #d3ff76, #fff8b6, #2bb673);
24
+ background-size: 300% 100%;
25
+ background-position: 0 0;
26
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.14);
27
+ transition: background-position 0.8s ease-in-out, box-shadow 0.2s ease, transform 0.1s ease;
28
+ }
29
+
30
+ .ilml-btn:hover {
31
+ background-position: 100% 0;
32
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
33
+ }
34
+
35
+ .ilml-btn:active {
36
+ transform: translateY(1px);
37
+ }
38
+
39
+ .ilml-btn[disabled],
40
+ .ilml-btn[aria-disabled='true'] {
41
+ opacity: 0.55;
42
+ cursor: default;
43
+ pointer-events: none;
44
+ }