@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,92 @@
1
+ import {
2
+ type ApiKeyAuthClient,
3
+ type ListedApiKey,
4
+ useAuth,
5
+ useAuthPlugin,
6
+ useDeleteApiKey
7
+ } from "@better-auth-ui/react"
8
+ import { Key } from "@gravity-ui/icons"
9
+ import {
10
+ AlertDialog,
11
+ Button,
12
+ Input,
13
+ Label,
14
+ Spinner,
15
+ TextField
16
+ } from "@heroui/react"
17
+
18
+ import { apiKeyPlugin } from "../../../lib/auth/api-key-plugin"
19
+
20
+ export type DeleteApiKeyDialogProps = {
21
+ isOpen: boolean
22
+ onOpenChange: (open: boolean) => void
23
+ apiKey: ListedApiKey
24
+ }
25
+
26
+ export function DeleteApiKeyDialog({
27
+ isOpen,
28
+ onOpenChange,
29
+ apiKey
30
+ }: DeleteApiKeyDialogProps) {
31
+ const { authClient, localization } = useAuth()
32
+ const { localization: apiKeyLocalization } = useAuthPlugin(apiKeyPlugin)
33
+ const preview = `${apiKey.start}${"*".repeat(16)}`
34
+ const { mutate: deleteApiKey, isPending: isDeleting } = useDeleteApiKey(
35
+ authClient as ApiKeyAuthClient,
36
+ {
37
+ onSuccess: () => onOpenChange(false)
38
+ }
39
+ )
40
+
41
+ return (
42
+ <AlertDialog.Backdrop isOpen={isOpen} onOpenChange={onOpenChange}>
43
+ <AlertDialog.Container>
44
+ <AlertDialog.Dialog>
45
+ <AlertDialog.CloseTrigger />
46
+
47
+ <AlertDialog.Header>
48
+ <AlertDialog.Icon status="danger">
49
+ <Key />
50
+ </AlertDialog.Icon>
51
+
52
+ <AlertDialog.Heading>
53
+ {apiKeyLocalization.deleteApiKey}
54
+ </AlertDialog.Heading>
55
+ </AlertDialog.Header>
56
+
57
+ <AlertDialog.Body className="flex flex-col gap-4 overflow-visible">
58
+ <p className="text-muted text-sm">
59
+ {apiKeyLocalization.deleteApiKeyWarning}
60
+ </p>
61
+
62
+ <TextField
63
+ value={preview}
64
+ className="font-mono text-xs"
65
+ variant="secondary"
66
+ >
67
+ <Label>{apiKey.name || apiKeyLocalization.apiKey}</Label>
68
+
69
+ <Input readOnly className="font-mono text-xs" />
70
+ </TextField>
71
+ </AlertDialog.Body>
72
+
73
+ <AlertDialog.Footer>
74
+ <Button slot="close" variant="tertiary" isDisabled={isDeleting}>
75
+ {localization.settings.cancel}
76
+ </Button>
77
+
78
+ <Button
79
+ variant="danger"
80
+ onPress={() => deleteApiKey({ keyId: apiKey.id })}
81
+ isPending={isDeleting}
82
+ >
83
+ {isDeleting && <Spinner color="current" size="sm" />}
84
+
85
+ {apiKeyLocalization.deleteApiKey}
86
+ </Button>
87
+ </AlertDialog.Footer>
88
+ </AlertDialog.Dialog>
89
+ </AlertDialog.Container>
90
+ </AlertDialog.Backdrop>
91
+ )
92
+ }
@@ -0,0 +1,94 @@
1
+ import { useAuth, useAuthPlugin } from "@better-auth-ui/react"
2
+ import { Check, Copy, Key } from "@gravity-ui/icons"
3
+ import {
4
+ AlertDialog,
5
+ Button,
6
+ InputGroup,
7
+ Label,
8
+ TextField,
9
+ toast
10
+ } from "@heroui/react"
11
+ import { useState } from "react"
12
+
13
+ import { apiKeyPlugin } from "../../../lib/auth/api-key-plugin"
14
+
15
+ export type NewApiKeyDialogProps = {
16
+ isOpen: boolean
17
+ onOpenChange: (open: boolean) => void
18
+ name: string | null
19
+ secretKey: string | null
20
+ }
21
+
22
+ export function NewApiKeyDialog({
23
+ isOpen,
24
+ onOpenChange,
25
+ name,
26
+ secretKey
27
+ }: NewApiKeyDialogProps) {
28
+ const { localization } = useAuth()
29
+ const { localization: apiKeyLocalization } = useAuthPlugin(apiKeyPlugin)
30
+
31
+ const [copied, setCopied] = useState(false)
32
+
33
+ const copySecretKey = async () => {
34
+ if (!secretKey) return
35
+
36
+ try {
37
+ await navigator.clipboard.writeText(secretKey)
38
+ setCopied(true)
39
+ setTimeout(() => setCopied(false), 1500)
40
+ } catch (error) {
41
+ toast.danger(error instanceof Error ? error.message : String(error))
42
+ }
43
+ }
44
+
45
+ return (
46
+ <AlertDialog.Backdrop isOpen={isOpen} onOpenChange={onOpenChange}>
47
+ <AlertDialog.Container>
48
+ <AlertDialog.Dialog>
49
+ <AlertDialog.CloseTrigger />
50
+
51
+ <AlertDialog.Header>
52
+ <AlertDialog.Icon status="warning">
53
+ <Key />
54
+ </AlertDialog.Icon>
55
+
56
+ <AlertDialog.Heading>
57
+ {apiKeyLocalization.newApiKey}
58
+ </AlertDialog.Heading>
59
+ </AlertDialog.Header>
60
+
61
+ <AlertDialog.Body className="flex flex-col gap-4 overflow-visible">
62
+ <p className="text-muted text-sm">
63
+ {apiKeyLocalization.newApiKeyWarning}
64
+ </p>
65
+
66
+ <TextField value={secretKey ?? ""} className="font-mono text-xs">
67
+ <Label>{name || apiKeyLocalization.apiKey}</Label>
68
+
69
+ <InputGroup variant="secondary">
70
+ <InputGroup.Input readOnly className="font-mono text-xs" />
71
+
72
+ <InputGroup.Suffix className="px-0">
73
+ <Button
74
+ isIconOnly
75
+ aria-label={localization.settings.copyToClipboard}
76
+ size="sm"
77
+ variant="ghost"
78
+ onPress={copySecretKey}
79
+ >
80
+ {copied ? <Check /> : <Copy />}
81
+ </Button>
82
+ </InputGroup.Suffix>
83
+ </InputGroup>
84
+ </TextField>
85
+ </AlertDialog.Body>
86
+
87
+ <AlertDialog.Footer>
88
+ <Button slot="close">{apiKeyLocalization.dismissNewKey}</Button>
89
+ </AlertDialog.Footer>
90
+ </AlertDialog.Dialog>
91
+ </AlertDialog.Container>
92
+ </AlertDialog.Backdrop>
93
+ )
94
+ }
@@ -6,7 +6,7 @@ import {
6
6
  useSession
7
7
  } from "@better-auth-ui/react"
8
8
  import { Check, CirclePlus } from "@gravity-ui/icons"
9
- import { Dropdown, Label, Separator } from "@heroui/react"
9
+ import { Dropdown, Label } from "@heroui/react"
10
10
 
11
11
  import { multiSessionPlugin } from "../../../lib/auth/multi-session-plugin"
12
12
  import { UserView } from "../user/user-view"
@@ -50,8 +50,6 @@ export function SwitchAccountSubmenuContent() {
50
50
  />
51
51
  ))}
52
52
 
53
- <Separator />
54
-
55
53
  <Dropdown.Item
56
54
  textValue={multiSessionLocalization.addAccount}
57
55
  href={`${basePaths.auth}/${viewPaths.auth.signIn}`}
@@ -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
+ }