@better-auth-ui/heroui 1.6.7 → 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.
@@ -16,8 +16,8 @@ function v({ isOpen: v, onOpenChange: y }) {
16
16
  children: /* @__PURE__ */ g(o.Container, { children: /* @__PURE__ */ g(o.Dialog, { children: /* @__PURE__ */ _(l, {
17
17
  onSubmit: (e) => {
18
18
  e.preventDefault();
19
- let t = new FormData(e.target).get("name");
20
- C(t ? { name: t.trim() } : void 0, { onSuccess: (e) => {
19
+ let t = new FormData(e.target).get("name")?.trim();
20
+ C(t ? { name: t } : void 0, { onSuccess: (e) => {
21
21
  j(!1), O(t), A(e.key), E(!0);
22
22
  } });
23
23
  },
@@ -0,0 +1,5 @@
1
+ export type AddPasskeyDialogProps = {
2
+ isOpen: boolean;
3
+ onOpenChange: (open: boolean) => void;
4
+ };
5
+ export declare function AddPasskeyDialog({ isOpen, onOpenChange }: AddPasskeyDialogProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,65 @@
1
+ import { passkeyPlugin as e } from "../../../lib/auth/passkey-plugin.js";
2
+ import { useAddPasskey as t, useAuth as n, useAuthPlugin as r } from "@better-auth-ui/react";
3
+ import { Fingerprint as i } from "@gravity-ui/icons";
4
+ import { AlertDialog as a, Button as o, FieldError as s, Form as c, Input as l, Label as u, Spinner as d, TextField as f } from "@heroui/react";
5
+ import { jsx as p, jsxs as m } from "react/jsx-runtime";
6
+ //#region src/components/auth/passkey/add-passkey-dialog.tsx
7
+ function h({ isOpen: h, onOpenChange: g }) {
8
+ let { authClient: _, localization: v } = n(), { localization: y } = r(e), { mutate: b, isPending: x } = t(_), S = (e) => {
9
+ g(e);
10
+ };
11
+ return /* @__PURE__ */ p(a.Backdrop, {
12
+ isOpen: h,
13
+ onOpenChange: S,
14
+ children: /* @__PURE__ */ p(a.Container, { children: /* @__PURE__ */ p(a.Dialog, { children: /* @__PURE__ */ m(c, {
15
+ onSubmit: (e) => {
16
+ e.preventDefault();
17
+ let t = new FormData(e.target).get("name")?.trim();
18
+ b(t ? { name: t } : void 0, { onSuccess: () => S(!1) });
19
+ },
20
+ children: [
21
+ /* @__PURE__ */ p(a.CloseTrigger, {}),
22
+ /* @__PURE__ */ m(a.Header, { children: [/* @__PURE__ */ p(a.Icon, {
23
+ status: "default",
24
+ children: /* @__PURE__ */ p(i, {})
25
+ }), /* @__PURE__ */ p(a.Heading, { children: y.addPasskey })] }),
26
+ /* @__PURE__ */ m(a.Body, {
27
+ className: "overflow-visible",
28
+ children: [/* @__PURE__ */ p("p", {
29
+ className: "text-muted text-sm",
30
+ children: y.passkeysDescription
31
+ }), /* @__PURE__ */ m(f, {
32
+ className: "mt-4",
33
+ id: "name",
34
+ name: "name",
35
+ isDisabled: x,
36
+ children: [
37
+ /* @__PURE__ */ p(u, { children: y.name }),
38
+ /* @__PURE__ */ p(l, {
39
+ autoFocus: !0,
40
+ placeholder: v.settings.optional,
41
+ variant: "secondary"
42
+ }),
43
+ /* @__PURE__ */ p(s, {})
44
+ ]
45
+ })]
46
+ }),
47
+ /* @__PURE__ */ m(a.Footer, { children: [/* @__PURE__ */ p(o, {
48
+ slot: "close",
49
+ variant: "tertiary",
50
+ isDisabled: x,
51
+ children: v.settings.cancel
52
+ }), /* @__PURE__ */ m(o, {
53
+ type: "submit",
54
+ isPending: x,
55
+ children: [x && /* @__PURE__ */ p(d, {
56
+ color: "current",
57
+ size: "sm"
58
+ }), y.addPasskey]
59
+ })] })
60
+ ]
61
+ }) }) })
62
+ });
63
+ }
64
+ //#endregion
65
+ export { h as AddPasskeyDialog };
@@ -0,0 +1,11 @@
1
+ export type ListedPasskey = {
2
+ id: string;
3
+ name?: string | null;
4
+ createdAt: Date;
5
+ };
6
+ export type DeletePasskeyDialogProps = {
7
+ isOpen: boolean;
8
+ onOpenChange: (open: boolean) => void;
9
+ passkey: ListedPasskey;
10
+ };
11
+ export declare function DeletePasskeyDialog({ isOpen, onOpenChange, passkey }: DeletePasskeyDialogProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,47 @@
1
+ import { passkeyPlugin as e } from "../../../lib/auth/passkey-plugin.js";
2
+ import { useAuth as t, useAuthPlugin as n, useDeletePasskey as r } from "@better-auth-ui/react";
3
+ import { Fingerprint as i } from "@gravity-ui/icons";
4
+ import { AlertDialog as a, Button as o, Input as s, Label as c, Spinner as l, TextField as u } from "@heroui/react";
5
+ import { jsx as d, jsxs as f } from "react/jsx-runtime";
6
+ //#region src/components/auth/passkey/delete-passkey-dialog.tsx
7
+ function p({ isOpen: p, onOpenChange: m, passkey: h }) {
8
+ let { authClient: g, localization: _ } = t(), { localization: v } = n(e), y = h.name || v.passkey, { mutate: b, isPending: x } = r(g, { onSuccess: () => m(!1) });
9
+ return /* @__PURE__ */ d(a.Backdrop, {
10
+ isOpen: p,
11
+ onOpenChange: m,
12
+ children: /* @__PURE__ */ d(a.Container, { children: /* @__PURE__ */ f(a.Dialog, { children: [
13
+ /* @__PURE__ */ d(a.CloseTrigger, {}),
14
+ /* @__PURE__ */ f(a.Header, { children: [/* @__PURE__ */ d(a.Icon, {
15
+ status: "danger",
16
+ children: /* @__PURE__ */ d(i, {})
17
+ }), /* @__PURE__ */ d(a.Heading, { children: v.deletePasskeyTitle })] }),
18
+ /* @__PURE__ */ f(a.Body, {
19
+ className: "flex flex-col gap-4 overflow-visible",
20
+ children: [/* @__PURE__ */ d("p", {
21
+ className: "text-muted text-sm",
22
+ children: v.deletePasskeyWarning
23
+ }), /* @__PURE__ */ f(u, {
24
+ value: y,
25
+ variant: "secondary",
26
+ children: [/* @__PURE__ */ d(c, { children: h.name || v.passkey }), /* @__PURE__ */ d(s, { readOnly: !0 })]
27
+ })]
28
+ }),
29
+ /* @__PURE__ */ f(a.Footer, { children: [/* @__PURE__ */ d(o, {
30
+ slot: "close",
31
+ variant: "tertiary",
32
+ isDisabled: x,
33
+ children: _.settings.cancel
34
+ }), /* @__PURE__ */ f(o, {
35
+ variant: "danger",
36
+ onPress: () => b({ id: h.id }),
37
+ isPending: x,
38
+ children: [x && /* @__PURE__ */ d(l, {
39
+ color: "current",
40
+ size: "sm"
41
+ }), v.deletePasskeyTitle]
42
+ })] })
43
+ ] }) })
44
+ });
45
+ }
46
+ //#endregion
47
+ export { p as DeletePasskeyDialog };
@@ -0,0 +1 @@
1
+ export declare function PasskeySkeleton(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { Skeleton as e } from "@heroui/react";
2
+ import { jsx as t, jsxs as n } from "react/jsx-runtime";
3
+ //#region src/components/auth/passkey/passkey-skeleton.tsx
4
+ function r() {
5
+ return /* @__PURE__ */ t("div", {
6
+ className: "flex items-center justify-between",
7
+ children: /* @__PURE__ */ n("div", {
8
+ className: "flex items-center gap-3",
9
+ children: [/* @__PURE__ */ t(e, { className: "size-10 rounded-xl" }), /* @__PURE__ */ n("div", {
10
+ className: "flex flex-col gap-1",
11
+ children: [/* @__PURE__ */ t(e, { className: "h-4 w-28 rounded-lg" }), /* @__PURE__ */ t(e, { className: "h-3 w-32 rounded-lg" })]
12
+ })]
13
+ })
14
+ });
15
+ }
16
+ //#endregion
17
+ export { r as PasskeySkeleton };
@@ -1,8 +1,5 @@
1
+ import { ListedPasskey } from './delete-passkey-dialog';
1
2
  export type PasskeyProps = {
2
- passkey: {
3
- id: string;
4
- name?: string | null;
5
- createdAt: Date;
6
- };
3
+ passkey: ListedPasskey;
7
4
  };
8
5
  export declare function Passkey({ passkey }: PasskeyProps): import("react/jsx-runtime").JSX.Element;
@@ -1,11 +1,13 @@
1
1
  import { passkeyPlugin as e } from "../../../lib/auth/passkey-plugin.js";
2
- import { useAuth as t, useAuthPlugin as n, useDeletePasskey as r } from "@better-auth-ui/react";
2
+ import { DeletePasskeyDialog as t } from "./delete-passkey-dialog.js";
3
+ import { useAuth as n, useAuthPlugin as r } from "@better-auth-ui/react";
3
4
  import { Fingerprint as i, Xmark as a } from "@gravity-ui/icons";
4
- import { Button as o, Spinner as s } from "@heroui/react";
5
+ import { Button as o } from "@heroui/react";
6
+ import { useState as s } from "react";
5
7
  import { jsx as c, jsxs as l } from "react/jsx-runtime";
6
8
  //#region src/components/auth/passkey/passkey.tsx
7
9
  function u({ passkey: u }) {
8
- let { authClient: d, localization: f } = t(), { localization: p } = n(e), { mutate: m, isPending: h } = r(d), g = u.name || p.passkey;
10
+ let { localization: d } = n(), { localization: f } = r(e), [p, m] = s(!1), h = u.name || f.passkey;
9
11
  return /* @__PURE__ */ l("div", {
10
12
  className: "flex items-center gap-3",
11
13
  children: [
@@ -14,10 +16,10 @@ function u({ passkey: u }) {
14
16
  children: /* @__PURE__ */ c(i, { className: "size-4.5" })
15
17
  }),
16
18
  /* @__PURE__ */ l("div", {
17
- className: "flex flex-col min-w-0",
19
+ className: "flex min-w-0 flex-col",
18
20
  children: [/* @__PURE__ */ c("span", {
19
- className: "text-sm font-medium leading-tight",
20
- children: g
21
+ className: "truncate text-sm font-medium leading-tight",
22
+ children: h
21
23
  }), /* @__PURE__ */ c("span", {
22
24
  className: "text-xs text-muted",
23
25
  children: new Date(u.createdAt).toLocaleString(void 0, {
@@ -30,13 +32,14 @@ function u({ passkey: u }) {
30
32
  className: "ml-auto shrink-0",
31
33
  variant: "outline",
32
34
  size: "sm",
33
- isPending: h,
34
- onPress: () => m({ id: u.id }),
35
- "aria-label": p.deletePasskey.replace("{{name}}", g),
36
- children: [h ? /* @__PURE__ */ c(s, {
37
- color: "current",
38
- size: "sm"
39
- }) : /* @__PURE__ */ c(a, {}), f.settings.delete]
35
+ onPress: () => m(!0),
36
+ "aria-label": f.deletePasskey.replace("{{name}}", h),
37
+ children: [/* @__PURE__ */ c(a, {}), d.settings.delete]
38
+ }),
39
+ /* @__PURE__ */ c(t, {
40
+ isOpen: p,
41
+ onOpenChange: m,
42
+ passkey: u
40
43
  })
41
44
  ]
42
45
  });
@@ -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 };
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth-ui/heroui",
3
- "version": "1.6.7",
3
+ "version": "1.6.8",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "build": "vite build",
@@ -54,9 +54,9 @@ export function CreateApiKeyDialog({
54
54
  e.preventDefault()
55
55
 
56
56
  const formData = new FormData(e.target as HTMLFormElement)
57
- const name = formData.get("name") as string
57
+ const name = (formData.get("name") as string)?.trim()
58
58
 
59
- createApiKey(name ? { name: name.trim() } : undefined, {
59
+ createApiKey(name ? { name } : undefined, {
60
60
  onSuccess: (result) => {
61
61
  handleOpenChange(false)
62
62
  setKeyName(name)
@@ -0,0 +1,109 @@
1
+ import {
2
+ type PasskeyAuthClient,
3
+ useAddPasskey,
4
+ useAuth,
5
+ useAuthPlugin
6
+ } from "@better-auth-ui/react"
7
+ import { Fingerprint } 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 } from "react"
19
+
20
+ import { passkeyPlugin } from "../../../lib/auth/passkey-plugin"
21
+
22
+ export type AddPasskeyDialogProps = {
23
+ isOpen: boolean
24
+ onOpenChange: (open: boolean) => void
25
+ }
26
+
27
+ export function AddPasskeyDialog({
28
+ isOpen,
29
+ onOpenChange
30
+ }: AddPasskeyDialogProps) {
31
+ const { authClient, localization } = useAuth()
32
+ const { localization: passkeyLocalization } = useAuthPlugin(passkeyPlugin)
33
+
34
+ const { mutate: addPasskey, isPending: isAdding } = useAddPasskey(
35
+ authClient as PasskeyAuthClient
36
+ )
37
+
38
+ const handleOpenChange = (open: boolean) => {
39
+ onOpenChange(open)
40
+ }
41
+
42
+ const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {
43
+ e.preventDefault()
44
+
45
+ const formData = new FormData(e.target as HTMLFormElement)
46
+ const name = (formData.get("name") as string)?.trim()
47
+
48
+ addPasskey(name ? { name } : undefined, {
49
+ onSuccess: () => handleOpenChange(false)
50
+ })
51
+ }
52
+
53
+ return (
54
+ <AlertDialog.Backdrop isOpen={isOpen} onOpenChange={handleOpenChange}>
55
+ <AlertDialog.Container>
56
+ <AlertDialog.Dialog>
57
+ <Form onSubmit={handleSubmit}>
58
+ <AlertDialog.CloseTrigger />
59
+
60
+ <AlertDialog.Header>
61
+ <AlertDialog.Icon status="default">
62
+ <Fingerprint />
63
+ </AlertDialog.Icon>
64
+
65
+ <AlertDialog.Heading>
66
+ {passkeyLocalization.addPasskey}
67
+ </AlertDialog.Heading>
68
+ </AlertDialog.Header>
69
+
70
+ <AlertDialog.Body className="overflow-visible">
71
+ <p className="text-muted text-sm">
72
+ {passkeyLocalization.passkeysDescription}
73
+ </p>
74
+
75
+ <TextField
76
+ className="mt-4"
77
+ id="name"
78
+ name="name"
79
+ isDisabled={isAdding}
80
+ >
81
+ <Label>{passkeyLocalization.name}</Label>
82
+
83
+ <Input
84
+ autoFocus
85
+ placeholder={localization.settings.optional}
86
+ variant="secondary"
87
+ />
88
+
89
+ <FieldError />
90
+ </TextField>
91
+ </AlertDialog.Body>
92
+
93
+ <AlertDialog.Footer>
94
+ <Button slot="close" variant="tertiary" isDisabled={isAdding}>
95
+ {localization.settings.cancel}
96
+ </Button>
97
+
98
+ <Button type="submit" isPending={isAdding}>
99
+ {isAdding && <Spinner color="current" size="sm" />}
100
+
101
+ {passkeyLocalization.addPasskey}
102
+ </Button>
103
+ </AlertDialog.Footer>
104
+ </Form>
105
+ </AlertDialog.Dialog>
106
+ </AlertDialog.Container>
107
+ </AlertDialog.Backdrop>
108
+ )
109
+ }
@@ -0,0 +1,95 @@
1
+ import {
2
+ type PasskeyAuthClient,
3
+ useAuth,
4
+ useAuthPlugin,
5
+ useDeletePasskey
6
+ } from "@better-auth-ui/react"
7
+ import { Fingerprint } from "@gravity-ui/icons"
8
+ import {
9
+ AlertDialog,
10
+ Button,
11
+ Input,
12
+ Label,
13
+ Spinner,
14
+ TextField
15
+ } from "@heroui/react"
16
+
17
+ import { passkeyPlugin } from "../../../lib/auth/passkey-plugin"
18
+
19
+ export type ListedPasskey = {
20
+ id: string
21
+ name?: string | null
22
+ createdAt: Date
23
+ }
24
+
25
+ export type DeletePasskeyDialogProps = {
26
+ isOpen: boolean
27
+ onOpenChange: (open: boolean) => void
28
+ passkey: ListedPasskey
29
+ }
30
+
31
+ export function DeletePasskeyDialog({
32
+ isOpen,
33
+ onOpenChange,
34
+ passkey
35
+ }: DeletePasskeyDialogProps) {
36
+ const { authClient, localization } = useAuth()
37
+ const { localization: passkeyLocalization } = useAuthPlugin(passkeyPlugin)
38
+
39
+ const passkeyName = passkey.name || passkeyLocalization.passkey
40
+
41
+ const { mutate: deletePasskey, isPending: isDeleting } = useDeletePasskey(
42
+ authClient as PasskeyAuthClient,
43
+ {
44
+ onSuccess: () => onOpenChange(false)
45
+ }
46
+ )
47
+
48
+ return (
49
+ <AlertDialog.Backdrop isOpen={isOpen} onOpenChange={onOpenChange}>
50
+ <AlertDialog.Container>
51
+ <AlertDialog.Dialog>
52
+ <AlertDialog.CloseTrigger />
53
+
54
+ <AlertDialog.Header>
55
+ <AlertDialog.Icon status="danger">
56
+ <Fingerprint />
57
+ </AlertDialog.Icon>
58
+
59
+ <AlertDialog.Heading>
60
+ {passkeyLocalization.deletePasskeyTitle}
61
+ </AlertDialog.Heading>
62
+ </AlertDialog.Header>
63
+
64
+ <AlertDialog.Body className="flex flex-col gap-4 overflow-visible">
65
+ <p className="text-muted text-sm">
66
+ {passkeyLocalization.deletePasskeyWarning}
67
+ </p>
68
+
69
+ <TextField value={passkeyName} variant="secondary">
70
+ <Label>{passkey.name || passkeyLocalization.passkey}</Label>
71
+
72
+ <Input readOnly />
73
+ </TextField>
74
+ </AlertDialog.Body>
75
+
76
+ <AlertDialog.Footer>
77
+ <Button slot="close" variant="tertiary" isDisabled={isDeleting}>
78
+ {localization.settings.cancel}
79
+ </Button>
80
+
81
+ <Button
82
+ variant="danger"
83
+ onPress={() => deletePasskey({ id: passkey.id })}
84
+ isPending={isDeleting}
85
+ >
86
+ {isDeleting && <Spinner color="current" size="sm" />}
87
+
88
+ {passkeyLocalization.deletePasskeyTitle}
89
+ </Button>
90
+ </AlertDialog.Footer>
91
+ </AlertDialog.Dialog>
92
+ </AlertDialog.Container>
93
+ </AlertDialog.Backdrop>
94
+ )
95
+ }
@@ -0,0 +1,16 @@
1
+ import { Skeleton } from "@heroui/react"
2
+
3
+ export function PasskeySkeleton() {
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-32 rounded-lg" />
12
+ </div>
13
+ </div>
14
+ </div>
15
+ )
16
+ }
@@ -1,29 +1,22 @@
1
- import {
2
- type PasskeyAuthClient,
3
- useAuth,
4
- useAuthPlugin,
5
- useDeletePasskey
6
- } from "@better-auth-ui/react"
1
+ import { useAuth, useAuthPlugin } from "@better-auth-ui/react"
7
2
  import { Fingerprint, Xmark } from "@gravity-ui/icons"
8
- import { Button, Spinner } from "@heroui/react"
3
+ import { Button } from "@heroui/react"
4
+ import { useState } from "react"
9
5
 
10
6
  import { passkeyPlugin } from "../../../lib/auth/passkey-plugin"
7
+ import {
8
+ DeletePasskeyDialog,
9
+ type ListedPasskey
10
+ } from "./delete-passkey-dialog"
11
11
 
12
12
  export type PasskeyProps = {
13
- passkey: {
14
- id: string
15
- name?: string | null
16
- createdAt: Date
17
- }
13
+ passkey: ListedPasskey
18
14
  }
19
15
 
20
16
  export function Passkey({ passkey }: PasskeyProps) {
21
- const { authClient, localization } = useAuth()
17
+ const { localization } = useAuth()
22
18
  const { localization: passkeyLocalization } = useAuthPlugin(passkeyPlugin)
23
-
24
- const { mutate: deletePasskey, isPending } = useDeletePasskey(
25
- authClient as PasskeyAuthClient
26
- )
19
+ const [deleteOpen, setDeleteOpen] = useState(false)
27
20
 
28
21
  const passkeyName = passkey.name || passkeyLocalization.passkey
29
22
 
@@ -33,8 +26,10 @@ export function Passkey({ passkey }: PasskeyProps) {
33
26
  <Fingerprint className="size-4.5" />
34
27
  </div>
35
28
 
36
- <div className="flex flex-col min-w-0">
37
- <span className="text-sm font-medium leading-tight">{passkeyName}</span>
29
+ <div className="flex min-w-0 flex-col">
30
+ <span className="truncate text-sm font-medium leading-tight">
31
+ {passkeyName}
32
+ </span>
38
33
 
39
34
  <span className="text-xs text-muted">
40
35
  {new Date(passkey.createdAt).toLocaleString(undefined, {
@@ -48,16 +43,22 @@ export function Passkey({ passkey }: PasskeyProps) {
48
43
  className="ml-auto shrink-0"
49
44
  variant="outline"
50
45
  size="sm"
51
- isPending={isPending}
52
- onPress={() => deletePasskey({ id: passkey.id })}
46
+ onPress={() => setDeleteOpen(true)}
53
47
  aria-label={passkeyLocalization.deletePasskey.replace(
54
48
  "{{name}}",
55
49
  passkeyName
56
50
  )}
57
51
  >
58
- {isPending ? <Spinner color="current" size="sm" /> : <Xmark />}
52
+ <Xmark />
53
+
59
54
  {localization.settings.delete}
60
55
  </Button>
56
+
57
+ <DeletePasskeyDialog
58
+ isOpen={deleteOpen}
59
+ onOpenChange={setDeleteOpen}
60
+ passkey={passkey}
61
+ />
61
62
  </div>
62
63
  )
63
64
  }
@@ -0,0 +1,35 @@
1
+ import { useAuthPlugin } from "@better-auth-ui/react"
2
+ import { Fingerprint } from "@gravity-ui/icons"
3
+ import { Button } from "@heroui/react"
4
+
5
+ import { passkeyPlugin } from "../../../lib/auth/passkey-plugin"
6
+
7
+ export type PasskeysEmptyProps = {
8
+ onAddPress: () => void
9
+ }
10
+
11
+ export function PasskeysEmpty({ onAddPress }: PasskeysEmptyProps) {
12
+ const { localization: passkeyLocalization } = useAuthPlugin(passkeyPlugin)
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
+ <Fingerprint 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">
22
+ {passkeyLocalization.noPasskeys}
23
+ </p>
24
+
25
+ <p className="text-muted text-xs">
26
+ {passkeyLocalization.passkeysDescription}
27
+ </p>
28
+ </div>
29
+
30
+ <Button size="sm" onPress={onAddPress}>
31
+ {passkeyLocalization.addPasskey}
32
+ </Button>
33
+ </div>
34
+ )
35
+ }
@@ -1,29 +1,17 @@
1
1
  import {
2
2
  type PasskeyAuthClient,
3
- useAddPasskey,
4
3
  useAuth,
5
4
  useAuthPlugin,
6
5
  useListPasskeys
7
6
  } from "@better-auth-ui/react"
8
- import { Fingerprint } from "@gravity-ui/icons"
9
- import {
10
- AlertDialog,
11
- Button,
12
- Card,
13
- type CardProps,
14
- cn,
15
- FieldError,
16
- Form,
17
- Input,
18
- Label,
19
- Skeleton,
20
- Spinner,
21
- TextField
22
- } from "@heroui/react"
23
- import { type SyntheticEvent, useState } from "react"
7
+ import { Button, Card, type CardProps, cn } from "@heroui/react"
8
+ import { useState } from "react"
24
9
 
25
10
  import { passkeyPlugin } from "../../../lib/auth/passkey-plugin"
11
+ import { AddPasskeyDialog } from "./add-passkey-dialog"
26
12
  import { Passkey } from "./passkey"
13
+ import { PasskeySkeleton } from "./passkey-skeleton"
14
+ import { PasskeysEmpty } from "./passkeys-empty"
27
15
 
28
16
  export type PasskeysProps = {
29
17
  className?: string
@@ -35,162 +23,53 @@ export function Passkeys({
35
23
  variant,
36
24
  ...props
37
25
  }: PasskeysProps & Omit<CardProps, "children">) {
38
- const { authClient, localization } = useAuth()
26
+ const { authClient } = useAuth()
39
27
  const { localization: passkeyLocalization } = useAuthPlugin(passkeyPlugin)
40
28
 
41
29
  const { data: passkeys, isPending } = useListPasskeys(
42
30
  authClient as PasskeyAuthClient
43
31
  )
44
- const { mutate: addPasskey, isPending: isAdding } = useAddPasskey(
45
- authClient as PasskeyAuthClient
46
- )
47
-
48
- const [nameOpen, setNameOpen] = useState(false)
49
- const [name, setName] = useState("")
50
32
 
51
- const handleDialogOpenChange = (open: boolean) => {
52
- setNameOpen(open)
53
- setName("")
54
- }
55
-
56
- const handleSubmit = async (e: SyntheticEvent<HTMLFormElement>) => {
57
- e.preventDefault()
58
-
59
- addPasskey(
60
- { name: name.trim() || undefined },
61
- {
62
- onSuccess: () => {
63
- setNameOpen(false)
64
- setName("")
65
- }
66
- }
67
- )
68
- }
33
+ const [addOpen, setAddOpen] = useState(false)
69
34
 
70
35
  return (
71
- <div>
72
- <h2 className={cn("text-sm font-semibold mb-3")}>
73
- {passkeyLocalization.passkeys}
74
- </h2>
75
-
76
- <Card className={cn(className)} variant={variant} {...props}>
77
- <Card.Content className="gap-0">
78
- <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
79
- <div>
80
- <p className="text-sm font-medium leading-tight">
81
- {passkeyLocalization.passkeysDescription}
82
- </p>
83
-
84
- <p className="text-muted text-xs mt-0.5">
85
- {passkeyLocalization.passkeysInstructions}
86
- </p>
87
- </div>
88
-
89
- <AlertDialog>
90
- <Button
91
- className="shrink-0"
92
- size="sm"
93
- isDisabled={isPending}
94
- onPress={() => setNameOpen(true)}
95
- >
96
- {passkeyLocalization.addPasskey}
97
- </Button>
98
-
99
- <AlertDialog.Backdrop
100
- isOpen={nameOpen}
101
- onOpenChange={handleDialogOpenChange}
102
- >
103
- <AlertDialog.Container>
104
- <AlertDialog.Dialog>
105
- <Form onSubmit={handleSubmit}>
106
- <AlertDialog.CloseTrigger />
107
-
108
- <AlertDialog.Header>
109
- <AlertDialog.Icon status="default">
110
- <Fingerprint />
111
- </AlertDialog.Icon>
112
-
113
- <AlertDialog.Heading>
114
- {passkeyLocalization.addPasskey}
115
- </AlertDialog.Heading>
116
- </AlertDialog.Header>
117
-
118
- <AlertDialog.Body className="overflow-visible">
119
- <p className="text-muted text-sm">
120
- {passkeyLocalization.passkeysInstructions}
121
- </p>
122
-
123
- <TextField
124
- className="mt-4"
125
- name="passkey-name"
126
- isDisabled={isAdding}
127
- value={name}
128
- onChange={setName}
129
- >
130
- <Label>{passkeyLocalization.passkey}</Label>
131
-
132
- <Input
133
- autoFocus
134
- placeholder={localization.settings.optional}
135
- variant="secondary"
136
- />
137
-
138
- <FieldError />
139
- </TextField>
140
- </AlertDialog.Body>
141
-
142
- <AlertDialog.Footer>
143
- <Button
144
- slot="close"
145
- variant="tertiary"
146
- isDisabled={isAdding}
147
- >
148
- {localization.settings.cancel}
149
- </Button>
150
-
151
- <Button type="submit" isPending={isAdding}>
152
- {isAdding && <Spinner color="current" size="sm" />}
153
-
154
- {passkeyLocalization.addPasskey}
155
- </Button>
156
- </AlertDialog.Footer>
157
- </Form>
158
- </AlertDialog.Dialog>
159
- </AlertDialog.Container>
160
- </AlertDialog.Backdrop>
161
- </AlertDialog>
162
- </div>
36
+ <div className={cn("flex flex-col gap-3", className)}>
37
+ <div className="flex items-end justify-between gap-3">
38
+ <h2 className="text-sm font-semibold truncate">
39
+ {passkeyLocalization.passkeys}
40
+ </h2>
41
+
42
+ <Button
43
+ className="shrink-0"
44
+ size="sm"
45
+ isDisabled={isPending}
46
+ onPress={() => setAddOpen(true)}
47
+ >
48
+ {passkeyLocalization.addPasskey}
49
+ </Button>
50
+ </div>
163
51
 
52
+ <Card variant={variant} {...props}>
53
+ <Card.Content>
164
54
  {isPending ? (
165
- <>
166
- <div className="border-b border-dashed -mx-4 md:-mx-6 my-4" />
167
- <PasskeySkeleton />
168
- </>
55
+ <PasskeySkeleton />
56
+ ) : !passkeys?.length ? (
57
+ <PasskeysEmpty onAddPress={() => setAddOpen(true)} />
169
58
  ) : (
170
- passkeys?.map((passkey) => (
59
+ passkeys.map((passkey, index) => (
171
60
  <div key={passkey.id}>
172
- <div className="border-b border-dashed -mx-4 md:-mx-6 my-4" />
61
+ {index > 0 && (
62
+ <div className="border-b border-dashed -mx-4 my-4" />
63
+ )}
64
+
173
65
  <Passkey passkey={passkey} />
174
66
  </div>
175
67
  ))
176
68
  )}
177
69
  </Card.Content>
178
70
  </Card>
179
- </div>
180
- )
181
- }
182
71
 
183
- function PasskeySkeleton() {
184
- return (
185
- <div className="flex items-center justify-between">
186
- <div className="flex items-center gap-3">
187
- <Skeleton className="size-10 rounded-xl" />
188
-
189
- <div className="flex flex-col gap-1">
190
- <Skeleton className="h-4 w-24 rounded-lg" />
191
- <Skeleton className="h-3 w-20 rounded-lg" />
192
- </div>
193
- </div>
72
+ <AddPasskeyDialog isOpen={addOpen} onOpenChange={setAddOpen} />
194
73
  </div>
195
74
  )
196
75
  }