ab-ecosystem-sso 0.1.17 → 0.1.19

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 CHANGED
@@ -4,6 +4,7 @@ Drop-in React auth & account UI for the partner-ecosystem **Auth-as-a-Service**
4
4
 
5
5
  - ⚛️ React 18 / 19, shipped as ESM + CJS with full TypeScript types.
6
6
  - 🎨 Self-contained, theme-aware CSS that **won't leak into or be overridden by** your app.
7
+ - 🌗 Built-in **light and dark mode** — per component, via a single `mode` prop.
7
8
  - 🔌 Use it declaratively (`<AccountSettings />`) or imperatively (`openAccountSettings()`).
8
9
  - 🧩 Every network call also available as a plain function for custom UIs.
9
10
 
@@ -21,6 +22,7 @@ Drop-in React auth & account UI for the partner-ecosystem **Auth-as-a-Service**
21
22
  - [Fetching user data](#fetching-user-data) — `getUserProfile` · `getUserWallet` · `getUserCreditWallet` · `getUserSubscription`
22
23
  - [Two-factor setup](#two-factor-setup)
23
24
  - [Hooks](#hooks)
25
+ - [Light & dark mode](#light--dark-mode)
24
26
  - [Theming](#theming)
25
27
  - [API reference](#api-reference)
26
28
  - [TypeScript](#typescript)
@@ -84,6 +86,8 @@ function LoginPage() {
84
86
  }
85
87
  ```
86
88
 
89
+ The password field has a **show/hide (eye) toggle** — it always starts masked, and the same control appears on `Signup`, the `ForgotPassword` reset step, and the Account Settings change-password modal.
90
+
87
91
  `Login` first fetches the project's `auth-config` (captcha provider/site-key, branding, which links to show) and renders accordingly. If the account has **2FA** enabled it automatically shows a **segmented 6-digit OTP** step (with captcha when the project requires it), and only calls `onSuccess` after verification succeeds.
88
92
 
89
93
  **Magic-link sign-in** works automatically: point your magic link at `/login#<access_token>` (a bare `/login#access_token=<token>` is also accepted). On mount, `Login` reads the token from the URL hash, validates it with a `/me` call, and — if valid — strips it from the URL and fires `onSuccess({ token, user })`, exactly like a password sign-in. While it validates, a loading state shows; if the token is invalid or expired, it falls back to the normal sign-in form with a short notice (and calls `onError`). No extra wiring needed beyond routing the link to your login page.
@@ -97,6 +101,41 @@ import { Signup, ForgotPassword } from "ab-ecosystem-sso";
97
101
  <ForgotPassword projectId="…" onBackToLogin={() => navigate("/login")} />
98
102
  ```
99
103
 
104
+ `Signup` loads the project's currencies from `GET /currency` and offers only those. Its country dropdown uses a bundled default list; pass **`countries`** (an array of `{ code, name }`) to replace it:
105
+
106
+ ```tsx
107
+ <Signup
108
+ projectId="…"
109
+ countries={[
110
+ { code: "US", name: "United States" },
111
+ { code: "IN", name: "India" },
112
+ ]}
113
+ onSuccess={handleAuth}
114
+ />
115
+ ```
116
+
117
+ **Password reset.** `ForgotPassword` has two steps. With no token it shows the request-a-link form (emails a reset link). When the user returns via that link, extract the token from the URL and pass it as **`resetToken`** — the component then renders the reset step (new password + confirm + captcha) and POSTs to `/auth/reset-password`:
118
+
119
+ ```tsx
120
+ // e.g. reset link is /reset-password?token=abc123
121
+ const token = new URLSearchParams(location.search).get("token") ?? undefined;
122
+
123
+ <ForgotPassword
124
+ projectId="…"
125
+ resetToken={token}
126
+ onResetSuccess={() => navigate("/login")}
127
+ onBackToLogin={() => navigate("/login")}
128
+ />
129
+ ```
130
+
131
+ The new password must be at least 8 characters and match the confirmation. On success it shows a confirmation and calls `onResetSuccess`. Extracting the token is the host's responsibility (the link format is yours to define).
132
+
133
+ | `ForgotPassword` prop | Type | Notes |
134
+ | --- | --- | --- |
135
+ | `resetToken` | `string` | Present ⇒ render the reset-password step instead of request-a-link. |
136
+ | `onResetSuccess` | `() => void` | Fires after the password is reset. |
137
+ | `onBackToLogin` | `() => void` | Back-to-login link (both steps). |
138
+
100
139
  ### `VerifyEmail`
101
140
 
102
141
  The email-verification screen for a `/verify/:token` route — the link you email to new users. It shows a captcha (site key from the project's auth-config), then posts the captcha + token to `/auth/verify-email`.
@@ -142,8 +181,9 @@ On success it calls `onVerified` (send the user to login) and shows a **Continue
142
181
  | `onSuccess` | `({ token, user }) => void` | On successful auth. |
143
182
  | `onError` | `(error: Error) => void` | On failure. |
144
183
  | `classNames` | `AuthClassNames` | Per-element class overrides: `card`, `input`, `label`, `button`, `link`, `captcha`, `error`, … |
184
+ | `mode` | `"light" \| "dark"` | Colour scheme. Default `"light"`. See [Light & dark mode](#light--dark-mode). |
145
185
 
146
- Per-screen extras: `Login` adds `onForgotPassword` / `onSignupClick`; `Signup` adds `onLoginClick`; `ForgotPassword` adds `onBackToLogin`; `VerifyEmail` adds `token` / `onVerified` / `onLoginClick` / `onSignupClick` (and has no `onSuccess`, since no session is created).
186
+ Per-screen extras: `Login` adds `onForgotPassword` / `onSignupClick`; `Signup` adds `onLoginClick` / `countries`; `ForgotPassword` adds `onBackToLogin` / `resetToken` / `onResetSuccess`; `VerifyEmail` adds `token` / `onVerified` / `onLoginClick` / `onSignupClick` (and has no `onSuccess`, since no session is created).
147
187
 
148
188
  ---
149
189
 
@@ -236,8 +276,10 @@ const [open, setOpen] = useState(false);
236
276
  | `token` | `string` | Bearer token. |
237
277
  | `apiBaseUrl` | `string` | Optional API origin. |
238
278
  | `initialSection` | `"general" \| "account" \| "wallet" \| "plans" \| "credits"` | First section shown. Default `"general"`. |
279
+ | `customComponents` | `AccountSettingsCustomPanel[]` | Add your own panels to the modal. See [Custom panels](#custom-panels). |
239
280
  | `syncUrlHash` | `boolean` | Mirror open state + active section in the URL hash. Default `true`. See [URL hash & refresh](#url-hash--refresh). |
240
281
  | `title` | `string` | Modal heading. Default `"Settings"`. |
282
+ | `mode` | `"light" \| "dark"` | Colour scheme. Default `"light"`. See [Light & dark mode](#light--dark-mode). |
241
283
  | `data` | `Partial<AccountSettingsData>` | Override sample data (non-API mode / previews). |
242
284
  | `onLogout` | `() => void` | Sign-out action (closes the modal, then runs this). |
243
285
  | `onSaveProfile` | `(profile) => void` | Fires alongside the profile save. |
@@ -247,6 +289,45 @@ const [open, setOpen] = useState(false);
247
289
 
248
290
  > In live mode the modal performs the API calls itself (save profile, change password, subscribe, recharge, etc.); the callbacks are observation hooks, not required wiring.
249
291
 
292
+ ### Custom panels
293
+
294
+ Add your own screens to the modal via `customComponents` — an array of `AccountSettingsCustomPanel`. Each entry renders a **trigger** (in the sidebar, pinned to the sidebar bottom, or in the header next to the close button) and shows its `component` in the modal body when activated.
295
+
296
+ ```tsx
297
+ <AccountSettings
298
+ open={open}
299
+ onOpenChange={setOpen}
300
+ projectId="your-project-id"
301
+ token={token}
302
+ customComponents={[
303
+ {
304
+ id: "referrals",
305
+ location: "sidebar", // "sidebar" | "sidebar-bottom" | "header"
306
+ label: "Referrals",
307
+ icon: <GiftIcon />, // any node (an inline SVG works best)
308
+ component: <ReferralsPanel />, // rendered in the modal body
309
+ },
310
+ {
311
+ id: "help",
312
+ location: "header",
313
+ label: "Help",
314
+ icon: <HelpIcon />,
315
+ component: <HelpCenter />,
316
+ },
317
+ ]}
318
+ />
319
+ ```
320
+
321
+ | Field | Type | Notes |
322
+ | --- | --- | --- |
323
+ | `id` | `string` | **Required**, unique. Tracks which panel is active. |
324
+ | `location` | `"sidebar" \| "sidebar-bottom" \| "header"` | Where the trigger appears. Default `"sidebar"`. |
325
+ | `label` | `string` | Sidebar text; header button tooltip + `aria-label`. |
326
+ | `icon` | `ReactNode` | Optional trigger icon. |
327
+ | `component` | `ReactNode` | Rendered in the body when the panel is active. |
328
+
329
+ The same `customComponents` array works with the imperative `openAccountSettings({ customComponents: [...] })`. Panels are host-owned — they don't affect the URL hash, and activating one doesn't run the built-in section's API calls.
330
+
250
331
  ### URL hash & refresh
251
332
 
252
333
  While the modal is open it reflects the active section in the URL hash as `#settings-<section>` (e.g. `#settings-general`, `#settings-plans`), updating as the user switches tabs and clearing it on close. This means a **page refresh can re-open the modal on the same section**. It uses `history.replaceState`, so it never adds browser-history entries and never clobbers an unrelated `#anchor`. Opt out with `syncUrlHash={false}`.
@@ -294,7 +375,7 @@ onClick={() => logout({
294
375
  await logoutApi("your-project-id", token, "https://your-api");
295
376
  ```
296
377
 
297
- `logout` POSTs to `/auth/logout` and **always runs `onSuccess` once the request settles** — so the user is logged out client-side even if the server call fails (`onError` fires for observability). Inside a `SessionProvider`, `<LogoutButton />` with no props uses the provider's logout instead.
378
+ `<LogoutButton />` also takes `mode="dark"`. `logout` POSTs to `/auth/logout` and **always runs `onSuccess` once the request settles** — so the user is logged out client-side even if the server call fails (`onError` fires for observability). Inside a `SessionProvider`, `<LogoutButton />` with no props uses the provider's logout instead.
298
379
 
299
380
  ---
300
381
 
@@ -354,6 +435,7 @@ import { TwoFactorSetup } from "ab-ecosystem-sso";
354
435
  projectId="your-project-id"
355
436
  token={token}
356
437
  apiBaseUrl="https://your-api"
438
+ mode="dark" // optional — "light" (default) or "dark"
357
439
  onEnabled={() => …}
358
440
  onDisabled={() => …}
359
441
  onError={(err) => …}
@@ -372,6 +454,58 @@ import { TwoFactorSetup } from "ab-ecosystem-sso";
372
454
 
373
455
  ---
374
456
 
457
+ ## Light & dark mode
458
+
459
+ Every component renders in **light mode by default**. Pass `mode="dark"` to switch:
460
+
461
+ ```tsx
462
+ <Login projectId="…" mode="dark" onSuccess={handleAuth} />
463
+ <Signup projectId="…" mode="dark" onLoginClick={() => navigate("/login")} />
464
+ <ForgotPassword projectId="…" mode="dark" />
465
+ <VerifyEmail projectId="…" mode="dark" />
466
+ <AccountSettings open={open} onOpenChange={setOpen} mode="dark" />
467
+ <TwoFactorSetup projectId="…" token={token} mode="dark" />
468
+ <LogoutButton mode="dark" />
469
+
470
+ openAccountSettings({ projectId, token, mode: "dark" });
471
+ ```
472
+
473
+ `mode` is **per component**, not global — each component root carries a
474
+ `data-aas-theme="light" | "dark"` attribute and the palette inherits from there, so a dark
475
+ login card and a light settings modal can coexist on the same page.
476
+
477
+ Follow your app's own theme by passing the value you already track:
478
+
479
+ ```tsx
480
+ const { theme } = useTheme(); // your app's theme state
481
+ <Login projectId="…" mode={theme === "dark" ? "dark" : "light"} />
482
+ ```
483
+
484
+ To follow the OS setting instead:
485
+
486
+ ```tsx
487
+ import type { AuthMode } from "ab-ecosystem-sso";
488
+
489
+ const [mode, setMode] = useState<AuthMode>(
490
+ () => (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
491
+ );
492
+
493
+ useEffect(() => {
494
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
495
+ const onChange = (e: MediaQueryListEvent) => setMode(e.matches ? "dark" : "light");
496
+ mq.addEventListener("change", onChange);
497
+ return () => mq.removeEventListener("change", onChange);
498
+ }, []);
499
+
500
+ <Login projectId="…" mode={mode} />
501
+ ```
502
+
503
+ > The package deliberately does **not** read `prefers-color-scheme` itself — an auth screen
504
+ > that silently disagrees with the host app's theme is worse than one that follows it, and only
505
+ > your app knows which theme it's in.
506
+
507
+ ---
508
+
375
509
  ## Theming
376
510
 
377
511
  The UI reads `--aas-*` CSS custom properties, so you can retheme without touching the components. Override them on `:root` (or any ancestor) after importing the stylesheet:
@@ -389,6 +523,28 @@ The UI reads `--aas-*` CSS custom properties, so you can retheme without touchin
389
523
  }
390
524
  ```
391
525
 
526
+ Dark mode is the same variables redeclared on `[data-aas-theme='dark']`, so retheming dark is symmetrical — override there and light stays untouched:
527
+
528
+ ```css
529
+ [data-aas-theme="dark"] {
530
+ --aas-background: oklch(0.16 0.01 265);
531
+ --aas-card: oklch(0.21 0.01 265);
532
+ --aas-primary: oklch(0.72 0.17 265);
533
+ --aas-primary-foreground: oklch(0.16 0.01 265);
534
+ }
535
+ ```
536
+
537
+ Beyond the shadcn-style tokens above, a few extra ones exist so dark mode is a pure variable
538
+ swap — override them to fine-tune either palette:
539
+
540
+ | Token | Used for |
541
+ | --- | --- |
542
+ | `--aas-sidebar` | Account Settings sidebar panel. |
543
+ | `--aas-hover` / `--aas-hover-strong` | Nav/icon-button hover and active fills. |
544
+ | `--aas-overlay` / `--aas-overlay-strong` | The blur overlays on loading cards and settings sections. |
545
+ | `--aas-success` / `--aas-success-bg` | "Verified" badges and credit amounts. |
546
+ | `--aas-switch-track`, `--aas-switch-knob`, `--aas-switch-knob-on` | The settings toggle switch. |
547
+
392
548
  The styles are theme-aware and scoped to the components — they won't affect the rest of your app, and your app's global resets can't override them.
393
549
 
394
550
  ---
@@ -401,6 +557,8 @@ Beyond the components, these functions are exported for building custom UIs:
401
557
  | --- | --- |
402
558
  | `getAuthConfig(projectId, apiBaseUrl?)` | `GET /auth-config` → `AuthConfig` (captcha, branding, card layout; no token) |
403
559
  | `verifyEmailApi(projectId, { token, captchaToken }, apiBaseUrl?)` | `POST /auth/verify-email` |
560
+ | `resetPasswordApi(projectId, { token, password, captchaToken }, apiBaseUrl?)` | `POST /auth/reset-password` |
561
+ | `getCurrenciesApi(projectId, apiBaseUrl?)` | `GET /currency` → `ProjectCurrency[]` |
404
562
  | `getUser(projectId, token, apiBaseUrl?)` | `GET /me` (raw envelope) |
405
563
  | `getUserProfile(projectId, token, apiBaseUrl?)` | `GET /me` → `MappedProfile` |
406
564
  | `getUserWallet(projectId, token, apiBaseUrl?)` | `GET /me` → `UserWallets` |
@@ -419,7 +577,7 @@ Everything is fully typed. Notable exports:
419
577
 
420
578
  ```ts
421
579
  import type {
422
- LoginProps, SignupProps, ForgotPasswordProps, VerifyEmailProps, AuthClassNames,
580
+ LoginProps, SignupProps, ForgotPasswordProps, VerifyEmailProps, AuthClassNames, AuthMode,
423
581
  AccountSettingsProps, AccountSettingsSection, AccountSettingsData, ProfileFormValues,
424
582
  OpenAccountSettingsOptions, AccountSettingsHandle,
425
583
  AuthConfig, CaptchaConfig, CardLayoutConfig,