@workos-inc/widgets 1.1.4 → 1.2.0
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/LICENSE +21 -0
- package/dist/cjs/lib/organization-switcher.d.ts +10 -1
- package/dist/cjs/lib/organization-switcher.d.ts.map +1 -1
- package/dist/cjs/lib/organization-switcher.js +31 -3
- package/dist/cjs/lib/organization-switcher.js.map +1 -1
- package/dist/cjs/workos-widgets.client.d.ts +6 -0
- package/dist/cjs/workos-widgets.client.d.ts.map +1 -1
- package/dist/cjs/workos-widgets.client.js +23 -3
- package/dist/cjs/workos-widgets.client.js.map +1 -1
- package/dist/esm/lib/organization-switcher.d.ts +10 -1
- package/dist/esm/lib/organization-switcher.d.ts.map +1 -1
- package/dist/esm/lib/organization-switcher.js +31 -3
- package/dist/esm/lib/organization-switcher.js.map +1 -1
- package/dist/esm/workos-widgets.client.d.ts +6 -0
- package/dist/esm/workos-widgets.client.d.ts.map +1 -1
- package/dist/esm/workos-widgets.client.js +25 -5
- package/dist/esm/workos-widgets.client.js.map +1 -1
- package/package.json +40 -47
- package/src/api/api-provider.tsx +0 -158
- package/src/api/constants.ts +0 -1
- package/src/api/endpoint.ts +0 -3097
- package/src/api/errors.ts +0 -48
- package/src/api/index.ts +0 -2
- package/src/api/utils.ts +0 -42
- package/src/api/widgets-api-client.ts +0 -87
- package/src/card-list.tsx +0 -26
- package/src/index.ts +0 -9
- package/src/lib/add-mfa-dialog.tsx +0 -379
- package/src/lib/api/config.ts +0 -9
- package/src/lib/api/user.ts +0 -98
- package/src/lib/change-password-dialog.tsx +0 -290
- package/src/lib/constants.ts +0 -3
- package/src/lib/copy-button.tsx +0 -53
- package/src/lib/delete-user-dialog.tsx +0 -110
- package/src/lib/edit-user-profile-dialog.tsx +0 -181
- package/src/lib/edit-user-role-dialog.tsx +0 -178
- package/src/lib/elements.tsx +0 -428
- package/src/lib/elevated-access.tsx +0 -261
- package/src/lib/error-boundary.tsx +0 -166
- package/src/lib/errors.ts +0 -49
- package/src/lib/generic-error.tsx +0 -70
- package/src/lib/icon-panel.tsx +0 -26
- package/src/lib/icons.tsx +0 -21
- package/src/lib/invite-user-dialog.tsx +0 -327
- package/src/lib/logout-all-sessions-dialog.tsx +0 -82
- package/src/lib/logout-dialog.tsx +0 -85
- package/src/lib/marker.tsx +0 -39
- package/src/lib/oauth-icons.tsx +0 -138
- package/src/lib/organization-switcher.tsx +0 -156
- package/src/lib/otp-input.tsx +0 -276
- package/src/lib/resend-invite-dialog.tsx +0 -145
- package/src/lib/reset-mfa-dialog.tsx +0 -104
- package/src/lib/revoke-invite-dialog.tsx +0 -111
- package/src/lib/save-button.tsx +0 -113
- package/src/lib/search-provider.tsx +0 -51
- package/src/lib/set-password-dialog.tsx +0 -204
- package/src/lib/use-dialog-close.tsx +0 -19
- package/src/lib/use-is-hydrated.ts +0 -13
- package/src/lib/use-layout-effect.ts +0 -6
- package/src/lib/use-security-settings.tsx +0 -49
- package/src/lib/user-actions-dropdown.tsx +0 -157
- package/src/lib/user-profile.tsx +0 -227
- package/src/lib/user-security.tsx +0 -187
- package/src/lib/user-sessions.tsx +0 -204
- package/src/lib/users-filter.tsx +0 -62
- package/src/lib/users-management-context.tsx +0 -74
- package/src/lib/users-management-state.ts +0 -165
- package/src/lib/users-management.tsx +0 -594
- package/src/lib/users-search.tsx +0 -73
- package/src/lib/utils.ts +0 -131
- package/src/lib/widgets-context.ts +0 -29
- package/src/organization-switcher.client.tsx +0 -81
- package/src/user-profile.client.tsx +0 -55
- package/src/user-security.client.tsx +0 -55
- package/src/user-sessions.client.tsx +0 -100
- package/src/users-management.client.tsx +0 -73
- package/src/workos-widgets.client.tsx +0 -75
- /package/{src → dist/css}/base.css +0 -0
- /package/{src → dist/css}/lib/card-list.css +0 -0
- /package/{src → dist/css}/lib/marker.css +0 -0
- /package/{src → dist/css}/lib/save-button.css +0 -0
- /package/{src → dist/css}/styles.css +0 -0
- /package/{src → dist/css}/users-management.css +0 -0
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { CheckIcon } from "@radix-ui/react-icons";
|
|
4
|
-
import {
|
|
5
|
-
Box,
|
|
6
|
-
Button,
|
|
7
|
-
ChevronDownIcon,
|
|
8
|
-
DropdownMenu,
|
|
9
|
-
Flex,
|
|
10
|
-
Skeleton,
|
|
11
|
-
Text,
|
|
12
|
-
VisuallyHidden,
|
|
13
|
-
} from "@radix-ui/themes";
|
|
14
|
-
import { OrganizationInfo, Organizations403, Organizations404 } from "../api";
|
|
15
|
-
|
|
16
|
-
type OrganizationSwitcherVariant = "ghost" | "outline";
|
|
17
|
-
|
|
18
|
-
// Rename all uses of `org` to `organization`
|
|
19
|
-
export type OrganizationSwitcherPassthroughProps = {
|
|
20
|
-
switchToOrganization: ({
|
|
21
|
-
organizationId,
|
|
22
|
-
}: {
|
|
23
|
-
organizationId: string;
|
|
24
|
-
}) => void;
|
|
25
|
-
// Simple props to affect the overall style
|
|
26
|
-
variant?: OrganizationSwitcherVariant;
|
|
27
|
-
organizationLabel?: string | null;
|
|
28
|
-
children?: React.ReactNode;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export interface OrganizationSwitcherProps
|
|
32
|
-
extends OrganizationSwitcherPassthroughProps {
|
|
33
|
-
organizations: OrganizationInfo[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export const OrganizationSwitcher = ({
|
|
37
|
-
organizations,
|
|
38
|
-
switchToOrganization,
|
|
39
|
-
variant = "outline",
|
|
40
|
-
organizationLabel = "Organizations",
|
|
41
|
-
children,
|
|
42
|
-
}: OrganizationSwitcherProps) => {
|
|
43
|
-
const currentOrganization = organizations.find(
|
|
44
|
-
(organization) => organization.current,
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
// Possible if the user has no organizations - we should figure out what to do in this case
|
|
48
|
-
if (!currentOrganization) {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<DropdownMenu.Root>
|
|
54
|
-
<DropdownMenu.Trigger>
|
|
55
|
-
<Button
|
|
56
|
-
color="gray"
|
|
57
|
-
variant={variant}
|
|
58
|
-
className="OrganizationSwitcherTrigger"
|
|
59
|
-
>
|
|
60
|
-
<Flex align="center" justify="between" gap="2" flexGrow="1">
|
|
61
|
-
<Text>{currentOrganization.name}</Text>
|
|
62
|
-
<DropdownMenu.TriggerIcon />
|
|
63
|
-
</Flex>
|
|
64
|
-
</Button>
|
|
65
|
-
</DropdownMenu.Trigger>
|
|
66
|
-
<DropdownMenu.Content>
|
|
67
|
-
<DropdownMenu.Group>
|
|
68
|
-
{organizationLabel ? (
|
|
69
|
-
<DropdownMenu.Label>
|
|
70
|
-
<Text>{organizationLabel}</Text>
|
|
71
|
-
</DropdownMenu.Label>
|
|
72
|
-
) : null}
|
|
73
|
-
{organizations.map((organization) => (
|
|
74
|
-
<Flex
|
|
75
|
-
key={organization.id}
|
|
76
|
-
asChild
|
|
77
|
-
pr="2"
|
|
78
|
-
maxWidth="280px"
|
|
79
|
-
minWidth="180px"
|
|
80
|
-
>
|
|
81
|
-
<DropdownMenu.Item
|
|
82
|
-
onClick={() => {
|
|
83
|
-
if (organization.id !== currentOrganization.id) {
|
|
84
|
-
switchToOrganization({ organizationId: organization.id });
|
|
85
|
-
}
|
|
86
|
-
}}
|
|
87
|
-
>
|
|
88
|
-
<Flex
|
|
89
|
-
justify="between"
|
|
90
|
-
align="center"
|
|
91
|
-
gap="4"
|
|
92
|
-
flexGrow="1"
|
|
93
|
-
overflow="hidden"
|
|
94
|
-
>
|
|
95
|
-
<Text truncate>
|
|
96
|
-
{organization.name}
|
|
97
|
-
{organization.current && (
|
|
98
|
-
<VisuallyHidden> (current)</VisuallyHidden>
|
|
99
|
-
)}
|
|
100
|
-
</Text>
|
|
101
|
-
<Flex
|
|
102
|
-
aria-hidden
|
|
103
|
-
align="center"
|
|
104
|
-
justify="center"
|
|
105
|
-
flexShrink="0"
|
|
106
|
-
>
|
|
107
|
-
{organization.current ? (
|
|
108
|
-
<CheckIcon width="18px" height="18px" />
|
|
109
|
-
) : (
|
|
110
|
-
// make the extra space for
|
|
111
|
-
<Box width="18px" height="18px" />
|
|
112
|
-
)}
|
|
113
|
-
</Flex>
|
|
114
|
-
</Flex>
|
|
115
|
-
</DropdownMenu.Item>
|
|
116
|
-
</Flex>
|
|
117
|
-
))}
|
|
118
|
-
</DropdownMenu.Group>
|
|
119
|
-
{children}
|
|
120
|
-
</DropdownMenu.Content>
|
|
121
|
-
</DropdownMenu.Root>
|
|
122
|
-
);
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
export function OrganizationSwitcherLoading({
|
|
126
|
-
variant = "outline",
|
|
127
|
-
organizations,
|
|
128
|
-
}: {
|
|
129
|
-
variant?: OrganizationSwitcherVariant;
|
|
130
|
-
organizations: OrganizationInfo[];
|
|
131
|
-
}) {
|
|
132
|
-
const currentOrganization = organizations.find(
|
|
133
|
-
(organization) => organization.current,
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
return (
|
|
137
|
-
// Always need DropdownMenu.Root to wrap children than may include
|
|
138
|
-
<Button color="gray" variant={variant} disabled>
|
|
139
|
-
<Flex align="center" gap="2">
|
|
140
|
-
<Skeleton loading={!currentOrganization}>
|
|
141
|
-
<Text>{currentOrganization?.name ?? "Loading..."}</Text>
|
|
142
|
-
</Skeleton>
|
|
143
|
-
<ChevronDownIcon />
|
|
144
|
-
</Flex>
|
|
145
|
-
</Button>
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
interface OrganizationSwitcherErrorProps {
|
|
150
|
-
error: Organizations403 | Organizations404;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export function OrganizationSwitcherError(_: OrganizationSwitcherErrorProps) {
|
|
154
|
-
// TODO: consider other error state options
|
|
155
|
-
return null;
|
|
156
|
-
}
|
package/src/lib/otp-input.tsx
DELETED
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { composeRefs, useComposedRefs } from "@radix-ui/react-compose-refs";
|
|
4
|
-
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
|
5
|
-
import * as Form from "@radix-ui/react-form";
|
|
6
|
-
import { Grid } from "@radix-ui/themes";
|
|
7
|
-
import * as React from "react";
|
|
8
|
-
import { TextField } from "./elements";
|
|
9
|
-
|
|
10
|
-
interface OptContextType {
|
|
11
|
-
value: string[];
|
|
12
|
-
readOnly?: boolean;
|
|
13
|
-
state?: "valid" | "invalid";
|
|
14
|
-
onEnterPressed: () => void;
|
|
15
|
-
onChildAdd: (input: HTMLInputElement) => void;
|
|
16
|
-
onCharChange: (char: string, index: number) => void;
|
|
17
|
-
allChildrenAdded: boolean;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const OtpContext = React.createContext<OptContextType | undefined>(undefined);
|
|
21
|
-
|
|
22
|
-
type OtpRootProps = React.ComponentPropsWithoutRef<typeof Grid> & {
|
|
23
|
-
onValueChange?: (value: string) => void;
|
|
24
|
-
id?: string;
|
|
25
|
-
name?: string;
|
|
26
|
-
readOnly?: boolean;
|
|
27
|
-
state?: "valid" | "invalid";
|
|
28
|
-
value?: string;
|
|
29
|
-
defaultValue?: string;
|
|
30
|
-
autoSubmit?: boolean;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export const Root = React.forwardRef<HTMLInputElement, OtpRootProps>(
|
|
34
|
-
function Root(
|
|
35
|
-
{
|
|
36
|
-
name,
|
|
37
|
-
id,
|
|
38
|
-
defaultValue,
|
|
39
|
-
value: valueProp,
|
|
40
|
-
onValueChange,
|
|
41
|
-
autoSubmit,
|
|
42
|
-
children,
|
|
43
|
-
readOnly,
|
|
44
|
-
state,
|
|
45
|
-
...gridProps
|
|
46
|
-
},
|
|
47
|
-
forwardedRef,
|
|
48
|
-
) {
|
|
49
|
-
const [lastCharIndex, setLastCharIndex] = React.useState<number>(0);
|
|
50
|
-
const [allChildrenAdded, setAllChildrenAdded] =
|
|
51
|
-
React.useState<boolean>(false);
|
|
52
|
-
const childCount = React.Children.count(children);
|
|
53
|
-
|
|
54
|
-
const [value, setValue] = useControllableState({
|
|
55
|
-
prop: getValueAsArray(valueProp, childCount),
|
|
56
|
-
defaultProp: getValueAsArray(defaultValue, childCount),
|
|
57
|
-
onChange: (value) => onValueChange?.(value.join("")),
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const hiddenInputRef = React.useRef<HTMLInputElement>(null);
|
|
61
|
-
const childrenRefs = React.useRef<HTMLInputElement[]>([]);
|
|
62
|
-
|
|
63
|
-
const attemptAutoSubmit = React.useCallback(
|
|
64
|
-
(enterPressed = false) => {
|
|
65
|
-
if (
|
|
66
|
-
autoSubmit &&
|
|
67
|
-
value &&
|
|
68
|
-
value.every((char) => char !== "") &&
|
|
69
|
-
(enterPressed || lastCharIndex + 1 === childCount)
|
|
70
|
-
) {
|
|
71
|
-
hiddenInputRef.current?.form?.requestSubmit();
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
[value, childCount, lastCharIndex, autoSubmit],
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
const handleEnterPressed = React.useCallback(
|
|
78
|
-
() => attemptAutoSubmit(true),
|
|
79
|
-
[attemptAutoSubmit],
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
const handleChildAdd = React.useCallback(
|
|
83
|
-
(input: HTMLInputElement) => {
|
|
84
|
-
if (input) {
|
|
85
|
-
input.dataset.index = `${childrenRefs.current.length}`;
|
|
86
|
-
childrenRefs.current.push(input);
|
|
87
|
-
} else {
|
|
88
|
-
childrenRefs.current.pop();
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (childrenRefs.current.length === childCount) {
|
|
92
|
-
setAllChildrenAdded(true);
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
[childCount],
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
const handleCharChange = React.useCallback(
|
|
99
|
-
(char: string, index: number) => {
|
|
100
|
-
setValue((previousValue) => {
|
|
101
|
-
const arrayToCopy = previousValue ?? createEmptyArray(childCount);
|
|
102
|
-
const newValue = [...arrayToCopy];
|
|
103
|
-
newValue[index] = char;
|
|
104
|
-
return newValue;
|
|
105
|
-
});
|
|
106
|
-
setLastCharIndex(index);
|
|
107
|
-
},
|
|
108
|
-
[childCount, setValue],
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
const otpContext = React.useMemo(
|
|
112
|
-
() => ({
|
|
113
|
-
value: value ?? createEmptyArray(childCount),
|
|
114
|
-
readOnly,
|
|
115
|
-
state,
|
|
116
|
-
allChildrenAdded,
|
|
117
|
-
onEnterPressed: handleEnterPressed,
|
|
118
|
-
onChildAdd: handleChildAdd,
|
|
119
|
-
onCharChange: handleCharChange,
|
|
120
|
-
}),
|
|
121
|
-
[
|
|
122
|
-
value,
|
|
123
|
-
allChildrenAdded,
|
|
124
|
-
readOnly,
|
|
125
|
-
state,
|
|
126
|
-
childCount,
|
|
127
|
-
handleEnterPressed,
|
|
128
|
-
handleChildAdd,
|
|
129
|
-
handleCharChange,
|
|
130
|
-
],
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
React.useEffect(attemptAutoSubmit, [attemptAutoSubmit]);
|
|
134
|
-
|
|
135
|
-
return (
|
|
136
|
-
<OtpContext.Provider value={otpContext}>
|
|
137
|
-
<Grid
|
|
138
|
-
columns={`repeat(${childCount}, 1fr)`}
|
|
139
|
-
{...gridProps}
|
|
140
|
-
onPaste={(event: React.ClipboardEvent<HTMLDivElement>) => {
|
|
141
|
-
event.preventDefault();
|
|
142
|
-
const pastedValue = event.clipboardData.getData("Text");
|
|
143
|
-
const sanitizedValue = pastedValue
|
|
144
|
-
.replace(/[^\d]/g, "")
|
|
145
|
-
.slice(0, childCount);
|
|
146
|
-
const value = sanitizedValue
|
|
147
|
-
.padEnd(childCount, "#")
|
|
148
|
-
.split("")
|
|
149
|
-
.map((char) => (char === "#" ? "" : char));
|
|
150
|
-
|
|
151
|
-
setValue(value);
|
|
152
|
-
setLastCharIndex(sanitizedValue.length - 1);
|
|
153
|
-
|
|
154
|
-
const index = Math.min(sanitizedValue.length, childCount - 1);
|
|
155
|
-
childrenRefs.current?.[index]?.focus();
|
|
156
|
-
}}
|
|
157
|
-
>
|
|
158
|
-
{children}
|
|
159
|
-
<input
|
|
160
|
-
ref={composeRefs(forwardedRef, hiddenInputRef)}
|
|
161
|
-
defaultValue={value?.join("")}
|
|
162
|
-
minLength={childCount}
|
|
163
|
-
name={name}
|
|
164
|
-
type="hidden"
|
|
165
|
-
/>
|
|
166
|
-
</Grid>
|
|
167
|
-
</OtpContext.Provider>
|
|
168
|
-
);
|
|
169
|
-
},
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
interface InputProps extends React.ComponentProps<typeof TextField> {
|
|
173
|
-
autoComplete?: "one-time-code" | "off";
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
177
|
-
function Input(
|
|
178
|
-
{ style, readOnly, autoComplete = "off", ...props },
|
|
179
|
-
forwardedRef,
|
|
180
|
-
) {
|
|
181
|
-
const otpContext = useOptContext();
|
|
182
|
-
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
183
|
-
const composedInputRef = useComposedRefs(
|
|
184
|
-
forwardedRef,
|
|
185
|
-
inputRef,
|
|
186
|
-
otpContext.onChildAdd,
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
const index = Number(inputRef.current?.dataset.index ?? -1);
|
|
190
|
-
const char = otpContext.value[index] ?? "";
|
|
191
|
-
|
|
192
|
-
return (
|
|
193
|
-
<Form.Field name={`otp-${index}`} asChild>
|
|
194
|
-
<Form.Control asChild>
|
|
195
|
-
<TextField
|
|
196
|
-
ref={composedInputRef}
|
|
197
|
-
autoComplete={index === 0 ? autoComplete : "off"}
|
|
198
|
-
color={otpContext.state === "invalid" ? "red" : undefined}
|
|
199
|
-
inputMode="numeric"
|
|
200
|
-
maxLength={1}
|
|
201
|
-
pattern="\d{1}"
|
|
202
|
-
readOnly={readOnly ?? otpContext.readOnly}
|
|
203
|
-
size="3"
|
|
204
|
-
value={char}
|
|
205
|
-
variant={otpContext.state === "invalid" ? "soft" : undefined}
|
|
206
|
-
style={{
|
|
207
|
-
...style,
|
|
208
|
-
height: "auto",
|
|
209
|
-
"--text-field-padding": 0,
|
|
210
|
-
textAlign: "center",
|
|
211
|
-
}}
|
|
212
|
-
onChange={(event) => {
|
|
213
|
-
// Only update the value if it matches the input pattern (number only)
|
|
214
|
-
if (event.target.validity.valid) {
|
|
215
|
-
const char = event.target.value;
|
|
216
|
-
const index = Number(event.target.dataset.index ?? -1);
|
|
217
|
-
otpContext.onCharChange(char, index);
|
|
218
|
-
if (char !== "") {
|
|
219
|
-
focusSibling(event.currentTarget, { back: char === "" });
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}}
|
|
223
|
-
onKeyDown={(event) => {
|
|
224
|
-
if (event.key === "ArrowLeft") {
|
|
225
|
-
focusSibling(event.currentTarget, { back: true });
|
|
226
|
-
event.preventDefault();
|
|
227
|
-
} else if (event.key === "ArrowRight") {
|
|
228
|
-
focusSibling(event.currentTarget);
|
|
229
|
-
event.preventDefault();
|
|
230
|
-
} else if (event.key === "Backspace" && char === "") {
|
|
231
|
-
focusSibling(event.currentTarget, { back: true });
|
|
232
|
-
} else if (event.key === "Enter" && char !== "") {
|
|
233
|
-
otpContext.onEnterPressed();
|
|
234
|
-
}
|
|
235
|
-
}}
|
|
236
|
-
{...props}
|
|
237
|
-
/>
|
|
238
|
-
</Form.Control>
|
|
239
|
-
</Form.Field>
|
|
240
|
-
);
|
|
241
|
-
},
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
const useOptContext = () => {
|
|
245
|
-
const optContext = React.useContext(OtpContext);
|
|
246
|
-
|
|
247
|
-
if (!optContext) {
|
|
248
|
-
throw new Error(
|
|
249
|
-
"OtpInput compound components cannot be rendered outside the OtpRoot component",
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return optContext;
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
function focusSibling(input: HTMLInputElement, { back = false } = {}) {
|
|
257
|
-
const sibling = back
|
|
258
|
-
? input.parentElement?.previousSibling
|
|
259
|
-
: input.parentElement?.nextSibling;
|
|
260
|
-
const siblingInput = sibling?.firstChild;
|
|
261
|
-
if (siblingInput && siblingInput instanceof HTMLInputElement) {
|
|
262
|
-
siblingInput?.focus();
|
|
263
|
-
siblingInput?.select();
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const getValueAsArray = (value: string | undefined, length: number) => {
|
|
268
|
-
if (!value) {
|
|
269
|
-
return undefined;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return createEmptyArray(length).map((_, index) => value?.[index] ?? "");
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
const createEmptyArray = (length: number): string[] =>
|
|
276
|
-
Array.from<string>({ length }).fill("");
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
AlertDialog,
|
|
5
|
-
Callout,
|
|
6
|
-
Dialog,
|
|
7
|
-
Flex,
|
|
8
|
-
Text,
|
|
9
|
-
VisuallyHidden,
|
|
10
|
-
} from "@radix-ui/themes";
|
|
11
|
-
import * as React from "react";
|
|
12
|
-
import { useResendUserInvite } from "./api/user";
|
|
13
|
-
import {
|
|
14
|
-
AlertDialogContent,
|
|
15
|
-
DestructiveButton,
|
|
16
|
-
DialogContent,
|
|
17
|
-
PrimaryButton,
|
|
18
|
-
SecondaryButton,
|
|
19
|
-
} from "./elements";
|
|
20
|
-
import { Member } from "../api";
|
|
21
|
-
|
|
22
|
-
interface ResendInviteDialogProps extends AlertDialog.RootProps {
|
|
23
|
-
user: Member;
|
|
24
|
-
children?: React.ReactNode;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function ResendInviteDialog({
|
|
28
|
-
children,
|
|
29
|
-
user,
|
|
30
|
-
...props
|
|
31
|
-
}: ResendInviteDialogProps) {
|
|
32
|
-
const resendInvite = useResendUserInvite();
|
|
33
|
-
const cancelButtonRef = React.useRef<HTMLButtonElement>(null);
|
|
34
|
-
const successButtonRef = React.useRef<HTMLButtonElement>(null);
|
|
35
|
-
const [successDialogIsOpen, setSuccessDialogIsOpen] = React.useState(false);
|
|
36
|
-
|
|
37
|
-
const onSubmitForm = () => {
|
|
38
|
-
resendInvite.mutate(
|
|
39
|
-
{ userId: user.id },
|
|
40
|
-
{
|
|
41
|
-
onSuccess: () => {
|
|
42
|
-
setSuccessDialogIsOpen(true);
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
);
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
return (
|
|
49
|
-
<>
|
|
50
|
-
<AlertDialog.Root {...props}>
|
|
51
|
-
{children && <AlertDialog.Trigger>{children}</AlertDialog.Trigger>}
|
|
52
|
-
|
|
53
|
-
<AlertDialogContent
|
|
54
|
-
maxWidth="480px"
|
|
55
|
-
onOpenAutoFocus={() => {
|
|
56
|
-
requestAnimationFrame(() => {
|
|
57
|
-
cancelButtonRef.current?.focus();
|
|
58
|
-
});
|
|
59
|
-
}}
|
|
60
|
-
>
|
|
61
|
-
<AlertDialog.Title>Resend invite?</AlertDialog.Title>
|
|
62
|
-
<Flex mb="4" direction="column" gap="3">
|
|
63
|
-
<AlertDialog.Description>
|
|
64
|
-
Are you sure you want to resend the invite to{" "}
|
|
65
|
-
<Text weight="bold">{user.email}</Text>?
|
|
66
|
-
</AlertDialog.Description>
|
|
67
|
-
</Flex>
|
|
68
|
-
|
|
69
|
-
{resendInvite.error ? (
|
|
70
|
-
<Callout.Root color="red" mt="4" mb="-2">
|
|
71
|
-
<Callout.Text>
|
|
72
|
-
{getMutationErrorMessage(resendInvite.error)}
|
|
73
|
-
</Callout.Text>
|
|
74
|
-
</Callout.Root>
|
|
75
|
-
) : null}
|
|
76
|
-
|
|
77
|
-
<Flex gap="3" justify="end" mt="5" asChild>
|
|
78
|
-
<form
|
|
79
|
-
onSubmit={(event) => {
|
|
80
|
-
event.preventDefault();
|
|
81
|
-
onSubmitForm();
|
|
82
|
-
}}
|
|
83
|
-
>
|
|
84
|
-
<AlertDialog.Cancel>
|
|
85
|
-
<SecondaryButton
|
|
86
|
-
ref={cancelButtonRef}
|
|
87
|
-
disabled={resendInvite.isPending}
|
|
88
|
-
>
|
|
89
|
-
Cancel
|
|
90
|
-
</SecondaryButton>
|
|
91
|
-
</AlertDialog.Cancel>
|
|
92
|
-
<DestructiveButton type="submit" loading={resendInvite.isPending}>
|
|
93
|
-
Resend
|
|
94
|
-
</DestructiveButton>
|
|
95
|
-
</form>
|
|
96
|
-
</Flex>
|
|
97
|
-
</AlertDialogContent>
|
|
98
|
-
|
|
99
|
-
{/* mirror errors in a live region */}
|
|
100
|
-
<VisuallyHidden asChild>
|
|
101
|
-
<section aria-live="polite">
|
|
102
|
-
{getMutationErrorMessage(resendInvite.error)}
|
|
103
|
-
</section>
|
|
104
|
-
</VisuallyHidden>
|
|
105
|
-
</AlertDialog.Root>
|
|
106
|
-
<Dialog.Root
|
|
107
|
-
open={successDialogIsOpen}
|
|
108
|
-
onOpenChange={(isOpen) => {
|
|
109
|
-
if (!isOpen) {
|
|
110
|
-
props.onOpenChange?.(false);
|
|
111
|
-
}
|
|
112
|
-
setSuccessDialogIsOpen(isOpen);
|
|
113
|
-
}}
|
|
114
|
-
>
|
|
115
|
-
<DialogContent
|
|
116
|
-
maxWidth="360px"
|
|
117
|
-
onOpenAutoFocus={() => {
|
|
118
|
-
requestAnimationFrame(() => {
|
|
119
|
-
successButtonRef.current?.focus();
|
|
120
|
-
});
|
|
121
|
-
}}
|
|
122
|
-
>
|
|
123
|
-
<Dialog.Title>Invite sent</Dialog.Title>
|
|
124
|
-
<Dialog.Description>
|
|
125
|
-
The invite email has been resent to{" "}
|
|
126
|
-
<Text weight="bold">{user.email}</Text>
|
|
127
|
-
</Dialog.Description>
|
|
128
|
-
<Flex gap="3" justify="end" mt="5">
|
|
129
|
-
<Dialog.Close>
|
|
130
|
-
<PrimaryButton ref={successButtonRef}>Close</PrimaryButton>
|
|
131
|
-
</Dialog.Close>
|
|
132
|
-
</Flex>
|
|
133
|
-
</DialogContent>
|
|
134
|
-
</Dialog.Root>
|
|
135
|
-
</>
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function getMutationErrorMessage(error: unknown) {
|
|
140
|
-
if (!error) {
|
|
141
|
-
return "";
|
|
142
|
-
}
|
|
143
|
-
// TODO Handle server errors
|
|
144
|
-
return "There was an error sending the invite. Please try again.";
|
|
145
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
import { AlertDialog, Flex } from "@radix-ui/themes";
|
|
5
|
-
import { type ReactNode } from "react";
|
|
6
|
-
import {
|
|
7
|
-
AlertDialogContent,
|
|
8
|
-
DestructiveButton,
|
|
9
|
-
SecondaryButton,
|
|
10
|
-
} from "./elements";
|
|
11
|
-
import { useDeleteTotpFactors } from "../api";
|
|
12
|
-
import { useSecuritySettings } from "./use-security-settings";
|
|
13
|
-
import { ElevatedAccess } from "./elevated-access";
|
|
14
|
-
import { SaveButton } from "./save-button";
|
|
15
|
-
import { useDialogClose } from "./use-dialog-close";
|
|
16
|
-
|
|
17
|
-
interface ResetMfaDialogProps extends AlertDialog.RootProps {
|
|
18
|
-
children?: ReactNode;
|
|
19
|
-
isPasswordSet: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function ResetMfaDialog({
|
|
23
|
-
children,
|
|
24
|
-
isPasswordSet,
|
|
25
|
-
...props
|
|
26
|
-
}: ResetMfaDialogProps) {
|
|
27
|
-
const [open, setOpen] = React.useState(false);
|
|
28
|
-
|
|
29
|
-
const handleClose = React.useCallback(() => {
|
|
30
|
-
setOpen(false);
|
|
31
|
-
}, []);
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<AlertDialog.Root {...props} open={open} onOpenChange={setOpen}>
|
|
35
|
-
<AlertDialog.Trigger>{children}</AlertDialog.Trigger>
|
|
36
|
-
|
|
37
|
-
<AlertDialogContent maxWidth="480px">
|
|
38
|
-
<ElevatedAccess type="alert">
|
|
39
|
-
<Content onClose={handleClose} isPasswordSet={isPasswordSet} />
|
|
40
|
-
</ElevatedAccess>
|
|
41
|
-
</AlertDialogContent>
|
|
42
|
-
</AlertDialog.Root>
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function Content({
|
|
47
|
-
onClose,
|
|
48
|
-
isPasswordSet,
|
|
49
|
-
}: {
|
|
50
|
-
onClose: () => void;
|
|
51
|
-
isPasswordSet: boolean;
|
|
52
|
-
}) {
|
|
53
|
-
const securitySettings = useSecuritySettings();
|
|
54
|
-
const resetMfa = useDeleteTotpFactors();
|
|
55
|
-
|
|
56
|
-
const onSubmitForm = () => {
|
|
57
|
-
resetMfa.mutate();
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
useDialogClose(resetMfa.isSuccess, () => {
|
|
61
|
-
securitySettings.update("Mfa", false);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<>
|
|
66
|
-
<AlertDialog.Title>
|
|
67
|
-
Disable multi-factor authentication?
|
|
68
|
-
</AlertDialog.Title>
|
|
69
|
-
<AlertDialog.Description>
|
|
70
|
-
Turning off MFA will remove the additional layer of security on your
|
|
71
|
-
account.{" "}
|
|
72
|
-
{isPasswordSet
|
|
73
|
-
? "We will only ask for your password during sign-in."
|
|
74
|
-
: "We will not ask for additional verification during sign-in."}
|
|
75
|
-
</AlertDialog.Description>
|
|
76
|
-
|
|
77
|
-
<Flex gap="3" justify="end" mt="5" asChild>
|
|
78
|
-
<form
|
|
79
|
-
onSubmit={(event) => {
|
|
80
|
-
event.preventDefault();
|
|
81
|
-
onSubmitForm();
|
|
82
|
-
}}
|
|
83
|
-
>
|
|
84
|
-
<AlertDialog.Cancel>
|
|
85
|
-
<SecondaryButton
|
|
86
|
-
disabled={resetMfa.isPending || resetMfa.isSuccess}
|
|
87
|
-
>
|
|
88
|
-
Cancel
|
|
89
|
-
</SecondaryButton>
|
|
90
|
-
</AlertDialog.Cancel>
|
|
91
|
-
|
|
92
|
-
<SaveButton
|
|
93
|
-
asChild
|
|
94
|
-
loading={resetMfa.isPending}
|
|
95
|
-
done={resetMfa.isSuccess}
|
|
96
|
-
onDone={onClose}
|
|
97
|
-
>
|
|
98
|
-
<DestructiveButton type="submit">Disable</DestructiveButton>
|
|
99
|
-
</SaveButton>
|
|
100
|
-
</form>
|
|
101
|
-
</Flex>
|
|
102
|
-
</>
|
|
103
|
-
);
|
|
104
|
-
}
|