@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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/lib/organization-switcher.d.ts +10 -1
  3. package/dist/cjs/lib/organization-switcher.d.ts.map +1 -1
  4. package/dist/cjs/lib/organization-switcher.js +31 -3
  5. package/dist/cjs/lib/organization-switcher.js.map +1 -1
  6. package/dist/cjs/workos-widgets.client.d.ts +6 -0
  7. package/dist/cjs/workos-widgets.client.d.ts.map +1 -1
  8. package/dist/cjs/workos-widgets.client.js +23 -3
  9. package/dist/cjs/workos-widgets.client.js.map +1 -1
  10. package/dist/esm/lib/organization-switcher.d.ts +10 -1
  11. package/dist/esm/lib/organization-switcher.d.ts.map +1 -1
  12. package/dist/esm/lib/organization-switcher.js +31 -3
  13. package/dist/esm/lib/organization-switcher.js.map +1 -1
  14. package/dist/esm/workos-widgets.client.d.ts +6 -0
  15. package/dist/esm/workos-widgets.client.d.ts.map +1 -1
  16. package/dist/esm/workos-widgets.client.js +25 -5
  17. package/dist/esm/workos-widgets.client.js.map +1 -1
  18. package/package.json +40 -47
  19. package/src/api/api-provider.tsx +0 -158
  20. package/src/api/constants.ts +0 -1
  21. package/src/api/endpoint.ts +0 -3097
  22. package/src/api/errors.ts +0 -48
  23. package/src/api/index.ts +0 -2
  24. package/src/api/utils.ts +0 -42
  25. package/src/api/widgets-api-client.ts +0 -87
  26. package/src/card-list.tsx +0 -26
  27. package/src/index.ts +0 -9
  28. package/src/lib/add-mfa-dialog.tsx +0 -379
  29. package/src/lib/api/config.ts +0 -9
  30. package/src/lib/api/user.ts +0 -98
  31. package/src/lib/change-password-dialog.tsx +0 -290
  32. package/src/lib/constants.ts +0 -3
  33. package/src/lib/copy-button.tsx +0 -53
  34. package/src/lib/delete-user-dialog.tsx +0 -110
  35. package/src/lib/edit-user-profile-dialog.tsx +0 -181
  36. package/src/lib/edit-user-role-dialog.tsx +0 -178
  37. package/src/lib/elements.tsx +0 -428
  38. package/src/lib/elevated-access.tsx +0 -261
  39. package/src/lib/error-boundary.tsx +0 -166
  40. package/src/lib/errors.ts +0 -49
  41. package/src/lib/generic-error.tsx +0 -70
  42. package/src/lib/icon-panel.tsx +0 -26
  43. package/src/lib/icons.tsx +0 -21
  44. package/src/lib/invite-user-dialog.tsx +0 -327
  45. package/src/lib/logout-all-sessions-dialog.tsx +0 -82
  46. package/src/lib/logout-dialog.tsx +0 -85
  47. package/src/lib/marker.tsx +0 -39
  48. package/src/lib/oauth-icons.tsx +0 -138
  49. package/src/lib/organization-switcher.tsx +0 -156
  50. package/src/lib/otp-input.tsx +0 -276
  51. package/src/lib/resend-invite-dialog.tsx +0 -145
  52. package/src/lib/reset-mfa-dialog.tsx +0 -104
  53. package/src/lib/revoke-invite-dialog.tsx +0 -111
  54. package/src/lib/save-button.tsx +0 -113
  55. package/src/lib/search-provider.tsx +0 -51
  56. package/src/lib/set-password-dialog.tsx +0 -204
  57. package/src/lib/use-dialog-close.tsx +0 -19
  58. package/src/lib/use-is-hydrated.ts +0 -13
  59. package/src/lib/use-layout-effect.ts +0 -6
  60. package/src/lib/use-security-settings.tsx +0 -49
  61. package/src/lib/user-actions-dropdown.tsx +0 -157
  62. package/src/lib/user-profile.tsx +0 -227
  63. package/src/lib/user-security.tsx +0 -187
  64. package/src/lib/user-sessions.tsx +0 -204
  65. package/src/lib/users-filter.tsx +0 -62
  66. package/src/lib/users-management-context.tsx +0 -74
  67. package/src/lib/users-management-state.ts +0 -165
  68. package/src/lib/users-management.tsx +0 -594
  69. package/src/lib/users-search.tsx +0 -73
  70. package/src/lib/utils.ts +0 -131
  71. package/src/lib/widgets-context.ts +0 -29
  72. package/src/organization-switcher.client.tsx +0 -81
  73. package/src/user-profile.client.tsx +0 -55
  74. package/src/user-security.client.tsx +0 -55
  75. package/src/user-sessions.client.tsx +0 -100
  76. package/src/users-management.client.tsx +0 -73
  77. package/src/workos-widgets.client.tsx +0 -75
  78. /package/{src → dist/css}/base.css +0 -0
  79. /package/{src → dist/css}/lib/card-list.css +0 -0
  80. /package/{src → dist/css}/lib/marker.css +0 -0
  81. /package/{src → dist/css}/lib/save-button.css +0 -0
  82. /package/{src → dist/css}/styles.css +0 -0
  83. /package/{src → dist/css}/users-management.css +0 -0
