@better-auth-ui/heroui 1.6.6 → 1.6.8

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.
Files changed (50) hide show
  1. package/dist/components/auth/api-key/api-key-skeleton.d.ts +1 -0
  2. package/dist/components/auth/api-key/api-key-skeleton.js +21 -0
  3. package/dist/components/auth/api-key/api-key.d.ts +5 -0
  4. package/dist/components/auth/api-key/api-key.js +55 -0
  5. package/dist/components/auth/api-key/api-keys-empty.d.ts +4 -0
  6. package/dist/components/auth/api-key/api-keys-empty.js +35 -0
  7. package/dist/components/auth/api-key/api-keys.d.ts +6 -0
  8. package/dist/components/auth/api-key/api-keys.js +41 -0
  9. package/dist/components/auth/api-key/create-api-key-dialog.d.ts +5 -0
  10. package/dist/components/auth/api-key/create-api-key-dialog.js +74 -0
  11. package/dist/components/auth/api-key/delete-api-key-dialog.d.ts +7 -0
  12. package/dist/components/auth/api-key/delete-api-key-dialog.js +51 -0
  13. package/dist/components/auth/api-key/new-api-key-dialog.d.ts +7 -0
  14. package/dist/components/auth/api-key/new-api-key-dialog.js +60 -0
  15. package/dist/components/auth/multi-session/switch-account-submenu-content.js +14 -15
  16. package/dist/components/auth/passkey/add-passkey-dialog.d.ts +5 -0
  17. package/dist/components/auth/passkey/add-passkey-dialog.js +65 -0
  18. package/dist/components/auth/passkey/delete-passkey-dialog.d.ts +11 -0
  19. package/dist/components/auth/passkey/delete-passkey-dialog.js +47 -0
  20. package/dist/components/auth/passkey/passkey-skeleton.d.ts +1 -0
  21. package/dist/components/auth/passkey/passkey-skeleton.js +17 -0
  22. package/dist/components/auth/passkey/passkey.d.ts +2 -5
  23. package/dist/components/auth/passkey/passkey.js +16 -13
  24. package/dist/components/auth/passkey/passkeys-empty.d.ts +4 -0
  25. package/dist/components/auth/passkey/passkeys-empty.js +35 -0
  26. package/dist/components/auth/passkey/passkeys.js +35 -99
  27. package/dist/components/auth/settings/security/linked-account.js +1 -1
  28. package/dist/lib/auth/api-key-plugin.d.ts +23 -0
  29. package/dist/lib/auth/api-key-plugin.js +10 -0
  30. package/dist/lib/auth/passkey-plugin.d.ts +4 -1
  31. package/dist/plugins.d.ts +2 -0
  32. package/dist/plugins.js +22 -20
  33. package/package.json +3 -3
  34. package/src/components/auth/api-key/api-key-skeleton.tsx +17 -0
  35. package/src/components/auth/api-key/api-key.tsx +64 -0
  36. package/src/components/auth/api-key/api-keys-empty.tsx +33 -0
  37. package/src/components/auth/api-key/api-keys.tsx +71 -0
  38. package/src/components/auth/api-key/create-api-key-dialog.tsx +134 -0
  39. package/src/components/auth/api-key/delete-api-key-dialog.tsx +92 -0
  40. package/src/components/auth/api-key/new-api-key-dialog.tsx +94 -0
  41. package/src/components/auth/multi-session/switch-account-submenu-content.tsx +1 -3
  42. package/src/components/auth/passkey/add-passkey-dialog.tsx +109 -0
  43. package/src/components/auth/passkey/delete-passkey-dialog.tsx +95 -0
  44. package/src/components/auth/passkey/passkey-skeleton.tsx +16 -0
  45. package/src/components/auth/passkey/passkey.tsx +23 -22
  46. package/src/components/auth/passkey/passkeys-empty.tsx +35 -0
  47. package/src/components/auth/passkey/passkeys.tsx +33 -154
  48. package/src/components/auth/settings/security/linked-account.tsx +2 -4
  49. package/src/lib/auth/api-key-plugin.ts +15 -0
  50. package/src/plugins.ts +2 -0
