@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 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.jsx)("div", { style: { display: "flex", alignItems: "center", justifyContent: "center", minHeight: "100vh", background: "#0a0a0a", color: "#fff" }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { textAlign: "center" }, children: [
343
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { width: "32px", height: "32px", border: "3px solid rgba(255,255,255,0.1)", borderTopColor: "#d4a574", borderRadius: "50%", animation: "nexus-spin 0.8s linear infinite", margin: "0 auto 16px" } }),
344
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { fontFamily: "monospace", fontSize: "14px", opacity: 0.6 }, children: "Authenticating with Nexus..." }),
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__ */ jsx2("div", { style: { display: "flex", alignItems: "center", justifyContent: "center", minHeight: "100vh", background: "#0a0a0a", color: "#fff" }, children: /* @__PURE__ */ jsxs("div", { style: { textAlign: "center" }, children: [
306
- /* @__PURE__ */ jsx2("div", { style: { width: "32px", height: "32px", border: "3px solid rgba(255,255,255,0.1)", borderTopColor: "#d4a574", borderRadius: "50%", animation: "nexus-spin 0.8s linear infinite", margin: "0 auto 16px" } }),
307
- /* @__PURE__ */ jsx2("p", { style: { fontFamily: "monospace", fontSize: "14px", opacity: 0.6 }, children: "Authenticating with Nexus..." }),
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vylth/nexus-react",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Nexus SSO SDK for React — Sign in with Nexus",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",