@vylth/nexus-react 1.0.0 → 1.0.2
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/README.md +110 -0
- package/dist/index.js +43 -8
- package/dist/index.mjs +44 -9
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# @vylth/nexus-react
|
|
2
|
+
|
|
3
|
+
Nexus SSO SDK for React — add authentication to any VYLTH app in minutes.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @vylth/nexus-react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### 1. Wrap with NexusProvider
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { BrowserRouter } from 'react-router-dom';
|
|
17
|
+
import { NexusProvider } from '@vylth/nexus-react';
|
|
18
|
+
|
|
19
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
20
|
+
<BrowserRouter>
|
|
21
|
+
<NexusProvider
|
|
22
|
+
nexusUrl="https://accounts.vylth.com"
|
|
23
|
+
clientId="YOUR_CLIENT_ID"
|
|
24
|
+
>
|
|
25
|
+
<App />
|
|
26
|
+
</NexusProvider>
|
|
27
|
+
</BrowserRouter>
|
|
28
|
+
);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 2. Add Callback Route
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { NexusCallback } from '@vylth/nexus-react';
|
|
35
|
+
|
|
36
|
+
<Route path="/auth/callback" element={
|
|
37
|
+
<NexusCallback
|
|
38
|
+
onSuccess={() => window.location.replace('/')}
|
|
39
|
+
onError={() => window.location.replace('/login')}
|
|
40
|
+
/>
|
|
41
|
+
} />
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The callback route must match the redirect URI registered in Nexus.
|
|
45
|
+
|
|
46
|
+
### 3. Use Auth in Components
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { useNexus } from '@vylth/nexus-react';
|
|
50
|
+
|
|
51
|
+
function MyComponent() {
|
|
52
|
+
const { user, isAuthenticated, isLoading, login, logout } = useNexus();
|
|
53
|
+
|
|
54
|
+
if (isLoading) return <Spinner />;
|
|
55
|
+
if (!isAuthenticated) { login(); return null; }
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div>
|
|
59
|
+
<p>Welcome, {user.username}</p>
|
|
60
|
+
<p>Role: {user.globalRole}</p>
|
|
61
|
+
<button onClick={logout}>Logout</button>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 4. Login Button
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { NexusLoginButton } from '@vylth/nexus-react';
|
|
71
|
+
|
|
72
|
+
<NexusLoginButton />
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Exports
|
|
76
|
+
|
|
77
|
+
| Export | Type | Description |
|
|
78
|
+
|--------|------|-------------|
|
|
79
|
+
| `NexusProvider` | Component | Context provider — wrap your app |
|
|
80
|
+
| `NexusCallback` | Component | OAuth callback handler |
|
|
81
|
+
| `NexusLoginButton` | Component | Pre-styled login button |
|
|
82
|
+
| `useNexus` | Hook | Access auth state and user info |
|
|
83
|
+
|
|
84
|
+
## User Fields
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
interface NexusUser {
|
|
88
|
+
id: string;
|
|
89
|
+
email: string;
|
|
90
|
+
username?: string;
|
|
91
|
+
firstName: string;
|
|
92
|
+
lastName: string;
|
|
93
|
+
avatar?: string;
|
|
94
|
+
globalRole: string; // citizen, pioneer, navigator, commander, executor
|
|
95
|
+
emailVerified: boolean;
|
|
96
|
+
affiliateCode?: string;
|
|
97
|
+
appRoles?: Record<string, string>;
|
|
98
|
+
twoFactorVerified?: boolean;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Prerequisites
|
|
103
|
+
|
|
104
|
+
1. Register your app in Nexus (accounts.vylth.com → Admin → Manage Apps)
|
|
105
|
+
2. Save the `client_id` from registration
|
|
106
|
+
3. Set your redirect URI (e.g., `https://yourapp.vylth.com/auth/callback`)
|
|
107
|
+
|
|
108
|
+
## Full Docs
|
|
109
|
+
|
|
110
|
+
See the complete integration guide at [accounts.vylth.com/docs](https://accounts.vylth.com/docs)
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,7 @@ var import_react = require("react");
|
|
|
42
42
|
var TOKEN_KEY = "nexus_access_token";
|
|
43
43
|
var REFRESH_KEY = "nexus_refresh_token";
|
|
44
44
|
var STATE_KEY = "nexus_oauth_state";
|
|
45
|
+
var PROFILE_KEY = "nexus_user_profile";
|
|
45
46
|
var memoryAccessToken = null;
|
|
46
47
|
var memoryRefreshToken = null;
|
|
47
48
|
var refreshPromise = null;
|
|
@@ -68,6 +69,18 @@ function clearTokens() {
|
|
|
68
69
|
memoryRefreshToken = null;
|
|
69
70
|
localStorage.removeItem(TOKEN_KEY);
|
|
70
71
|
localStorage.removeItem(REFRESH_KEY);
|
|
72
|
+
localStorage.removeItem(PROFILE_KEY);
|
|
73
|
+
}
|
|
74
|
+
function saveUserProfile(firstName, lastName, avatar) {
|
|
75
|
+
localStorage.setItem(PROFILE_KEY, JSON.stringify({ firstName, lastName, avatar }));
|
|
76
|
+
}
|
|
77
|
+
function loadUserProfile() {
|
|
78
|
+
try {
|
|
79
|
+
const raw = localStorage.getItem(PROFILE_KEY);
|
|
80
|
+
return raw ? JSON.parse(raw) : null;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
71
84
|
}
|
|
72
85
|
function isTokenValid() {
|
|
73
86
|
const token = getAccessToken();
|
|
@@ -82,13 +95,14 @@ function isTokenValid() {
|
|
|
82
95
|
function decodeUser(token) {
|
|
83
96
|
try {
|
|
84
97
|
const payload = JSON.parse(atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")));
|
|
98
|
+
const profile = loadUserProfile();
|
|
85
99
|
return {
|
|
86
100
|
id: payload.sub,
|
|
87
101
|
email: payload.email,
|
|
88
102
|
username: payload.username || "",
|
|
89
|
-
firstName: "",
|
|
90
|
-
lastName: "",
|
|
91
|
-
avatar: payload.avatar || "",
|
|
103
|
+
firstName: profile?.firstName || "",
|
|
104
|
+
lastName: profile?.lastName || "",
|
|
105
|
+
avatar: profile?.avatar || payload.avatar || "",
|
|
92
106
|
globalRole: payload.global_role || "citizen",
|
|
93
107
|
emailVerified: payload.email_verified || false,
|
|
94
108
|
affiliateCode: payload.affiliate_code || "",
|
|
@@ -285,8 +299,10 @@ var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
|
285
299
|
function NexusCallback({ onSuccess, onError }) {
|
|
286
300
|
const config = (0, import_react2.useContext)(NexusConfigContext);
|
|
287
301
|
const [error, setError] = (0, import_react2.useState)("");
|
|
302
|
+
const processed = (0, import_react2.useRef)(false);
|
|
288
303
|
(0, import_react2.useEffect)(() => {
|
|
289
|
-
if (!config) return;
|
|
304
|
+
if (!config || processed.current) return;
|
|
305
|
+
processed.current = true;
|
|
290
306
|
const params = new URLSearchParams(window.location.search);
|
|
291
307
|
const code = params.get("code");
|
|
292
308
|
const state = params.get("state");
|
|
@@ -317,6 +333,7 @@ function NexusCallback({ onSuccess, onError }) {
|
|
|
317
333
|
user.lastName = result.investor.last_name;
|
|
318
334
|
user.avatar = result.investor.avatar_url || user.avatar;
|
|
319
335
|
user.emailVerified = result.investor.email_verified;
|
|
336
|
+
saveUserProfile(user.firstName, user.lastName, user.avatar);
|
|
320
337
|
config.setUser(user);
|
|
321
338
|
}
|
|
322
339
|
onSuccess?.();
|
|
@@ -339,11 +356,29 @@ function NexusCallback({ onSuccess, onError }) {
|
|
|
339
356
|
)
|
|
340
357
|
] });
|
|
341
358
|
}
|
|
342
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.
|
|
343
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { width: "
|
|
344
|
-
|
|
359
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", minHeight: "100vh", background: "#0a0a0a", color: "#fff", gap: "20px" }, children: [
|
|
360
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { width: "48px", height: "48px", animation: "nexus-spin 2s linear infinite" }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("svg", { viewBox: "0 0 64 64", fill: "none", xmlns: "http://www.w3.org/2000/svg", style: { width: "100%", height: "100%" }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("g", { stroke: "rgba(255,255,255,0.5)", strokeWidth: "2", strokeLinecap: "round", children: [
|
|
361
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "32", cy: "32", r: "4", fill: "rgba(255,255,255,0.7)" }),
|
|
362
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "32", cy: "12", r: "3", fill: "rgba(255,255,255,0.5)" }),
|
|
363
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "32", y1: "15", x2: "32", y2: "28", opacity: "0.4" }),
|
|
364
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "32", cy: "52", r: "3", fill: "rgba(255,255,255,0.5)" }),
|
|
365
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "32", y1: "36", x2: "32", y2: "49", opacity: "0.4" }),
|
|
366
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "12", cy: "32", r: "3", fill: "rgba(255,255,255,0.5)" }),
|
|
367
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "15", y1: "32", x2: "28", y2: "32", opacity: "0.4" }),
|
|
368
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "52", cy: "32", r: "3", fill: "rgba(255,255,255,0.5)" }),
|
|
369
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "36", y1: "32", x2: "49", y2: "32", opacity: "0.4" }),
|
|
370
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "18", cy: "18", r: "2.5", fill: "rgba(255,255,255,0.35)" }),
|
|
371
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "20", y1: "20", x2: "29", y2: "29", opacity: "0.25" }),
|
|
372
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "46", cy: "18", r: "2.5", fill: "rgba(255,255,255,0.35)" }),
|
|
373
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "44", y1: "20", x2: "35", y2: "29", opacity: "0.25" }),
|
|
374
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "18", cy: "46", r: "2.5", fill: "rgba(255,255,255,0.35)" }),
|
|
375
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "20", y1: "44", x2: "29", y2: "35", opacity: "0.25" }),
|
|
376
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "46", cy: "46", r: "2.5", fill: "rgba(255,255,255,0.35)" }),
|
|
377
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: "44", y1: "44", x2: "35", y2: "35", opacity: "0.25" })
|
|
378
|
+
] }) }) }),
|
|
379
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { fontFamily: "monospace", fontSize: "14px", opacity: 0.5 }, children: "Authenticating with Nexus..." }),
|
|
345
380
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: `@keyframes nexus-spin { to { transform: rotate(360deg) } }` })
|
|
346
|
-
] })
|
|
381
|
+
] });
|
|
347
382
|
}
|
|
348
383
|
|
|
349
384
|
// src/NexusLoginButton.tsx
|
package/dist/index.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { createContext, useCallback, useEffect, useState } from "react";
|
|
|
5
5
|
var TOKEN_KEY = "nexus_access_token";
|
|
6
6
|
var REFRESH_KEY = "nexus_refresh_token";
|
|
7
7
|
var STATE_KEY = "nexus_oauth_state";
|
|
8
|
+
var PROFILE_KEY = "nexus_user_profile";
|
|
8
9
|
var memoryAccessToken = null;
|
|
9
10
|
var memoryRefreshToken = null;
|
|
10
11
|
var refreshPromise = null;
|
|
@@ -31,6 +32,18 @@ function clearTokens() {
|
|
|
31
32
|
memoryRefreshToken = null;
|
|
32
33
|
localStorage.removeItem(TOKEN_KEY);
|
|
33
34
|
localStorage.removeItem(REFRESH_KEY);
|
|
35
|
+
localStorage.removeItem(PROFILE_KEY);
|
|
36
|
+
}
|
|
37
|
+
function saveUserProfile(firstName, lastName, avatar) {
|
|
38
|
+
localStorage.setItem(PROFILE_KEY, JSON.stringify({ firstName, lastName, avatar }));
|
|
39
|
+
}
|
|
40
|
+
function loadUserProfile() {
|
|
41
|
+
try {
|
|
42
|
+
const raw = localStorage.getItem(PROFILE_KEY);
|
|
43
|
+
return raw ? JSON.parse(raw) : null;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
34
47
|
}
|
|
35
48
|
function isTokenValid() {
|
|
36
49
|
const token = getAccessToken();
|
|
@@ -45,13 +58,14 @@ function isTokenValid() {
|
|
|
45
58
|
function decodeUser(token) {
|
|
46
59
|
try {
|
|
47
60
|
const payload = JSON.parse(atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")));
|
|
61
|
+
const profile = loadUserProfile();
|
|
48
62
|
return {
|
|
49
63
|
id: payload.sub,
|
|
50
64
|
email: payload.email,
|
|
51
65
|
username: payload.username || "",
|
|
52
|
-
firstName: "",
|
|
53
|
-
lastName: "",
|
|
54
|
-
avatar: payload.avatar || "",
|
|
66
|
+
firstName: profile?.firstName || "",
|
|
67
|
+
lastName: profile?.lastName || "",
|
|
68
|
+
avatar: profile?.avatar || payload.avatar || "",
|
|
55
69
|
globalRole: payload.global_role || "citizen",
|
|
56
70
|
emailVerified: payload.email_verified || false,
|
|
57
71
|
affiliateCode: payload.affiliate_code || "",
|
|
@@ -243,13 +257,15 @@ function NexusProvider({
|
|
|
243
257
|
var NexusConfigContext = createContext(null);
|
|
244
258
|
|
|
245
259
|
// src/NexusCallback.tsx
|
|
246
|
-
import { useContext, useEffect as useEffect2, useState as useState2 } from "react";
|
|
260
|
+
import { useContext, useEffect as useEffect2, useRef, useState as useState2 } from "react";
|
|
247
261
|
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
248
262
|
function NexusCallback({ onSuccess, onError }) {
|
|
249
263
|
const config = useContext(NexusConfigContext);
|
|
250
264
|
const [error, setError] = useState2("");
|
|
265
|
+
const processed = useRef(false);
|
|
251
266
|
useEffect2(() => {
|
|
252
|
-
if (!config) return;
|
|
267
|
+
if (!config || processed.current) return;
|
|
268
|
+
processed.current = true;
|
|
253
269
|
const params = new URLSearchParams(window.location.search);
|
|
254
270
|
const code = params.get("code");
|
|
255
271
|
const state = params.get("state");
|
|
@@ -280,6 +296,7 @@ function NexusCallback({ onSuccess, onError }) {
|
|
|
280
296
|
user.lastName = result.investor.last_name;
|
|
281
297
|
user.avatar = result.investor.avatar_url || user.avatar;
|
|
282
298
|
user.emailVerified = result.investor.email_verified;
|
|
299
|
+
saveUserProfile(user.firstName, user.lastName, user.avatar);
|
|
283
300
|
config.setUser(user);
|
|
284
301
|
}
|
|
285
302
|
onSuccess?.();
|
|
@@ -302,11 +319,29 @@ function NexusCallback({ onSuccess, onError }) {
|
|
|
302
319
|
)
|
|
303
320
|
] });
|
|
304
321
|
}
|
|
305
|
-
return /* @__PURE__ */
|
|
306
|
-
/* @__PURE__ */ jsx2("div", { style: { width: "
|
|
307
|
-
|
|
322
|
+
return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", minHeight: "100vh", background: "#0a0a0a", color: "#fff", gap: "20px" }, children: [
|
|
323
|
+
/* @__PURE__ */ jsx2("div", { style: { width: "48px", height: "48px", animation: "nexus-spin 2s linear infinite" }, children: /* @__PURE__ */ jsx2("svg", { viewBox: "0 0 64 64", fill: "none", xmlns: "http://www.w3.org/2000/svg", style: { width: "100%", height: "100%" }, children: /* @__PURE__ */ jsxs("g", { stroke: "rgba(255,255,255,0.5)", strokeWidth: "2", strokeLinecap: "round", children: [
|
|
324
|
+
/* @__PURE__ */ jsx2("circle", { cx: "32", cy: "32", r: "4", fill: "rgba(255,255,255,0.7)" }),
|
|
325
|
+
/* @__PURE__ */ jsx2("circle", { cx: "32", cy: "12", r: "3", fill: "rgba(255,255,255,0.5)" }),
|
|
326
|
+
/* @__PURE__ */ jsx2("line", { x1: "32", y1: "15", x2: "32", y2: "28", opacity: "0.4" }),
|
|
327
|
+
/* @__PURE__ */ jsx2("circle", { cx: "32", cy: "52", r: "3", fill: "rgba(255,255,255,0.5)" }),
|
|
328
|
+
/* @__PURE__ */ jsx2("line", { x1: "32", y1: "36", x2: "32", y2: "49", opacity: "0.4" }),
|
|
329
|
+
/* @__PURE__ */ jsx2("circle", { cx: "12", cy: "32", r: "3", fill: "rgba(255,255,255,0.5)" }),
|
|
330
|
+
/* @__PURE__ */ jsx2("line", { x1: "15", y1: "32", x2: "28", y2: "32", opacity: "0.4" }),
|
|
331
|
+
/* @__PURE__ */ jsx2("circle", { cx: "52", cy: "32", r: "3", fill: "rgba(255,255,255,0.5)" }),
|
|
332
|
+
/* @__PURE__ */ jsx2("line", { x1: "36", y1: "32", x2: "49", y2: "32", opacity: "0.4" }),
|
|
333
|
+
/* @__PURE__ */ jsx2("circle", { cx: "18", cy: "18", r: "2.5", fill: "rgba(255,255,255,0.35)" }),
|
|
334
|
+
/* @__PURE__ */ jsx2("line", { x1: "20", y1: "20", x2: "29", y2: "29", opacity: "0.25" }),
|
|
335
|
+
/* @__PURE__ */ jsx2("circle", { cx: "46", cy: "18", r: "2.5", fill: "rgba(255,255,255,0.35)" }),
|
|
336
|
+
/* @__PURE__ */ jsx2("line", { x1: "44", y1: "20", x2: "35", y2: "29", opacity: "0.25" }),
|
|
337
|
+
/* @__PURE__ */ jsx2("circle", { cx: "18", cy: "46", r: "2.5", fill: "rgba(255,255,255,0.35)" }),
|
|
338
|
+
/* @__PURE__ */ jsx2("line", { x1: "20", y1: "44", x2: "29", y2: "35", opacity: "0.25" }),
|
|
339
|
+
/* @__PURE__ */ jsx2("circle", { cx: "46", cy: "46", r: "2.5", fill: "rgba(255,255,255,0.35)" }),
|
|
340
|
+
/* @__PURE__ */ jsx2("line", { x1: "44", y1: "44", x2: "35", y2: "35", opacity: "0.25" })
|
|
341
|
+
] }) }) }),
|
|
342
|
+
/* @__PURE__ */ jsx2("p", { style: { fontFamily: "monospace", fontSize: "14px", opacity: 0.5 }, children: "Authenticating with Nexus..." }),
|
|
308
343
|
/* @__PURE__ */ jsx2("style", { children: `@keyframes nexus-spin { to { transform: rotate(360deg) } }` })
|
|
309
|
-
] })
|
|
344
|
+
] });
|
|
310
345
|
}
|
|
311
346
|
|
|
312
347
|
// src/NexusLoginButton.tsx
|