@workos-inc/widgets 1.1.5 → 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/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 +8 -9
- 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,111 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
AlertDialog,
|
|
5
|
-
Callout,
|
|
6
|
-
Flex,
|
|
7
|
-
Text,
|
|
8
|
-
VisuallyHidden,
|
|
9
|
-
} from "@radix-ui/themes";
|
|
10
|
-
import { type ReactNode, useRef } from "react";
|
|
11
|
-
import { useRevokeUserInvite } from "./api/user";
|
|
12
|
-
import {
|
|
13
|
-
AlertDialogContent,
|
|
14
|
-
DestructiveButton,
|
|
15
|
-
SecondaryButton,
|
|
16
|
-
} from "./elements";
|
|
17
|
-
import { Member } from "../api";
|
|
18
|
-
|
|
19
|
-
interface RevokeInviteDialogProps extends AlertDialog.RootProps {
|
|
20
|
-
user: Member;
|
|
21
|
-
children?: ReactNode;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function RevokeInviteDialog({
|
|
25
|
-
children,
|
|
26
|
-
user,
|
|
27
|
-
...props
|
|
28
|
-
}: RevokeInviteDialogProps) {
|
|
29
|
-
const revokeInvite = useRevokeUserInvite();
|
|
30
|
-
const cancelButtonRef = useRef<HTMLButtonElement>(null);
|
|
31
|
-
|
|
32
|
-
const onSubmitForm = () => {
|
|
33
|
-
revokeInvite.mutate(
|
|
34
|
-
{ userId: user.id },
|
|
35
|
-
{
|
|
36
|
-
onSuccess: () => {
|
|
37
|
-
props.onOpenChange?.(false);
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
);
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
<AlertDialog.Root {...props}>
|
|
45
|
-
{children && <AlertDialog.Trigger>{children}</AlertDialog.Trigger>}
|
|
46
|
-
|
|
47
|
-
<AlertDialogContent
|
|
48
|
-
maxWidth="480px"
|
|
49
|
-
onOpenAutoFocus={() => {
|
|
50
|
-
requestAnimationFrame(() => {
|
|
51
|
-
cancelButtonRef.current?.focus();
|
|
52
|
-
});
|
|
53
|
-
}}
|
|
54
|
-
>
|
|
55
|
-
<AlertDialog.Title>Revoke invite?</AlertDialog.Title>
|
|
56
|
-
<Flex mb="4" direction="column" gap="3">
|
|
57
|
-
<AlertDialog.Description>
|
|
58
|
-
Are you sure you want to revoke the invite to{" "}
|
|
59
|
-
<Text weight="bold">{user.email}</Text>? This action is immediate
|
|
60
|
-
and cannot be undone.
|
|
61
|
-
</AlertDialog.Description>
|
|
62
|
-
</Flex>
|
|
63
|
-
|
|
64
|
-
{revokeInvite.error ? (
|
|
65
|
-
<Callout.Root color="red" mt="4" mb="-2">
|
|
66
|
-
<Callout.Text>
|
|
67
|
-
{getMutationErrorMessage(revokeInvite.error)}
|
|
68
|
-
</Callout.Text>
|
|
69
|
-
</Callout.Root>
|
|
70
|
-
) : null}
|
|
71
|
-
|
|
72
|
-
<Flex gap="3" justify="end" mt="5" asChild>
|
|
73
|
-
<form
|
|
74
|
-
onSubmit={(event) => {
|
|
75
|
-
event.preventDefault();
|
|
76
|
-
onSubmitForm();
|
|
77
|
-
}}
|
|
78
|
-
>
|
|
79
|
-
<AlertDialog.Cancel>
|
|
80
|
-
<SecondaryButton
|
|
81
|
-
ref={cancelButtonRef}
|
|
82
|
-
disabled={revokeInvite.isPending}
|
|
83
|
-
>
|
|
84
|
-
Cancel
|
|
85
|
-
</SecondaryButton>
|
|
86
|
-
</AlertDialog.Cancel>
|
|
87
|
-
|
|
88
|
-
<DestructiveButton type="submit" loading={revokeInvite.isPending}>
|
|
89
|
-
Revoke
|
|
90
|
-
</DestructiveButton>
|
|
91
|
-
</form>
|
|
92
|
-
</Flex>
|
|
93
|
-
</AlertDialogContent>
|
|
94
|
-
|
|
95
|
-
{/* mirror errors in a live region */}
|
|
96
|
-
<VisuallyHidden asChild>
|
|
97
|
-
<section aria-live="polite">
|
|
98
|
-
{getMutationErrorMessage(revokeInvite.error)}
|
|
99
|
-
</section>
|
|
100
|
-
</VisuallyHidden>
|
|
101
|
-
</AlertDialog.Root>
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function getMutationErrorMessage(error: unknown) {
|
|
106
|
-
if (!error) {
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
// TODO Handle server errors
|
|
110
|
-
return "There was an error revoking the invite. Please try again.";
|
|
111
|
-
}
|
package/src/lib/save-button.tsx
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ButtonProps,
|
|
3
|
-
Flex,
|
|
4
|
-
Slot,
|
|
5
|
-
Slottable,
|
|
6
|
-
Spinner,
|
|
7
|
-
Text,
|
|
8
|
-
} from "@radix-ui/themes";
|
|
9
|
-
import { CheckIcon } from "@radix-ui/react-icons";
|
|
10
|
-
import { useEffect, useRef, useState } from "react";
|
|
11
|
-
import { namespaceClassNames } from "./utils";
|
|
12
|
-
|
|
13
|
-
const DONE_TIMEOUT_MS = 1500;
|
|
14
|
-
const SAVING_TIMEOUT_MS = 600;
|
|
15
|
-
|
|
16
|
-
interface SaveButtonProps extends ButtonProps {
|
|
17
|
-
asChild?: boolean;
|
|
18
|
-
children: React.ReactNode;
|
|
19
|
-
loading?: boolean;
|
|
20
|
-
done?: boolean;
|
|
21
|
-
onDone?: () => void;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function SaveButton({
|
|
25
|
-
asChild = false,
|
|
26
|
-
children,
|
|
27
|
-
loading,
|
|
28
|
-
done,
|
|
29
|
-
onDone,
|
|
30
|
-
...props
|
|
31
|
-
}: SaveButtonProps) {
|
|
32
|
-
const [state, setState] = useState<"idle" | "loading" | "done">(
|
|
33
|
-
loading ? "loading" : done ? "done" : "idle",
|
|
34
|
-
);
|
|
35
|
-
const loadingStartTime = useRef<number | null>(null);
|
|
36
|
-
const Button = asChild ? Slot : "button";
|
|
37
|
-
|
|
38
|
-
useEffect(() => {
|
|
39
|
-
if (loading) {
|
|
40
|
-
setState("loading");
|
|
41
|
-
loadingStartTime.current = Date.now();
|
|
42
|
-
} else if (done) {
|
|
43
|
-
const currentTime = Date.now();
|
|
44
|
-
const loadingDuration = loadingStartTime.current
|
|
45
|
-
? currentTime - loadingStartTime.current
|
|
46
|
-
: 0;
|
|
47
|
-
|
|
48
|
-
// If loading lasted less than 500 ms, wait for the remaining time
|
|
49
|
-
const remainingDelay = Math.max(0, SAVING_TIMEOUT_MS - loadingDuration);
|
|
50
|
-
|
|
51
|
-
let doneTimer: NodeJS.Timeout | null = null;
|
|
52
|
-
const savedTimer = setTimeout(() => {
|
|
53
|
-
setState("done");
|
|
54
|
-
|
|
55
|
-
doneTimer = setTimeout(() => {
|
|
56
|
-
onDone?.();
|
|
57
|
-
}, DONE_TIMEOUT_MS);
|
|
58
|
-
}, remainingDelay);
|
|
59
|
-
|
|
60
|
-
return () => {
|
|
61
|
-
clearTimeout(savedTimer);
|
|
62
|
-
if (doneTimer) {
|
|
63
|
-
clearTimeout(doneTimer);
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
} else if (!loading && !done) {
|
|
67
|
-
setState("idle");
|
|
68
|
-
}
|
|
69
|
-
}, [loading, done, onDone]);
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<Button
|
|
73
|
-
{...props}
|
|
74
|
-
className={namespaceClassNames("save-button")}
|
|
75
|
-
data-save-state={state}
|
|
76
|
-
>
|
|
77
|
-
<Slottable>{children}</Slottable>
|
|
78
|
-
|
|
79
|
-
{state === "loading" && (
|
|
80
|
-
<Flex
|
|
81
|
-
as="span"
|
|
82
|
-
align="center"
|
|
83
|
-
justify="center"
|
|
84
|
-
position="absolute"
|
|
85
|
-
inset="0"
|
|
86
|
-
>
|
|
87
|
-
<Spinner size="1" />
|
|
88
|
-
</Flex>
|
|
89
|
-
)}
|
|
90
|
-
|
|
91
|
-
{state === "done" && (
|
|
92
|
-
<Flex
|
|
93
|
-
as="span"
|
|
94
|
-
align="center"
|
|
95
|
-
justify="center"
|
|
96
|
-
position="absolute"
|
|
97
|
-
inset="0"
|
|
98
|
-
gap="1"
|
|
99
|
-
>
|
|
100
|
-
<CheckIcon
|
|
101
|
-
width="18px"
|
|
102
|
-
height="18px"
|
|
103
|
-
style={{ marginLeft: "-4px" }}
|
|
104
|
-
className={namespaceClassNames("save-button__done-icon")}
|
|
105
|
-
/>
|
|
106
|
-
<Text className={namespaceClassNames("save-button__done-text")}>
|
|
107
|
-
Done
|
|
108
|
-
</Text>
|
|
109
|
-
</Flex>
|
|
110
|
-
)}
|
|
111
|
-
</Button>
|
|
112
|
-
);
|
|
113
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import { flushSync } from "react-dom";
|
|
3
|
-
import { useUsersManagementContext } from "./users-management-context";
|
|
4
|
-
|
|
5
|
-
interface SearchContextType {
|
|
6
|
-
inputRef: React.RefObject<HTMLInputElement>;
|
|
7
|
-
clearSearch: () => void;
|
|
8
|
-
searchValue: string;
|
|
9
|
-
setSearchValue: React.Dispatch<React.SetStateAction<string>>;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const SearchContext = React.createContext<SearchContextType | undefined>(
|
|
13
|
-
undefined,
|
|
14
|
-
);
|
|
15
|
-
SearchContext.displayName = "SearchContext";
|
|
16
|
-
|
|
17
|
-
export const SearchProvider: React.FC<React.PropsWithChildren> = ({
|
|
18
|
-
children,
|
|
19
|
-
}) => {
|
|
20
|
-
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
21
|
-
const {
|
|
22
|
-
state: { searchQuery },
|
|
23
|
-
dispatch,
|
|
24
|
-
} = useUsersManagementContext();
|
|
25
|
-
|
|
26
|
-
const [searchValue, setSearchValue] = React.useState(searchQuery || "");
|
|
27
|
-
|
|
28
|
-
const clearSearch = React.useCallback(() => {
|
|
29
|
-
flushSync(() => {
|
|
30
|
-
setSearchValue("");
|
|
31
|
-
dispatch({ type: "FILTER_BY_SEARCH", searchQuery: "" });
|
|
32
|
-
});
|
|
33
|
-
inputRef.current?.focus();
|
|
34
|
-
}, [dispatch]);
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<SearchContext.Provider
|
|
38
|
-
value={{ inputRef, clearSearch, searchValue, setSearchValue }}
|
|
39
|
-
>
|
|
40
|
-
{children}
|
|
41
|
-
</SearchContext.Provider>
|
|
42
|
-
);
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export const useSearchContext = () => {
|
|
46
|
-
const context = React.useContext(SearchContext);
|
|
47
|
-
if (!context) {
|
|
48
|
-
throw new Error("useSearchContext must be used within a SearchProvider");
|
|
49
|
-
}
|
|
50
|
-
return context;
|
|
51
|
-
};
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { Callout, Dialog, Flex, Text, VisuallyHidden } from "@radix-ui/themes";
|
|
4
|
-
import * as React from "react";
|
|
5
|
-
import { type ReactNode } from "react";
|
|
6
|
-
import {
|
|
7
|
-
DialogContent,
|
|
8
|
-
Label,
|
|
9
|
-
PasswordField,
|
|
10
|
-
PrimaryButton,
|
|
11
|
-
SecondaryButton,
|
|
12
|
-
} from "./elements";
|
|
13
|
-
import * as Form from "@radix-ui/react-form";
|
|
14
|
-
import { useCreatePassword } from "../api";
|
|
15
|
-
import { useSecuritySettings } from "./use-security-settings";
|
|
16
|
-
import { ElevatedAccess } from "./elevated-access";
|
|
17
|
-
import { SaveButton } from "./save-button";
|
|
18
|
-
import { useDialogClose } from "./use-dialog-close";
|
|
19
|
-
|
|
20
|
-
interface SetPasswordDialogProps extends Dialog.RootProps {
|
|
21
|
-
children?: ReactNode;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function SetPasswordDialog({
|
|
25
|
-
children,
|
|
26
|
-
...props
|
|
27
|
-
}: SetPasswordDialogProps) {
|
|
28
|
-
const [open, setOpen] = React.useState(false);
|
|
29
|
-
|
|
30
|
-
const handleClose = React.useCallback(() => {
|
|
31
|
-
setOpen(false);
|
|
32
|
-
}, []);
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<Dialog.Root {...props} open={open} onOpenChange={setOpen}>
|
|
36
|
-
<Dialog.Trigger>{children}</Dialog.Trigger>
|
|
37
|
-
|
|
38
|
-
<DialogContent maxWidth="480px">
|
|
39
|
-
<ElevatedAccess>
|
|
40
|
-
<Content onClose={handleClose} />
|
|
41
|
-
</ElevatedAccess>
|
|
42
|
-
</DialogContent>
|
|
43
|
-
</Dialog.Root>
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface ContentProps {
|
|
48
|
-
onClose: () => void;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function Content({ onClose }: ContentProps) {
|
|
52
|
-
const setPassword = useCreatePassword();
|
|
53
|
-
const securitySettings = useSecuritySettings();
|
|
54
|
-
const [disableSubmit, setDisableSubmit] = React.useState(true);
|
|
55
|
-
|
|
56
|
-
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
57
|
-
event.preventDefault();
|
|
58
|
-
|
|
59
|
-
const formData = new FormData(event.currentTarget);
|
|
60
|
-
const password = formData.get("password")?.toString();
|
|
61
|
-
|
|
62
|
-
if (!password) {
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
setPassword.mutate({ data: { password } });
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
useDialogClose(setPassword.isSuccess, () => {
|
|
70
|
-
securitySettings.update("Password", true);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<>
|
|
75
|
-
<Dialog.Title mb="5">Set New Password</Dialog.Title>
|
|
76
|
-
<VisuallyHidden>
|
|
77
|
-
<Dialog.Description>
|
|
78
|
-
Set a new password for your account
|
|
79
|
-
</Dialog.Description>
|
|
80
|
-
</VisuallyHidden>
|
|
81
|
-
|
|
82
|
-
{setPassword.error ? (
|
|
83
|
-
<Callout.Root color="red" my="-2">
|
|
84
|
-
<Callout.Text>
|
|
85
|
-
{getMutationErrorMessage(setPassword.error)}
|
|
86
|
-
</Callout.Text>
|
|
87
|
-
</Callout.Root>
|
|
88
|
-
) : null}
|
|
89
|
-
|
|
90
|
-
<Form.Root
|
|
91
|
-
onSubmit={handleSubmit}
|
|
92
|
-
onChange={(event) => {
|
|
93
|
-
const formData = new FormData(event.currentTarget);
|
|
94
|
-
const password = formData.get("password")?.toString();
|
|
95
|
-
const confirmPassword = formData.get("confirmPassword")?.toString();
|
|
96
|
-
setDisableSubmit(password === "" || confirmPassword === "");
|
|
97
|
-
}}
|
|
98
|
-
>
|
|
99
|
-
<Flex mt="5" direction="column" gap="4">
|
|
100
|
-
<Form.Field name="password" asChild>
|
|
101
|
-
<Flex direction="column" gap="2">
|
|
102
|
-
<Form.Label asChild>
|
|
103
|
-
<Label>Enter a new password</Label>
|
|
104
|
-
</Form.Label>
|
|
105
|
-
<Form.Control asChild>
|
|
106
|
-
<PasswordField
|
|
107
|
-
autoFocus
|
|
108
|
-
required
|
|
109
|
-
autoComplete="new-password"
|
|
110
|
-
placeholder="New password"
|
|
111
|
-
disabled={setPassword.isPending || setPassword.isSuccess}
|
|
112
|
-
/>
|
|
113
|
-
</Form.Control>
|
|
114
|
-
<Form.Message match="valueMissing" asChild>
|
|
115
|
-
<Text size="2" color="red">
|
|
116
|
-
Please enter a new password
|
|
117
|
-
</Text>
|
|
118
|
-
</Form.Message>
|
|
119
|
-
<Form.Message match={(value) => value.length < 8} asChild>
|
|
120
|
-
<Text size="2" color="red">
|
|
121
|
-
Password must be at least 8 characters
|
|
122
|
-
</Text>
|
|
123
|
-
</Form.Message>
|
|
124
|
-
</Flex>
|
|
125
|
-
</Form.Field>
|
|
126
|
-
|
|
127
|
-
<Form.Field name="confirmPassword" asChild>
|
|
128
|
-
<Flex direction="column" gap="2">
|
|
129
|
-
<Form.Label asChild>
|
|
130
|
-
<Label>Confirm your new password</Label>
|
|
131
|
-
</Form.Label>
|
|
132
|
-
<Form.Control asChild>
|
|
133
|
-
<PasswordField
|
|
134
|
-
required
|
|
135
|
-
autoComplete="new-password"
|
|
136
|
-
placeholder="Confirm new password"
|
|
137
|
-
disabled={setPassword.isPending || setPassword.isSuccess}
|
|
138
|
-
/>
|
|
139
|
-
</Form.Control>
|
|
140
|
-
<Form.Message match="valueMissing" asChild>
|
|
141
|
-
<Text size="2" color="red">
|
|
142
|
-
Please confirm your new password
|
|
143
|
-
</Text>
|
|
144
|
-
</Form.Message>
|
|
145
|
-
<Form.Message
|
|
146
|
-
match={(value, formData) => value !== formData.get("password")}
|
|
147
|
-
asChild
|
|
148
|
-
>
|
|
149
|
-
<Text size="2" color="red">
|
|
150
|
-
The passwords you entered don’t match.
|
|
151
|
-
</Text>
|
|
152
|
-
</Form.Message>
|
|
153
|
-
</Flex>
|
|
154
|
-
</Form.Field>
|
|
155
|
-
</Flex>
|
|
156
|
-
|
|
157
|
-
<Flex mt="5" gap="3" justify="end">
|
|
158
|
-
<Dialog.Close>
|
|
159
|
-
<SecondaryButton
|
|
160
|
-
disabled={setPassword.isPending || setPassword.isSuccess}
|
|
161
|
-
>
|
|
162
|
-
Cancel
|
|
163
|
-
</SecondaryButton>
|
|
164
|
-
</Dialog.Close>
|
|
165
|
-
|
|
166
|
-
<SaveButton
|
|
167
|
-
asChild
|
|
168
|
-
loading={setPassword.isPending}
|
|
169
|
-
done={setPassword.isSuccess}
|
|
170
|
-
onDone={onClose}
|
|
171
|
-
>
|
|
172
|
-
<PrimaryButton type="submit" disabled={disableSubmit}>
|
|
173
|
-
Set password
|
|
174
|
-
</PrimaryButton>
|
|
175
|
-
</SaveButton>
|
|
176
|
-
</Flex>
|
|
177
|
-
|
|
178
|
-
{/* mirror errors in a live region */}
|
|
179
|
-
<VisuallyHidden asChild>
|
|
180
|
-
<section aria-live="polite">
|
|
181
|
-
{getMutationErrorMessage(setPassword.error)}
|
|
182
|
-
</section>
|
|
183
|
-
</VisuallyHidden>
|
|
184
|
-
</Form.Root>
|
|
185
|
-
</>
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function getMutationErrorMessage(error: unknown) {
|
|
190
|
-
if (error instanceof Error) {
|
|
191
|
-
return error.message;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (
|
|
195
|
-
typeof error === "object" &&
|
|
196
|
-
error !== null &&
|
|
197
|
-
"message" in error &&
|
|
198
|
-
typeof error.message === "string"
|
|
199
|
-
) {
|
|
200
|
-
return error.message;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return "Something went wrong. Please try again.";
|
|
204
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
export function useDialogClose(enabled: boolean, callback: () => void) {
|
|
4
|
-
const callbackRef = React.useRef(callback);
|
|
5
|
-
const firstRenderRef = React.useRef(true);
|
|
6
|
-
|
|
7
|
-
React.useEffect(() => {
|
|
8
|
-
if (firstRenderRef.current) {
|
|
9
|
-
firstRenderRef.current = false;
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const cb = callbackRef.current;
|
|
14
|
-
|
|
15
|
-
return () => {
|
|
16
|
-
if (enabled) cb();
|
|
17
|
-
};
|
|
18
|
-
}, [enabled]);
|
|
19
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AuthenticationInformationResponse,
|
|
3
|
-
getAuthenticationInformationQueryKey,
|
|
4
|
-
} from "../api";
|
|
5
|
-
import { useQueryClient } from "@tanstack/react-query";
|
|
6
|
-
|
|
7
|
-
type Methods = "Password" | "Mfa" | "Passkey";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* A hook that provides utilities for managing security settings.
|
|
11
|
-
* Allows optimistically updating verification method status (password, MFA, passkey)
|
|
12
|
-
* while also triggering a server revalidation.
|
|
13
|
-
*/
|
|
14
|
-
export function useSecuritySettings() {
|
|
15
|
-
const client = useQueryClient();
|
|
16
|
-
|
|
17
|
-
const update = (method: Methods, isSetUp: boolean) => {
|
|
18
|
-
const queryKey = getAuthenticationInformationQueryKey();
|
|
19
|
-
|
|
20
|
-
// Optimistic update
|
|
21
|
-
client.setQueriesData<AuthenticationInformationResponse>(
|
|
22
|
-
{ queryKey, exact: false },
|
|
23
|
-
(data) => {
|
|
24
|
-
if (!data) {
|
|
25
|
-
return data;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
...data,
|
|
30
|
-
data: {
|
|
31
|
-
...data.data,
|
|
32
|
-
verificationMethods: {
|
|
33
|
-
...data.data.verificationMethods,
|
|
34
|
-
[method]: { ...data.data.verificationMethods[method], isSetUp },
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
},
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
// Also validate it with the server
|
|
42
|
-
client.invalidateQueries({
|
|
43
|
-
queryKey: getAuthenticationInformationQueryKey(),
|
|
44
|
-
exact: false,
|
|
45
|
-
});
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
return { update };
|
|
49
|
-
}
|