@better-auth-ui/heroui 1.6.8 → 1.6.10
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/dist/components/auth/api-key/api-key.d.ts +5 -1
- package/dist/components/auth/api-key/api-keys-empty.d.ts +3 -1
- package/dist/components/auth/api-key/api-keys.d.ts +9 -1
- package/dist/components/auth/api-key/create-api-key-dialog.d.ts +3 -1
- package/dist/components/auth/api-key/delete-api-key-dialog.d.ts +3 -1
- package/dist/components/auth/api-key/organization-api-keys.d.ts +14 -0
- package/dist/components/auth/delete-user/danger-zone.d.ts +1 -1
- package/dist/components/auth/delete-user/{delete-user.d.ts → delete-account.d.ts} +2 -2
- package/dist/components/auth/email/organization-invitation-email.d.ts +3 -0
- package/dist/components/auth/multi-session/switch-account-submenu-content.d.ts +3 -1
- package/dist/components/auth/multi-session/switch-account-submenu-item.d.ts +2 -1
- package/dist/components/auth/multi-session/switch-account-submenu.d.ts +2 -1
- package/dist/components/auth/organization/change-organization-logo.d.ts +4 -0
- package/dist/components/auth/organization/create-organization-dialog.d.ts +13 -0
- package/dist/components/auth/organization/delete-organization-dialog.d.ts +7 -0
- package/dist/components/auth/organization/delete-organization-skeleton.d.ts +4 -0
- package/dist/components/auth/organization/delete-organization.d.ts +5 -0
- package/dist/components/auth/organization/invite-member-dialog.d.ts +13 -0
- package/dist/components/auth/organization/leave-organization-dialog.d.ts +7 -0
- package/dist/components/auth/organization/leave-organization.d.ts +4 -0
- package/dist/components/auth/organization/organization-danger-zone.d.ts +16 -0
- package/dist/components/auth/organization/organization-invitation-row-skeleton.d.ts +5 -0
- package/dist/components/auth/organization/organization-invitation-row.d.ts +5 -0
- package/dist/components/auth/organization/organization-invitations-empty.d.ts +8 -0
- package/dist/components/auth/organization/organization-invitations.d.ts +9 -0
- package/dist/components/auth/organization/organization-logo.d.ts +15 -0
- package/dist/components/auth/organization/organization-member-row-skeleton.d.ts +4 -0
- package/dist/components/auth/organization/organization-member-row.d.ts +9 -0
- package/dist/components/auth/organization/organization-members.d.ts +9 -0
- package/dist/components/auth/organization/organization-people.d.ts +10 -0
- package/dist/components/auth/organization/organization-profile.d.ts +9 -0
- package/dist/components/auth/organization/organization-row.d.ts +8 -0
- package/dist/components/auth/organization/organization-settings.d.ts +12 -0
- package/dist/components/auth/organization/organization-switcher.d.ts +20 -0
- package/dist/components/auth/organization/organization-view-skeleton.d.ts +6 -0
- package/dist/components/auth/organization/organization-view.d.ts +15 -0
- package/dist/components/auth/organization/organization.d.ts +17 -0
- package/dist/components/auth/organization/organizations-empty.d.ts +4 -0
- package/dist/components/auth/organization/organizations-settings.d.ts +17 -0
- package/dist/components/auth/organization/organizations.d.ts +10 -0
- package/dist/components/auth/organization/remove-member-dialog.d.ts +9 -0
- package/dist/components/auth/organization/slug-field.d.ts +19 -0
- package/dist/components/auth/organization/user-invitation-row-skeleton.d.ts +4 -0
- package/dist/components/auth/organization/user-invitation-row.d.ts +10 -0
- package/dist/components/auth/organization/user-invitations-empty.d.ts +4 -0
- package/dist/components/auth/organization/user-invitations.d.ts +10 -0
- package/dist/components/auth/settings/settings.d.ts +1 -1
- package/dist/components/auth/user/user-avatar.d.ts +1 -1
- package/dist/components/auth/user/user-button.d.ts +4 -5
- package/dist/components/auth/user/user-view.d.ts +10 -4
- package/dist/email.d.ts +1 -0
- package/dist/email.js +211 -8
- package/dist/index.js +1682 -22
- package/dist/lib/auth/api-key-plugin.d.ts +5 -2
- package/dist/lib/auth/auth-plugin.d.ts +6 -2
- package/dist/lib/auth/delete-user-plugin.d.ts +4 -4
- package/dist/lib/auth/magic-link-plugin.d.ts +2 -2
- package/dist/lib/auth/multi-session-plugin.d.ts +2 -2
- package/dist/lib/auth/organization-plugin.d.ts +37 -0
- package/dist/lib/auth/passkey-plugin.d.ts +2 -2
- package/dist/lib/auth/theme-plugin.d.ts +3 -3
- package/dist/lib/auth/username-plugin.d.ts +8 -8
- package/dist/plugins.d.ts +25 -1
- package/dist/plugins.js +2824 -22
- package/dist/user-view--47tVtJ3.js +113 -0
- package/package.json +23 -18
- package/src/components/auth/api-key/api-key.tsx +25 -16
- package/src/components/auth/api-key/api-keys-empty.tsx +8 -4
- package/src/components/auth/api-key/api-keys.tsx +52 -14
- package/src/components/auth/api-key/create-api-key-dialog.tsx +15 -2
- package/src/components/auth/api-key/delete-api-key-dialog.tsx +10 -2
- package/src/components/auth/api-key/organization-api-keys.tsx +55 -0
- package/src/components/auth/delete-user/danger-zone.tsx +3 -3
- package/src/components/auth/delete-user/{delete-user.tsx → delete-account.tsx} +9 -9
- package/src/components/auth/email/organization-invitation-email.tsx +38 -0
- package/src/components/auth/error-toaster.tsx +28 -10
- package/src/components/auth/forgot-password.tsx +10 -2
- package/src/components/auth/multi-session/manage-accounts.tsx +1 -1
- package/src/components/auth/multi-session/switch-account-submenu-content.tsx +7 -2
- package/src/components/auth/multi-session/switch-account-submenu-item.tsx +4 -2
- package/src/components/auth/multi-session/switch-account-submenu.tsx +8 -4
- package/src/components/auth/organization/change-organization-logo.tsx +171 -0
- package/src/components/auth/organization/create-organization-dialog.tsx +133 -0
- package/src/components/auth/organization/delete-organization-dialog.tsx +97 -0
- package/src/components/auth/organization/delete-organization-skeleton.tsx +17 -0
- package/src/components/auth/organization/delete-organization.tsx +77 -0
- package/src/components/auth/organization/invite-member-dialog.tsx +177 -0
- package/src/components/auth/organization/leave-organization-dialog.tsx +95 -0
- package/src/components/auth/organization/leave-organization.tsx +59 -0
- package/src/components/auth/organization/organization-danger-zone.tsx +68 -0
- package/src/components/auth/organization/organization-invitation-row-skeleton.tsx +29 -0
- package/src/components/auth/organization/organization-invitation-row.tsx +97 -0
- package/src/components/auth/organization/organization-invitations-empty.tsx +39 -0
- package/src/components/auth/organization/organization-invitations.tsx +351 -0
- package/src/components/auth/organization/organization-logo.tsx +64 -0
- package/src/components/auth/organization/organization-member-row-skeleton.tsx +24 -0
- package/src/components/auth/organization/organization-member-row.tsx +157 -0
- package/src/components/auth/organization/organization-members.tsx +284 -0
- package/src/components/auth/organization/organization-people.tsx +26 -0
- package/src/components/auth/organization/organization-profile.tsx +141 -0
- package/src/components/auth/organization/organization-row.tsx +66 -0
- package/src/components/auth/organization/organization-settings.tsx +38 -0
- package/src/components/auth/organization/organization-switcher.tsx +233 -0
- package/src/components/auth/organization/organization-view-skeleton.tsx +36 -0
- package/src/components/auth/organization/organization-view.tsx +109 -0
- package/src/components/auth/organization/organization.tsx +162 -0
- package/src/components/auth/organization/organizations-empty.tsx +35 -0
- package/src/components/auth/organization/organizations-settings.tsx +36 -0
- package/src/components/auth/organization/organizations.tsx +80 -0
- package/src/components/auth/organization/remove-member-dialog.tsx +95 -0
- package/src/components/auth/organization/slug-field.tsx +111 -0
- package/src/components/auth/organization/user-invitation-row-skeleton.tsx +17 -0
- package/src/components/auth/organization/user-invitation-row.tsx +90 -0
- package/src/components/auth/organization/user-invitations-empty.tsx +30 -0
- package/src/components/auth/organization/user-invitations.tsx +59 -0
- package/src/components/auth/settings/account/change-email.tsx +1 -1
- package/src/components/auth/settings/account/user-profile.tsx +5 -5
- package/src/components/auth/settings/security/active-sessions.tsx +6 -4
- package/src/components/auth/settings/security/linked-accounts.tsx +2 -2
- package/src/components/auth/settings/settings.tsx +77 -14
- package/src/components/auth/user/user-avatar.tsx +4 -1
- package/src/components/auth/user/user-button.tsx +14 -11
- package/src/components/auth/user/user-view.tsx +32 -12
- package/src/email.ts +1 -0
- package/src/lib/auth/api-key-plugin.ts +10 -4
- package/src/lib/auth/auth-plugin.ts +6 -1
- package/src/lib/auth/organization-plugin.tsx +32 -0
- package/src/plugins.ts +25 -1
- package/dist/components/auth/additional-field.js +0 -270
- package/dist/components/auth/api-key/api-key-skeleton.js +0 -21
- package/dist/components/auth/api-key/api-key.js +0 -55
- package/dist/components/auth/api-key/api-keys-empty.js +0 -35
- package/dist/components/auth/api-key/api-keys.js +0 -41
- package/dist/components/auth/api-key/create-api-key-dialog.js +0 -74
- package/dist/components/auth/api-key/delete-api-key-dialog.js +0 -51
- package/dist/components/auth/api-key/new-api-key-dialog.js +0 -60
- package/dist/components/auth/auth-provider.js +0 -17
- package/dist/components/auth/auth.js +0 -63
- package/dist/components/auth/delete-user/danger-zone.js +0 -18
- package/dist/components/auth/delete-user/delete-user.js +0 -95
- package/dist/components/auth/email/email-changed-email.js +0 -30
- package/dist/components/auth/email/email-verification-email.js +0 -30
- package/dist/components/auth/email/magic-link-email.js +0 -30
- package/dist/components/auth/email/new-device-email.js +0 -30
- package/dist/components/auth/email/otp-email.js +0 -29
- package/dist/components/auth/email/password-changed-email.js +0 -30
- package/dist/components/auth/email/reset-password-email.js +0 -30
- package/dist/components/auth/error-toaster.js +0 -17
- package/dist/components/auth/field-separator.js +0 -18
- package/dist/components/auth/forgot-password.js +0 -89
- package/dist/components/auth/magic-link/magic-link-button.js +0 -20
- package/dist/components/auth/magic-link/magic-link.js +0 -87
- package/dist/components/auth/multi-session/manage-account.js +0 -49
- package/dist/components/auth/multi-session/manage-accounts.js +0 -34
- package/dist/components/auth/multi-session/switch-account-submenu-content.js +0 -28
- package/dist/components/auth/multi-session/switch-account-submenu-item.js +0 -20
- package/dist/components/auth/multi-session/switch-account-submenu.js +0 -24
- package/dist/components/auth/passkey/add-passkey-dialog.js +0 -65
- package/dist/components/auth/passkey/delete-passkey-dialog.js +0 -47
- package/dist/components/auth/passkey/passkey-button.js +0 -24
- package/dist/components/auth/passkey/passkey-skeleton.js +0 -17
- package/dist/components/auth/passkey/passkey.js +0 -48
- package/dist/components/auth/passkey/passkeys-empty.js +0 -35
- package/dist/components/auth/passkey/passkeys.js +0 -42
- package/dist/components/auth/provider-button.js +0 -25
- package/dist/components/auth/provider-buttons.js +0 -19
- package/dist/components/auth/reset-password.js +0 -147
- package/dist/components/auth/settings/account/account-settings.js +0 -20
- package/dist/components/auth/settings/account/change-avatar.js +0 -89
- package/dist/components/auth/settings/account/change-email.js +0 -54
- package/dist/components/auth/settings/account/user-profile.js +0 -86
- package/dist/components/auth/settings/security/active-session.js +0 -59
- package/dist/components/auth/settings/security/active-sessions.js +0 -34
- package/dist/components/auth/settings/security/change-password.js +0 -189
- package/dist/components/auth/settings/security/linked-account.js +0 -56
- package/dist/components/auth/settings/security/linked-accounts.js +0 -45
- package/dist/components/auth/settings/security/security-settings.js +0 -22
- package/dist/components/auth/settings/settings.js +0 -45
- package/dist/components/auth/sign-in.js +0 -133
- package/dist/components/auth/sign-out.js +0 -27
- package/dist/components/auth/sign-up.js +0 -212
- package/dist/components/auth/theme/appearance.js +0 -72
- package/dist/components/auth/theme/theme-toggle-item.js +0 -62
- package/dist/components/auth/user/user-avatar.js +0 -29
- package/dist/components/auth/user/user-button.js +0 -74
- package/dist/components/auth/user/user-view.js +0 -37
- package/dist/components/auth/username/sign-in-username.js +0 -148
- package/dist/components/auth/username/username-field.js +0 -53
- package/dist/lib/auth/api-key-plugin.js +0 -10
- package/dist/lib/auth/delete-user-plugin.js +0 -10
- package/dist/lib/auth/magic-link-plugin.js +0 -13
- package/dist/lib/auth/multi-session-plugin.js +0 -12
- package/dist/lib/auth/passkey-plugin.js +0 -12
- package/dist/lib/auth/theme-plugin.js +0 -23
- package/dist/lib/auth/username-plugin.js +0 -18
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type OrganizationAuthClient,
|
|
3
|
+
useAuth,
|
|
4
|
+
useAuthPlugin,
|
|
5
|
+
useRemoveMember
|
|
6
|
+
} from "@better-auth-ui/react"
|
|
7
|
+
import { TrashBin } from "@gravity-ui/icons"
|
|
8
|
+
import { AlertDialog, Button, Card, Chip, Spinner, toast } from "@heroui/react"
|
|
9
|
+
import type { Member, User } from "better-auth/client"
|
|
10
|
+
|
|
11
|
+
import { organizationPlugin } from "../../../lib/auth/organization-plugin"
|
|
12
|
+
import { UserView } from "../user/user-view"
|
|
13
|
+
|
|
14
|
+
export type RemoveMemberDialogProps = {
|
|
15
|
+
isOpen: boolean
|
|
16
|
+
onOpenChange: (open: boolean) => void
|
|
17
|
+
member: Member & { user: Partial<User> }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function RemoveMemberDialog({
|
|
21
|
+
isOpen,
|
|
22
|
+
onOpenChange,
|
|
23
|
+
member
|
|
24
|
+
}: RemoveMemberDialogProps) {
|
|
25
|
+
const { authClient, localization } = useAuth()
|
|
26
|
+
const { localization: organizationLocalization, roles } =
|
|
27
|
+
useAuthPlugin(organizationPlugin)
|
|
28
|
+
|
|
29
|
+
const { mutate: removeMember, isPending } = useRemoveMember(
|
|
30
|
+
authClient as OrganizationAuthClient,
|
|
31
|
+
{
|
|
32
|
+
onSuccess: () => {
|
|
33
|
+
onOpenChange(false)
|
|
34
|
+
toast.success(organizationLocalization.memberRemoved)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<AlertDialog.Backdrop isOpen={isOpen} onOpenChange={onOpenChange}>
|
|
41
|
+
<AlertDialog.Container>
|
|
42
|
+
<AlertDialog.Dialog>
|
|
43
|
+
<AlertDialog.CloseTrigger />
|
|
44
|
+
|
|
45
|
+
<AlertDialog.Header>
|
|
46
|
+
<AlertDialog.Icon status="danger">
|
|
47
|
+
<TrashBin />
|
|
48
|
+
</AlertDialog.Icon>
|
|
49
|
+
|
|
50
|
+
<AlertDialog.Heading>
|
|
51
|
+
{organizationLocalization.removeMember}
|
|
52
|
+
</AlertDialog.Heading>
|
|
53
|
+
</AlertDialog.Header>
|
|
54
|
+
|
|
55
|
+
<AlertDialog.Body className="flex flex-col gap-4 overflow-visible">
|
|
56
|
+
<p className="text-muted text-sm">
|
|
57
|
+
{organizationLocalization.removeMemberWarning}
|
|
58
|
+
</p>
|
|
59
|
+
|
|
60
|
+
<Card variant="secondary">
|
|
61
|
+
<Card.Content className="justify-between flex-row items-center gap-2">
|
|
62
|
+
<UserView user={member.user} />
|
|
63
|
+
|
|
64
|
+
<Chip size="sm" variant="tertiary">
|
|
65
|
+
<Chip.Label>{roles?.[member.role] ?? member.role}</Chip.Label>
|
|
66
|
+
</Chip>
|
|
67
|
+
</Card.Content>
|
|
68
|
+
</Card>
|
|
69
|
+
</AlertDialog.Body>
|
|
70
|
+
|
|
71
|
+
<AlertDialog.Footer>
|
|
72
|
+
<Button slot="close" variant="tertiary" isDisabled={isPending}>
|
|
73
|
+
{localization.settings.cancel}
|
|
74
|
+
</Button>
|
|
75
|
+
|
|
76
|
+
<Button
|
|
77
|
+
variant="danger"
|
|
78
|
+
isPending={isPending}
|
|
79
|
+
onPress={() =>
|
|
80
|
+
removeMember({
|
|
81
|
+
memberIdOrEmail: member.id,
|
|
82
|
+
organizationId: member.organizationId
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
>
|
|
86
|
+
{isPending && <Spinner color="current" size="sm" />}
|
|
87
|
+
|
|
88
|
+
{organizationLocalization.removeMember}
|
|
89
|
+
</Button>
|
|
90
|
+
</AlertDialog.Footer>
|
|
91
|
+
</AlertDialog.Dialog>
|
|
92
|
+
</AlertDialog.Container>
|
|
93
|
+
</AlertDialog.Backdrop>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type OrganizationAuthClient,
|
|
3
|
+
useAuth,
|
|
4
|
+
useAuthPlugin,
|
|
5
|
+
useCheckSlug
|
|
6
|
+
} from "@better-auth-ui/react"
|
|
7
|
+
import { Check, Xmark } from "@gravity-ui/icons"
|
|
8
|
+
import {
|
|
9
|
+
FieldError,
|
|
10
|
+
InputGroup,
|
|
11
|
+
type InputProps,
|
|
12
|
+
Label,
|
|
13
|
+
Spinner,
|
|
14
|
+
TextField,
|
|
15
|
+
type TextFieldProps
|
|
16
|
+
} from "@heroui/react"
|
|
17
|
+
import { useDebouncer } from "@tanstack/react-pacer"
|
|
18
|
+
import { useEffect } from "react"
|
|
19
|
+
|
|
20
|
+
import { organizationPlugin } from "../../../lib/auth/organization-plugin"
|
|
21
|
+
|
|
22
|
+
/** Props for the {@link SlugField} component. */
|
|
23
|
+
export type SlugFieldProps = {
|
|
24
|
+
value: string
|
|
25
|
+
onChange: (value: string) => void
|
|
26
|
+
currentSlug?: string
|
|
27
|
+
isDisabled?: boolean
|
|
28
|
+
variant?: InputProps["variant"]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Sanitize a slug value so it only contains lowercase alphanumeric characters
|
|
33
|
+
* and dashes. Runs of disallowed characters are collapsed to a single dash, but
|
|
34
|
+
* leading/trailing dashes are preserved while the user is still typing.
|
|
35
|
+
*/
|
|
36
|
+
export function sanitizeSlug(value: string) {
|
|
37
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Organization slug field with debounced availability checking.
|
|
42
|
+
*/
|
|
43
|
+
export function SlugField({
|
|
44
|
+
value,
|
|
45
|
+
onChange,
|
|
46
|
+
currentSlug,
|
|
47
|
+
variant,
|
|
48
|
+
...props
|
|
49
|
+
}: SlugFieldProps & TextFieldProps) {
|
|
50
|
+
const { authClient } = useAuth()
|
|
51
|
+
const { localization, checkSlug: checkSlugEnabled } =
|
|
52
|
+
useAuthPlugin(organizationPlugin)
|
|
53
|
+
|
|
54
|
+
const {
|
|
55
|
+
mutate: checkSlug,
|
|
56
|
+
data: checkSlugData,
|
|
57
|
+
error: checkSlugError,
|
|
58
|
+
reset: resetCheckSlug
|
|
59
|
+
} = useCheckSlug(authClient as OrganizationAuthClient)
|
|
60
|
+
|
|
61
|
+
const debouncer = useDebouncer(
|
|
62
|
+
(value) => {
|
|
63
|
+
if (!checkSlugEnabled || !value.trim() || value.trim() === currentSlug)
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
checkSlug({ slug: value.trim() })
|
|
67
|
+
},
|
|
68
|
+
{ wait: 500 }
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!checkSlugEnabled) return
|
|
73
|
+
|
|
74
|
+
resetCheckSlug()
|
|
75
|
+
debouncer.maybeExecute(value)
|
|
76
|
+
}, [checkSlugEnabled, value, debouncer.maybeExecute, resetCheckSlug])
|
|
77
|
+
|
|
78
|
+
const handleChange = (next: string) => {
|
|
79
|
+
onChange(sanitizeSlug(next))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<TextField
|
|
84
|
+
id="slug"
|
|
85
|
+
name="slug"
|
|
86
|
+
{...props}
|
|
87
|
+
value={value}
|
|
88
|
+
onChange={handleChange}
|
|
89
|
+
>
|
|
90
|
+
<Label>{localization.slug}</Label>
|
|
91
|
+
|
|
92
|
+
<InputGroup variant={variant}>
|
|
93
|
+
<InputGroup.Input placeholder={localization.slugPlaceholder} required />
|
|
94
|
+
|
|
95
|
+
{checkSlugEnabled && !!value.trim() && value.trim() !== currentSlug && (
|
|
96
|
+
<InputGroup.Suffix className="px-2">
|
|
97
|
+
{checkSlugData?.status ? (
|
|
98
|
+
<Check className="text-success" />
|
|
99
|
+
) : checkSlugError ? (
|
|
100
|
+
<Xmark className="text-danger" />
|
|
101
|
+
) : (
|
|
102
|
+
<Spinner size="sm" color="current" />
|
|
103
|
+
)}
|
|
104
|
+
</InputGroup.Suffix>
|
|
105
|
+
)}
|
|
106
|
+
</InputGroup>
|
|
107
|
+
|
|
108
|
+
<FieldError />
|
|
109
|
+
</TextField>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Skeleton } from "@heroui/react"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Placeholder row matching {@link UserInvitationRow} while invitations load.
|
|
5
|
+
*/
|
|
6
|
+
export function UserInvitationRowSkeleton() {
|
|
7
|
+
return (
|
|
8
|
+
<div className="flex items-center gap-3">
|
|
9
|
+
<Skeleton className="size-10 shrink-0 rounded-xl" />
|
|
10
|
+
|
|
11
|
+
<div className="flex flex-col gap-1">
|
|
12
|
+
<Skeleton className="h-4 w-40 rounded-lg" />
|
|
13
|
+
<Skeleton className="h-3 w-28 rounded-lg" />
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type OrganizationAuthClient,
|
|
3
|
+
useAcceptInvitation,
|
|
4
|
+
useAuth,
|
|
5
|
+
useAuthPlugin,
|
|
6
|
+
useRejectInvitation
|
|
7
|
+
} from "@better-auth-ui/react"
|
|
8
|
+
import { Check, Clock, Xmark } from "@gravity-ui/icons"
|
|
9
|
+
import { Button, Chip, Spinner } from "@heroui/react"
|
|
10
|
+
import type { Invitation } from "better-auth/client"
|
|
11
|
+
|
|
12
|
+
import { organizationPlugin } from "../../../lib/auth/organization-plugin"
|
|
13
|
+
|
|
14
|
+
export type UserInvitationRowProps = {
|
|
15
|
+
invitation: Invitation & { organizationName?: string }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Single invitation row with accept/reject actions for the current user.
|
|
20
|
+
*/
|
|
21
|
+
export function UserInvitationRow({ invitation }: UserInvitationRowProps) {
|
|
22
|
+
const { authClient } = useAuth()
|
|
23
|
+
const { localization: organizationLocalization, roles } =
|
|
24
|
+
useAuthPlugin(organizationPlugin)
|
|
25
|
+
|
|
26
|
+
const { mutate: acceptInvitation, isPending: isAccepting } =
|
|
27
|
+
useAcceptInvitation(authClient as OrganizationAuthClient)
|
|
28
|
+
|
|
29
|
+
const { mutate: rejectInvitation, isPending: isRejecting } =
|
|
30
|
+
useRejectInvitation(authClient as OrganizationAuthClient)
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="flex items-center gap-3">
|
|
34
|
+
<div className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-surface-secondary">
|
|
35
|
+
<Clock className="size-4.5" />
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div className="flex flex-col">
|
|
39
|
+
<div className="flex items-center gap-1.5">
|
|
40
|
+
<span className="truncate text-sm font-medium leading-tight">
|
|
41
|
+
{invitation.organizationName}
|
|
42
|
+
</span>
|
|
43
|
+
|
|
44
|
+
<Chip size="sm">{roles?.[invitation.role] ?? invitation.role}</Chip>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<span className="truncate text-muted text-xs">
|
|
48
|
+
{new Date(invitation.createdAt).toLocaleString(undefined, {
|
|
49
|
+
dateStyle: "medium",
|
|
50
|
+
timeStyle: "short"
|
|
51
|
+
})}
|
|
52
|
+
</span>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div className="ml-auto flex shrink-0 items-center gap-2">
|
|
56
|
+
<Button
|
|
57
|
+
variant="outline"
|
|
58
|
+
size="sm"
|
|
59
|
+
isPending={isAccepting}
|
|
60
|
+
isDisabled={isRejecting}
|
|
61
|
+
onPress={() =>
|
|
62
|
+
acceptInvitation({
|
|
63
|
+
invitationId: invitation.id
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
>
|
|
67
|
+
{isAccepting ? <Spinner color="current" size="sm" /> : <Check />}
|
|
68
|
+
|
|
69
|
+
{organizationLocalization.accept}
|
|
70
|
+
</Button>
|
|
71
|
+
|
|
72
|
+
<Button
|
|
73
|
+
variant="danger-soft"
|
|
74
|
+
size="sm"
|
|
75
|
+
isIconOnly
|
|
76
|
+
isPending={isRejecting}
|
|
77
|
+
isDisabled={isAccepting}
|
|
78
|
+
onPress={() =>
|
|
79
|
+
rejectInvitation({
|
|
80
|
+
invitationId: invitation.id
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
aria-label={organizationLocalization.rejectInvitation}
|
|
84
|
+
>
|
|
85
|
+
{isRejecting ? <Spinner color="current" size="sm" /> : <Xmark />}
|
|
86
|
+
</Button>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useAuthPlugin } from "@better-auth-ui/react"
|
|
2
|
+
import { PaperPlane } from "@gravity-ui/icons"
|
|
3
|
+
import { EmptyState } from "@heroui/react"
|
|
4
|
+
import { organizationPlugin } from "../../../lib/auth/organization-plugin"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Empty state for `UserInvitations`
|
|
8
|
+
*/
|
|
9
|
+
export function UserInvitationsEmpty() {
|
|
10
|
+
const { localization: organizationLocalization } =
|
|
11
|
+
useAuthPlugin(organizationPlugin)
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<EmptyState className="flex flex-col items-center gap-4 text-center">
|
|
15
|
+
<div className="flex size-12 items-center justify-center rounded-full bg-surface-secondary">
|
|
16
|
+
<PaperPlane className="size-5" />
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div className="flex flex-col gap-2">
|
|
20
|
+
<p className="text-sm font-semibold text-foreground">
|
|
21
|
+
{organizationLocalization.noInvitations}
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<span className="text-sm text-muted">
|
|
25
|
+
{organizationLocalization.userInvitationsEmptyDescription}
|
|
26
|
+
</span>
|
|
27
|
+
</div>
|
|
28
|
+
</EmptyState>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type OrganizationAuthClient,
|
|
3
|
+
useAuth,
|
|
4
|
+
useAuthPlugin,
|
|
5
|
+
useListUserInvitations
|
|
6
|
+
} from "@better-auth-ui/react"
|
|
7
|
+
import { Card, type CardProps } from "@heroui/react"
|
|
8
|
+
|
|
9
|
+
import { organizationPlugin } from "../../../lib/auth/organization-plugin"
|
|
10
|
+
import { UserInvitationRow } from "./user-invitation-row"
|
|
11
|
+
import { UserInvitationRowSkeleton } from "./user-invitation-row-skeleton"
|
|
12
|
+
import { UserInvitationsEmpty } from "./user-invitations-empty"
|
|
13
|
+
|
|
14
|
+
export type UserInvitationsProps = {
|
|
15
|
+
variant?: CardProps["variant"]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Organization invitations for the signed-in user (from
|
|
20
|
+
* {@link useListUserInvitations}). Always renders the section card; uses
|
|
21
|
+
* {@link UserInvitationsEmpty} when there are no pending invitations.
|
|
22
|
+
*/
|
|
23
|
+
export function UserInvitations({ variant }: UserInvitationsProps) {
|
|
24
|
+
const { authClient } = useAuth()
|
|
25
|
+
const { localization: organizationLocalization } =
|
|
26
|
+
useAuthPlugin(organizationPlugin)
|
|
27
|
+
|
|
28
|
+
const { data: invitations, isPending } = useListUserInvitations(
|
|
29
|
+
authClient as OrganizationAuthClient
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="flex flex-col gap-3">
|
|
34
|
+
<h2 className="truncate text-sm font-semibold">
|
|
35
|
+
{organizationLocalization.invitations}
|
|
36
|
+
</h2>
|
|
37
|
+
|
|
38
|
+
<Card variant={variant}>
|
|
39
|
+
<Card.Content>
|
|
40
|
+
{isPending ? (
|
|
41
|
+
<UserInvitationRowSkeleton />
|
|
42
|
+
) : !invitations?.length ? (
|
|
43
|
+
<UserInvitationsEmpty />
|
|
44
|
+
) : (
|
|
45
|
+
invitations?.map((invitation, index) => (
|
|
46
|
+
<div key={invitation.id}>
|
|
47
|
+
{index > 0 && (
|
|
48
|
+
<div className="border-b border-dashed -mx-4 my-4" />
|
|
49
|
+
)}
|
|
50
|
+
|
|
51
|
+
<UserInvitationRow invitation={invitation} />
|
|
52
|
+
</div>
|
|
53
|
+
))
|
|
54
|
+
)}
|
|
55
|
+
</Card.Content>
|
|
56
|
+
</Card>
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -64,7 +64,7 @@ export function ChangeEmail({
|
|
|
64
64
|
<Fieldset className="w-full gap-4">
|
|
65
65
|
<Fieldset.Group>
|
|
66
66
|
<TextField
|
|
67
|
-
key={session?.user.email}
|
|
67
|
+
key={`${session?.user.id}-${session?.user.email}-email`}
|
|
68
68
|
name="email"
|
|
69
69
|
type="email"
|
|
70
70
|
defaultValue={session?.user.email}
|
|
@@ -81,7 +81,7 @@ export function UserProfile({
|
|
|
81
81
|
return (
|
|
82
82
|
<div>
|
|
83
83
|
<h2 className={cn("text-sm font-semibold mb-3")}>
|
|
84
|
-
{localization.settings.
|
|
84
|
+
{localization.settings.userProfile}
|
|
85
85
|
</h2>
|
|
86
86
|
|
|
87
87
|
<Card className={cn("p-4 gap-4", className)} variant={variant} {...props}>
|
|
@@ -90,7 +90,7 @@ export function UserProfile({
|
|
|
90
90
|
<ChangeAvatar />
|
|
91
91
|
|
|
92
92
|
<TextField
|
|
93
|
-
key={session?.user?.name}
|
|
93
|
+
key={`${session?.user?.id}-${session?.user?.name}-name`}
|
|
94
94
|
name="name"
|
|
95
95
|
defaultValue={session?.user.name}
|
|
96
96
|
isDisabled={isPending || !session}
|
|
@@ -131,9 +131,9 @@ export function UserProfile({
|
|
|
131
131
|
field.name
|
|
132
132
|
]
|
|
133
133
|
|
|
134
|
-
// Re-mount when the session value
|
|
135
|
-
// uncontrolled `defaultValue` reflects the latest data.
|
|
136
|
-
const key = `${field.name}
|
|
134
|
+
// Re-mount when the user or the session value changes so the
|
|
135
|
+
// field's uncontrolled `defaultValue` reflects the latest data.
|
|
136
|
+
const key = `${session.user.id}-${field.name}-${
|
|
137
137
|
value instanceof Date
|
|
138
138
|
? value.toISOString()
|
|
139
139
|
: String(value ?? "")
|
|
@@ -25,9 +25,11 @@ export function ActiveSessions({
|
|
|
25
25
|
|
|
26
26
|
const { data: sessions, isPending } = useListSessions(authClient)
|
|
27
27
|
|
|
28
|
-
const activeSessions =
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
const activeSessions =
|
|
29
|
+
sessions &&
|
|
30
|
+
[...sessions].sort((a, b) =>
|
|
31
|
+
a.id === session?.session.id ? -1 : b.id === session?.session.id ? 1 : 0
|
|
32
|
+
)
|
|
31
33
|
|
|
32
34
|
return (
|
|
33
35
|
<div>
|
|
@@ -43,7 +45,7 @@ export function ActiveSessions({
|
|
|
43
45
|
activeSessions?.map((activeSession, index) => (
|
|
44
46
|
<div key={activeSession.id}>
|
|
45
47
|
{index > 0 && (
|
|
46
|
-
<div className="border-b border-dashed -mx-4
|
|
48
|
+
<div className="border-b border-dashed -mx-4 my-4" />
|
|
47
49
|
)}
|
|
48
50
|
|
|
49
51
|
<ActiveSession activeSession={activeSession} />
|
|
@@ -54,7 +54,7 @@ export function LinkedAccounts({
|
|
|
54
54
|
? socialProviders?.map((provider, index) => (
|
|
55
55
|
<div key={provider}>
|
|
56
56
|
{index > 0 && (
|
|
57
|
-
<div className="border-b border-dashed -mx-4
|
|
57
|
+
<div className="border-b border-dashed -mx-4 my-4" />
|
|
58
58
|
)}
|
|
59
59
|
<AccountRowSkeleton />
|
|
60
60
|
</div>
|
|
@@ -62,7 +62,7 @@ export function LinkedAccounts({
|
|
|
62
62
|
: allRows.map((row, index) => (
|
|
63
63
|
<div key={row.key}>
|
|
64
64
|
{index > 0 && (
|
|
65
|
-
<div className="border-b border-dashed -mx-4
|
|
65
|
+
<div className="border-b border-dashed -mx-4 my-4" />
|
|
66
66
|
)}
|
|
67
67
|
|
|
68
68
|
<LinkedAccount
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { SettingsView } from "@better-auth-ui/core"
|
|
2
2
|
import { useAuth, useAuthenticate } from "@better-auth-ui/react"
|
|
3
|
+
import { Person, Shield } from "@gravity-ui/icons"
|
|
3
4
|
import { type CardProps, cn, Tabs } from "@heroui/react"
|
|
4
5
|
import { type ComponentProps, useMemo } from "react"
|
|
5
|
-
|
|
6
6
|
import { AccountSettings } from "./account/account-settings"
|
|
7
7
|
import { SecuritySettings } from "./security/security-settings"
|
|
8
8
|
|
|
@@ -25,7 +25,7 @@ export type SettingsProps = {
|
|
|
25
25
|
* @param variant - Card variant forwarded to each settings section card
|
|
26
26
|
* @returns A DOM tree containing the responsive settings tabs and the currently selected settings panel
|
|
27
27
|
*
|
|
28
|
-
* @throws Error if neither `view` nor `path` is provided
|
|
28
|
+
* @throws Error if neither `view` nor `path` is provided, or if `path` does not match any settings view
|
|
29
29
|
*/
|
|
30
30
|
export function Settings({
|
|
31
31
|
className,
|
|
@@ -35,22 +35,39 @@ export function Settings({
|
|
|
35
35
|
view,
|
|
36
36
|
...props
|
|
37
37
|
}: SettingsProps & ComponentProps<"div">) {
|
|
38
|
-
const { authClient, basePaths, localization, viewPaths } = useAuth()
|
|
39
|
-
useAuthenticate(authClient)
|
|
40
|
-
|
|
41
38
|
if (!view && !path) {
|
|
42
39
|
throw new Error("[Better Auth UI] Either `view` or `path` must be provided")
|
|
43
40
|
}
|
|
44
41
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
const { authClient, basePaths, localization, viewPaths, plugins } = useAuth()
|
|
43
|
+
useAuthenticate(authClient)
|
|
44
|
+
|
|
45
|
+
// Built-ins first, then plugin segments
|
|
46
|
+
const currentView = useMemo(() => {
|
|
47
|
+
if (view) return view
|
|
48
|
+
if (!path) return undefined
|
|
52
49
|
|
|
53
|
-
|
|
50
|
+
const match = [
|
|
51
|
+
viewPaths.settings,
|
|
52
|
+
...plugins.map((plugin) => plugin.viewPaths?.settings)
|
|
53
|
+
]
|
|
54
|
+
.flatMap((source) => Object.entries(source ?? {}))
|
|
55
|
+
.find(([, segment]) => segment === path)
|
|
56
|
+
|
|
57
|
+
return match?.[0] as SettingsView | undefined
|
|
58
|
+
}, [view, path, viewPaths.settings, plugins])
|
|
59
|
+
|
|
60
|
+
if (!currentView) {
|
|
61
|
+
const validPaths = [
|
|
62
|
+
viewPaths.settings,
|
|
63
|
+
...plugins.map((plugin) => plugin.viewPaths?.settings)
|
|
64
|
+
]
|
|
65
|
+
.flatMap((source) => Object.values(source ?? {}))
|
|
66
|
+
.join(", ")
|
|
67
|
+
throw new Error(
|
|
68
|
+
`[Better Auth UI] Unknown settings path "${path}". Valid paths are: ${validPaths}`
|
|
69
|
+
)
|
|
70
|
+
}
|
|
54
71
|
|
|
55
72
|
return (
|
|
56
73
|
<Tabs
|
|
@@ -62,12 +79,18 @@ export function Settings({
|
|
|
62
79
|
<Tabs.ListContainer>
|
|
63
80
|
<Tabs.List
|
|
64
81
|
aria-label={localization.settings.settings}
|
|
65
|
-
className="overflow-auto
|
|
82
|
+
className="max-w-fit overflow-x-auto [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden"
|
|
66
83
|
>
|
|
67
84
|
<Tabs.Tab
|
|
68
85
|
id="account"
|
|
69
86
|
href={`${basePaths.settings}/${viewPaths.settings.account}`}
|
|
87
|
+
className="gap-2"
|
|
88
|
+
onPress={(e) =>
|
|
89
|
+
e.target.scrollIntoView({ behavior: "smooth", inline: "center" })
|
|
90
|
+
}
|
|
70
91
|
>
|
|
92
|
+
<Person className="text-muted" />
|
|
93
|
+
|
|
71
94
|
{localization.settings.account}
|
|
72
95
|
|
|
73
96
|
<Tabs.Indicator />
|
|
@@ -76,11 +99,39 @@ export function Settings({
|
|
|
76
99
|
<Tabs.Tab
|
|
77
100
|
id="security"
|
|
78
101
|
href={`${basePaths.settings}/${viewPaths.settings.security}`}
|
|
102
|
+
className="gap-2"
|
|
103
|
+
onPress={(e) =>
|
|
104
|
+
e.target.scrollIntoView({ behavior: "smooth", inline: "center" })
|
|
105
|
+
}
|
|
79
106
|
>
|
|
107
|
+
<Shield className="text-muted" />
|
|
108
|
+
|
|
80
109
|
{localization.settings.security}
|
|
81
110
|
|
|
82
111
|
<Tabs.Indicator />
|
|
83
112
|
</Tabs.Tab>
|
|
113
|
+
|
|
114
|
+
{plugins.flatMap(
|
|
115
|
+
(plugin) =>
|
|
116
|
+
plugin.settingsTabs?.map((settingsTab, index) => (
|
|
117
|
+
<Tabs.Tab
|
|
118
|
+
key={`${plugin.id}-${index.toString()}`}
|
|
119
|
+
id={settingsTab.view}
|
|
120
|
+
href={`${basePaths.settings}/${plugin.viewPaths?.settings?.[settingsTab.view]}`}
|
|
121
|
+
className="gap-2"
|
|
122
|
+
onPress={(e) =>
|
|
123
|
+
e.target.scrollIntoView({
|
|
124
|
+
behavior: "smooth",
|
|
125
|
+
inline: "center"
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
>
|
|
129
|
+
{settingsTab.label}
|
|
130
|
+
|
|
131
|
+
<Tabs.Indicator />
|
|
132
|
+
</Tabs.Tab>
|
|
133
|
+
)) ?? []
|
|
134
|
+
)}
|
|
84
135
|
</Tabs.List>
|
|
85
136
|
</Tabs.ListContainer>
|
|
86
137
|
|
|
@@ -91,6 +142,18 @@ export function Settings({
|
|
|
91
142
|
<Tabs.Panel id="security" className="px-0">
|
|
92
143
|
<SecuritySettings variant={variant} />
|
|
93
144
|
</Tabs.Panel>
|
|
145
|
+
|
|
146
|
+
{plugins.flatMap((plugin) =>
|
|
147
|
+
plugin.settingsTabs?.map((settingsTab, index) => (
|
|
148
|
+
<Tabs.Panel
|
|
149
|
+
key={`${plugin.id}-${index.toString()}`}
|
|
150
|
+
id={settingsTab.view}
|
|
151
|
+
className="px-0"
|
|
152
|
+
>
|
|
153
|
+
<settingsTab.component variant={variant} />
|
|
154
|
+
</Tabs.Panel>
|
|
155
|
+
))
|
|
156
|
+
)}
|
|
94
157
|
</Tabs>
|
|
95
158
|
)
|
|
96
159
|
}
|
|
@@ -13,7 +13,10 @@ export type UserAvatarProps = {
|
|
|
13
13
|
fallback?: ReactNode
|
|
14
14
|
isPending?: boolean
|
|
15
15
|
/** @remarks `User` */
|
|
16
|
-
user?: User & {
|
|
16
|
+
user?: Partial<User> & {
|
|
17
|
+
username?: string | null
|
|
18
|
+
displayUsername?: string | null
|
|
19
|
+
}
|
|
17
20
|
size?: AvatarProps["size"]
|
|
18
21
|
}
|
|
19
22
|
|