@@ -0,0 +1,4 @@
1
+ export type PasskeysEmptyProps = {
2
+ onAddPress: () => void;
3
+ };
4
+ export declare function PasskeysEmpty({ onAddPress }: PasskeysEmptyProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,35 @@
1
+ import { passkeyPlugin as e } from "../../../lib/auth/passkey-plugin.js";
2
+ import { useAuthPlugin as t } from "@better-auth-ui/react";
3
+ import { Fingerprint as n } from "@gravity-ui/icons";
4
+ import { Button as r } from "@heroui/react";
5
+ import { jsx as i, jsxs as a } from "react/jsx-runtime";
6
+ //#region src/components/auth/passkey/passkeys-empty.tsx
7
+ function o({ onAddPress: o }) {
8
+ let { localization: s } = t(e);
9
+ return /* @__PURE__ */ a("div", {
10
+ className: "flex flex-col items-center justify-center gap-4",
11
+ children: [
12
+ /* @__PURE__ */ i("div", {
13
+ className: "flex size-10 items-center justify-center rounded-xl bg-surface-secondary",
14
+ children: /* @__PURE__ */ i(n, { className: "size-4.5" })
15
+ }),
16
+ /* @__PURE__ */ a("div", {
17
+ className: "flex flex-col items-center justify-center gap-1 text-center",
18
+ children: [/* @__PURE__ */ i("p", {
19
+ className: "text-sm font-semibold",
20
+ children: s.noPasskeys
21
+ }), /* @__PURE__ */ i("p", {
22
+ className: "text-muted text-xs",
23
+ children: s.passkeysDescription
24
+ })]
25
+ }),
26
+ /* @__PURE__ */ i(r, {
27
+ size: "sm",
28
+ onPress: o,
29
+ children: s.addPasskey
30
+ })
31
+ ]
32
+ });
33
+ }
34
+ //#endregion
35
+ export { o as PasskeysEmpty };
@@ -1,106 +1,42 @@
1
1
  import { passkeyPlugin as e } from "../../../lib/auth/passkey-plugin.js";
2
- import { Passkey as t } from "./passkey.js";
3
- import { useAddPasskey as n, useAuth as r, useAuthPlugin as i, useListPasskeys as a } from "@better-auth-ui/react";
4
- import { Fingerprint as o } from "@gravity-ui/icons";
5
- import { AlertDialog as s, Button as c, Card as l, FieldError as u, Form as d, Input as f, Label as p, Skeleton as m, Spinner as h, TextField as g, cn as _ } from "@heroui/react";
6
- import { useState as v } from "react";
7
- import { Fragment as y, jsx as b, jsxs as x } from "react/jsx-runtime";
2
+ import { AddPasskeyDialog as t } from "./add-passkey-dialog.js";
3
+ import { Passkey as n } from "./passkey.js";
4
+ import { PasskeySkeleton as r } from "./passkey-skeleton.js";
5
+ import { PasskeysEmpty as i } from "./passkeys-empty.js";
6
+ import { useAuth as a, useAuthPlugin as o, useListPasskeys as s } from "@better-auth-ui/react";
7
+ import { Button as c, Card as l, cn as u } from "@heroui/react";
8
+ import { useState as d } from "react";
9
+ import { jsx as f, jsxs as p } from "react/jsx-runtime";
8
10
  //#region src/components/auth/passkey/passkeys.tsx
9
- function S({ className: m, variant: S, ...w }) {
10
- let { authClient: T, localization: E } = r(), { localization: D } = i(e), { data: O, isPending: k } = a(T), { mutate: A, isPending: j } = n(T), [M, N] = v(!1), [P, F] = v(""), I = (e) => {
11
- N(e), F("");
12
- }, L = async (e) => {
13
- e.preventDefault(), A({ name: P.trim() || void 0 }, { onSuccess: () => {
14
- N(!1), F("");
15
- } });
16
- };
17
- return /* @__PURE__ */ x("div", { children: [/* @__PURE__ */ b("h2", {
18
- className: _("text-sm font-semibold mb-3"),
19
- children: D.passkeys
20
- }), /* @__PURE__ */ b(l, {
21
- className: _(m),
22
- variant: S,
23
- ...w,
24
- children: /* @__PURE__ */ x(l.Content, {
25
- className: "gap-0",
26
- children: [/* @__PURE__ */ x("div", {
27
- className: "flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between",
28
- children: [/* @__PURE__ */ x("div", { children: [/* @__PURE__ */ b("p", {
29
- className: "text-sm font-medium leading-tight",
30
- children: D.passkeysDescription
31
- }), /* @__PURE__ */ b("p", {
32
- className: "text-muted text-xs mt-0.5",
33
- children: D.passkeysInstructions
34
- })] }), /* @__PURE__ */ x(s, { children: [/* @__PURE__ */ b(c, {
11
+ function m({ className: m, variant: h, ...g }) {
12
+ let { authClient: _ } = a(), { localization: v } = o(e), { data: y, isPending: b } = s(_), [x, S] = d(!1);
13
+ return /* @__PURE__ */ p("div", {
14
+ className: u("flex flex-col gap-3", m),
15
+ children: [
16
+ /* @__PURE__ */ p("div", {
17
+ className: "flex items-end justify-between gap-3",
18
+ children: [/* @__PURE__ */ f("h2", {
19
+ className: "text-sm font-semibold truncate",
20
+ children: v.passkeys
21
+ }), /* @__PURE__ */ f(c, {
35
22
  className: "shrink-0",
36
23
  size: "sm",
37
- isDisabled: k,
38
- onPress: () => N(!0),
39
- children: D.addPasskey
40
- }), /* @__PURE__ */ b(s.Backdrop, {
41
- isOpen: M,
42
- onOpenChange: I,
43
- children: /* @__PURE__ */ b(s.Container, { children: /* @__PURE__ */ b(s.Dialog, { children: /* @__PURE__ */ x(d, {
44
- onSubmit: L,
45
- children: [
46
- /* @__PURE__ */ b(s.CloseTrigger, {}),
47
- /* @__PURE__ */ x(s.Header, { children: [/* @__PURE__ */ b(s.Icon, {
48
- status: "default",
49
- children: /* @__PURE__ */ b(o, {})
50
- }), /* @__PURE__ */ b(s.Heading, { children: D.addPasskey })] }),
51
- /* @__PURE__ */ x(s.Body, {
52
- className: "overflow-visible",
53
- children: [/* @__PURE__ */ b("p", {
54
- className: "text-muted text-sm",
55
- children: D.passkeysInstructions
56
- }), /* @__PURE__ */ x(g, {
57
- className: "mt-4",
58
- name: "passkey-name",
59
- isDisabled: j,
60
- value: P,
61
- onChange: F,
62
- children: [
63
- /* @__PURE__ */ b(p, { children: D.passkey }),
64
- /* @__PURE__ */ b(f, {
65
- autoFocus: !0,
66
- placeholder: E.settings.optional,
67
- variant: "secondary"
68
- }),
69
- /* @__PURE__ */ b(u, {})
70
- ]
71
- })]
72
- }),
73
- /* @__PURE__ */ x(s.Footer, { children: [/* @__PURE__ */ b(c, {
74
- slot: "close",
75
- variant: "tertiary",
76
- isDisabled: j,
77
- children: E.settings.cancel
78
- }), /* @__PURE__ */ x(c, {
79
- type: "submit",
80
- isPending: j,
81
- children: [j && /* @__PURE__ */ b(h, {
82
- color: "current",
83
- size: "sm"
84
- }), D.addPasskey]
85
- })] })
86
- ]
87
- }) }) })
88
- })] })]
89
- }), k ? /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ b("div", { className: "border-b border-dashed -mx-4 md:-mx-6 my-4" }), /* @__PURE__ */ b(C, {})] }) : O?.map((e) => /* @__PURE__ */ x("div", { children: [/* @__PURE__ */ b("div", { className: "border-b border-dashed -mx-4 md:-mx-6 my-4" }), /* @__PURE__ */ b(t, { passkey: e })] }, e.id))]
90
- })
91
- })] });
92
- }
93
- function C() {
94
- return /* @__PURE__ */ b("div", {
95
- className: "flex items-center justify-between",
96
- children: /* @__PURE__ */ x("div", {
97
- className: "flex items-center gap-3",
98
- children: [/* @__PURE__ */ b(m, { className: "size-10 rounded-xl" }), /* @__PURE__ */ x("div", {
99
- className: "flex flex-col gap-1",
100
- children: [/* @__PURE__ */ b(m, { className: "h-4 w-24 rounded-lg" }), /* @__PURE__ */ b(m, { className: "h-3 w-20 rounded-lg" })]
101
- })]
102
- })
24
+ isDisabled: b,
25
+ onPress: () => S(!0),
26
+ children: v.addPasskey
27
+ })]
28
+ }),
29
+ /* @__PURE__ */ f(l, {
30
+ variant: h,
31
+ ...g,
32
+ children: /* @__PURE__ */ f(l.Content, { children: b ? /* @__PURE__ */ f(r, {}) : y?.length ? y.map((e, t) => /* @__PURE__ */ p("div", { children: [t > 0 && /* @__PURE__ */ f("div", { className: "border-b border-dashed -mx-4 my-4" }), /* @__PURE__ */ f(n, { passkey: e })] }, e.id)) : /* @__PURE__ */ f(i, { onAddPress: () => S(!0) }) })
33
+ }),
34
+ /* @__PURE__ */ f(t, {
35
+ isOpen: x,
36
+ onOpenChange: S
37
+ })
38
+ ]
103
39
  });
104
40
  }
105
41
  //#endregion
106
- export { S as Passkeys };
42
+ export { m as Passkeys };
@@ -11,7 +11,7 @@ function g({ account: g, provider: _ }) {
11
11
  children: [
12
12
  /* @__PURE__ */ m("div", {
13
13
  className: f("flex size-10 shrink-0 items-center justify-center rounded-xl bg-surface-secondary"),
14
- children: m(D || c, { className: f("size-4.5", !g && "opacity-50") })
14
+ children: m(D || c, { className: "size-4.5" })
15
15
  }),
16
16
  /* @__PURE__ */ h("div", {
17
17
  className: "flex flex-col min-w-0",
@@ -0,0 +1,23 @@
1
+ import { ApiKeyPluginOptions } from '@better-auth-ui/core/plugins';
2
+ import { ApiKeys } from '../../components/auth/api-key/api-keys';
3
+ export declare const apiKeyPlugin: ((options?: ApiKeyPluginOptions | undefined) => Omit<{
4
+ securityCards: (typeof ApiKeys)[];
5
+ localization: {
6
+ apiKey: string;
7
+ apiKeys: string;
8
+ apiKeysDescription: string;
9
+ createApiKey: string;
10
+ noApiKeys: string;
11
+ name: string;
12
+ newApiKey: string;
13
+ newApiKeyWarning: string;
14
+ deleteApiKey: string;
15
+ deleteApiKeyWarning: string;
16
+ dismissNewKey: string;
17
+ };
18
+ id: "apiKey";
19
+ }, "id"> & {
20
+ id: "apiKey";
21
+ }) & {
22
+ id: "apiKey";
23
+ };
@@ -0,0 +1,10 @@
1
+ import { ApiKeys as e } from "../../components/auth/api-key/api-keys.js";
2
+ import { createAuthPlugin as t } from "@better-auth-ui/core";
3
+ import { apiKeyPlugin as n } from "@better-auth-ui/core/plugins";
4
+ //#region src/lib/auth/api-key-plugin.ts
5
+ var r = t(n.id, (t = {}) => ({
6
+ ...n(t),
7
+ securityCards: [e]
8
+ }));
9
+ //#endregion
10
+ export { r as apiKeyPlugin };
@@ -8,9 +8,12 @@ export declare const passkeyPlugin: ((options?: PasskeyPluginOptions | undefined
8
8
  passkey: string;
9
9
  addPasskey: string;
10
10
  deletePasskey: string;
11
+ deletePasskeyTitle: string;
12
+ deletePasskeyWarning: string;
11
13
  passkeys: string;
12
14
  passkeysDescription: string;
13
- passkeysInstructions: string;
15
+ noPasskeys: string;
16
+ name: string;
14
17
  };
15
18
  id: "passkey";
16
19
  }, "id"> & {
package/dist/plugins.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './components/auth/api-key/api-keys';
1
2
  export * from './components/auth/delete-user/danger-zone';
2
3
  export * from './components/auth/delete-user/delete-user';
3
4
  export * from './components/auth/magic-link/magic-link';
@@ -11,6 +12,7 @@ export * from './components/auth/theme/appearance';
11
12
  export * from './components/auth/theme/theme-toggle-item';
12
13
  export * from './components/auth/username/sign-in-username';
13
14
  export * from './components/auth/username/username-field';
15
+ export * from './lib/auth/api-key-plugin';
14
16
  export * from './lib/auth/auth-plugin';
15
17
  export * from './lib/auth/delete-user-plugin';
16
18
  export * from './lib/auth/magic-link-plugin';
package/dist/plugins.js CHANGED
@@ -1,21 +1,23 @@
1
1
  "use client";
2
- import { deleteUserPlugin as e } from "./lib/auth/delete-user-plugin.js";
3
- import { DeleteUser as t } from "./components/auth/delete-user/delete-user.js";
4
- import { DangerZone as n } from "./components/auth/delete-user/danger-zone.js";
5
- import { magicLinkPlugin as r } from "./lib/auth/magic-link-plugin.js";
6
- import { MagicLink as i } from "./components/auth/magic-link/magic-link.js";
7
- import { ManageAccounts as a } from "./components/auth/multi-session/manage-accounts.js";
8
- import { SwitchAccountSubmenuItem as o } from "./components/auth/multi-session/switch-account-submenu-item.js";
9
- import { SwitchAccountSubmenuContent as s } from "./components/auth/multi-session/switch-account-submenu-content.js";
10
- import { SwitchAccountSubmenu as c } from "./components/auth/multi-session/switch-account-submenu.js";
11
- import { multiSessionPlugin as l } from "./lib/auth/multi-session-plugin.js";
12
- import { ManageAccount as u } from "./components/auth/multi-session/manage-account.js";
13
- import { passkeyPlugin as d } from "./lib/auth/passkey-plugin.js";
14
- import { Passkeys as f } from "./components/auth/passkey/passkeys.js";
15
- import { ThemeToggleItem as p } from "./components/auth/theme/theme-toggle-item.js";
16
- import { themePlugin as m } from "./lib/auth/theme-plugin.js";
17
- import { Appearance as h } from "./components/auth/theme/appearance.js";
18
- import { UsernameField as g } from "./components/auth/username/username-field.js";
19
- import { usernamePlugin as _ } from "./lib/auth/username-plugin.js";
20
- import { SignInUsername as v } from "./components/auth/username/sign-in-username.js";
21
- export { h as Appearance, n as DangerZone, t as DeleteUser, i as MagicLink, u as ManageAccount, a as ManageAccounts, f as Passkeys, v as SignInUsername, c as SwitchAccountSubmenu, s as SwitchAccountSubmenuContent, o as SwitchAccountSubmenuItem, p as ThemeToggleItem, g as UsernameField, e as deleteUserPlugin, r as magicLinkPlugin, l as multiSessionPlugin, d as passkeyPlugin, m as themePlugin, _ as usernamePlugin };
2
+ import { apiKeyPlugin as e } from "./lib/auth/api-key-plugin.js";
3
+ import { ApiKeys as t } from "./components/auth/api-key/api-keys.js";
4
+ import { deleteUserPlugin as n } from "./lib/auth/delete-user-plugin.js";
5
+ import { DeleteUser as r } from "./components/auth/delete-user/delete-user.js";
6
+ import { DangerZone as i } from "./components/auth/delete-user/danger-zone.js";
7
+ import { magicLinkPlugin as a } from "./lib/auth/magic-link-plugin.js";
8
+ import { MagicLink as o } from "./components/auth/magic-link/magic-link.js";
9
+ import { ManageAccounts as s } from "./components/auth/multi-session/manage-accounts.js";
10
+ import { SwitchAccountSubmenuItem as c } from "./components/auth/multi-session/switch-account-submenu-item.js";
11
+ import { SwitchAccountSubmenuContent as l } from "./components/auth/multi-session/switch-account-submenu-content.js";
12
+ import { SwitchAccountSubmenu as u } from "./components/auth/multi-session/switch-account-submenu.js";
13
+ import { multiSessionPlugin as d } from "./lib/auth/multi-session-plugin.js";
14
+ import { ManageAccount as f } from "./components/auth/multi-session/manage-account.js";
15
+ import { passkeyPlugin as p } from "./lib/auth/passkey-plugin.js";
16
+ import { Passkeys as m } from "./components/auth/passkey/passkeys.js";
17
+ import { ThemeToggleItem as h } from "./components/auth/theme/theme-toggle-item.js";
18
+ import { themePlugin as g } from "./lib/auth/theme-plugin.js";
19
+ import { Appearance as _ } from "./components/auth/theme/appearance.js";
20
+ import { UsernameField as v } from "./components/auth/username/username-field.js";
21
+ import { usernamePlugin as y } from "./lib/auth/username-plugin.js";
22
+ import { SignInUsername as b } from "./components/auth/username/sign-in-username.js";
23
+ export { t as ApiKeys, _ as Appearance, i as DangerZone, r as DeleteUser, o as MagicLink, f as ManageAccount, s as ManageAccounts, m as Passkeys, b as SignInUsername, u as SwitchAccountSubmenu, l as SwitchAccountSubmenuContent, c as SwitchAccountSubmenuItem, h as ThemeToggleItem, v as UsernameField, e as apiKeyPlugin, n as deleteUserPlugin, a as magicLinkPlugin, d as multiSessionPlugin, p as passkeyPlugin, g as themePlugin, y as usernamePlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth-ui/heroui",
3
- "version": "1.6.6",
3
+ "version": "1.6.8",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "vite build",
@@ -38,7 +38,7 @@
38
38
  "@types/react": "^19.2.14",
39
39
  "@vitejs/plugin-react": "^6.0.1",
40
40
  "@vitest/browser-playwright": "^4.1.5",
41
- "better-auth": "^1.6.9",
41
+ "better-auth": "^1.6.10",
42
42
  "bowser": "^2.11.0",
43
43
  "playwright": "^1.59.1",
44
44
  "react": "^19.2.6",
@@ -54,7 +54,7 @@
54
54
  "@internationalized/date": ">=3.12.1",
55
55
  "@tanstack/react-pacer": ">=0.22.0",
56
56
  "@tanstack/react-query": ">=5.100.9",
57
- "better-auth": ">=1.6.9",
57
+ "better-auth": ">=1.6.10",
58
58
  "react": ">=19.2.6",
59
59
  "react-dom": ">=19.2.6",
60
60
  "bowser": ">=2.11.0"
@@ -0,0 +1,17 @@
1
+ import { Skeleton } from "@heroui/react"
2
+
3
+ export function ApiKeySkeleton() {
4
+ return (
5
+ <div className="flex items-center justify-between">
6
+ <div className="flex items-center gap-3">
7
+ <Skeleton className="size-10 rounded-xl" />
8
+
9
+ <div className="flex flex-col gap-1">
10
+ <Skeleton className="h-4 w-28 rounded-lg" />
11
+ <Skeleton className="h-3 w-36 rounded-lg" />
12
+ <Skeleton className="h-3 w-32 rounded-lg" />
13
+ </div>
14
+ </div>
15
+ </div>
16
+ )
17
+ }
@@ -0,0 +1,64 @@
1
+ import {
2
+ type ListedApiKey,
3
+ useAuth,
4
+ useAuthPlugin
5
+ } from "@better-auth-ui/react"
6
+ import { Key, Xmark } from "@gravity-ui/icons"
7
+ import { Button } from "@heroui/react"
8
+ import { useState } from "react"
9
+
10
+ import { apiKeyPlugin } from "../../../lib/auth/api-key-plugin"
11
+ import { DeleteApiKeyDialog } from "./delete-api-key-dialog"
12
+
13
+ export type ApiKeyProps = {
14
+ apiKey: ListedApiKey
15
+ }
16
+
17
+ export function ApiKey({ apiKey }: ApiKeyProps) {
18
+ const { localization } = useAuth()
19
+ const { localization: apiKeyLocalization } = useAuthPlugin(apiKeyPlugin)
20
+ const [deleteOpen, setDeleteOpen] = useState(false)
21
+
22
+ const preview = `${apiKey.start}${"*".repeat(16)}`
23
+
24
+ return (
25
+ <div className="flex items-center gap-3">
26
+ <div className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-surface-secondary">
27
+ <Key className="size-4.5" />
28
+ </div>
29
+
30
+ <div className="flex flex-col min-w-0">
31
+ <span className="text-sm font-medium leading-tight truncate">
32
+ {apiKey.name || apiKeyLocalization.apiKey}
33
+ </span>
34
+
35
+ <span className="text-xs text-muted font-mono truncate">{preview}</span>
36
+
37
+ <span className="text-xs text-muted">
38
+ {new Date(apiKey.createdAt).toLocaleString(undefined, {
39
+ dateStyle: "medium",
40
+ timeStyle: "short"
41
+ })}
42
+ </span>
43
+ </div>
44
+
45
+ <Button
46
+ className="ml-auto shrink-0"
47
+ variant="outline"
48
+ size="sm"
49
+ onPress={() => setDeleteOpen(true)}
50
+ aria-label={apiKeyLocalization.deleteApiKey}
51
+ >
52
+ <Xmark />
53
+
54
+ {localization.settings.delete}
55
+ </Button>
56
+
57
+ <DeleteApiKeyDialog
58
+ isOpen={deleteOpen}
59
+ onOpenChange={setDeleteOpen}
60
+ apiKey={apiKey}
61
+ />
62
+ </div>
63
+ )
64
+ }
@@ -0,0 +1,33 @@
1
+ import { useAuthPlugin } from "@better-auth-ui/react"
2
+ import { Key } from "@gravity-ui/icons"
3
+ import { Button } from "@heroui/react"
4
+
5
+ import { apiKeyPlugin } from "../../../lib/auth/api-key-plugin"
6
+
7
+ export type ApiKeysEmptyProps = {
8
+ onCreatePress: () => void
9
+ }
10
+
11
+ export function ApiKeysEmpty({ onCreatePress }: ApiKeysEmptyProps) {
12
+ const { localization: apiKeyLocalization } = useAuthPlugin(apiKeyPlugin)
13
+
14
+ return (
15
+ <div className="flex flex-col items-center justify-center gap-4">
16
+ <div className="flex size-10 items-center justify-center rounded-xl bg-surface-secondary">
17
+ <Key className="size-4.5" />
18
+ </div>
19
+
20
+ <div className="flex flex-col items-center justify-center gap-1 text-center">
21
+ <p className="text-sm font-semibold">{apiKeyLocalization.noApiKeys}</p>
22
+
23
+ <p className="text-muted text-xs">
24
+ {apiKeyLocalization.apiKeysDescription}
25
+ </p>
26
+ </div>
27
+
28
+ <Button size="sm" onPress={onCreatePress}>
29
+ {apiKeyLocalization.createApiKey}
30
+ </Button>
31
+ </div>
32
+ )
33
+ }
@@ -0,0 +1,71 @@
1
+ import {
2
+ type ApiKeyAuthClient,
3
+ useAuth,
4
+ useAuthPlugin,
5
+ useListApiKeys
6
+ } from "@better-auth-ui/react"
7
+ import { Button, Card, type CardProps, cn } from "@heroui/react"
8
+ import { useState } from "react"
9
+
10
+ import { apiKeyPlugin } from "../../../lib/auth/api-key-plugin"
11
+ import { ApiKey } from "./api-key"
12
+ import { ApiKeySkeleton } from "./api-key-skeleton"
13
+ import { ApiKeysEmpty } from "./api-keys-empty"
14
+ import { CreateApiKeyDialog } from "./create-api-key-dialog"
15
+
16
+ export type ApiKeysProps = {
17
+ className?: string
18
+ variant?: CardProps["variant"]
19
+ }
20
+
21
+ export function ApiKeys({ className, variant }: ApiKeysProps) {
22
+ const { authClient } = useAuth()
23
+ const { localization: apiKeyLocalization } = useAuthPlugin(apiKeyPlugin)
24
+
25
+ const { data: listData, isPending } = useListApiKeys(
26
+ authClient as ApiKeyAuthClient
27
+ )
28
+
29
+ const [createOpen, setCreateOpen] = useState(false)
30
+
31
+ return (
32
+ <div className={cn("flex flex-col gap-3", className)}>
33
+ <div className="flex items-end justify-between gap-3">
34
+ <h2 className="text-sm font-semibold truncate">
35
+ {apiKeyLocalization.apiKeys}
36
+ </h2>
37
+
38
+ <Button
39
+ className="shrink-0"
40
+ size="sm"
41
+ isDisabled={isPending}
42
+ onPress={() => setCreateOpen(true)}
43
+ >
44
+ {apiKeyLocalization.createApiKey}
45
+ </Button>
46
+ </div>
47
+
48
+ <Card variant={variant}>
49
+ <Card.Content>
50
+ {isPending ? (
51
+ <ApiKeySkeleton />
52
+ ) : !listData?.apiKeys.length ? (
53
+ <ApiKeysEmpty onCreatePress={() => setCreateOpen(true)} />
54
+ ) : (
55
+ listData?.apiKeys.map((key, index) => (
56
+ <div key={key.id}>
57
+ {index > 0 && (
58
+ <div className="border-b border-dashed -mx-4 my-4" />
59
+ )}
60
+
61
+ <ApiKey apiKey={key} />
62
+ </div>
63
+ ))
64
+ )}
65
+ </Card.Content>
66
+ </Card>
67
+
68
+ <CreateApiKeyDialog isOpen={createOpen} onOpenChange={setCreateOpen} />
69
+ </div>
70
+ )
71
+ }
@@ -0,0 +1,134 @@
1
+ import {
2
+ type ApiKeyAuthClient,
3
+ useAuth,
4
+ useAuthPlugin,
5
+ useCreateApiKey
6
+ } from "@better-auth-ui/react"
7
+ import { Key } from "@gravity-ui/icons"
8
+ import {
9
+ AlertDialog,
10
+ Button,
11
+ FieldError,
12
+ Form,
13
+ Input,
14
+ Label,
15
+ Spinner,
16
+ TextField
17
+ } from "@heroui/react"
18
+ import { type SyntheticEvent, useState } from "react"
19
+
20
+ import { apiKeyPlugin } from "../../../lib/auth/api-key-plugin"
21
+
22
+ import { NewApiKeyDialog } from "./new-api-key-dialog"
23
+
24
+ export type CreateApiKeyDialogProps = {
25
+ isOpen: boolean
26
+ onOpenChange: (open: boolean) => void
27
+ }
28
+
29
+ export function CreateApiKeyDialog({
30
+ isOpen,
31
+ onOpenChange
32
+ }: CreateApiKeyDialogProps) {
33
+ const { authClient, localization } = useAuth()
34
+ const { localization: apiKeyLocalization } = useAuthPlugin(apiKeyPlugin)
35
+
36
+ const { mutate: createApiKey, isPending: isCreating } = useCreateApiKey(
37
+ authClient as ApiKeyAuthClient
38
+ )
39
+
40
+ const [isNewKeyDialogOpen, setIsNewKeyDialogOpen] = useState(false)
41
+ const [keyName, setKeyName] = useState<string | null>(null)
42
+ const [secretKey, setSecretKey] = useState<string | null>(null)
43
+
44
+ const handleOpenChange = (open: boolean) => {
45
+ if (!open) {
46
+ setKeyName(null)
47
+ setSecretKey(null)
48
+ }
49
+
50
+ onOpenChange(open)
51
+ }
52
+
53
+ const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {
54
+ e.preventDefault()
55
+
56
+ const formData = new FormData(e.target as HTMLFormElement)
57
+ const name = (formData.get("name") as string)?.trim()
58
+
59
+ createApiKey(name ? { name } : undefined, {
60
+ onSuccess: (result) => {
61
+ handleOpenChange(false)
62
+ setKeyName(name)
63
+ setSecretKey(result.key)
64
+ setIsNewKeyDialogOpen(true)
65
+ }
66
+ })
67
+ }
68
+
69
+ return (
70
+ <>
71
+ <AlertDialog.Backdrop isOpen={isOpen} onOpenChange={handleOpenChange}>
72
+ <AlertDialog.Container>
73
+ <AlertDialog.Dialog>
74
+ <Form onSubmit={handleSubmit}>
75
+ <AlertDialog.CloseTrigger />
76
+
77
+ <AlertDialog.Header>
78
+ <AlertDialog.Icon status="default">
79
+ <Key />
80
+ </AlertDialog.Icon>
81
+
82
+ <AlertDialog.Heading>
83
+ {apiKeyLocalization.createApiKey}
84
+ </AlertDialog.Heading>
85
+ </AlertDialog.Header>
86
+
87
+ <AlertDialog.Body className="overflow-visible">
88
+ <p className="text-muted text-sm">
89
+ {apiKeyLocalization.apiKeysDescription}
90
+ </p>
91
+
92
+ <TextField
93
+ className="mt-4"
94
+ id="name"
95
+ name="name"
96
+ isDisabled={isCreating}
97
+ >
98
+ <Label>{apiKeyLocalization.name}</Label>
99
+
100
+ <Input
101
+ autoFocus
102
+ placeholder={localization.settings.optional}
103
+ variant="secondary"
104
+ />
105
+
106
+ <FieldError />
107
+ </TextField>
108
+ </AlertDialog.Body>
109
+
110
+ <AlertDialog.Footer>
111
+ <Button slot="close" variant="tertiary" isDisabled={isCreating}>
112
+ {localization.settings.cancel}
113
+ </Button>
114
+
115
+ <Button type="submit" isPending={isCreating}>
116
+ {isCreating && <Spinner color="current" size="sm" />}
117
+
118
+ {apiKeyLocalization.createApiKey}
119
+ </Button>
120
+ </AlertDialog.Footer>
121
+ </Form>
122
+ </AlertDialog.Dialog>
123
+ </AlertDialog.Container>
124
+ </AlertDialog.Backdrop>
125
+
126
+ <NewApiKeyDialog
127
+ isOpen={isNewKeyDialogOpen}
128
+ onOpenChange={setIsNewKeyDialogOpen}
129
+ secretKey={secretKey}
130
+ name={keyName}
131
+ />
132
+ </>
133
+ )
134
+ }