@@ -1,290 +0,0 @@
1
- "use client";
2
-
3
- import { Callout, Dialog, Flex, Text, VisuallyHidden } from "@radix-ui/themes";
4
- import * as Form from "@radix-ui/react-form";
5
- import * as React from "react";
6
- import { type ReactNode } from "react";
7
- import {
8
- DialogContent,
9
- Label,
10
- PasswordField,
11
- PrimaryButton,
12
- SecondaryButton,
13
- } from "./elements";
14
- import { useUpdatePassword } from "../api";
15
- import { SaveButton } from "./save-button";
16
-
17
- interface ChangePasswordDialogProps extends Dialog.RootProps {
18
- children?: ReactNode;
19
- }
20
-
21
- export function ChangePasswordDialog({
22
- children,
23
- ...props
24
- }: ChangePasswordDialogProps) {
25
- const [open, setOpen] = React.useState(false);
26
-
27
- const handleClose = React.useCallback(() => {
28
- setOpen(false);
29
- }, []);
30
-
31
- return (
32
- <Dialog.Root
33
- {...props}
34
- open={props.open || open}
35
- onOpenChange={props.onOpenChange || setOpen}
36
- >
37
- <Dialog.Trigger>{children}</Dialog.Trigger>
38
-
39
- <DialogContent maxWidth="480px">
40
- <Content onClose={handleClose} />
41
- </DialogContent>
42
- </Dialog.Root>
43
- );
44
- }
45
-
46
- interface ContentProps {
47
- onClose: () => void;
48
- }
49
-
50
- function Content({ onClose }: ContentProps) {
51
- const changePassword = useUpdatePassword({
52
- mutation: {
53
- onError: (error) => {
54
- const errorMsg = getMutationErrorMessage(error);
55
- if (errorMsg === "Invalid credentials") {
56
- setServerErrors({
57
- currentPassword:
58
- "Your current password is incorrect. Please, try again.",
59
- });
60
- }
61
- },
62
- },
63
- });
64
- const [disableSubmit, setDisableSubmit] = React.useState(true);
65
- const [serverErrors, setServerErrors] = React.useState<{
66
- currentPassword?: string;
67
- password?: string;
68
- confirmPassword?: string;
69
- }>({});
70
-
71
- const errorMessage = React.useMemo(() => {
72
- if (changePassword.error) {
73
- return getMutationErrorMessage(changePassword.error);
74
- }
75
-
76
- return null;
77
- }, [changePassword.error]);
78
-
79
- const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
80
- event.preventDefault();
81
-
82
- const formData = new FormData(event.currentTarget);
83
- const currentPassword = formData.get("oldPassword")?.toString() ?? "";
84
- const newPassword = formData.get("password")?.toString() ?? "";
85
-
86
- changePassword.mutate({
87
- data: { currentPassword, newPassword },
88
- });
89
- };
90
-
91
- return (
92
- <>
93
- <Dialog.Title mb="5">Change Password</Dialog.Title>
94
- <VisuallyHidden>
95
- <Dialog.Description>Change your account password</Dialog.Description>
96
- </VisuallyHidden>
97
-
98
- {errorMessage && errorMessage !== "Invalid credentials" ? (
99
- <Callout.Root color="red" my="-2">
100
- <Callout.Text>{errorMessage}</Callout.Text>
101
- </Callout.Root>
102
- ) : null}
103
-
104
- <Form.Root
105
- onSubmit={handleSubmit}
106
- onChange={(event) => {
107
- const formData = new FormData(event.currentTarget);
108
- const currentPassword = formData.get("oldPassword")?.toString() ?? "";
109
- const newPassword = formData.get("password")?.toString() ?? "";
110
- const confirmPassword =
111
- formData.get("confirmPassword")?.toString() ?? "";
112
-
113
- setDisableSubmit(
114
- currentPassword === "" ||
115
- newPassword === "" ||
116
- confirmPassword === "",
117
- );
118
- setServerErrors({});
119
- }}
120
- onClearServerErrors={() => {
121
- setServerErrors({});
122
- }}
123
- >
124
- <Flex mt="5" direction="column" gap="4">
125
- <Form.Field
126
- name="oldPassword"
127
- asChild
128
- serverInvalid={!serverErrors.currentPassword}
129
- >
130
- <Flex direction="column" gap="2">
131
- <Form.Label asChild>
132
- <Label>Enter your current password</Label>
133
- </Form.Label>
134
-
135
- <Form.Control asChild>
136
- <PasswordField
137
- autoFocus
138
- required
139
- disabled={
140
- changePassword.isPending || changePassword.isSuccess
141
- }
142
- autoComplete="current-password"
143
- placeholder="Current password"
144
- />
145
- </Form.Control>
146
-
147
- <Form.Message match="valueMissing" asChild>
148
- <Text size="2" color="red">
149
- Please enter your current password
150
- </Text>
151
- </Form.Message>
152
-
153
- {serverErrors.currentPassword && (
154
- <Form.Message asChild>
155
- <Text size="2" color="red">
156
- {serverErrors.currentPassword}
157
- </Text>
158
- </Form.Message>
159
- )}
160
- </Flex>
161
- </Form.Field>
162
-
163
- <Form.Field
164
- name="password"
165
- asChild
166
- serverInvalid={!serverErrors.password}
167
- >
168
- <Flex direction="column" gap="2">
169
- <Form.Label asChild>
170
- <Label>Enter your new password</Label>
171
- </Form.Label>
172
-
173
- <Form.Control asChild>
174
- <PasswordField
175
- autoComplete="new-password"
176
- placeholder="New password"
177
- required
178
- disabled={
179
- changePassword.isPending || changePassword.isSuccess
180
- }
181
- />
182
- </Form.Control>
183
-
184
- <Form.Message match="valueMissing" asChild>
185
- <Text size="2" color="red">
186
- Please enter your new password
187
- </Text>
188
- </Form.Message>
189
- <Form.Message match={(value) => value.length < 8} asChild>
190
- <Text size="2" color="red">
191
- Password must be at least 8 characters
192
- </Text>
193
- </Form.Message>
194
- <Form.Message
195
- match={(value, formData) =>
196
- value === formData.get("oldPassword")
197
- }
198
- asChild
199
- >
200
- <Text size="2" color="red">
201
- You cannot reuse your current password
202
- </Text>
203
- </Form.Message>
204
- </Flex>
205
- </Form.Field>
206
-
207
- <Form.Field
208
- name="confirmPassword"
209
- asChild
210
- serverInvalid={!serverErrors.confirmPassword}
211
- >
212
- <Flex direction="column" gap="2">
213
- <Form.Label asChild>
214
- <Label>Confirm your new password</Label>
215
- </Form.Label>
216
-
217
- <Form.Control asChild>
218
- <PasswordField
219
- autoComplete="new-password"
220
- placeholder="Confirm new password"
221
- required
222
- disabled={
223
- changePassword.isPending || changePassword.isSuccess
224
- }
225
- />
226
- </Form.Control>
227
- <Form.Message match="valueMissing" asChild>
228
- <Text size="2" color="red">
229
- Please confirm your new password
230
- </Text>
231
- </Form.Message>
232
- <Form.Message
233
- match={(value, formData) => value !== formData.get("password")}
234
- asChild
235
- >
236
- <Text size="2" color="red">
237
- The new passwords you entered don’t match.
238
- </Text>
239
- </Form.Message>
240
- </Flex>
241
- </Form.Field>
242
- </Flex>
243
-
244
- <Flex mt="5" gap="3" justify="end">
245
- <Dialog.Close>
246
- <SecondaryButton
247
- type="button"
248
- disabled={changePassword.isPending || changePassword.isSuccess}
249
- >
250
- Cancel
251
- </SecondaryButton>
252
- </Dialog.Close>
253
-
254
- <SaveButton
255
- asChild
256
- loading={changePassword.isPending}
257
- done={changePassword.isSuccess}
258
- onDone={onClose}
259
- >
260
- <PrimaryButton type="submit" disabled={disableSubmit}>
261
- Change password
262
- </PrimaryButton>
263
- </SaveButton>
264
- </Flex>
265
- </Form.Root>
266
-
267
- {/* mirror errors in a live region */}
268
- <VisuallyHidden asChild>
269
- <section aria-live="polite">{errorMessage}</section>
270
- </VisuallyHidden>
271
- </>
272
- );
273
- }
274
-
275
- function getMutationErrorMessage(error: unknown) {
276
- if (error instanceof Error) {
277
- return error.message;
278
- }
279
-
280
- if (
281
- typeof error === "object" &&
282
- error !== null &&
283
- "message" in error &&
284
- typeof error.message === "string"
285
- ) {
286
- return error.message;
287
- }
288
-
289
- return "Something went wrong. Please try again.";
290
- }
@@ -1,3 +0,0 @@
1
- export const USER_ROW_LIMIT = 10;
2
- export const WIDGETS_API_VERSION = "1";
3
- export const WIDGETS_CLASS_NAMESPACE = "woswidgets";
@@ -1,53 +0,0 @@
1
- import * as React from "react";
2
- import { CheckIcon, CopyIcon } from "@radix-ui/react-icons";
3
- import { Slot, Slottable } from "@radix-ui/themes";
4
-
5
- interface CopyButtonProps
6
- extends React.ButtonHTMLAttributes<HTMLButtonElement> {
7
- value: string;
8
- asChild?: boolean;
9
- }
10
-
11
- export const CopyButton = React.forwardRef<HTMLButtonElement, CopyButtonProps>(
12
- function CopyButton({ value, asChild, children, style, ...props }, ref) {
13
- const [hasCopied, setHasCopied] = React.useState(false);
14
- const Comp = asChild ? Slot : "button";
15
- const timeoutRef = React.useRef<NodeJS.Timeout>();
16
-
17
- const handleClick = async () => {
18
- try {
19
- console.log("Copying text:", value);
20
- await navigator.clipboard.writeText(value);
21
- setHasCopied(true);
22
-
23
- // Clear any existing timeout
24
- if (timeoutRef.current) {
25
- clearTimeout(timeoutRef.current);
26
- }
27
-
28
- // Set new timeout
29
- timeoutRef.current = setTimeout(() => {
30
- setHasCopied(false);
31
- }, 3000);
32
- } catch (err) {
33
- console.error("Failed to copy text:", err);
34
- }
35
- };
36
-
37
- // Cleanup timeout on unmount
38
- React.useEffect(() => {
39
- return () => {
40
- if (timeoutRef.current) {
41
- clearTimeout(timeoutRef.current);
42
- }
43
- };
44
- }, []);
45
-
46
- return (
47
- <Comp onPointerUp={handleClick} ref={ref} {...props}>
48
- {hasCopied ? <CheckIcon /> : <CopyIcon />}
49
- <Slottable>{children}</Slottable>
50
- </Comp>
51
- );
52
- },
53
- );
@@ -1,110 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import {
5
- AlertDialog,
6
- Callout,
7
- Flex,
8
- Text,
9
- VisuallyHidden,
10
- } from "@radix-ui/themes";
11
- import { type ReactNode, useRef } from "react";
12
- import { useDeleteUser } from "./api/user";
13
- import {
14
- AlertDialogContent,
15
- DestructiveButton,
16
- SecondaryButton,
17
- } from "./elements";
18
- import { Member } from "../api";
19
-
20
- interface DeleteUserDialogProps extends AlertDialog.RootProps {
21
- open: boolean;
22
- onOpenChange: (open: boolean) => void;
23
- user: Member;
24
- children?: ReactNode;
25
- }
26
-
27
- export function DeleteUserDialog({
28
- children,
29
- user,
30
- ...props
31
- }: DeleteUserDialogProps) {
32
- const deleteUser = useDeleteUser();
33
- const inputRef = useRef<HTMLInputElement>(null);
34
-
35
- const onSubmitForm = () => {
36
- deleteUser.mutate(
37
- { userId: user.id },
38
- {
39
- onSuccess: () => {
40
- props.onOpenChange(false);
41
- },
42
- },
43
- );
44
- };
45
-
46
- return (
47
- <AlertDialog.Root {...props}>
48
- {children && <AlertDialog.Trigger>{children}</AlertDialog.Trigger>}
49
-
50
- <AlertDialogContent
51
- maxWidth="480px"
52
- onOpenAutoFocus={() => {
53
- requestAnimationFrame(() => {
54
- inputRef.current?.focus();
55
- });
56
- }}
57
- >
58
- <AlertDialog.Title>Remove user</AlertDialog.Title>
59
- <Flex direction="column" gap="3">
60
- <AlertDialog.Description>
61
- Are you sure you want to remove{" "}
62
- <Text weight="bold">{user.email}</Text>? This action is immediate
63
- and cannot be undone.
64
- </AlertDialog.Description>
65
- </Flex>
66
-
67
- {deleteUser.error ? (
68
- <Callout.Root color="red" mt="4" mb="-2">
69
- <Callout.Text>
70
- {getMutationErrorMessage(deleteUser.error)}
71
- </Callout.Text>
72
- </Callout.Root>
73
- ) : null}
74
-
75
- <Flex gap="3" justify="end" mt="5" asChild>
76
- <form
77
- onSubmit={(event) => {
78
- event.preventDefault();
79
- onSubmitForm();
80
- }}
81
- >
82
- <AlertDialog.Cancel>
83
- <SecondaryButton disabled={deleteUser.isPending}>
84
- Cancel
85
- </SecondaryButton>
86
- </AlertDialog.Cancel>
87
-
88
- <DestructiveButton type="submit" loading={deleteUser.isPending}>
89
- Remove
90
- </DestructiveButton>
91
- </form>
92
- </Flex>
93
- {/* mirror errors in a live region */}
94
- <VisuallyHidden asChild>
95
- <section aria-live="polite">
96
- {getMutationErrorMessage(deleteUser.error)}
97
- </section>
98
- </VisuallyHidden>
99
- </AlertDialogContent>
100
- </AlertDialog.Root>
101
- );
102
- }
103
-
104
- function getMutationErrorMessage(error: unknown) {
105
- if (!error) {
106
- return null;
107
- }
108
- // TODO Handle server errors
109
- return "There was an error removing the user. Please try again.";
110
- }
@@ -1,181 +0,0 @@
1
- "use client";
2
-
3
- import { Callout, Dialog, Flex, Text, VisuallyHidden } from "@radix-ui/themes";
4
- import { getMeQueryKey, Me, useUpdateMe } from "../api";
5
- import { useQueryClient } from "@tanstack/react-query";
6
- import * as React from "react";
7
- import { type ReactNode } from "react";
8
- import * as Form from "@radix-ui/react-form";
9
- import {
10
- DialogContent,
11
- Label,
12
- PrimaryButton,
13
- SecondaryButton,
14
- TextField,
15
- } from "./elements";
16
- import { SaveButton } from "./save-button";
17
-
18
- interface EditUserProfileDialogProps extends Dialog.RootProps {
19
- user: Me;
20
- children?: ReactNode;
21
- }
22
-
23
- export function EditUserProfileDialog({
24
- children,
25
- user,
26
- ...props
27
- }: EditUserProfileDialogProps) {
28
- const [open, setOpen] = React.useState(false);
29
-
30
- const handleClose = React.useCallback(() => {
31
- setOpen(false);
32
- }, []);
33
-
34
- return (
35
- <Dialog.Root
36
- {...props}
37
- open={props.open ?? open}
38
- onOpenChange={props.onOpenChange ?? setOpen}
39
- >
40
- <Dialog.Trigger>{children}</Dialog.Trigger>
41
-
42
- <DialogContent maxWidth="480px">
43
- <Content user={user} onClose={handleClose} />
44
- </DialogContent>
45
- </Dialog.Root>
46
- );
47
- }
48
-
49
- interface ContentProps extends Pick<EditUserProfileDialogProps, "user"> {
50
- onClose: () => void;
51
- }
52
-
53
- function Content({ user, onClose }: ContentProps) {
54
- const client = useQueryClient();
55
- const updateMe = useUpdateMe({
56
- mutation: {
57
- onSettled: () => {
58
- client.invalidateQueries({ queryKey: getMeQueryKey() });
59
- },
60
- onSuccess: (newProfile) => {
61
- client.setQueryData(getMeQueryKey(), {
62
- ...user,
63
- firstName: newProfile.firstName || user.firstName,
64
- lastName: newProfile.lastName || user.lastName,
65
- });
66
- },
67
- },
68
- });
69
-
70
- return (
71
- <>
72
- <Dialog.Title mb="5">Edit name</Dialog.Title>
73
- <VisuallyHidden>
74
- <Dialog.Description>
75
- Edit the details of <Text weight="bold">{user.email}</Text>
76
- </Dialog.Description>
77
- </VisuallyHidden>
78
-
79
- {updateMe.error ? (
80
- <Callout.Root color="red" my="-2">
81
- <Callout.Text>{getMutationErrorMessage(updateMe.error)}</Callout.Text>
82
- </Callout.Root>
83
- ) : null}
84
-
85
- <Form.Root
86
- onSubmit={async (event) => {
87
- event.preventDefault();
88
- const formData = new FormData(event.currentTarget);
89
- const firstName = formData.get("firstName")?.toString();
90
- const lastName = formData.get("lastName")?.toString();
91
- updateMe.mutate({
92
- data: {
93
- firstName: firstName ?? undefined,
94
- lastName: lastName ?? undefined,
95
- },
96
- });
97
- }}
98
- >
99
- <Flex my="5" direction="column" gap="4">
100
- <Form.Field name="firstName" asChild>
101
- <Flex direction="column" gap="1">
102
- <Form.Label asChild>
103
- <Label>First name</Label>
104
- </Form.Label>
105
- <Form.Control asChild>
106
- <TextField
107
- data-1p-ignore
108
- autoComplete="given-name"
109
- defaultValue={user.firstName ?? ""}
110
- placeholder="Your first name"
111
- disabled={updateMe.isPending || updateMe.isSuccess}
112
- />
113
- </Form.Control>
114
- </Flex>
115
- </Form.Field>
116
-
117
- <Form.Field name="lastName" asChild>
118
- <Flex direction="column" gap="1">
119
- <Form.Label asChild>
120
- <Label>Last name</Label>
121
- </Form.Label>
122
- <Form.Control asChild>
123
- <TextField
124
- data-1p-ignore
125
- autoComplete="family-name"
126
- defaultValue={user.lastName ?? ""}
127
- placeholder="Your last name"
128
- disabled={updateMe.isPending || updateMe.isSuccess}
129
- />
130
- </Form.Control>
131
- </Flex>
132
- </Form.Field>
133
- </Flex>
134
-
135
- <Flex mt="5" gap="3" justify="end">
136
- <Dialog.Close>
137
- <SecondaryButton
138
- disabled={updateMe.isPending || updateMe.isSuccess}
139
- >
140
- Cancel
141
- </SecondaryButton>
142
- </Dialog.Close>
143
-
144
- <SaveButton
145
- asChild
146
- loading={updateMe.isPending}
147
- done={updateMe.isSuccess}
148
- onDone={onClose}
149
- >
150
- <PrimaryButton>Save</PrimaryButton>
151
- </SaveButton>
152
- </Flex>
153
-
154
- {/* mirror errors in a live region */}
155
- <VisuallyHidden asChild>
156
- <section aria-live="polite">
157
- {getMutationErrorMessage(updateMe.error)}
158
- </section>
159
- </VisuallyHidden>
160
- </Form.Root>
161
- </>
162
- );
163
- }
164
-
165
- function getMutationErrorMessage(error: unknown) {
166
- if (error instanceof Error) {
167
- return error.message;
168
- }
169
-
170
- if (
171
- typeof error === "object" &&
172
- error !== null &&
173
- "message" in error &&
174
- typeof error.message === "string"
175
- ) {
176
- return error.message;
177
- }
178
-
179
- // TODO Handle server errors
180
- return "Something went wrong. Please try again.";
181
- }