@workos-inc/widgets 1.20.2-next.1792439904014 → 1.20.2-next.1792451814155

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/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Changelog
2
2
 
3
- ## 1.20.2-next.1792439904014
3
+ ## 1.20.2-next.1792451814155
4
4
 
5
5
  ### Patch Changes
6
6
 
@@ -12,6 +12,8 @@
12
12
 
13
13
  - Honor `patternFlags` when validating configuration fields in `PipesAdmin`, so uppercase Snowflake account identifiers are accepted.
14
14
 
15
+ - Fixed edge-case bugs with React's concurrent rendering
16
+
15
17
  - Memoize i18n translate function
16
18
 
17
19
  - Update internal GraphQL API calls
@@ -1 +1 @@
1
- {"version":3,"file":"compose-refs.cjs","names":["React"],"sources":["../../../src/lib/compose-refs.ts"],"sourcesContent":["import * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n if (typeof ref === \"function\") {\n return ref(value);\n } else if (ref !== null && ref !== undefined) {\n ref.current = value;\n }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n return (node) => {\n let hasCleanup = false;\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node);\n if (!hasCleanup && typeof cleanup == \"function\") {\n hasCleanup = true;\n }\n return cleanup;\n });\n\n // React <19 will log an error to the console if a callback ref returns a\n // value. We don't use ref cleanups internally so this will only happen if a\n // user's ref callback returns a value, which we only expect if they are\n // using the cleanup functionality added in React 19.\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < cleanups.length; i++) {\n const cleanup = cleanups[i];\n if (typeof cleanup == \"function\") {\n cleanup();\n } else {\n setRef(refs[i], null);\n }\n }\n };\n }\n };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n // eslint-disable-next-line react-hooks/exhaustive-deps\n return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { useComposedRefs };\n"],"mappings":";;;;;;;;;AAQA,SAAS,OAAU,KAAqB,OAAU;CAChD,IAAI,OAAO,QAAQ,YACjB,OAAO,IAAI,KAAK;MACX,IAAI,QAAQ,QAAQ,QAAQ,QACjC,IAAI,UAAU;AAElB;;;;;AAMA,SAAS,YAAe,GAAG,MAA8C;CACvE,QAAQ,SAAS;EACf,IAAI,aAAa;EACjB,MAAM,WAAW,KAAK,KAAK,QAAQ;GACjC,MAAM,UAAU,OAAO,KAAK,IAAI;GAChC,IAAI,CAAC,cAAc,OAAO,WAAW,YACnC,aAAa;GAEf,OAAO;EACT,CAAC;EAMD,IAAI,YACF,aAAa;GACX,KAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;IACxC,MAAM,UAAU,SAAS;IACzB,IAAI,OAAO,WAAW,YACpB,QAAQ;SAER,OAAO,KAAK,IAAI,IAAI;GAExB;EACF;CAEJ;AACF;;;;;AAMA,SAAS,gBAAmB,GAAG,MAA8C;CAE3E,OAAOA,MAAM,YAAY,YAAY,GAAG,IAAI,GAAG,IAAI;AACrD"}
1
+ {"version":3,"file":"compose-refs.cjs","names":["React"],"sources":["../../../src/lib/compose-refs.ts"],"sourcesContent":["import * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n if (typeof ref === \"function\") {\n return ref(value);\n } else if (ref !== null && ref !== undefined) {\n ref.current = value;\n }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n return (node) => {\n let hasCleanup = false;\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node);\n if (!hasCleanup && typeof cleanup == \"function\") {\n hasCleanup = true;\n }\n return cleanup;\n });\n\n // React <19 will log an error to the console if a callback ref returns a\n // value. We don't use ref cleanups internally so this will only happen if a\n // user's ref callback returns a value, which we only expect if they are\n // using the cleanup functionality added in React 19.\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < cleanups.length; i++) {\n const cleanup = cleanups[i];\n if (typeof cleanup == \"function\") {\n cleanup();\n } else {\n setRef(refs[i], null);\n }\n }\n };\n }\n };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { useComposedRefs };\n"],"mappings":";;;;;;;;;AAQA,SAAS,OAAU,KAAqB,OAAU;CAChD,IAAI,OAAO,QAAQ,YACjB,OAAO,IAAI,KAAK;MACX,IAAI,QAAQ,QAAQ,QAAQ,QACjC,IAAI,UAAU;AAElB;;;;;AAMA,SAAS,YAAe,GAAG,MAA8C;CACvE,QAAQ,SAAS;EACf,IAAI,aAAa;EACjB,MAAM,WAAW,KAAK,KAAK,QAAQ;GACjC,MAAM,UAAU,OAAO,KAAK,IAAI;GAChC,IAAI,CAAC,cAAc,OAAO,WAAW,YACnC,aAAa;GAEf,OAAO;EACT,CAAC;EAMD,IAAI,YACF,aAAa;GACX,KAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;IACxC,MAAM,UAAU,SAAS;IACzB,IAAI,OAAO,WAAW,YACpB,QAAQ;SAER,OAAO,KAAK,IAAI,IAAI;GAExB;EACF;CAEJ;AACF;;;;;AAMA,SAAS,gBAAmB,GAAG,MAA8C;CAE3E,OAAOA,MAAM,YAAY,YAAY,GAAG,IAAI,GAAG,IAAI;AACrD"}
@@ -18,11 +18,15 @@ let _radix_ui_themes_components_alert_dialog = require("@radix-ui/themes/compone
18
18
  function ElevatedAccess({ type = "dialog", children, onVerified }) {
19
19
  const { elevatedAccess } = require_api_api_provider.useElevatedAccessToken();
20
20
  const [authenticationChallengeId, setAuthenticationChallengeId] = (0, react.useState)();
21
- const prevAccessToken = (0, react.useRef)(elevatedAccess);
21
+ const [{ prevAccessToken, hasTokenExpired }, setPrevAccessState] = (0, react.useState)({
22
+ prevAccessToken: elevatedAccess,
23
+ hasTokenExpired: false
24
+ });
25
+ if (elevatedAccess !== prevAccessToken) setPrevAccessState({
26
+ prevAccessToken: elevatedAccess,
27
+ hasTokenExpired: hasTokenExpired || !!prevAccessToken && !elevatedAccess
28
+ });
22
29
  const hasCalledOnVerified = (0, react.useRef)(false);
23
- (0, react.useEffect)(() => {
24
- prevAccessToken.current = elevatedAccess;
25
- }, [elevatedAccess]);
26
30
  const handleVerificationSuccess = (0, react.useCallback)((elevatedAccessToken) => {
27
31
  setAuthenticationChallengeId(void 0);
28
32
  if (!hasCalledOnVerified.current) {
@@ -31,16 +35,13 @@ function ElevatedAccess({ type = "dialog", children, onVerified }) {
31
35
  }
32
36
  }, [onVerified]);
33
37
  if (elevatedAccess) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children });
34
- if (!authenticationChallengeId) {
35
- const hasTokenExpired = !!prevAccessToken.current;
36
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SendVerificationEmailForm, {
37
- type,
38
- hasTokenExpired,
39
- onSuccess: (challengeId) => {
40
- setAuthenticationChallengeId(challengeId);
41
- }
42
- });
43
- }
38
+ if (!authenticationChallengeId) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SendVerificationEmailForm, {
39
+ type,
40
+ hasTokenExpired,
41
+ onSuccess: (challengeId) => {
42
+ setAuthenticationChallengeId(challengeId);
43
+ }
44
+ });
44
45
  if (authenticationChallengeId) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VerificationIdentityForm, {
45
46
  type,
46
47
  authenticationChallengeId,
@@ -1 +1 @@
1
- {"version":3,"file":"elevated-access.cjs","names":["useElevatedAccessToken","useState","useRef","useCallback","useMe","useClientGraphqlMutation","expectResultMember","Translation","Text","Callout","Flex","Button"],"sources":["../../../src/lib/elevated-access.tsx"],"sourcesContent":["import { Callout, Flex, Text } from \"@radix-ui/themes\";\nimport type { PropsWithChildren } from \"react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useElevatedAccessToken } from \"../api/api-provider.js\";\nimport { useClientGraphqlMutation } from \"../api/client-graphql/hooks.js\";\nimport { useMe } from \"./api/me.js\";\nimport { expectResultMember } from \"./client-graphql-result.js\";\nimport { AlertDialog, Button, Dialog } from \"./elements.js\";\nimport * as Form from \"./form.js\";\nimport { Translation } from \"./i18n/translation.js\";\nimport * as Otp from \"./otp-input.js\";\n\ninterface ElevatedAccessProps extends PropsWithChildren {\n onVerified?: (elevatedAccessToken: string) => Promise<unknown>;\n type?: \"dialog\" | \"alert\";\n}\n\nexport function ElevatedAccess({\n type = \"dialog\",\n children,\n onVerified,\n}: ElevatedAccessProps) {\n const { elevatedAccess } = useElevatedAccessToken();\n const [authenticationChallengeId, setAuthenticationChallengeId] =\n useState<string>();\n\n const prevAccessToken = useRef(elevatedAccess);\n const hasCalledOnVerified = useRef(false);\n\n useEffect(() => {\n prevAccessToken.current = elevatedAccess;\n }, [elevatedAccess]);\n\n const handleVerificationSuccess = useCallback(\n (elevatedAccessToken: string) => {\n setAuthenticationChallengeId(undefined);\n\n // Only call onVerified on the initial verification, not on\n // re-verifications after token expiry, so side effects (e.g. creating a\n // new TOTP factor) don't re-run and discard the user's progress.\n if (!hasCalledOnVerified.current) {\n hasCalledOnVerified.current = true;\n return onVerified?.(elevatedAccessToken);\n }\n },\n [onVerified],\n );\n\n if (elevatedAccess) {\n return <>{children}</>;\n }\n\n if (!authenticationChallengeId) {\n // FIXME: This should be refactored\n // eslint-disable-next-line react-hooks/refs\n const hasTokenExpired = !!prevAccessToken.current;\n\n return (\n <SendVerificationEmailForm\n type={type}\n hasTokenExpired={hasTokenExpired}\n onSuccess={(challengeId) => {\n setAuthenticationChallengeId(challengeId);\n }}\n />\n );\n }\n\n if (authenticationChallengeId) {\n return (\n <VerificationIdentityForm\n type={type}\n authenticationChallengeId={authenticationChallengeId}\n onSuccess={handleVerificationSuccess}\n />\n );\n }\n\n return null;\n}\n\ninterface SendVerificationEmailFormProps {\n onSuccess: (challengeId: string) => unknown | Promise<unknown>;\n type: \"dialog\" | \"alert\";\n hasTokenExpired: boolean;\n}\n\nfunction SendVerificationEmailForm({\n onSuccess,\n type,\n hasTokenExpired,\n}: SendVerificationEmailFormProps) {\n const { data: me } = useMe();\n const sendVerification = useClientGraphqlMutation({\n mutationFn: async (sdk) =>\n expectResultMember(\n (await sdk.SendVerificationCode()).sendVerificationCode,\n \"VerificationCodeSent\",\n ),\n onSuccess: (data) => onSuccess(data.authenticationChallengeId),\n });\n\n const Title = type === \"dialog\" ? Dialog.Title : AlertDialog.Title;\n const Description =\n type === \"dialog\" ? Dialog.Description : AlertDialog.Description;\n const Close = type === \"dialog\" ? Dialog.Close : AlertDialog.Cancel;\n\n const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n sendVerification.mutate();\n };\n\n return (\n <form onSubmit={handleSubmit}>\n <Title>\n {hasTokenExpired ? (\n <Translation\n defaultMessage=\"Your verification token has expired\"\n id=\"lb/AsC\"\n description=\"Dialog title when verification token has expired\"\n />\n ) : (\n <Translation\n defaultMessage=\"Verify your identity\"\n id=\"7tGCOh\"\n description=\"Dialog title for identity verification\"\n />\n )}\n </Title>\n\n <Description mb=\"5\">\n <Translation\n defaultMessage=\"To continue, we need to confirm your identity. We'll send a temporary verification code to {email}.\"\n id=\"FTUSRM\"\n description=\"Description explaining verification code will be sent\"\n values={{\n email: (\n <Text weight=\"bold\" highContrast>\n {me?.email}\n </Text>\n ),\n }}\n />\n </Description>\n\n {sendVerification.error && (\n <Callout.Root color=\"red\" mt=\"-2\" mb=\"0\">\n <Callout.Text>\n {getMutationErrorMessage(sendVerification.error)}\n </Callout.Text>\n </Callout.Root>\n )}\n\n <Flex justify=\"end\" align=\"center\" gap=\"3\" mt=\"5\">\n <Close>\n <Button\n variant=\"secondary\"\n type=\"button\"\n disabled={sendVerification.isPending}\n >\n <Translation\n defaultMessage=\"Cancel\"\n id=\"hHNj31\"\n description=\"Cancel button text\"\n />\n </Button>\n </Close>\n <Button type=\"submit\" loading={sendVerification.isPending}>\n <Translation\n defaultMessage=\"Send verification code\"\n id=\"XsTeXJ\"\n description=\"Button text to send verification code\"\n />\n </Button>\n </Flex>\n </form>\n );\n}\n\ninterface VerificationIdentityFormProps {\n onSuccess?: (elevatedAccessToken: string) => unknown | Promise<unknown>;\n authenticationChallengeId: string;\n type: \"dialog\" | \"alert\";\n}\n\nfunction VerificationIdentityForm({\n onSuccess,\n authenticationChallengeId,\n type,\n}: VerificationIdentityFormProps) {\n const { data: me } = useMe();\n const { setElevatedAccess } = useElevatedAccessToken();\n const verifyIdentity = useClientGraphqlMutation({\n mutationFn: async (\n sdk,\n input: { code: string; authenticationChallengeId: string },\n ) =>\n expectResultMember(\n (await sdk.VerifyCurrentEmail({ input })).verifyCurrentEmail,\n \"CurrentEmailVerified\",\n ),\n });\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [otpValue, setOtpValue] = useState<string>(\"\");\n\n const Title = type === \"dialog\" ? Dialog.Title : AlertDialog.Title;\n const Description =\n type === \"dialog\" ? Dialog.Description : AlertDialog.Description;\n const Close = type === \"dialog\" ? Dialog.Close : AlertDialog.Cancel;\n const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n const formData = new FormData(event.currentTarget);\n const code = formData.get(\"otp-code\")?.toString() ?? \"\";\n\n setIsSubmitting(true);\n\n try {\n // Mutate async so we can wait for the onSuccess callback as well\n const newAuthState = await verifyIdentity.mutateAsync({\n code,\n authenticationChallengeId,\n });\n\n setElevatedAccess({\n token: newAuthState.elevatedAccessToken,\n expiresAt: newAuthState.expiresAt,\n });\n\n if (onSuccess) {\n await new Promise((resolve) => setTimeout(resolve, 200));\n await onSuccess(newAuthState.elevatedAccessToken);\n }\n } catch (error) {\n console.error(error);\n }\n\n setIsSubmitting(false);\n };\n\n return (\n <Form.Root onSubmit={handleSubmit}>\n <Title>\n <Translation\n defaultMessage=\"Verify your identity\"\n id=\"7tGCOh\"\n description=\"Dialog title for identity verification\"\n />\n </Title>\n\n <Description>\n <Translation\n defaultMessage=\"A verification code was sent to {email}. Please enter it below.\"\n id=\"kvxE0S\"\n description=\"Description explaining where verification code was sent\"\n values={{\n email: (\n <Text weight=\"bold\" highContrast>\n {me?.email}\n </Text>\n ),\n }}\n />\n </Description>\n\n <Flex direction=\"column\" gap=\"2\" mt=\"5\" mx=\"auto\" width=\"fit-content\">\n <Otp.Root\n autoSubmit\n gap=\"2\"\n justify=\"center\"\n columns=\"repeat(6, 48px)\"\n width=\"fit-content\"\n rows=\"48px\"\n name=\"otp-code\"\n readOnly={isSubmitting}\n value={otpValue}\n onValueChange={setOtpValue}\n >\n <Otp.Input required autoFocus autoComplete=\"off\" />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n </Otp.Root>\n\n {verifyIdentity.error && (\n <Text color=\"red\" size=\"2\" as=\"p\">\n {getMutationErrorMessage(verifyIdentity.error)}\n </Text>\n )}\n </Flex>\n\n <Flex justify=\"end\" align=\"center\" gap=\"3\" mt=\"5\">\n <Close>\n <Button variant=\"secondary\" type=\"button\">\n <Translation\n defaultMessage=\"Cancel\"\n id=\"hHNj31\"\n description=\"Cancel button text\"\n />\n </Button>\n </Close>\n <Button type=\"submit\" loading={isSubmitting}>\n <Translation\n defaultMessage=\"Confirm\"\n id=\"EmYENK\"\n description=\"Confirm button text for verification\"\n />\n </Button>\n </Flex>\n </Form.Root>\n );\n}\n\nfunction getMutationErrorMessage(error: unknown) {\n let message = typeof error === \"string\" ? error : \"\";\n\n if (error instanceof Error) {\n message = error.message;\n } else if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof error.message === \"string\"\n ) {\n message = error.message;\n }\n\n if (!message || message === \"Bad Request\") {\n message = \"Invalid code, please try again.\";\n }\n\n return message;\n}\n\n// Note: Error messages in getMutationErrorMessage are kept as plain strings\n// since they are displayed dynamically\n"],"mappings":"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"}
1
+ {"version":3,"file":"elevated-access.cjs","names":["useElevatedAccessToken","useState","useRef","useCallback","useMe","useClientGraphqlMutation","expectResultMember","Translation","Text","Callout","Flex","Button"],"sources":["../../../src/lib/elevated-access.tsx"],"sourcesContent":["import { Callout, Flex, Text } from \"@radix-ui/themes\";\nimport type { PropsWithChildren } from \"react\";\nimport { useCallback, useRef, useState } from \"react\";\nimport { useElevatedAccessToken } from \"../api/api-provider.js\";\nimport { useClientGraphqlMutation } from \"../api/client-graphql/hooks.js\";\nimport { useMe } from \"./api/me.js\";\nimport { expectResultMember } from \"./client-graphql-result.js\";\nimport { AlertDialog, Button, Dialog } from \"./elements.js\";\nimport * as Form from \"./form.js\";\nimport { Translation } from \"./i18n/translation.js\";\nimport * as Otp from \"./otp-input.js\";\n\ninterface ElevatedAccessProps extends PropsWithChildren {\n onVerified?: (elevatedAccessToken: string) => Promise<unknown>;\n type?: \"dialog\" | \"alert\";\n}\n\nexport function ElevatedAccess({\n type = \"dialog\",\n children,\n onVerified,\n}: ElevatedAccessProps) {\n const { elevatedAccess } = useElevatedAccessToken();\n const [authenticationChallengeId, setAuthenticationChallengeId] =\n useState<string>();\n\n const [{ prevAccessToken, hasTokenExpired }, setPrevAccessState] = useState({\n prevAccessToken: elevatedAccess,\n hasTokenExpired: false,\n });\n if (elevatedAccess !== prevAccessToken) {\n setPrevAccessState({\n prevAccessToken: elevatedAccess,\n hasTokenExpired:\n hasTokenExpired || (!!prevAccessToken && !elevatedAccess),\n });\n }\n\n const hasCalledOnVerified = useRef(false);\n\n const handleVerificationSuccess = useCallback(\n (elevatedAccessToken: string) => {\n setAuthenticationChallengeId(undefined);\n\n // Only call onVerified on the initial verification, not on\n // re-verifications after token expiry, so side effects (e.g. creating a\n // new TOTP factor) don't re-run and discard the user's progress.\n if (!hasCalledOnVerified.current) {\n hasCalledOnVerified.current = true;\n return onVerified?.(elevatedAccessToken);\n }\n },\n [onVerified],\n );\n\n if (elevatedAccess) {\n return <>{children}</>;\n }\n\n if (!authenticationChallengeId) {\n return (\n <SendVerificationEmailForm\n type={type}\n hasTokenExpired={hasTokenExpired}\n onSuccess={(challengeId) => {\n setAuthenticationChallengeId(challengeId);\n }}\n />\n );\n }\n\n if (authenticationChallengeId) {\n return (\n <VerificationIdentityForm\n type={type}\n authenticationChallengeId={authenticationChallengeId}\n onSuccess={handleVerificationSuccess}\n />\n );\n }\n\n return null;\n}\n\ninterface SendVerificationEmailFormProps {\n onSuccess: (challengeId: string) => unknown | Promise<unknown>;\n type: \"dialog\" | \"alert\";\n hasTokenExpired: boolean;\n}\n\nfunction SendVerificationEmailForm({\n onSuccess,\n type,\n hasTokenExpired,\n}: SendVerificationEmailFormProps) {\n const { data: me } = useMe();\n const sendVerification = useClientGraphqlMutation({\n mutationFn: async (sdk) =>\n expectResultMember(\n (await sdk.SendVerificationCode()).sendVerificationCode,\n \"VerificationCodeSent\",\n ),\n onSuccess: (data) => onSuccess(data.authenticationChallengeId),\n });\n\n const Title = type === \"dialog\" ? Dialog.Title : AlertDialog.Title;\n const Description =\n type === \"dialog\" ? Dialog.Description : AlertDialog.Description;\n const Close = type === \"dialog\" ? Dialog.Close : AlertDialog.Cancel;\n\n const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n sendVerification.mutate();\n };\n\n return (\n <form onSubmit={handleSubmit}>\n <Title>\n {hasTokenExpired ? (\n <Translation\n defaultMessage=\"Your verification token has expired\"\n id=\"lb/AsC\"\n description=\"Dialog title when verification token has expired\"\n />\n ) : (\n <Translation\n defaultMessage=\"Verify your identity\"\n id=\"7tGCOh\"\n description=\"Dialog title for identity verification\"\n />\n )}\n </Title>\n\n <Description mb=\"5\">\n <Translation\n defaultMessage=\"To continue, we need to confirm your identity. We'll send a temporary verification code to {email}.\"\n id=\"FTUSRM\"\n description=\"Description explaining verification code will be sent\"\n values={{\n email: (\n <Text weight=\"bold\" highContrast>\n {me?.email}\n </Text>\n ),\n }}\n />\n </Description>\n\n {sendVerification.error && (\n <Callout.Root color=\"red\" mt=\"-2\" mb=\"0\">\n <Callout.Text>\n {getMutationErrorMessage(sendVerification.error)}\n </Callout.Text>\n </Callout.Root>\n )}\n\n <Flex justify=\"end\" align=\"center\" gap=\"3\" mt=\"5\">\n <Close>\n <Button\n variant=\"secondary\"\n type=\"button\"\n disabled={sendVerification.isPending}\n >\n <Translation\n defaultMessage=\"Cancel\"\n id=\"hHNj31\"\n description=\"Cancel button text\"\n />\n </Button>\n </Close>\n <Button type=\"submit\" loading={sendVerification.isPending}>\n <Translation\n defaultMessage=\"Send verification code\"\n id=\"XsTeXJ\"\n description=\"Button text to send verification code\"\n />\n </Button>\n </Flex>\n </form>\n );\n}\n\ninterface VerificationIdentityFormProps {\n onSuccess?: (elevatedAccessToken: string) => unknown | Promise<unknown>;\n authenticationChallengeId: string;\n type: \"dialog\" | \"alert\";\n}\n\nfunction VerificationIdentityForm({\n onSuccess,\n authenticationChallengeId,\n type,\n}: VerificationIdentityFormProps) {\n const { data: me } = useMe();\n const { setElevatedAccess } = useElevatedAccessToken();\n const verifyIdentity = useClientGraphqlMutation({\n mutationFn: async (\n sdk,\n input: { code: string; authenticationChallengeId: string },\n ) =>\n expectResultMember(\n (await sdk.VerifyCurrentEmail({ input })).verifyCurrentEmail,\n \"CurrentEmailVerified\",\n ),\n });\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [otpValue, setOtpValue] = useState<string>(\"\");\n\n const Title = type === \"dialog\" ? Dialog.Title : AlertDialog.Title;\n const Description =\n type === \"dialog\" ? Dialog.Description : AlertDialog.Description;\n const Close = type === \"dialog\" ? Dialog.Close : AlertDialog.Cancel;\n const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n const formData = new FormData(event.currentTarget);\n const code = formData.get(\"otp-code\")?.toString() ?? \"\";\n\n setIsSubmitting(true);\n\n try {\n // Mutate async so we can wait for the onSuccess callback as well\n const newAuthState = await verifyIdentity.mutateAsync({\n code,\n authenticationChallengeId,\n });\n\n setElevatedAccess({\n token: newAuthState.elevatedAccessToken,\n expiresAt: newAuthState.expiresAt,\n });\n\n if (onSuccess) {\n await new Promise((resolve) => setTimeout(resolve, 200));\n await onSuccess(newAuthState.elevatedAccessToken);\n }\n } catch (error) {\n console.error(error);\n }\n\n setIsSubmitting(false);\n };\n\n return (\n <Form.Root onSubmit={handleSubmit}>\n <Title>\n <Translation\n defaultMessage=\"Verify your identity\"\n id=\"7tGCOh\"\n description=\"Dialog title for identity verification\"\n />\n </Title>\n\n <Description>\n <Translation\n defaultMessage=\"A verification code was sent to {email}. Please enter it below.\"\n id=\"kvxE0S\"\n description=\"Description explaining where verification code was sent\"\n values={{\n email: (\n <Text weight=\"bold\" highContrast>\n {me?.email}\n </Text>\n ),\n }}\n />\n </Description>\n\n <Flex direction=\"column\" gap=\"2\" mt=\"5\" mx=\"auto\" width=\"fit-content\">\n <Otp.Root\n autoSubmit\n gap=\"2\"\n justify=\"center\"\n columns=\"repeat(6, 48px)\"\n width=\"fit-content\"\n rows=\"48px\"\n name=\"otp-code\"\n readOnly={isSubmitting}\n value={otpValue}\n onValueChange={setOtpValue}\n >\n <Otp.Input required autoFocus autoComplete=\"off\" />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n </Otp.Root>\n\n {verifyIdentity.error && (\n <Text color=\"red\" size=\"2\" as=\"p\">\n {getMutationErrorMessage(verifyIdentity.error)}\n </Text>\n )}\n </Flex>\n\n <Flex justify=\"end\" align=\"center\" gap=\"3\" mt=\"5\">\n <Close>\n <Button variant=\"secondary\" type=\"button\">\n <Translation\n defaultMessage=\"Cancel\"\n id=\"hHNj31\"\n description=\"Cancel button text\"\n />\n </Button>\n </Close>\n <Button type=\"submit\" loading={isSubmitting}>\n <Translation\n defaultMessage=\"Confirm\"\n id=\"EmYENK\"\n description=\"Confirm button text for verification\"\n />\n </Button>\n </Flex>\n </Form.Root>\n );\n}\n\nfunction getMutationErrorMessage(error: unknown) {\n let message = typeof error === \"string\" ? error : \"\";\n\n if (error instanceof Error) {\n message = error.message;\n } else if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof error.message === \"string\"\n ) {\n message = error.message;\n }\n\n if (!message || message === \"Bad Request\") {\n message = \"Invalid code, please try again.\";\n }\n\n return message;\n}\n\n// Note: Error messages in getMutationErrorMessage are kept as plain strings\n// since they are displayed dynamically\n"],"mappings":"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"}
@@ -10,9 +10,11 @@ react = require_runtime.__toESM(react, 1);
10
10
  let react_jsx_runtime = require("react/jsx-runtime");
11
11
  //#region src/lib/otp-input.tsx
12
12
  const OtpContext = react.createContext(void 0);
13
+ OtpContext.displayName = "OtpContext";
14
+ const OtpIndexContext = react.createContext(-1);
15
+ OtpIndexContext.displayName = "OtpIndexContext";
13
16
  const Root = react.forwardRef(function Root({ name, id, value: valueProp, onValueChange, autoSubmit, children, readOnly, state, ...gridProps }, forwardedRef) {
14
17
  const [lastCharIndex, setLastCharIndex] = react.useState(0);
15
- const [allChildrenAdded, setAllChildrenAdded] = react.useState(false);
16
18
  const childCount = react.Children.count(children);
17
19
  const [value, setValueState] = react.useState(() => getValueAsArray(valueProp, childCount));
18
20
  const [previousValueProp, setPreviousValueProp] = react.useState(valueProp);
@@ -36,13 +38,9 @@ const Root = react.forwardRef(function Root({ name, id, value: valueProp, onValu
36
38
  autoSubmit
37
39
  ]);
38
40
  const handleEnterPressed = react.useCallback(() => attemptAutoSubmit(true), [attemptAutoSubmit]);
39
- const handleChildAdd = react.useCallback((input) => {
40
- if (input) {
41
- input.dataset.index = `${childrenRefs.current.length}`;
42
- childrenRefs.current.push(input);
43
- } else childrenRefs.current.pop();
44
- if (childrenRefs.current.length === childCount) setAllChildrenAdded(true);
45
- }, [childCount]);
41
+ const handleChildAdd = react.useCallback((element, index) => {
42
+ childrenRefs.current[index] = element;
43
+ }, []);
46
44
  const handleCharChange = react.useCallback((char, index) => {
47
45
  setValue((previousValue) => {
48
46
  const newValue = [...previousValue];
@@ -55,18 +53,16 @@ const Root = react.forwardRef(function Root({ name, id, value: valueProp, onValu
55
53
  value,
56
54
  readOnly,
57
55
  state,
58
- allChildrenAdded,
59
56
  onEnterPressed: handleEnterPressed,
60
- onChildAdd: handleChildAdd,
61
- onCharChange: handleCharChange
57
+ onCharChange: handleCharChange,
58
+ onChildAdd: handleChildAdd
62
59
  }), [
63
60
  value,
64
- allChildrenAdded,
65
61
  readOnly,
66
62
  state,
67
63
  handleEnterPressed,
68
- handleChildAdd,
69
- handleCharChange
64
+ handleCharChange,
65
+ handleChildAdd
70
66
  ]);
71
67
  react.useEffect(attemptAutoSubmit, [attemptAutoSubmit]);
72
68
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OtpContext.Provider, {
@@ -83,7 +79,10 @@ const Root = react.forwardRef(function Root({ name, id, value: valueProp, onValu
83
79
  const index = Math.min(sanitizedValue.length, childCount - 1);
84
80
  childrenRefs.current?.[index]?.focus();
85
81
  },
86
- children: [children, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
82
+ children: [react.Children.map(children, (child, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OtpIndexContext.Provider, {
83
+ value: index,
84
+ children: child
85
+ })), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
87
86
  ref: require_lib_compose_refs.useComposedRefs(forwardedRef, hiddenInputRef),
88
87
  defaultValue: value.join(""),
89
88
  minLength: childCount,
@@ -95,9 +94,12 @@ const Root = react.forwardRef(function Root({ name, id, value: valueProp, onValu
95
94
  });
96
95
  const Input = react.forwardRef(function Input({ style, readOnly, autoComplete = "off", ...props }, forwardedRef) {
97
96
  const otpContext = useOptContext();
98
- const inputRef = react.useRef(null);
99
- const composedInputRef = require_lib_compose_refs.useComposedRefs(forwardedRef, inputRef, otpContext.onChildAdd);
100
- const index = Number(inputRef.current?.dataset.index ?? -1);
97
+ const { onChildAdd } = otpContext;
98
+ const index = react.useContext(OtpIndexContext);
99
+ const inputRef = react.useCallback((input) => {
100
+ onChildAdd(input, index);
101
+ }, [onChildAdd, index]);
102
+ const composedInputRef = require_lib_compose_refs.useComposedRefs(forwardedRef, inputRef);
101
103
  const char = otpContext.value[index] ?? "";
102
104
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_lib_form.Field, {
103
105
  name: `otp-${index}`,
@@ -124,7 +126,6 @@ const Input = react.forwardRef(function Input({ style, readOnly, autoComplete =
124
126
  onChange: (event) => {
125
127
  if (event.target.validity.valid || event.target.value === "") {
126
128
  const char = event.target.value;
127
- const index = Number(event.target.dataset.index ?? -1);
128
129
  otpContext.onCharChange(char, index);
129
130
  focusSibling(event.currentTarget, { back: char === "" });
130
131
  }
@@ -1 +1 @@
1
- {"version":3,"file":"otp-input.cjs","names":["React","Grid","useComposedRefs","TextField"],"sources":["../../../src/lib/otp-input.tsx"],"sourcesContent":["\"use client\";\nimport { Grid } from \"@radix-ui/themes\";\nimport * as React from \"react\";\nimport { useComposedRefs } from \"./compose-refs.js\";\nimport { TextField } from \"./elements.js\";\nimport * as Form from \"./form.js\";\n\ninterface OptContextType {\n value: string[];\n readOnly?: boolean;\n state?: \"valid\" | \"invalid\";\n onEnterPressed: () => void;\n onChildAdd: (input: HTMLInputElement) => void;\n onCharChange: (char: string, index: number) => void;\n allChildrenAdded: boolean;\n}\n\nconst OtpContext = React.createContext<OptContextType | undefined>(undefined);\n\ntype OtpRootProps = React.ComponentPropsWithoutRef<typeof Grid> & {\n id?: string;\n name?: string;\n readOnly?: boolean;\n state?: \"valid\" | \"invalid\";\n value: string;\n onValueChange: React.Dispatch<React.SetStateAction<string>>;\n autoSubmit?: boolean;\n};\n\nexport const Root = React.forwardRef<HTMLInputElement, OtpRootProps>(\n function Root(\n {\n name,\n id,\n value: valueProp,\n onValueChange,\n autoSubmit,\n children,\n readOnly,\n state,\n ...gridProps\n },\n forwardedRef,\n ) {\n const [lastCharIndex, setLastCharIndex] = React.useState<number>(0);\n const [allChildrenAdded, setAllChildrenAdded] =\n React.useState<boolean>(false);\n const childCount = React.Children.count(children);\n\n // Internal array state is the source of truth so that empty slots between\n // filled digits are preserved. A `value` string can't encode a gap (e.g.\n // `[\"1\", \"\", \"3\"]` and `[\"1\", \"3\"]` both serialize to \"13\"), so deriving\n // state straight from the prop would shift digits whenever an inner box is\n // cleared or filled out of order.\n const [value, setValueState] = React.useState<string[]>(() =>\n getValueAsArray(valueProp, childCount),\n );\n\n const [previousValueProp, setPreviousValueProp] = React.useState(valueProp);\n if (previousValueProp !== valueProp) {\n setPreviousValueProp(valueProp);\n if (valueProp !== value.join(\"\")) {\n setValueState(getValueAsArray(valueProp, childCount));\n }\n }\n\n const setValue = React.useCallback(\n (action: React.SetStateAction<string[]>) => {\n const nextArrayValue =\n typeof action === \"function\" ? action(value) : action;\n setValueState(nextArrayValue);\n onValueChange(nextArrayValue.join(\"\"));\n },\n [value, onValueChange],\n );\n\n const hiddenInputRef = React.useRef<HTMLInputElement>(null);\n const childrenRefs = React.useRef<HTMLInputElement[]>([]);\n\n const attemptAutoSubmit = React.useCallback(\n (enterPressed = false) => {\n if (\n autoSubmit &&\n value.length > 0 &&\n value.every((char) => char !== \"\") &&\n (enterPressed || lastCharIndex + 1 === childCount)\n ) {\n hiddenInputRef.current?.form?.requestSubmit();\n }\n },\n [value, childCount, lastCharIndex, autoSubmit],\n );\n\n const handleEnterPressed = React.useCallback(\n () => attemptAutoSubmit(true),\n [attemptAutoSubmit],\n );\n\n const handleChildAdd = React.useCallback(\n (input: HTMLInputElement) => {\n if (input) {\n input.dataset.index = `${childrenRefs.current.length}`;\n childrenRefs.current.push(input);\n } else {\n childrenRefs.current.pop();\n }\n\n if (childrenRefs.current.length === childCount) {\n setAllChildrenAdded(true);\n }\n },\n [childCount],\n );\n\n const handleCharChange = React.useCallback(\n (char: string, index: number) => {\n setValue((previousValue) => {\n const newValue = [...previousValue];\n newValue[index] = char;\n return newValue;\n });\n setLastCharIndex(index);\n },\n [setValue],\n );\n\n const otpContext = React.useMemo(\n () => ({\n value,\n readOnly,\n state,\n allChildrenAdded,\n onEnterPressed: handleEnterPressed,\n onChildAdd: handleChildAdd,\n onCharChange: handleCharChange,\n }),\n [\n value,\n allChildrenAdded,\n readOnly,\n state,\n handleEnterPressed,\n handleChildAdd,\n handleCharChange,\n ],\n );\n\n React.useEffect(attemptAutoSubmit, [attemptAutoSubmit]);\n\n return (\n <OtpContext.Provider value={otpContext}>\n <Grid\n columns={`repeat(${childCount}, 1fr)`}\n {...gridProps}\n onPaste={(event: React.ClipboardEvent<HTMLDivElement>) => {\n event.preventDefault();\n const pastedValue = event.clipboardData.getData(\"Text\");\n const sanitizedValue = pastedValue\n .replace(/[^\\d]/g, \"\")\n .slice(0, childCount);\n const value = sanitizedValue\n .padEnd(childCount, \"#\")\n .split(\"\")\n .map((char) => (char === \"#\" ? \"\" : char));\n\n setValue(value);\n setLastCharIndex(sanitizedValue.length - 1);\n\n const index = Math.min(sanitizedValue.length, childCount - 1);\n childrenRefs.current?.[index]?.focus();\n }}\n >\n {children}\n <input\n ref={useComposedRefs(forwardedRef, hiddenInputRef)}\n defaultValue={value.join(\"\")}\n minLength={childCount}\n name={name}\n type=\"hidden\"\n />\n </Grid>\n </OtpContext.Provider>\n );\n },\n);\n\ninterface InputProps extends React.ComponentProps<typeof TextField> {\n autoComplete?: \"one-time-code\" | \"off\";\n}\n\nexport const Input = React.forwardRef<HTMLInputElement, InputProps>(\n function Input(\n { style, readOnly, autoComplete = \"off\", ...props },\n forwardedRef,\n ) {\n const otpContext = useOptContext();\n const inputRef = React.useRef<HTMLInputElement>(null);\n const composedInputRef = useComposedRefs(\n forwardedRef,\n inputRef,\n otpContext.onChildAdd,\n );\n\n // FIXME: This should be refactored\n // eslint-disable-next-line react-hooks/refs\n const index = Number(inputRef.current?.dataset.index ?? -1);\n const char = otpContext.value[index] ?? \"\";\n\n return (\n <Form.Field name={`otp-${index}`} asChild>\n <Form.Control asChild>\n <TextField\n ref={composedInputRef}\n autoComplete={index === 0 ? autoComplete : \"off\"}\n color={otpContext.state === \"invalid\" ? \"red\" : undefined}\n inputMode=\"numeric\"\n maxLength={1}\n pattern=\"\\d{1}\"\n readOnly={readOnly ?? otpContext.readOnly}\n size=\"3\"\n value={char}\n variant={otpContext.state === \"invalid\" ? \"soft\" : undefined}\n style={{\n ...style,\n height: \"auto\",\n \"--text-field-padding\": 0,\n textAlign: \"center\",\n }}\n onChange={(event) => {\n // Update the value if it matches the input pattern (number only) or is empty (backspace)\n if (event.target.validity.valid || event.target.value === \"\") {\n const char = event.target.value;\n const index = Number(event.target.dataset.index ?? -1);\n otpContext.onCharChange(char, index);\n focusSibling(event.currentTarget, { back: char === \"\" });\n }\n }}\n onKeyDown={(event) => {\n if (event.key === \"ArrowLeft\") {\n focusSibling(event.currentTarget, { back: true });\n event.preventDefault();\n } else if (event.key === \"ArrowRight\") {\n focusSibling(event.currentTarget);\n event.preventDefault();\n } else if (event.key === \"Backspace\" && char === \"\") {\n focusSibling(event.currentTarget, { back: true });\n } else if (event.key === \"Enter\" && char !== \"\") {\n otpContext.onEnterPressed();\n }\n }}\n {...props}\n />\n </Form.Control>\n </Form.Field>\n );\n },\n);\n\nconst useOptContext = () => {\n const optContext = React.useContext(OtpContext);\n\n if (!optContext) {\n throw new Error(\n \"OtpInput compound components cannot be rendered outside the OtpRoot component\",\n );\n }\n\n return optContext;\n};\n\nfunction focusSibling(input: HTMLInputElement, { back = false } = {}) {\n const sibling = back\n ? input.parentElement?.previousSibling\n : input.parentElement?.nextSibling;\n const siblingInput = sibling?.firstChild;\n if (siblingInput && siblingInput instanceof HTMLInputElement) {\n siblingInput?.focus();\n siblingInput?.select();\n }\n}\n\nconst getValueAsArray = (value: string | undefined, length: number) => {\n if (!value) {\n return createEmptyArray(length);\n }\n\n return createEmptyArray(length).map((_, index) => value?.[index] ?? \"\");\n};\n\nconst createEmptyArray = (length: number): string[] =>\n Array.from<string>({ length }).fill(\"\");\n"],"mappings":"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"}
1
+ {"version":3,"file":"otp-input.cjs","names":["React","Grid","useComposedRefs","TextField"],"sources":["../../../src/lib/otp-input.tsx"],"sourcesContent":["\"use client\";\nimport { Grid } from \"@radix-ui/themes\";\nimport * as React from \"react\";\nimport { useComposedRefs } from \"./compose-refs.js\";\nimport { TextField } from \"./elements.js\";\nimport * as Form from \"./form.js\";\n\ninterface OptContextType {\n value: string[];\n readOnly?: boolean;\n state?: \"valid\" | \"invalid\";\n onEnterPressed: () => void;\n onCharChange: (char: string, index: number) => void;\n onChildAdd: (element: HTMLInputElement | null, index: number) => void;\n}\n\nconst OtpContext = React.createContext<OptContextType | undefined>(undefined);\nOtpContext.displayName = \"OtpContext\";\n\nconst OtpIndexContext = React.createContext<number>(-1);\nOtpIndexContext.displayName = \"OtpIndexContext\";\n\ntype OtpRootProps = React.ComponentPropsWithoutRef<typeof Grid> & {\n id?: string;\n name?: string;\n readOnly?: boolean;\n state?: \"valid\" | \"invalid\";\n value: string;\n onValueChange: React.Dispatch<React.SetStateAction<string>>;\n autoSubmit?: boolean;\n};\n\nexport const Root = React.forwardRef<HTMLInputElement, OtpRootProps>(\n function Root(\n {\n name,\n id,\n value: valueProp,\n onValueChange,\n autoSubmit,\n children,\n readOnly,\n state,\n ...gridProps\n },\n forwardedRef,\n ) {\n const [lastCharIndex, setLastCharIndex] = React.useState<number>(0);\n const childCount = React.Children.count(children);\n\n // Internal array state is the source of truth so that empty slots between\n // filled digits are preserved. A `value` string can't encode a gap (e.g.\n // `[\"1\", \"\", \"3\"]` and `[\"1\", \"3\"]` both serialize to \"13\"), so deriving\n // state straight from the prop would shift digits whenever an inner box is\n // cleared or filled out of order.\n const [value, setValueState] = React.useState<string[]>(() =>\n getValueAsArray(valueProp, childCount),\n );\n\n const [previousValueProp, setPreviousValueProp] = React.useState(valueProp);\n if (previousValueProp !== valueProp) {\n setPreviousValueProp(valueProp);\n if (valueProp !== value.join(\"\")) {\n setValueState(getValueAsArray(valueProp, childCount));\n }\n }\n\n const setValue = React.useCallback(\n (action: React.SetStateAction<string[]>) => {\n const nextArrayValue =\n typeof action === \"function\" ? action(value) : action;\n setValueState(nextArrayValue);\n onValueChange(nextArrayValue.join(\"\"));\n },\n [value, onValueChange],\n );\n\n const hiddenInputRef = React.useRef<HTMLInputElement>(null);\n const childrenRefs = React.useRef<(HTMLInputElement | null)[]>([]);\n\n const attemptAutoSubmit = React.useCallback(\n (enterPressed = false) => {\n if (\n autoSubmit &&\n value.length > 0 &&\n value.every((char) => char !== \"\") &&\n (enterPressed || lastCharIndex + 1 === childCount)\n ) {\n hiddenInputRef.current?.form?.requestSubmit();\n }\n },\n [value, childCount, lastCharIndex, autoSubmit],\n );\n\n const handleEnterPressed = React.useCallback(\n () => attemptAutoSubmit(true),\n [attemptAutoSubmit],\n );\n\n const handleChildAdd = React.useCallback(\n (element: HTMLInputElement | null, index: number) => {\n childrenRefs.current[index] = element;\n },\n [],\n );\n\n const handleCharChange = React.useCallback(\n (char: string, index: number) => {\n setValue((previousValue) => {\n const newValue = [...previousValue];\n newValue[index] = char;\n return newValue;\n });\n setLastCharIndex(index);\n },\n [setValue],\n );\n\n const otpContext = React.useMemo<OptContextType>(\n () =>\n ({\n value,\n readOnly,\n state,\n onEnterPressed: handleEnterPressed,\n onCharChange: handleCharChange,\n onChildAdd: handleChildAdd,\n }) satisfies OptContextType,\n [\n value,\n readOnly,\n state,\n handleEnterPressed,\n handleCharChange,\n handleChildAdd,\n ],\n );\n\n React.useEffect(attemptAutoSubmit, [attemptAutoSubmit]);\n\n return (\n <OtpContext.Provider value={otpContext}>\n <Grid\n columns={`repeat(${childCount}, 1fr)`}\n {...gridProps}\n onPaste={(event: React.ClipboardEvent<HTMLDivElement>) => {\n event.preventDefault();\n const pastedValue = event.clipboardData.getData(\"Text\");\n const sanitizedValue = pastedValue\n .replace(/[^\\d]/g, \"\")\n .slice(0, childCount);\n const value = sanitizedValue\n .padEnd(childCount, \"#\")\n .split(\"\")\n .map((char) => (char === \"#\" ? \"\" : char));\n\n setValue(value);\n setLastCharIndex(sanitizedValue.length - 1);\n\n const index = Math.min(sanitizedValue.length, childCount - 1);\n childrenRefs.current?.[index]?.focus();\n }}\n >\n {React.Children.map(children, (child, index) => (\n <OtpIndexContext.Provider value={index}>\n {child}\n </OtpIndexContext.Provider>\n ))}\n <input\n ref={useComposedRefs(forwardedRef, hiddenInputRef)}\n defaultValue={value.join(\"\")}\n minLength={childCount}\n name={name}\n type=\"hidden\"\n />\n </Grid>\n </OtpContext.Provider>\n );\n },\n);\n\ninterface InputProps extends React.ComponentProps<typeof TextField> {\n autoComplete?: \"one-time-code\" | \"off\";\n}\n\nexport const Input = React.forwardRef<HTMLInputElement, InputProps>(\n function Input(\n { style, readOnly, autoComplete = \"off\", ...props },\n forwardedRef,\n ) {\n const otpContext = useOptContext();\n const { onChildAdd } = otpContext;\n const index = React.useContext(OtpIndexContext);\n const inputRef = React.useCallback(\n (input: HTMLInputElement | null) => {\n onChildAdd(input, index);\n },\n [onChildAdd, index],\n );\n const composedInputRef = useComposedRefs(forwardedRef, inputRef);\n\n const char = otpContext.value[index] ?? \"\";\n\n return (\n <Form.Field name={`otp-${index}`} asChild>\n <Form.Control asChild>\n <TextField\n ref={composedInputRef}\n autoComplete={index === 0 ? autoComplete : \"off\"}\n color={otpContext.state === \"invalid\" ? \"red\" : undefined}\n inputMode=\"numeric\"\n maxLength={1}\n pattern=\"\\d{1}\"\n readOnly={readOnly ?? otpContext.readOnly}\n size=\"3\"\n value={char}\n variant={otpContext.state === \"invalid\" ? \"soft\" : undefined}\n style={{\n ...style,\n height: \"auto\",\n \"--text-field-padding\": 0,\n textAlign: \"center\",\n }}\n onChange={(event) => {\n // Update the value if it matches the input pattern (number only) or is empty (backspace)\n if (event.target.validity.valid || event.target.value === \"\") {\n const char = event.target.value;\n otpContext.onCharChange(char, index);\n focusSibling(event.currentTarget, { back: char === \"\" });\n }\n }}\n onKeyDown={(event) => {\n if (event.key === \"ArrowLeft\") {\n focusSibling(event.currentTarget, { back: true });\n event.preventDefault();\n } else if (event.key === \"ArrowRight\") {\n focusSibling(event.currentTarget);\n event.preventDefault();\n } else if (event.key === \"Backspace\" && char === \"\") {\n focusSibling(event.currentTarget, { back: true });\n } else if (event.key === \"Enter\" && char !== \"\") {\n otpContext.onEnterPressed();\n }\n }}\n {...props}\n />\n </Form.Control>\n </Form.Field>\n );\n },\n);\n\nconst useOptContext = () => {\n const optContext = React.useContext(OtpContext);\n if (!optContext) {\n throw new Error(\n \"OtpInput compound components cannot be rendered outside the OtpRoot component\",\n );\n }\n\n return optContext;\n};\n\nfunction focusSibling(input: HTMLInputElement, { back = false } = {}) {\n const sibling = back\n ? input.parentElement?.previousSibling\n : input.parentElement?.nextSibling;\n const siblingInput = sibling?.firstChild;\n if (siblingInput && siblingInput instanceof HTMLInputElement) {\n siblingInput?.focus();\n siblingInput?.select();\n }\n}\n\nconst getValueAsArray = (value: string | undefined, length: number) => {\n if (!value) {\n return createEmptyArray(length);\n }\n\n return createEmptyArray(length).map((_, index) => value?.[index] ?? \"\");\n};\n\nconst createEmptyArray = (length: number): string[] =>\n Array.from<string>({ length }).fill(\"\");\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"otp-input.d.cts","names":["TextField"],"sources":["../../../src/lib/otp-input.tsx"],"mappings":";;;;KAmBK,eAAe,MAAM,gCAAgC;EACxD;EACA;EACA;EACA;EACA;EACA,eAAe,MAAM,SAAS,MAAM;EACpC;;qBAGW,MAAI,MAAA,0BAAA,eAAA,MAAA,cAAA;UA6JP,mBAAmB,MAAM,sBAAsBA;EACvD;;qBAGW,OAAK,MAAA,0BAAA,KAAA,qBAAA,MAAA,cAAA"}
1
+ {"version":3,"file":"otp-input.d.cts","names":["TextField"],"sources":["../../../src/lib/otp-input.tsx"],"mappings":";;;;KAsBK,eAAe,MAAM,gCAAgC;EACxD;EACA;EACA;EACA;EACA;EACA,eAAe,MAAM,SAAS,MAAM;EACpC;;qBAGW,MAAI,MAAA,0BAAA,eAAA,MAAA,cAAA;UAqJP,mBAAmB,MAAM,sBAAsBA;EACvD;;qBAGW,OAAK,MAAA,0BAAA,KAAA,qBAAA,MAAA,cAAA"}
@@ -10,7 +10,13 @@ let _radix_ui_react_icons = require("@radix-ui/react-icons");
10
10
  const DONE_TIMEOUT_MS = 1500;
11
11
  const SAVING_TIMEOUT_MS = 600;
12
12
  function SaveButton({ asChild = false, children, loading, done, onDone, ...props }) {
13
- const [state, setState] = (0, react.useState)(loading ? "loading" : done ? "done" : "idle");
13
+ const [minElapsed, setMinElapsed] = (0, react.useState)(!!done);
14
+ const [prevLoading, setPrevLoading] = (0, react.useState)(loading);
15
+ if (loading !== prevLoading) {
16
+ setPrevLoading(loading);
17
+ if (loading) setMinElapsed(false);
18
+ }
19
+ const state = loading ? "loading" : done ? minElapsed ? "done" : "loading" : "idle";
14
20
  const loadingStartTime = (0, react.useRef)(null);
15
21
  const onDoneRef = (0, react.useRef)(onDone);
16
22
  const ButtonComponent = asChild ? _radix_ui_themes.Slot : require_lib_elements.Button;
@@ -19,24 +25,23 @@ function SaveButton({ asChild = false, children, loading, done, onDone, ...props
19
25
  }, [onDone]);
20
26
  (0, react.useEffect)(() => {
21
27
  if (loading) {
22
- setState("loading");
23
28
  loadingStartTime.current = Date.now();
24
- } else if (done) {
25
- const currentTime = Date.now();
26
- const loadingDuration = loadingStartTime.current ? currentTime - loadingStartTime.current : 0;
27
- const remainingDelay = Math.max(0, SAVING_TIMEOUT_MS - loadingDuration);
28
- let doneTimer = null;
29
- const savedTimer = window.setTimeout(() => {
30
- setState("done");
31
- doneTimer = window.setTimeout(() => {
32
- onDoneRef.current?.();
33
- }, DONE_TIMEOUT_MS);
34
- }, remainingDelay);
35
- return () => {
36
- window.clearTimeout(savedTimer);
37
- if (doneTimer !== null) window.clearTimeout(doneTimer);
38
- };
39
- } else if (!loading && !done) setState("idle");
29
+ return;
30
+ }
31
+ if (!done) return;
32
+ const loadingDuration = loadingStartTime.current ? Date.now() - loadingStartTime.current : 0;
33
+ const remainingDelay = Math.max(0, SAVING_TIMEOUT_MS - loadingDuration);
34
+ let doneTimer = null;
35
+ const minElapsedTimer = window.setTimeout(() => {
36
+ setMinElapsed(true);
37
+ doneTimer = window.setTimeout(() => {
38
+ onDoneRef.current?.();
39
+ }, DONE_TIMEOUT_MS);
40
+ }, remainingDelay);
41
+ return () => {
42
+ window.clearTimeout(minElapsedTimer);
43
+ if (doneTimer !== null) window.clearTimeout(doneTimer);
44
+ };
40
45
  }, [loading, done]);
41
46
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ButtonComponent, {
42
47
  ...props,
@@ -1 +1 @@
1
- {"version":3,"file":"save-button.cjs","names":["useState","useRef","Slot","Button","namespaceClassNames","Slottable","Flex","Spinner","CheckIcon","Text","Translation"],"sources":["../../../src/lib/save-button.tsx"],"sourcesContent":["import { CheckIcon } from \"@radix-ui/react-icons\";\nimport { Flex, Slot, Slottable, Spinner, Text } from \"@radix-ui/themes\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { Button } from \"./elements.js\";\nimport type { ButtonProps } from \"./elements.js\";\nimport { Translation } from \"./i18n/translation.js\";\nimport { namespaceClassNames } from \"./widget-dom-props.js\";\n\nconst DONE_TIMEOUT_MS = 1500;\nconst SAVING_TIMEOUT_MS = 600;\n\ninterface SaveButtonProps extends ButtonProps {\n asChild?: boolean;\n children: React.ReactNode;\n loading?: boolean;\n done?: boolean;\n onDone?: () => void;\n}\n\nexport function SaveButton({\n asChild = false,\n children,\n loading,\n done,\n onDone,\n ...props\n}: SaveButtonProps) {\n const [state, setState] = useState<\"idle\" | \"loading\" | \"done\">(\n loading ? \"loading\" : done ? \"done\" : \"idle\",\n );\n const loadingStartTime = useRef<number | null>(null);\n const onDoneRef = useRef(onDone);\n const ButtonComponent = asChild ? Slot : Button;\n\n useEffect(() => {\n onDoneRef.current = onDone;\n }, [onDone]);\n\n useEffect(() => {\n if (loading) {\n // FIXME: This should be refactored\n // eslint-disable-next-line react-hooks/set-state-in-effect\n setState(\"loading\");\n loadingStartTime.current = Date.now();\n } else if (done) {\n const currentTime = Date.now();\n const loadingDuration = loadingStartTime.current\n ? currentTime - loadingStartTime.current\n : 0;\n\n // If loading lasted less than 500 ms, wait for the remaining time\n const remainingDelay = Math.max(0, SAVING_TIMEOUT_MS - loadingDuration);\n\n let doneTimer: number | null = null;\n const savedTimer = window.setTimeout(() => {\n setState(\"done\");\n\n doneTimer = window.setTimeout(() => {\n onDoneRef.current?.();\n }, DONE_TIMEOUT_MS);\n }, remainingDelay);\n\n return () => {\n window.clearTimeout(savedTimer);\n if (doneTimer !== null) {\n window.clearTimeout(doneTimer);\n }\n };\n } else if (!loading && !done) {\n setState(\"idle\");\n }\n }, [loading, done]);\n\n return (\n <ButtonComponent\n {...props}\n className={namespaceClassNames(\"save-button\")}\n data-save-state={state}\n >\n <Slottable>{children}</Slottable>\n {state === \"loading\" && (\n <Flex\n as=\"span\"\n align=\"center\"\n justify=\"center\"\n position=\"absolute\"\n inset=\"0\"\n >\n <Spinner size=\"1\" />\n </Flex>\n )}\n\n {state === \"done\" && (\n <Flex\n as=\"span\"\n align=\"center\"\n justify=\"center\"\n position=\"absolute\"\n inset=\"0\"\n gap=\"1\"\n >\n <CheckIcon\n width=\"18px\"\n height=\"18px\"\n style={{ marginLeft: \"-4px\" }}\n className={namespaceClassNames(\"save-button__done-icon\")}\n />\n <Text className={namespaceClassNames(\"save-button__done-text\")}>\n <Translation\n defaultMessage=\"Done\"\n id=\"wjt0K3\"\n description=\"Button label indicating an action has completed successfully\"\n />\n </Text>\n </Flex>\n )}\n </ButtonComponent>\n );\n}\n"],"mappings":";;;;;;;;;AAQA,MAAM,kBAAkB;AACxB,MAAM,oBAAoB;AAU1B,SAAgB,WAAW,EACzB,UAAU,OACV,UACA,SACA,MACA,QACA,GAAG,SACe;CAClB,MAAM,CAAC,OAAO,aAAA,GAAYA,MAAAA,SAAAA,CACxB,UAAU,YAAY,OAAO,SAAS,MACxC;CACA,MAAM,oBAAA,GAAmBC,MAAAA,OAAAA,CAAsB,IAAI;CACnD,MAAM,aAAA,GAAYA,MAAAA,OAAAA,CAAO,MAAM;CAC/B,MAAM,kBAAkB,UAAUC,iBAAAA,OAAOC,qBAAAA;CAEzC,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,UAAU,UAAU;CACtB,GAAG,CAAC,MAAM,CAAC;CAEX,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,IAAI,SAAS;GAGX,SAAS,SAAS;GAClB,iBAAiB,UAAU,KAAK,IAAI;EACtC,OAAO,IAAI,MAAM;GACf,MAAM,cAAc,KAAK,IAAI;GAC7B,MAAM,kBAAkB,iBAAiB,UACrC,cAAc,iBAAiB,UAC/B;GAGJ,MAAM,iBAAiB,KAAK,IAAI,GAAG,oBAAoB,eAAe;GAEtE,IAAI,YAA2B;GAC/B,MAAM,aAAa,OAAO,iBAAiB;IACzC,SAAS,MAAM;IAEf,YAAY,OAAO,iBAAiB;KAClC,UAAU,UAAU;IACtB,GAAG,eAAe;GACpB,GAAG,cAAc;GAEjB,aAAa;IACX,OAAO,aAAa,UAAU;IAC9B,IAAI,cAAc,MAChB,OAAO,aAAa,SAAS;GAEjC;EACF,OAAO,IAAI,CAAC,WAAW,CAAC,MACtB,SAAS,MAAM;CAEnB,GAAG,CAAC,SAAS,IAAI,CAAC;CAElB,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,iBAAD;EACE,GAAI;EACJ,WAAWC,6BAAAA,oBAAoB,aAAa;EAC5C,mBAAiB;EAHnB,UAAA;GAKE,iBAAA,GAAA,kBAAA,IAAA,CAACC,iBAAAA,WAAD,EAAY,SAAoB,CAAA;GAC/B,UAAU,aACT,iBAAA,GAAA,kBAAA,IAAA,CAACC,iBAAAA,MAAD;IACE,IAAG;IACH,OAAM;IACN,SAAQ;IACR,UAAS;IACT,OAAM;IAEN,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAACC,iBAAAA,SAAD,EAAS,MAAK,IAAK,CAAA;GACf,CAAA;GAGP,UAAU,UACT,iBAAA,GAAA,kBAAA,KAAA,CAACD,iBAAAA,MAAD;IACE,IAAG;IACH,OAAM;IACN,SAAQ;IACR,UAAS;IACT,OAAM;IACN,KAAI;IANN,UAAA,CAQE,iBAAA,GAAA,kBAAA,IAAA,CAACE,sBAAAA,WAAD;KACE,OAAM;KACN,QAAO;KACP,OAAO,EAAE,YAAY,OAAO;KAC5B,WAAWJ,6BAAAA,oBAAoB,wBAAwB;IACxD,CAAA,GACD,iBAAA,GAAA,kBAAA,IAAA,CAACK,iBAAAA,MAAD;KAAM,WAAWL,6BAAAA,oBAAoB,wBAAwB;KAC3D,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAACM,6BAAAA,aAAD;MACE,gBAAe;MACf,IAAG;MACH,aAAY;KACb,CAAA;IACG,CAAA,CACF;;EAEO;;AAErB"}
1
+ {"version":3,"file":"save-button.cjs","names":["useState","useRef","Slot","Button","namespaceClassNames","Slottable","Flex","Spinner","CheckIcon","Text","Translation"],"sources":["../../../src/lib/save-button.tsx"],"sourcesContent":["import { CheckIcon } from \"@radix-ui/react-icons\";\nimport { Flex, Slot, Slottable, Spinner, Text } from \"@radix-ui/themes\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { Button } from \"./elements.js\";\nimport type { ButtonProps } from \"./elements.js\";\nimport { Translation } from \"./i18n/translation.js\";\nimport { namespaceClassNames } from \"./widget-dom-props.js\";\n\nconst DONE_TIMEOUT_MS = 1500;\nconst SAVING_TIMEOUT_MS = 600;\n\ninterface SaveButtonProps extends ButtonProps {\n asChild?: boolean;\n children: React.ReactNode;\n loading?: boolean;\n done?: boolean;\n onDone?: () => void;\n}\n\nexport function SaveButton({\n asChild = false,\n children,\n loading,\n done,\n onDone,\n ...props\n}: SaveButtonProps) {\n // Whether the spinner has been visible for at least SAVING_TIMEOUT_MS, so a\n // fast response doesn't flash it for a single frame.\n const [minElapsed, setMinElapsed] = useState(!!done);\n const [prevLoading, setPrevLoading] = useState(loading);\n if (loading !== prevLoading) {\n setPrevLoading(loading);\n if (loading) {\n setMinElapsed(false);\n }\n }\n\n const state = loading\n ? \"loading\"\n : done\n ? minElapsed\n ? \"done\"\n : \"loading\"\n : \"idle\";\n\n const loadingStartTime = useRef<number | null>(null);\n const onDoneRef = useRef(onDone);\n const ButtonComponent = asChild ? Slot : Button;\n\n useEffect(() => {\n onDoneRef.current = onDone;\n }, [onDone]);\n\n useEffect(() => {\n if (loading) {\n loadingStartTime.current = Date.now();\n return;\n }\n if (!done) {\n return;\n }\n\n const loadingDuration = loadingStartTime.current\n ? Date.now() - loadingStartTime.current\n : 0;\n const remainingDelay = Math.max(0, SAVING_TIMEOUT_MS - loadingDuration);\n\n let doneTimer: number | null = null;\n const minElapsedTimer = window.setTimeout(() => {\n setMinElapsed(true);\n\n doneTimer = window.setTimeout(() => {\n onDoneRef.current?.();\n }, DONE_TIMEOUT_MS);\n }, remainingDelay);\n\n return () => {\n window.clearTimeout(minElapsedTimer);\n if (doneTimer !== null) {\n window.clearTimeout(doneTimer);\n }\n };\n }, [loading, done]);\n\n return (\n <ButtonComponent\n {...props}\n className={namespaceClassNames(\"save-button\")}\n data-save-state={state}\n >\n <Slottable>{children}</Slottable>\n {state === \"loading\" && (\n <Flex\n as=\"span\"\n align=\"center\"\n justify=\"center\"\n position=\"absolute\"\n inset=\"0\"\n >\n <Spinner size=\"1\" />\n </Flex>\n )}\n\n {state === \"done\" && (\n <Flex\n as=\"span\"\n align=\"center\"\n justify=\"center\"\n position=\"absolute\"\n inset=\"0\"\n gap=\"1\"\n >\n <CheckIcon\n width=\"18px\"\n height=\"18px\"\n style={{ marginLeft: \"-4px\" }}\n className={namespaceClassNames(\"save-button__done-icon\")}\n />\n <Text className={namespaceClassNames(\"save-button__done-text\")}>\n <Translation\n defaultMessage=\"Done\"\n id=\"wjt0K3\"\n description=\"Button label indicating an action has completed successfully\"\n />\n </Text>\n </Flex>\n )}\n </ButtonComponent>\n );\n}\n"],"mappings":";;;;;;;;;AAQA,MAAM,kBAAkB;AACxB,MAAM,oBAAoB;AAU1B,SAAgB,WAAW,EACzB,UAAU,OACV,UACA,SACA,MACA,QACA,GAAG,SACe;CAGlB,MAAM,CAAC,YAAY,kBAAA,GAAiBA,MAAAA,SAAAA,CAAS,CAAC,CAAC,IAAI;CACnD,MAAM,CAAC,aAAa,mBAAA,GAAkBA,MAAAA,SAAAA,CAAS,OAAO;CACtD,IAAI,YAAY,aAAa;EAC3B,eAAe,OAAO;EACtB,IAAI,SACF,cAAc,KAAK;CAEvB;CAEA,MAAM,QAAQ,UACV,YACA,OACE,aACE,SACA,YACF;CAEN,MAAM,oBAAA,GAAmBC,MAAAA,OAAAA,CAAsB,IAAI;CACnD,MAAM,aAAA,GAAYA,MAAAA,OAAAA,CAAO,MAAM;CAC/B,MAAM,kBAAkB,UAAUC,iBAAAA,OAAOC,qBAAAA;CAEzC,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,UAAU,UAAU;CACtB,GAAG,CAAC,MAAM,CAAC;CAEX,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,IAAI,SAAS;GACX,iBAAiB,UAAU,KAAK,IAAI;GACpC;EACF;EACA,IAAI,CAAC,MACH;EAGF,MAAM,kBAAkB,iBAAiB,UACrC,KAAK,IAAI,IAAI,iBAAiB,UAC9B;EACJ,MAAM,iBAAiB,KAAK,IAAI,GAAG,oBAAoB,eAAe;EAEtE,IAAI,YAA2B;EAC/B,MAAM,kBAAkB,OAAO,iBAAiB;GAC9C,cAAc,IAAI;GAElB,YAAY,OAAO,iBAAiB;IAClC,UAAU,UAAU;GACtB,GAAG,eAAe;EACpB,GAAG,cAAc;EAEjB,aAAa;GACX,OAAO,aAAa,eAAe;GACnC,IAAI,cAAc,MAChB,OAAO,aAAa,SAAS;EAEjC;CACF,GAAG,CAAC,SAAS,IAAI,CAAC;CAElB,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,iBAAD;EACE,GAAI;EACJ,WAAWC,6BAAAA,oBAAoB,aAAa;EAC5C,mBAAiB;EAHnB,UAAA;GAKE,iBAAA,GAAA,kBAAA,IAAA,CAACC,iBAAAA,WAAD,EAAY,SAAoB,CAAA;GAC/B,UAAU,aACT,iBAAA,GAAA,kBAAA,IAAA,CAACC,iBAAAA,MAAD;IACE,IAAG;IACH,OAAM;IACN,SAAQ;IACR,UAAS;IACT,OAAM;IAEN,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAACC,iBAAAA,SAAD,EAAS,MAAK,IAAK,CAAA;GACf,CAAA;GAGP,UAAU,UACT,iBAAA,GAAA,kBAAA,KAAA,CAACD,iBAAAA,MAAD;IACE,IAAG;IACH,OAAM;IACN,SAAQ;IACR,UAAS;IACT,OAAM;IACN,KAAI;IANN,UAAA,CAQE,iBAAA,GAAA,kBAAA,IAAA,CAACE,sBAAAA,WAAD;KACE,OAAM;KACN,QAAO;KACP,OAAO,EAAE,YAAY,OAAO;KAC5B,WAAWJ,6BAAAA,oBAAoB,wBAAwB;IACxD,CAAA,GACD,iBAAA,GAAA,kBAAA,IAAA,CAACK,iBAAAA,MAAD;KAAM,WAAWL,6BAAAA,oBAAoB,wBAAwB;KAC3D,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAACM,6BAAAA,aAAD;MACE,gBAAe;MACf,IAAG;MACH,aAAY;KACb,CAAA;IACG,CAAA,CACF;;EAEO;;AAErB"}
@@ -1 +1 @@
1
- {"version":3,"file":"compose-refs.js","names":[],"sources":["../../../src/lib/compose-refs.ts"],"sourcesContent":["import * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n if (typeof ref === \"function\") {\n return ref(value);\n } else if (ref !== null && ref !== undefined) {\n ref.current = value;\n }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n return (node) => {\n let hasCleanup = false;\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node);\n if (!hasCleanup && typeof cleanup == \"function\") {\n hasCleanup = true;\n }\n return cleanup;\n });\n\n // React <19 will log an error to the console if a callback ref returns a\n // value. We don't use ref cleanups internally so this will only happen if a\n // user's ref callback returns a value, which we only expect if they are\n // using the cleanup functionality added in React 19.\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < cleanups.length; i++) {\n const cleanup = cleanups[i];\n if (typeof cleanup == \"function\") {\n cleanup();\n } else {\n setRef(refs[i], null);\n }\n }\n };\n }\n };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n // eslint-disable-next-line react-hooks/exhaustive-deps\n return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { useComposedRefs };\n"],"mappings":";;;;;;AAQA,SAAS,OAAU,KAAqB,OAAU;CAChD,IAAI,OAAO,QAAQ,YACjB,OAAO,IAAI,KAAK;MACX,IAAI,QAAQ,QAAQ,QAAQ,QACjC,IAAI,UAAU;AAElB;;;;;AAMA,SAAS,YAAe,GAAG,MAA8C;CACvE,QAAQ,SAAS;EACf,IAAI,aAAa;EACjB,MAAM,WAAW,KAAK,KAAK,QAAQ;GACjC,MAAM,UAAU,OAAO,KAAK,IAAI;GAChC,IAAI,CAAC,cAAc,OAAO,WAAW,YACnC,aAAa;GAEf,OAAO;EACT,CAAC;EAMD,IAAI,YACF,aAAa;GACX,KAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;IACxC,MAAM,UAAU,SAAS;IACzB,IAAI,OAAO,WAAW,YACpB,QAAQ;SAER,OAAO,KAAK,IAAI,IAAI;GAExB;EACF;CAEJ;AACF;;;;;AAMA,SAAS,gBAAmB,GAAG,MAA8C;CAE3E,OAAO,MAAM,YAAY,YAAY,GAAG,IAAI,GAAG,IAAI;AACrD"}
1
+ {"version":3,"file":"compose-refs.js","names":[],"sources":["../../../src/lib/compose-refs.ts"],"sourcesContent":["import * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n if (typeof ref === \"function\") {\n return ref(value);\n } else if (ref !== null && ref !== undefined) {\n ref.current = value;\n }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n return (node) => {\n let hasCleanup = false;\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node);\n if (!hasCleanup && typeof cleanup == \"function\") {\n hasCleanup = true;\n }\n return cleanup;\n });\n\n // React <19 will log an error to the console if a callback ref returns a\n // value. We don't use ref cleanups internally so this will only happen if a\n // user's ref callback returns a value, which we only expect if they are\n // using the cleanup functionality added in React 19.\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < cleanups.length; i++) {\n const cleanup = cleanups[i];\n if (typeof cleanup == \"function\") {\n cleanup();\n } else {\n setRef(refs[i], null);\n }\n }\n };\n }\n };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n // oxlint-disable-next-line react-hooks/exhaustive-deps\n return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { useComposedRefs };\n"],"mappings":";;;;;;AAQA,SAAS,OAAU,KAAqB,OAAU;CAChD,IAAI,OAAO,QAAQ,YACjB,OAAO,IAAI,KAAK;MACX,IAAI,QAAQ,QAAQ,QAAQ,QACjC,IAAI,UAAU;AAElB;;;;;AAMA,SAAS,YAAe,GAAG,MAA8C;CACvE,QAAQ,SAAS;EACf,IAAI,aAAa;EACjB,MAAM,WAAW,KAAK,KAAK,QAAQ;GACjC,MAAM,UAAU,OAAO,KAAK,IAAI;GAChC,IAAI,CAAC,cAAc,OAAO,WAAW,YACnC,aAAa;GAEf,OAAO;EACT,CAAC;EAMD,IAAI,YACF,aAAa;GACX,KAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;IACxC,MAAM,UAAU,SAAS;IACzB,IAAI,OAAO,WAAW,YACpB,QAAQ;SAER,OAAO,KAAK,IAAI,IAAI;GAExB;EACF;CAEJ;AACF;;;;;AAMA,SAAS,gBAAmB,GAAG,MAA8C;CAE3E,OAAO,MAAM,YAAY,YAAY,GAAG,IAAI,GAAG,IAAI;AACrD"}
@@ -9,17 +9,21 @@ import { expectResultMember } from "./client-graphql-result.js";
9
9
  import { Form } from "./form.js";
10
10
  import { Input, Root } from "./otp-input.js";
11
11
  import { Callout, Flex, Text } from "@radix-ui/themes";
12
- import { useCallback, useEffect, useRef, useState } from "react";
12
+ import { useCallback, useRef, useState } from "react";
13
13
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
14
14
  //#region src/lib/elevated-access.tsx
15
15
  function ElevatedAccess({ type = "dialog", children, onVerified }) {
16
16
  const { elevatedAccess } = useElevatedAccessToken();
17
17
  const [authenticationChallengeId, setAuthenticationChallengeId] = useState();
18
- const prevAccessToken = useRef(elevatedAccess);
18
+ const [{ prevAccessToken, hasTokenExpired }, setPrevAccessState] = useState({
19
+ prevAccessToken: elevatedAccess,
20
+ hasTokenExpired: false
21
+ });
22
+ if (elevatedAccess !== prevAccessToken) setPrevAccessState({
23
+ prevAccessToken: elevatedAccess,
24
+ hasTokenExpired: hasTokenExpired || !!prevAccessToken && !elevatedAccess
25
+ });
19
26
  const hasCalledOnVerified = useRef(false);
20
- useEffect(() => {
21
- prevAccessToken.current = elevatedAccess;
22
- }, [elevatedAccess]);
23
27
  const handleVerificationSuccess = useCallback((elevatedAccessToken) => {
24
28
  setAuthenticationChallengeId(void 0);
25
29
  if (!hasCalledOnVerified.current) {
@@ -28,16 +32,13 @@ function ElevatedAccess({ type = "dialog", children, onVerified }) {
28
32
  }
29
33
  }, [onVerified]);
30
34
  if (elevatedAccess) return /* @__PURE__ */ jsx(Fragment, { children });
31
- if (!authenticationChallengeId) {
32
- const hasTokenExpired = !!prevAccessToken.current;
33
- return /* @__PURE__ */ jsx(SendVerificationEmailForm, {
34
- type,
35
- hasTokenExpired,
36
- onSuccess: (challengeId) => {
37
- setAuthenticationChallengeId(challengeId);
38
- }
39
- });
40
- }
35
+ if (!authenticationChallengeId) return /* @__PURE__ */ jsx(SendVerificationEmailForm, {
36
+ type,
37
+ hasTokenExpired,
38
+ onSuccess: (challengeId) => {
39
+ setAuthenticationChallengeId(challengeId);
40
+ }
41
+ });
41
42
  if (authenticationChallengeId) return /* @__PURE__ */ jsx(VerificationIdentityForm, {
42
43
  type,
43
44
  authenticationChallengeId,
@@ -1 +1 @@
1
- {"version":3,"file":"elevated-access.js","names":["Title","Dialog.Title","AlertDialog.Title","Dialog.Description","AlertDialog.Description","Close","Dialog.Close","AlertDialog.Cancel","Button","Form.Root","Otp.Root","Otp.Input"],"sources":["../../../src/lib/elevated-access.tsx"],"sourcesContent":["import { Callout, Flex, Text } from \"@radix-ui/themes\";\nimport type { PropsWithChildren } from \"react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useElevatedAccessToken } from \"../api/api-provider.js\";\nimport { useClientGraphqlMutation } from \"../api/client-graphql/hooks.js\";\nimport { useMe } from \"./api/me.js\";\nimport { expectResultMember } from \"./client-graphql-result.js\";\nimport { AlertDialog, Button, Dialog } from \"./elements.js\";\nimport * as Form from \"./form.js\";\nimport { Translation } from \"./i18n/translation.js\";\nimport * as Otp from \"./otp-input.js\";\n\ninterface ElevatedAccessProps extends PropsWithChildren {\n onVerified?: (elevatedAccessToken: string) => Promise<unknown>;\n type?: \"dialog\" | \"alert\";\n}\n\nexport function ElevatedAccess({\n type = \"dialog\",\n children,\n onVerified,\n}: ElevatedAccessProps) {\n const { elevatedAccess } = useElevatedAccessToken();\n const [authenticationChallengeId, setAuthenticationChallengeId] =\n useState<string>();\n\n const prevAccessToken = useRef(elevatedAccess);\n const hasCalledOnVerified = useRef(false);\n\n useEffect(() => {\n prevAccessToken.current = elevatedAccess;\n }, [elevatedAccess]);\n\n const handleVerificationSuccess = useCallback(\n (elevatedAccessToken: string) => {\n setAuthenticationChallengeId(undefined);\n\n // Only call onVerified on the initial verification, not on\n // re-verifications after token expiry, so side effects (e.g. creating a\n // new TOTP factor) don't re-run and discard the user's progress.\n if (!hasCalledOnVerified.current) {\n hasCalledOnVerified.current = true;\n return onVerified?.(elevatedAccessToken);\n }\n },\n [onVerified],\n );\n\n if (elevatedAccess) {\n return <>{children}</>;\n }\n\n if (!authenticationChallengeId) {\n // FIXME: This should be refactored\n // eslint-disable-next-line react-hooks/refs\n const hasTokenExpired = !!prevAccessToken.current;\n\n return (\n <SendVerificationEmailForm\n type={type}\n hasTokenExpired={hasTokenExpired}\n onSuccess={(challengeId) => {\n setAuthenticationChallengeId(challengeId);\n }}\n />\n );\n }\n\n if (authenticationChallengeId) {\n return (\n <VerificationIdentityForm\n type={type}\n authenticationChallengeId={authenticationChallengeId}\n onSuccess={handleVerificationSuccess}\n />\n );\n }\n\n return null;\n}\n\ninterface SendVerificationEmailFormProps {\n onSuccess: (challengeId: string) => unknown | Promise<unknown>;\n type: \"dialog\" | \"alert\";\n hasTokenExpired: boolean;\n}\n\nfunction SendVerificationEmailForm({\n onSuccess,\n type,\n hasTokenExpired,\n}: SendVerificationEmailFormProps) {\n const { data: me } = useMe();\n const sendVerification = useClientGraphqlMutation({\n mutationFn: async (sdk) =>\n expectResultMember(\n (await sdk.SendVerificationCode()).sendVerificationCode,\n \"VerificationCodeSent\",\n ),\n onSuccess: (data) => onSuccess(data.authenticationChallengeId),\n });\n\n const Title = type === \"dialog\" ? Dialog.Title : AlertDialog.Title;\n const Description =\n type === \"dialog\" ? Dialog.Description : AlertDialog.Description;\n const Close = type === \"dialog\" ? Dialog.Close : AlertDialog.Cancel;\n\n const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n sendVerification.mutate();\n };\n\n return (\n <form onSubmit={handleSubmit}>\n <Title>\n {hasTokenExpired ? (\n <Translation\n defaultMessage=\"Your verification token has expired\"\n id=\"lb/AsC\"\n description=\"Dialog title when verification token has expired\"\n />\n ) : (\n <Translation\n defaultMessage=\"Verify your identity\"\n id=\"7tGCOh\"\n description=\"Dialog title for identity verification\"\n />\n )}\n </Title>\n\n <Description mb=\"5\">\n <Translation\n defaultMessage=\"To continue, we need to confirm your identity. We'll send a temporary verification code to {email}.\"\n id=\"FTUSRM\"\n description=\"Description explaining verification code will be sent\"\n values={{\n email: (\n <Text weight=\"bold\" highContrast>\n {me?.email}\n </Text>\n ),\n }}\n />\n </Description>\n\n {sendVerification.error && (\n <Callout.Root color=\"red\" mt=\"-2\" mb=\"0\">\n <Callout.Text>\n {getMutationErrorMessage(sendVerification.error)}\n </Callout.Text>\n </Callout.Root>\n )}\n\n <Flex justify=\"end\" align=\"center\" gap=\"3\" mt=\"5\">\n <Close>\n <Button\n variant=\"secondary\"\n type=\"button\"\n disabled={sendVerification.isPending}\n >\n <Translation\n defaultMessage=\"Cancel\"\n id=\"hHNj31\"\n description=\"Cancel button text\"\n />\n </Button>\n </Close>\n <Button type=\"submit\" loading={sendVerification.isPending}>\n <Translation\n defaultMessage=\"Send verification code\"\n id=\"XsTeXJ\"\n description=\"Button text to send verification code\"\n />\n </Button>\n </Flex>\n </form>\n );\n}\n\ninterface VerificationIdentityFormProps {\n onSuccess?: (elevatedAccessToken: string) => unknown | Promise<unknown>;\n authenticationChallengeId: string;\n type: \"dialog\" | \"alert\";\n}\n\nfunction VerificationIdentityForm({\n onSuccess,\n authenticationChallengeId,\n type,\n}: VerificationIdentityFormProps) {\n const { data: me } = useMe();\n const { setElevatedAccess } = useElevatedAccessToken();\n const verifyIdentity = useClientGraphqlMutation({\n mutationFn: async (\n sdk,\n input: { code: string; authenticationChallengeId: string },\n ) =>\n expectResultMember(\n (await sdk.VerifyCurrentEmail({ input })).verifyCurrentEmail,\n \"CurrentEmailVerified\",\n ),\n });\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [otpValue, setOtpValue] = useState<string>(\"\");\n\n const Title = type === \"dialog\" ? Dialog.Title : AlertDialog.Title;\n const Description =\n type === \"dialog\" ? Dialog.Description : AlertDialog.Description;\n const Close = type === \"dialog\" ? Dialog.Close : AlertDialog.Cancel;\n const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n const formData = new FormData(event.currentTarget);\n const code = formData.get(\"otp-code\")?.toString() ?? \"\";\n\n setIsSubmitting(true);\n\n try {\n // Mutate async so we can wait for the onSuccess callback as well\n const newAuthState = await verifyIdentity.mutateAsync({\n code,\n authenticationChallengeId,\n });\n\n setElevatedAccess({\n token: newAuthState.elevatedAccessToken,\n expiresAt: newAuthState.expiresAt,\n });\n\n if (onSuccess) {\n await new Promise((resolve) => setTimeout(resolve, 200));\n await onSuccess(newAuthState.elevatedAccessToken);\n }\n } catch (error) {\n console.error(error);\n }\n\n setIsSubmitting(false);\n };\n\n return (\n <Form.Root onSubmit={handleSubmit}>\n <Title>\n <Translation\n defaultMessage=\"Verify your identity\"\n id=\"7tGCOh\"\n description=\"Dialog title for identity verification\"\n />\n </Title>\n\n <Description>\n <Translation\n defaultMessage=\"A verification code was sent to {email}. Please enter it below.\"\n id=\"kvxE0S\"\n description=\"Description explaining where verification code was sent\"\n values={{\n email: (\n <Text weight=\"bold\" highContrast>\n {me?.email}\n </Text>\n ),\n }}\n />\n </Description>\n\n <Flex direction=\"column\" gap=\"2\" mt=\"5\" mx=\"auto\" width=\"fit-content\">\n <Otp.Root\n autoSubmit\n gap=\"2\"\n justify=\"center\"\n columns=\"repeat(6, 48px)\"\n width=\"fit-content\"\n rows=\"48px\"\n name=\"otp-code\"\n readOnly={isSubmitting}\n value={otpValue}\n onValueChange={setOtpValue}\n >\n <Otp.Input required autoFocus autoComplete=\"off\" />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n </Otp.Root>\n\n {verifyIdentity.error && (\n <Text color=\"red\" size=\"2\" as=\"p\">\n {getMutationErrorMessage(verifyIdentity.error)}\n </Text>\n )}\n </Flex>\n\n <Flex justify=\"end\" align=\"center\" gap=\"3\" mt=\"5\">\n <Close>\n <Button variant=\"secondary\" type=\"button\">\n <Translation\n defaultMessage=\"Cancel\"\n id=\"hHNj31\"\n description=\"Cancel button text\"\n />\n </Button>\n </Close>\n <Button type=\"submit\" loading={isSubmitting}>\n <Translation\n defaultMessage=\"Confirm\"\n id=\"EmYENK\"\n description=\"Confirm button text for verification\"\n />\n </Button>\n </Flex>\n </Form.Root>\n );\n}\n\nfunction getMutationErrorMessage(error: unknown) {\n let message = typeof error === \"string\" ? error : \"\";\n\n if (error instanceof Error) {\n message = error.message;\n } else if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof error.message === \"string\"\n ) {\n message = error.message;\n }\n\n if (!message || message === \"Bad Request\") {\n message = \"Invalid code, please try again.\";\n }\n\n return message;\n}\n\n// Note: Error messages in getMutationErrorMessage are kept as plain strings\n// since they are displayed dynamically\n"],"mappings":"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"}
1
+ {"version":3,"file":"elevated-access.js","names":["Title","Dialog.Title","AlertDialog.Title","Dialog.Description","AlertDialog.Description","Close","Dialog.Close","AlertDialog.Cancel","Button","Form.Root","Otp.Root","Otp.Input"],"sources":["../../../src/lib/elevated-access.tsx"],"sourcesContent":["import { Callout, Flex, Text } from \"@radix-ui/themes\";\nimport type { PropsWithChildren } from \"react\";\nimport { useCallback, useRef, useState } from \"react\";\nimport { useElevatedAccessToken } from \"../api/api-provider.js\";\nimport { useClientGraphqlMutation } from \"../api/client-graphql/hooks.js\";\nimport { useMe } from \"./api/me.js\";\nimport { expectResultMember } from \"./client-graphql-result.js\";\nimport { AlertDialog, Button, Dialog } from \"./elements.js\";\nimport * as Form from \"./form.js\";\nimport { Translation } from \"./i18n/translation.js\";\nimport * as Otp from \"./otp-input.js\";\n\ninterface ElevatedAccessProps extends PropsWithChildren {\n onVerified?: (elevatedAccessToken: string) => Promise<unknown>;\n type?: \"dialog\" | \"alert\";\n}\n\nexport function ElevatedAccess({\n type = \"dialog\",\n children,\n onVerified,\n}: ElevatedAccessProps) {\n const { elevatedAccess } = useElevatedAccessToken();\n const [authenticationChallengeId, setAuthenticationChallengeId] =\n useState<string>();\n\n const [{ prevAccessToken, hasTokenExpired }, setPrevAccessState] = useState({\n prevAccessToken: elevatedAccess,\n hasTokenExpired: false,\n });\n if (elevatedAccess !== prevAccessToken) {\n setPrevAccessState({\n prevAccessToken: elevatedAccess,\n hasTokenExpired:\n hasTokenExpired || (!!prevAccessToken && !elevatedAccess),\n });\n }\n\n const hasCalledOnVerified = useRef(false);\n\n const handleVerificationSuccess = useCallback(\n (elevatedAccessToken: string) => {\n setAuthenticationChallengeId(undefined);\n\n // Only call onVerified on the initial verification, not on\n // re-verifications after token expiry, so side effects (e.g. creating a\n // new TOTP factor) don't re-run and discard the user's progress.\n if (!hasCalledOnVerified.current) {\n hasCalledOnVerified.current = true;\n return onVerified?.(elevatedAccessToken);\n }\n },\n [onVerified],\n );\n\n if (elevatedAccess) {\n return <>{children}</>;\n }\n\n if (!authenticationChallengeId) {\n return (\n <SendVerificationEmailForm\n type={type}\n hasTokenExpired={hasTokenExpired}\n onSuccess={(challengeId) => {\n setAuthenticationChallengeId(challengeId);\n }}\n />\n );\n }\n\n if (authenticationChallengeId) {\n return (\n <VerificationIdentityForm\n type={type}\n authenticationChallengeId={authenticationChallengeId}\n onSuccess={handleVerificationSuccess}\n />\n );\n }\n\n return null;\n}\n\ninterface SendVerificationEmailFormProps {\n onSuccess: (challengeId: string) => unknown | Promise<unknown>;\n type: \"dialog\" | \"alert\";\n hasTokenExpired: boolean;\n}\n\nfunction SendVerificationEmailForm({\n onSuccess,\n type,\n hasTokenExpired,\n}: SendVerificationEmailFormProps) {\n const { data: me } = useMe();\n const sendVerification = useClientGraphqlMutation({\n mutationFn: async (sdk) =>\n expectResultMember(\n (await sdk.SendVerificationCode()).sendVerificationCode,\n \"VerificationCodeSent\",\n ),\n onSuccess: (data) => onSuccess(data.authenticationChallengeId),\n });\n\n const Title = type === \"dialog\" ? Dialog.Title : AlertDialog.Title;\n const Description =\n type === \"dialog\" ? Dialog.Description : AlertDialog.Description;\n const Close = type === \"dialog\" ? Dialog.Close : AlertDialog.Cancel;\n\n const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n sendVerification.mutate();\n };\n\n return (\n <form onSubmit={handleSubmit}>\n <Title>\n {hasTokenExpired ? (\n <Translation\n defaultMessage=\"Your verification token has expired\"\n id=\"lb/AsC\"\n description=\"Dialog title when verification token has expired\"\n />\n ) : (\n <Translation\n defaultMessage=\"Verify your identity\"\n id=\"7tGCOh\"\n description=\"Dialog title for identity verification\"\n />\n )}\n </Title>\n\n <Description mb=\"5\">\n <Translation\n defaultMessage=\"To continue, we need to confirm your identity. We'll send a temporary verification code to {email}.\"\n id=\"FTUSRM\"\n description=\"Description explaining verification code will be sent\"\n values={{\n email: (\n <Text weight=\"bold\" highContrast>\n {me?.email}\n </Text>\n ),\n }}\n />\n </Description>\n\n {sendVerification.error && (\n <Callout.Root color=\"red\" mt=\"-2\" mb=\"0\">\n <Callout.Text>\n {getMutationErrorMessage(sendVerification.error)}\n </Callout.Text>\n </Callout.Root>\n )}\n\n <Flex justify=\"end\" align=\"center\" gap=\"3\" mt=\"5\">\n <Close>\n <Button\n variant=\"secondary\"\n type=\"button\"\n disabled={sendVerification.isPending}\n >\n <Translation\n defaultMessage=\"Cancel\"\n id=\"hHNj31\"\n description=\"Cancel button text\"\n />\n </Button>\n </Close>\n <Button type=\"submit\" loading={sendVerification.isPending}>\n <Translation\n defaultMessage=\"Send verification code\"\n id=\"XsTeXJ\"\n description=\"Button text to send verification code\"\n />\n </Button>\n </Flex>\n </form>\n );\n}\n\ninterface VerificationIdentityFormProps {\n onSuccess?: (elevatedAccessToken: string) => unknown | Promise<unknown>;\n authenticationChallengeId: string;\n type: \"dialog\" | \"alert\";\n}\n\nfunction VerificationIdentityForm({\n onSuccess,\n authenticationChallengeId,\n type,\n}: VerificationIdentityFormProps) {\n const { data: me } = useMe();\n const { setElevatedAccess } = useElevatedAccessToken();\n const verifyIdentity = useClientGraphqlMutation({\n mutationFn: async (\n sdk,\n input: { code: string; authenticationChallengeId: string },\n ) =>\n expectResultMember(\n (await sdk.VerifyCurrentEmail({ input })).verifyCurrentEmail,\n \"CurrentEmailVerified\",\n ),\n });\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [otpValue, setOtpValue] = useState<string>(\"\");\n\n const Title = type === \"dialog\" ? Dialog.Title : AlertDialog.Title;\n const Description =\n type === \"dialog\" ? Dialog.Description : AlertDialog.Description;\n const Close = type === \"dialog\" ? Dialog.Close : AlertDialog.Cancel;\n const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n\n const formData = new FormData(event.currentTarget);\n const code = formData.get(\"otp-code\")?.toString() ?? \"\";\n\n setIsSubmitting(true);\n\n try {\n // Mutate async so we can wait for the onSuccess callback as well\n const newAuthState = await verifyIdentity.mutateAsync({\n code,\n authenticationChallengeId,\n });\n\n setElevatedAccess({\n token: newAuthState.elevatedAccessToken,\n expiresAt: newAuthState.expiresAt,\n });\n\n if (onSuccess) {\n await new Promise((resolve) => setTimeout(resolve, 200));\n await onSuccess(newAuthState.elevatedAccessToken);\n }\n } catch (error) {\n console.error(error);\n }\n\n setIsSubmitting(false);\n };\n\n return (\n <Form.Root onSubmit={handleSubmit}>\n <Title>\n <Translation\n defaultMessage=\"Verify your identity\"\n id=\"7tGCOh\"\n description=\"Dialog title for identity verification\"\n />\n </Title>\n\n <Description>\n <Translation\n defaultMessage=\"A verification code was sent to {email}. Please enter it below.\"\n id=\"kvxE0S\"\n description=\"Description explaining where verification code was sent\"\n values={{\n email: (\n <Text weight=\"bold\" highContrast>\n {me?.email}\n </Text>\n ),\n }}\n />\n </Description>\n\n <Flex direction=\"column\" gap=\"2\" mt=\"5\" mx=\"auto\" width=\"fit-content\">\n <Otp.Root\n autoSubmit\n gap=\"2\"\n justify=\"center\"\n columns=\"repeat(6, 48px)\"\n width=\"fit-content\"\n rows=\"48px\"\n name=\"otp-code\"\n readOnly={isSubmitting}\n value={otpValue}\n onValueChange={setOtpValue}\n >\n <Otp.Input required autoFocus autoComplete=\"off\" />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n <Otp.Input required />\n </Otp.Root>\n\n {verifyIdentity.error && (\n <Text color=\"red\" size=\"2\" as=\"p\">\n {getMutationErrorMessage(verifyIdentity.error)}\n </Text>\n )}\n </Flex>\n\n <Flex justify=\"end\" align=\"center\" gap=\"3\" mt=\"5\">\n <Close>\n <Button variant=\"secondary\" type=\"button\">\n <Translation\n defaultMessage=\"Cancel\"\n id=\"hHNj31\"\n description=\"Cancel button text\"\n />\n </Button>\n </Close>\n <Button type=\"submit\" loading={isSubmitting}>\n <Translation\n defaultMessage=\"Confirm\"\n id=\"EmYENK\"\n description=\"Confirm button text for verification\"\n />\n </Button>\n </Flex>\n </Form.Root>\n );\n}\n\nfunction getMutationErrorMessage(error: unknown) {\n let message = typeof error === \"string\" ? error : \"\";\n\n if (error instanceof Error) {\n message = error.message;\n } else if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof error.message === \"string\"\n ) {\n message = error.message;\n }\n\n if (!message || message === \"Bad Request\") {\n message = \"Invalid code, please try again.\";\n }\n\n return message;\n}\n\n// Note: Error messages in getMutationErrorMessage are kept as plain strings\n// since they are displayed dynamically\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"otp-input.d.ts","names":["TextField"],"sources":["../../../src/lib/otp-input.tsx"],"mappings":";;;;KAmBK,eAAe,MAAM,gCAAgC;EACxD;EACA;EACA;EACA;EACA;EACA,eAAe,MAAM,SAAS,MAAM;EACpC;;qBAGW,MAAI,MAAA,0BAAA,eAAA,MAAA,cAAA;UA6JP,mBAAmB,MAAM,sBAAsBA;EACvD;;qBAGW,OAAK,MAAA,0BAAA,KAAA,qBAAA,MAAA,cAAA"}
1
+ {"version":3,"file":"otp-input.d.ts","names":["TextField"],"sources":["../../../src/lib/otp-input.tsx"],"mappings":";;;;KAsBK,eAAe,MAAM,gCAAgC;EACxD;EACA;EACA;EACA;EACA;EACA,eAAe,MAAM,SAAS,MAAM;EACpC;;qBAGW,MAAI,MAAA,0BAAA,eAAA,MAAA,cAAA;UAqJP,mBAAmB,MAAM,sBAAsBA;EACvD;;qBAGW,OAAK,MAAA,0BAAA,KAAA,qBAAA,MAAA,cAAA"}
@@ -7,9 +7,11 @@ import * as React from "react";
7
7
  import { jsx, jsxs } from "react/jsx-runtime";
8
8
  //#region src/lib/otp-input.tsx
9
9
  const OtpContext = React.createContext(void 0);
10
+ OtpContext.displayName = "OtpContext";
11
+ const OtpIndexContext = React.createContext(-1);
12
+ OtpIndexContext.displayName = "OtpIndexContext";
10
13
  const Root = React.forwardRef(function Root({ name, id, value: valueProp, onValueChange, autoSubmit, children, readOnly, state, ...gridProps }, forwardedRef) {
11
14
  const [lastCharIndex, setLastCharIndex] = React.useState(0);
12
- const [allChildrenAdded, setAllChildrenAdded] = React.useState(false);
13
15
  const childCount = React.Children.count(children);
14
16
  const [value, setValueState] = React.useState(() => getValueAsArray(valueProp, childCount));
15
17
  const [previousValueProp, setPreviousValueProp] = React.useState(valueProp);
@@ -33,13 +35,9 @@ const Root = React.forwardRef(function Root({ name, id, value: valueProp, onValu
33
35
  autoSubmit
34
36
  ]);
35
37
  const handleEnterPressed = React.useCallback(() => attemptAutoSubmit(true), [attemptAutoSubmit]);
36
- const handleChildAdd = React.useCallback((input) => {
37
- if (input) {
38
- input.dataset.index = `${childrenRefs.current.length}`;
39
- childrenRefs.current.push(input);
40
- } else childrenRefs.current.pop();
41
- if (childrenRefs.current.length === childCount) setAllChildrenAdded(true);
42
- }, [childCount]);
38
+ const handleChildAdd = React.useCallback((element, index) => {
39
+ childrenRefs.current[index] = element;
40
+ }, []);
43
41
  const handleCharChange = React.useCallback((char, index) => {
44
42
  setValue((previousValue) => {
45
43
  const newValue = [...previousValue];
@@ -52,18 +50,16 @@ const Root = React.forwardRef(function Root({ name, id, value: valueProp, onValu
52
50
  value,
53
51
  readOnly,
54
52
  state,
55
- allChildrenAdded,
56
53
  onEnterPressed: handleEnterPressed,
57
- onChildAdd: handleChildAdd,
58
- onCharChange: handleCharChange
54
+ onCharChange: handleCharChange,
55
+ onChildAdd: handleChildAdd
59
56
  }), [
60
57
  value,
61
- allChildrenAdded,
62
58
  readOnly,
63
59
  state,
64
60
  handleEnterPressed,
65
- handleChildAdd,
66
- handleCharChange
61
+ handleCharChange,
62
+ handleChildAdd
67
63
  ]);
68
64
  React.useEffect(attemptAutoSubmit, [attemptAutoSubmit]);
69
65
  return /* @__PURE__ */ jsx(OtpContext.Provider, {
@@ -80,7 +76,10 @@ const Root = React.forwardRef(function Root({ name, id, value: valueProp, onValu
80
76
  const index = Math.min(sanitizedValue.length, childCount - 1);
81
77
  childrenRefs.current?.[index]?.focus();
82
78
  },
83
- children: [children, /* @__PURE__ */ jsx("input", {
79
+ children: [React.Children.map(children, (child, index) => /* @__PURE__ */ jsx(OtpIndexContext.Provider, {
80
+ value: index,
81
+ children: child
82
+ })), /* @__PURE__ */ jsx("input", {
84
83
  ref: useComposedRefs(forwardedRef, hiddenInputRef),
85
84
  defaultValue: value.join(""),
86
85
  minLength: childCount,
@@ -92,9 +91,12 @@ const Root = React.forwardRef(function Root({ name, id, value: valueProp, onValu
92
91
  });
93
92
  const Input = React.forwardRef(function Input({ style, readOnly, autoComplete = "off", ...props }, forwardedRef) {
94
93
  const otpContext = useOptContext();
95
- const inputRef = React.useRef(null);
96
- const composedInputRef = useComposedRefs(forwardedRef, inputRef, otpContext.onChildAdd);
97
- const index = Number(inputRef.current?.dataset.index ?? -1);
94
+ const { onChildAdd } = otpContext;
95
+ const index = React.useContext(OtpIndexContext);
96
+ const inputRef = React.useCallback((input) => {
97
+ onChildAdd(input, index);
98
+ }, [onChildAdd, index]);
99
+ const composedInputRef = useComposedRefs(forwardedRef, inputRef);
98
100
  const char = otpContext.value[index] ?? "";
99
101
  return /* @__PURE__ */ jsx(FormField, {
100
102
  name: `otp-${index}`,
@@ -121,7 +123,6 @@ const Input = React.forwardRef(function Input({ style, readOnly, autoComplete =
121
123
  onChange: (event) => {
122
124
  if (event.target.validity.valid || event.target.value === "") {
123
125
  const char = event.target.value;
124
- const index = Number(event.target.dataset.index ?? -1);
125
126
  otpContext.onCharChange(char, index);
126
127
  focusSibling(event.currentTarget, { back: char === "" });
127
128
  }
@@ -1 +1 @@
1
- {"version":3,"file":"otp-input.js","names":["Form.Field","Form.Control","TextField"],"sources":["../../../src/lib/otp-input.tsx"],"sourcesContent":["\"use client\";\nimport { Grid } from \"@radix-ui/themes\";\nimport * as React from \"react\";\nimport { useComposedRefs } from \"./compose-refs.js\";\nimport { TextField } from \"./elements.js\";\nimport * as Form from \"./form.js\";\n\ninterface OptContextType {\n value: string[];\n readOnly?: boolean;\n state?: \"valid\" | \"invalid\";\n onEnterPressed: () => void;\n onChildAdd: (input: HTMLInputElement) => void;\n onCharChange: (char: string, index: number) => void;\n allChildrenAdded: boolean;\n}\n\nconst OtpContext = React.createContext<OptContextType | undefined>(undefined);\n\ntype OtpRootProps = React.ComponentPropsWithoutRef<typeof Grid> & {\n id?: string;\n name?: string;\n readOnly?: boolean;\n state?: \"valid\" | \"invalid\";\n value: string;\n onValueChange: React.Dispatch<React.SetStateAction<string>>;\n autoSubmit?: boolean;\n};\n\nexport const Root = React.forwardRef<HTMLInputElement, OtpRootProps>(\n function Root(\n {\n name,\n id,\n value: valueProp,\n onValueChange,\n autoSubmit,\n children,\n readOnly,\n state,\n ...gridProps\n },\n forwardedRef,\n ) {\n const [lastCharIndex, setLastCharIndex] = React.useState<number>(0);\n const [allChildrenAdded, setAllChildrenAdded] =\n React.useState<boolean>(false);\n const childCount = React.Children.count(children);\n\n // Internal array state is the source of truth so that empty slots between\n // filled digits are preserved. A `value` string can't encode a gap (e.g.\n // `[\"1\", \"\", \"3\"]` and `[\"1\", \"3\"]` both serialize to \"13\"), so deriving\n // state straight from the prop would shift digits whenever an inner box is\n // cleared or filled out of order.\n const [value, setValueState] = React.useState<string[]>(() =>\n getValueAsArray(valueProp, childCount),\n );\n\n const [previousValueProp, setPreviousValueProp] = React.useState(valueProp);\n if (previousValueProp !== valueProp) {\n setPreviousValueProp(valueProp);\n if (valueProp !== value.join(\"\")) {\n setValueState(getValueAsArray(valueProp, childCount));\n }\n }\n\n const setValue = React.useCallback(\n (action: React.SetStateAction<string[]>) => {\n const nextArrayValue =\n typeof action === \"function\" ? action(value) : action;\n setValueState(nextArrayValue);\n onValueChange(nextArrayValue.join(\"\"));\n },\n [value, onValueChange],\n );\n\n const hiddenInputRef = React.useRef<HTMLInputElement>(null);\n const childrenRefs = React.useRef<HTMLInputElement[]>([]);\n\n const attemptAutoSubmit = React.useCallback(\n (enterPressed = false) => {\n if (\n autoSubmit &&\n value.length > 0 &&\n value.every((char) => char !== \"\") &&\n (enterPressed || lastCharIndex + 1 === childCount)\n ) {\n hiddenInputRef.current?.form?.requestSubmit();\n }\n },\n [value, childCount, lastCharIndex, autoSubmit],\n );\n\n const handleEnterPressed = React.useCallback(\n () => attemptAutoSubmit(true),\n [attemptAutoSubmit],\n );\n\n const handleChildAdd = React.useCallback(\n (input: HTMLInputElement) => {\n if (input) {\n input.dataset.index = `${childrenRefs.current.length}`;\n childrenRefs.current.push(input);\n } else {\n childrenRefs.current.pop();\n }\n\n if (childrenRefs.current.length === childCount) {\n setAllChildrenAdded(true);\n }\n },\n [childCount],\n );\n\n const handleCharChange = React.useCallback(\n (char: string, index: number) => {\n setValue((previousValue) => {\n const newValue = [...previousValue];\n newValue[index] = char;\n return newValue;\n });\n setLastCharIndex(index);\n },\n [setValue],\n );\n\n const otpContext = React.useMemo(\n () => ({\n value,\n readOnly,\n state,\n allChildrenAdded,\n onEnterPressed: handleEnterPressed,\n onChildAdd: handleChildAdd,\n onCharChange: handleCharChange,\n }),\n [\n value,\n allChildrenAdded,\n readOnly,\n state,\n handleEnterPressed,\n handleChildAdd,\n handleCharChange,\n ],\n );\n\n React.useEffect(attemptAutoSubmit, [attemptAutoSubmit]);\n\n return (\n <OtpContext.Provider value={otpContext}>\n <Grid\n columns={`repeat(${childCount}, 1fr)`}\n {...gridProps}\n onPaste={(event: React.ClipboardEvent<HTMLDivElement>) => {\n event.preventDefault();\n const pastedValue = event.clipboardData.getData(\"Text\");\n const sanitizedValue = pastedValue\n .replace(/[^\\d]/g, \"\")\n .slice(0, childCount);\n const value = sanitizedValue\n .padEnd(childCount, \"#\")\n .split(\"\")\n .map((char) => (char === \"#\" ? \"\" : char));\n\n setValue(value);\n setLastCharIndex(sanitizedValue.length - 1);\n\n const index = Math.min(sanitizedValue.length, childCount - 1);\n childrenRefs.current?.[index]?.focus();\n }}\n >\n {children}\n <input\n ref={useComposedRefs(forwardedRef, hiddenInputRef)}\n defaultValue={value.join(\"\")}\n minLength={childCount}\n name={name}\n type=\"hidden\"\n />\n </Grid>\n </OtpContext.Provider>\n );\n },\n);\n\ninterface InputProps extends React.ComponentProps<typeof TextField> {\n autoComplete?: \"one-time-code\" | \"off\";\n}\n\nexport const Input = React.forwardRef<HTMLInputElement, InputProps>(\n function Input(\n { style, readOnly, autoComplete = \"off\", ...props },\n forwardedRef,\n ) {\n const otpContext = useOptContext();\n const inputRef = React.useRef<HTMLInputElement>(null);\n const composedInputRef = useComposedRefs(\n forwardedRef,\n inputRef,\n otpContext.onChildAdd,\n );\n\n // FIXME: This should be refactored\n // eslint-disable-next-line react-hooks/refs\n const index = Number(inputRef.current?.dataset.index ?? -1);\n const char = otpContext.value[index] ?? \"\";\n\n return (\n <Form.Field name={`otp-${index}`} asChild>\n <Form.Control asChild>\n <TextField\n ref={composedInputRef}\n autoComplete={index === 0 ? autoComplete : \"off\"}\n color={otpContext.state === \"invalid\" ? \"red\" : undefined}\n inputMode=\"numeric\"\n maxLength={1}\n pattern=\"\\d{1}\"\n readOnly={readOnly ?? otpContext.readOnly}\n size=\"3\"\n value={char}\n variant={otpContext.state === \"invalid\" ? \"soft\" : undefined}\n style={{\n ...style,\n height: \"auto\",\n \"--text-field-padding\": 0,\n textAlign: \"center\",\n }}\n onChange={(event) => {\n // Update the value if it matches the input pattern (number only) or is empty (backspace)\n if (event.target.validity.valid || event.target.value === \"\") {\n const char = event.target.value;\n const index = Number(event.target.dataset.index ?? -1);\n otpContext.onCharChange(char, index);\n focusSibling(event.currentTarget, { back: char === \"\" });\n }\n }}\n onKeyDown={(event) => {\n if (event.key === \"ArrowLeft\") {\n focusSibling(event.currentTarget, { back: true });\n event.preventDefault();\n } else if (event.key === \"ArrowRight\") {\n focusSibling(event.currentTarget);\n event.preventDefault();\n } else if (event.key === \"Backspace\" && char === \"\") {\n focusSibling(event.currentTarget, { back: true });\n } else if (event.key === \"Enter\" && char !== \"\") {\n otpContext.onEnterPressed();\n }\n }}\n {...props}\n />\n </Form.Control>\n </Form.Field>\n );\n },\n);\n\nconst useOptContext = () => {\n const optContext = React.useContext(OtpContext);\n\n if (!optContext) {\n throw new Error(\n \"OtpInput compound components cannot be rendered outside the OtpRoot component\",\n );\n }\n\n return optContext;\n};\n\nfunction focusSibling(input: HTMLInputElement, { back = false } = {}) {\n const sibling = back\n ? input.parentElement?.previousSibling\n : input.parentElement?.nextSibling;\n const siblingInput = sibling?.firstChild;\n if (siblingInput && siblingInput instanceof HTMLInputElement) {\n siblingInput?.focus();\n siblingInput?.select();\n }\n}\n\nconst getValueAsArray = (value: string | undefined, length: number) => {\n if (!value) {\n return createEmptyArray(length);\n }\n\n return createEmptyArray(length).map((_, index) => value?.[index] ?? \"\");\n};\n\nconst createEmptyArray = (length: number): string[] =>\n Array.from<string>({ length }).fill(\"\");\n"],"mappings":"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"}
1
+ {"version":3,"file":"otp-input.js","names":["Form.Field","Form.Control","TextField"],"sources":["../../../src/lib/otp-input.tsx"],"sourcesContent":["\"use client\";\nimport { Grid } from \"@radix-ui/themes\";\nimport * as React from \"react\";\nimport { useComposedRefs } from \"./compose-refs.js\";\nimport { TextField } from \"./elements.js\";\nimport * as Form from \"./form.js\";\n\ninterface OptContextType {\n value: string[];\n readOnly?: boolean;\n state?: \"valid\" | \"invalid\";\n onEnterPressed: () => void;\n onCharChange: (char: string, index: number) => void;\n onChildAdd: (element: HTMLInputElement | null, index: number) => void;\n}\n\nconst OtpContext = React.createContext<OptContextType | undefined>(undefined);\nOtpContext.displayName = \"OtpContext\";\n\nconst OtpIndexContext = React.createContext<number>(-1);\nOtpIndexContext.displayName = \"OtpIndexContext\";\n\ntype OtpRootProps = React.ComponentPropsWithoutRef<typeof Grid> & {\n id?: string;\n name?: string;\n readOnly?: boolean;\n state?: \"valid\" | \"invalid\";\n value: string;\n onValueChange: React.Dispatch<React.SetStateAction<string>>;\n autoSubmit?: boolean;\n};\n\nexport const Root = React.forwardRef<HTMLInputElement, OtpRootProps>(\n function Root(\n {\n name,\n id,\n value: valueProp,\n onValueChange,\n autoSubmit,\n children,\n readOnly,\n state,\n ...gridProps\n },\n forwardedRef,\n ) {\n const [lastCharIndex, setLastCharIndex] = React.useState<number>(0);\n const childCount = React.Children.count(children);\n\n // Internal array state is the source of truth so that empty slots between\n // filled digits are preserved. A `value` string can't encode a gap (e.g.\n // `[\"1\", \"\", \"3\"]` and `[\"1\", \"3\"]` both serialize to \"13\"), so deriving\n // state straight from the prop would shift digits whenever an inner box is\n // cleared or filled out of order.\n const [value, setValueState] = React.useState<string[]>(() =>\n getValueAsArray(valueProp, childCount),\n );\n\n const [previousValueProp, setPreviousValueProp] = React.useState(valueProp);\n if (previousValueProp !== valueProp) {\n setPreviousValueProp(valueProp);\n if (valueProp !== value.join(\"\")) {\n setValueState(getValueAsArray(valueProp, childCount));\n }\n }\n\n const setValue = React.useCallback(\n (action: React.SetStateAction<string[]>) => {\n const nextArrayValue =\n typeof action === \"function\" ? action(value) : action;\n setValueState(nextArrayValue);\n onValueChange(nextArrayValue.join(\"\"));\n },\n [value, onValueChange],\n );\n\n const hiddenInputRef = React.useRef<HTMLInputElement>(null);\n const childrenRefs = React.useRef<(HTMLInputElement | null)[]>([]);\n\n const attemptAutoSubmit = React.useCallback(\n (enterPressed = false) => {\n if (\n autoSubmit &&\n value.length > 0 &&\n value.every((char) => char !== \"\") &&\n (enterPressed || lastCharIndex + 1 === childCount)\n ) {\n hiddenInputRef.current?.form?.requestSubmit();\n }\n },\n [value, childCount, lastCharIndex, autoSubmit],\n );\n\n const handleEnterPressed = React.useCallback(\n () => attemptAutoSubmit(true),\n [attemptAutoSubmit],\n );\n\n const handleChildAdd = React.useCallback(\n (element: HTMLInputElement | null, index: number) => {\n childrenRefs.current[index] = element;\n },\n [],\n );\n\n const handleCharChange = React.useCallback(\n (char: string, index: number) => {\n setValue((previousValue) => {\n const newValue = [...previousValue];\n newValue[index] = char;\n return newValue;\n });\n setLastCharIndex(index);\n },\n [setValue],\n );\n\n const otpContext = React.useMemo<OptContextType>(\n () =>\n ({\n value,\n readOnly,\n state,\n onEnterPressed: handleEnterPressed,\n onCharChange: handleCharChange,\n onChildAdd: handleChildAdd,\n }) satisfies OptContextType,\n [\n value,\n readOnly,\n state,\n handleEnterPressed,\n handleCharChange,\n handleChildAdd,\n ],\n );\n\n React.useEffect(attemptAutoSubmit, [attemptAutoSubmit]);\n\n return (\n <OtpContext.Provider value={otpContext}>\n <Grid\n columns={`repeat(${childCount}, 1fr)`}\n {...gridProps}\n onPaste={(event: React.ClipboardEvent<HTMLDivElement>) => {\n event.preventDefault();\n const pastedValue = event.clipboardData.getData(\"Text\");\n const sanitizedValue = pastedValue\n .replace(/[^\\d]/g, \"\")\n .slice(0, childCount);\n const value = sanitizedValue\n .padEnd(childCount, \"#\")\n .split(\"\")\n .map((char) => (char === \"#\" ? \"\" : char));\n\n setValue(value);\n setLastCharIndex(sanitizedValue.length - 1);\n\n const index = Math.min(sanitizedValue.length, childCount - 1);\n childrenRefs.current?.[index]?.focus();\n }}\n >\n {React.Children.map(children, (child, index) => (\n <OtpIndexContext.Provider value={index}>\n {child}\n </OtpIndexContext.Provider>\n ))}\n <input\n ref={useComposedRefs(forwardedRef, hiddenInputRef)}\n defaultValue={value.join(\"\")}\n minLength={childCount}\n name={name}\n type=\"hidden\"\n />\n </Grid>\n </OtpContext.Provider>\n );\n },\n);\n\ninterface InputProps extends React.ComponentProps<typeof TextField> {\n autoComplete?: \"one-time-code\" | \"off\";\n}\n\nexport const Input = React.forwardRef<HTMLInputElement, InputProps>(\n function Input(\n { style, readOnly, autoComplete = \"off\", ...props },\n forwardedRef,\n ) {\n const otpContext = useOptContext();\n const { onChildAdd } = otpContext;\n const index = React.useContext(OtpIndexContext);\n const inputRef = React.useCallback(\n (input: HTMLInputElement | null) => {\n onChildAdd(input, index);\n },\n [onChildAdd, index],\n );\n const composedInputRef = useComposedRefs(forwardedRef, inputRef);\n\n const char = otpContext.value[index] ?? \"\";\n\n return (\n <Form.Field name={`otp-${index}`} asChild>\n <Form.Control asChild>\n <TextField\n ref={composedInputRef}\n autoComplete={index === 0 ? autoComplete : \"off\"}\n color={otpContext.state === \"invalid\" ? \"red\" : undefined}\n inputMode=\"numeric\"\n maxLength={1}\n pattern=\"\\d{1}\"\n readOnly={readOnly ?? otpContext.readOnly}\n size=\"3\"\n value={char}\n variant={otpContext.state === \"invalid\" ? \"soft\" : undefined}\n style={{\n ...style,\n height: \"auto\",\n \"--text-field-padding\": 0,\n textAlign: \"center\",\n }}\n onChange={(event) => {\n // Update the value if it matches the input pattern (number only) or is empty (backspace)\n if (event.target.validity.valid || event.target.value === \"\") {\n const char = event.target.value;\n otpContext.onCharChange(char, index);\n focusSibling(event.currentTarget, { back: char === \"\" });\n }\n }}\n onKeyDown={(event) => {\n if (event.key === \"ArrowLeft\") {\n focusSibling(event.currentTarget, { back: true });\n event.preventDefault();\n } else if (event.key === \"ArrowRight\") {\n focusSibling(event.currentTarget);\n event.preventDefault();\n } else if (event.key === \"Backspace\" && char === \"\") {\n focusSibling(event.currentTarget, { back: true });\n } else if (event.key === \"Enter\" && char !== \"\") {\n otpContext.onEnterPressed();\n }\n }}\n {...props}\n />\n </Form.Control>\n </Form.Field>\n );\n },\n);\n\nconst useOptContext = () => {\n const optContext = React.useContext(OtpContext);\n if (!optContext) {\n throw new Error(\n \"OtpInput compound components cannot be rendered outside the OtpRoot component\",\n );\n }\n\n return optContext;\n};\n\nfunction focusSibling(input: HTMLInputElement, { back = false } = {}) {\n const sibling = back\n ? input.parentElement?.previousSibling\n : input.parentElement?.nextSibling;\n const siblingInput = sibling?.firstChild;\n if (siblingInput && siblingInput instanceof HTMLInputElement) {\n siblingInput?.focus();\n siblingInput?.select();\n }\n}\n\nconst getValueAsArray = (value: string | undefined, length: number) => {\n if (!value) {\n return createEmptyArray(length);\n }\n\n return createEmptyArray(length).map((_, index) => value?.[index] ?? \"\");\n};\n\nconst createEmptyArray = (length: number): string[] =>\n Array.from<string>({ length }).fill(\"\");\n"],"mappings":"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"}
@@ -9,7 +9,13 @@ import { CheckIcon } from "@radix-ui/react-icons";
9
9
  const DONE_TIMEOUT_MS = 1500;
10
10
  const SAVING_TIMEOUT_MS = 600;
11
11
  function SaveButton({ asChild = false, children, loading, done, onDone, ...props }) {
12
- const [state, setState] = useState(loading ? "loading" : done ? "done" : "idle");
12
+ const [minElapsed, setMinElapsed] = useState(!!done);
13
+ const [prevLoading, setPrevLoading] = useState(loading);
14
+ if (loading !== prevLoading) {
15
+ setPrevLoading(loading);
16
+ if (loading) setMinElapsed(false);
17
+ }
18
+ const state = loading ? "loading" : done ? minElapsed ? "done" : "loading" : "idle";
13
19
  const loadingStartTime = useRef(null);
14
20
  const onDoneRef = useRef(onDone);
15
21
  const ButtonComponent = asChild ? Slot : Button$1;
@@ -18,24 +24,23 @@ function SaveButton({ asChild = false, children, loading, done, onDone, ...props
18
24
  }, [onDone]);
19
25
  useEffect(() => {
20
26
  if (loading) {
21
- setState("loading");
22
27
  loadingStartTime.current = Date.now();
23
- } else if (done) {
24
- const currentTime = Date.now();
25
- const loadingDuration = loadingStartTime.current ? currentTime - loadingStartTime.current : 0;
26
- const remainingDelay = Math.max(0, SAVING_TIMEOUT_MS - loadingDuration);
27
- let doneTimer = null;
28
- const savedTimer = window.setTimeout(() => {
29
- setState("done");
30
- doneTimer = window.setTimeout(() => {
31
- onDoneRef.current?.();
32
- }, DONE_TIMEOUT_MS);
33
- }, remainingDelay);
34
- return () => {
35
- window.clearTimeout(savedTimer);
36
- if (doneTimer !== null) window.clearTimeout(doneTimer);
37
- };
38
- } else if (!loading && !done) setState("idle");
28
+ return;
29
+ }
30
+ if (!done) return;
31
+ const loadingDuration = loadingStartTime.current ? Date.now() - loadingStartTime.current : 0;
32
+ const remainingDelay = Math.max(0, SAVING_TIMEOUT_MS - loadingDuration);
33
+ let doneTimer = null;
34
+ const minElapsedTimer = window.setTimeout(() => {
35
+ setMinElapsed(true);
36
+ doneTimer = window.setTimeout(() => {
37
+ onDoneRef.current?.();
38
+ }, DONE_TIMEOUT_MS);
39
+ }, remainingDelay);
40
+ return () => {
41
+ window.clearTimeout(minElapsedTimer);
42
+ if (doneTimer !== null) window.clearTimeout(doneTimer);
43
+ };
39
44
  }, [loading, done]);
40
45
  return /* @__PURE__ */ jsxs(ButtonComponent, {
41
46
  ...props,
@@ -1 +1 @@
1
- {"version":3,"file":"save-button.js","names":["Button"],"sources":["../../../src/lib/save-button.tsx"],"sourcesContent":["import { CheckIcon } from \"@radix-ui/react-icons\";\nimport { Flex, Slot, Slottable, Spinner, Text } from \"@radix-ui/themes\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { Button } from \"./elements.js\";\nimport type { ButtonProps } from \"./elements.js\";\nimport { Translation } from \"./i18n/translation.js\";\nimport { namespaceClassNames } from \"./widget-dom-props.js\";\n\nconst DONE_TIMEOUT_MS = 1500;\nconst SAVING_TIMEOUT_MS = 600;\n\ninterface SaveButtonProps extends ButtonProps {\n asChild?: boolean;\n children: React.ReactNode;\n loading?: boolean;\n done?: boolean;\n onDone?: () => void;\n}\n\nexport function SaveButton({\n asChild = false,\n children,\n loading,\n done,\n onDone,\n ...props\n}: SaveButtonProps) {\n const [state, setState] = useState<\"idle\" | \"loading\" | \"done\">(\n loading ? \"loading\" : done ? \"done\" : \"idle\",\n );\n const loadingStartTime = useRef<number | null>(null);\n const onDoneRef = useRef(onDone);\n const ButtonComponent = asChild ? Slot : Button;\n\n useEffect(() => {\n onDoneRef.current = onDone;\n }, [onDone]);\n\n useEffect(() => {\n if (loading) {\n // FIXME: This should be refactored\n // eslint-disable-next-line react-hooks/set-state-in-effect\n setState(\"loading\");\n loadingStartTime.current = Date.now();\n } else if (done) {\n const currentTime = Date.now();\n const loadingDuration = loadingStartTime.current\n ? currentTime - loadingStartTime.current\n : 0;\n\n // If loading lasted less than 500 ms, wait for the remaining time\n const remainingDelay = Math.max(0, SAVING_TIMEOUT_MS - loadingDuration);\n\n let doneTimer: number | null = null;\n const savedTimer = window.setTimeout(() => {\n setState(\"done\");\n\n doneTimer = window.setTimeout(() => {\n onDoneRef.current?.();\n }, DONE_TIMEOUT_MS);\n }, remainingDelay);\n\n return () => {\n window.clearTimeout(savedTimer);\n if (doneTimer !== null) {\n window.clearTimeout(doneTimer);\n }\n };\n } else if (!loading && !done) {\n setState(\"idle\");\n }\n }, [loading, done]);\n\n return (\n <ButtonComponent\n {...props}\n className={namespaceClassNames(\"save-button\")}\n data-save-state={state}\n >\n <Slottable>{children}</Slottable>\n {state === \"loading\" && (\n <Flex\n as=\"span\"\n align=\"center\"\n justify=\"center\"\n position=\"absolute\"\n inset=\"0\"\n >\n <Spinner size=\"1\" />\n </Flex>\n )}\n\n {state === \"done\" && (\n <Flex\n as=\"span\"\n align=\"center\"\n justify=\"center\"\n position=\"absolute\"\n inset=\"0\"\n gap=\"1\"\n >\n <CheckIcon\n width=\"18px\"\n height=\"18px\"\n style={{ marginLeft: \"-4px\" }}\n className={namespaceClassNames(\"save-button__done-icon\")}\n />\n <Text className={namespaceClassNames(\"save-button__done-text\")}>\n <Translation\n defaultMessage=\"Done\"\n id=\"wjt0K3\"\n description=\"Button label indicating an action has completed successfully\"\n />\n </Text>\n </Flex>\n )}\n </ButtonComponent>\n );\n}\n"],"mappings":";;;;;;;;AAQA,MAAM,kBAAkB;AACxB,MAAM,oBAAoB;AAU1B,SAAgB,WAAW,EACzB,UAAU,OACV,UACA,SACA,MACA,QACA,GAAG,SACe;CAClB,MAAM,CAAC,OAAO,YAAY,SACxB,UAAU,YAAY,OAAO,SAAS,MACxC;CACA,MAAM,mBAAmB,OAAsB,IAAI;CACnD,MAAM,YAAY,OAAO,MAAM;CAC/B,MAAM,kBAAkB,UAAU,OAAOA;CAEzC,gBAAgB;EACd,UAAU,UAAU;CACtB,GAAG,CAAC,MAAM,CAAC;CAEX,gBAAgB;EACd,IAAI,SAAS;GAGX,SAAS,SAAS;GAClB,iBAAiB,UAAU,KAAK,IAAI;EACtC,OAAO,IAAI,MAAM;GACf,MAAM,cAAc,KAAK,IAAI;GAC7B,MAAM,kBAAkB,iBAAiB,UACrC,cAAc,iBAAiB,UAC/B;GAGJ,MAAM,iBAAiB,KAAK,IAAI,GAAG,oBAAoB,eAAe;GAEtE,IAAI,YAA2B;GAC/B,MAAM,aAAa,OAAO,iBAAiB;IACzC,SAAS,MAAM;IAEf,YAAY,OAAO,iBAAiB;KAClC,UAAU,UAAU;IACtB,GAAG,eAAe;GACpB,GAAG,cAAc;GAEjB,aAAa;IACX,OAAO,aAAa,UAAU;IAC9B,IAAI,cAAc,MAChB,OAAO,aAAa,SAAS;GAEjC;EACF,OAAO,IAAI,CAAC,WAAW,CAAC,MACtB,SAAS,MAAM;CAEnB,GAAG,CAAC,SAAS,IAAI,CAAC;CAElB,OACE,qBAAC,iBAAD;EACE,GAAI;EACJ,WAAW,oBAAoB,aAAa;EAC5C,mBAAiB;EAHnB,UAAA;GAKE,oBAAC,WAAD,EAAY,SAAoB,CAAA;GAC/B,UAAU,aACT,oBAAC,MAAD;IACE,IAAG;IACH,OAAM;IACN,SAAQ;IACR,UAAS;IACT,OAAM;IAEN,UAAA,oBAAC,SAAD,EAAS,MAAK,IAAK,CAAA;GACf,CAAA;GAGP,UAAU,UACT,qBAAC,MAAD;IACE,IAAG;IACH,OAAM;IACN,SAAQ;IACR,UAAS;IACT,OAAM;IACN,KAAI;IANN,UAAA,CAQE,oBAAC,WAAD;KACE,OAAM;KACN,QAAO;KACP,OAAO,EAAE,YAAY,OAAO;KAC5B,WAAW,oBAAoB,wBAAwB;IACxD,CAAA,GACD,oBAAC,MAAD;KAAM,WAAW,oBAAoB,wBAAwB;KAC3D,UAAA,oBAAC,aAAD;MACE,gBAAe;MACf,IAAG;MACH,aAAY;KACb,CAAA;IACG,CAAA,CACF;;EAEO;;AAErB"}
1
+ {"version":3,"file":"save-button.js","names":["Button"],"sources":["../../../src/lib/save-button.tsx"],"sourcesContent":["import { CheckIcon } from \"@radix-ui/react-icons\";\nimport { Flex, Slot, Slottable, Spinner, Text } from \"@radix-ui/themes\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { Button } from \"./elements.js\";\nimport type { ButtonProps } from \"./elements.js\";\nimport { Translation } from \"./i18n/translation.js\";\nimport { namespaceClassNames } from \"./widget-dom-props.js\";\n\nconst DONE_TIMEOUT_MS = 1500;\nconst SAVING_TIMEOUT_MS = 600;\n\ninterface SaveButtonProps extends ButtonProps {\n asChild?: boolean;\n children: React.ReactNode;\n loading?: boolean;\n done?: boolean;\n onDone?: () => void;\n}\n\nexport function SaveButton({\n asChild = false,\n children,\n loading,\n done,\n onDone,\n ...props\n}: SaveButtonProps) {\n // Whether the spinner has been visible for at least SAVING_TIMEOUT_MS, so a\n // fast response doesn't flash it for a single frame.\n const [minElapsed, setMinElapsed] = useState(!!done);\n const [prevLoading, setPrevLoading] = useState(loading);\n if (loading !== prevLoading) {\n setPrevLoading(loading);\n if (loading) {\n setMinElapsed(false);\n }\n }\n\n const state = loading\n ? \"loading\"\n : done\n ? minElapsed\n ? \"done\"\n : \"loading\"\n : \"idle\";\n\n const loadingStartTime = useRef<number | null>(null);\n const onDoneRef = useRef(onDone);\n const ButtonComponent = asChild ? Slot : Button;\n\n useEffect(() => {\n onDoneRef.current = onDone;\n }, [onDone]);\n\n useEffect(() => {\n if (loading) {\n loadingStartTime.current = Date.now();\n return;\n }\n if (!done) {\n return;\n }\n\n const loadingDuration = loadingStartTime.current\n ? Date.now() - loadingStartTime.current\n : 0;\n const remainingDelay = Math.max(0, SAVING_TIMEOUT_MS - loadingDuration);\n\n let doneTimer: number | null = null;\n const minElapsedTimer = window.setTimeout(() => {\n setMinElapsed(true);\n\n doneTimer = window.setTimeout(() => {\n onDoneRef.current?.();\n }, DONE_TIMEOUT_MS);\n }, remainingDelay);\n\n return () => {\n window.clearTimeout(minElapsedTimer);\n if (doneTimer !== null) {\n window.clearTimeout(doneTimer);\n }\n };\n }, [loading, done]);\n\n return (\n <ButtonComponent\n {...props}\n className={namespaceClassNames(\"save-button\")}\n data-save-state={state}\n >\n <Slottable>{children}</Slottable>\n {state === \"loading\" && (\n <Flex\n as=\"span\"\n align=\"center\"\n justify=\"center\"\n position=\"absolute\"\n inset=\"0\"\n >\n <Spinner size=\"1\" />\n </Flex>\n )}\n\n {state === \"done\" && (\n <Flex\n as=\"span\"\n align=\"center\"\n justify=\"center\"\n position=\"absolute\"\n inset=\"0\"\n gap=\"1\"\n >\n <CheckIcon\n width=\"18px\"\n height=\"18px\"\n style={{ marginLeft: \"-4px\" }}\n className={namespaceClassNames(\"save-button__done-icon\")}\n />\n <Text className={namespaceClassNames(\"save-button__done-text\")}>\n <Translation\n defaultMessage=\"Done\"\n id=\"wjt0K3\"\n description=\"Button label indicating an action has completed successfully\"\n />\n </Text>\n </Flex>\n )}\n </ButtonComponent>\n );\n}\n"],"mappings":";;;;;;;;AAQA,MAAM,kBAAkB;AACxB,MAAM,oBAAoB;AAU1B,SAAgB,WAAW,EACzB,UAAU,OACV,UACA,SACA,MACA,QACA,GAAG,SACe;CAGlB,MAAM,CAAC,YAAY,iBAAiB,SAAS,CAAC,CAAC,IAAI;CACnD,MAAM,CAAC,aAAa,kBAAkB,SAAS,OAAO;CACtD,IAAI,YAAY,aAAa;EAC3B,eAAe,OAAO;EACtB,IAAI,SACF,cAAc,KAAK;CAEvB;CAEA,MAAM,QAAQ,UACV,YACA,OACE,aACE,SACA,YACF;CAEN,MAAM,mBAAmB,OAAsB,IAAI;CACnD,MAAM,YAAY,OAAO,MAAM;CAC/B,MAAM,kBAAkB,UAAU,OAAOA;CAEzC,gBAAgB;EACd,UAAU,UAAU;CACtB,GAAG,CAAC,MAAM,CAAC;CAEX,gBAAgB;EACd,IAAI,SAAS;GACX,iBAAiB,UAAU,KAAK,IAAI;GACpC;EACF;EACA,IAAI,CAAC,MACH;EAGF,MAAM,kBAAkB,iBAAiB,UACrC,KAAK,IAAI,IAAI,iBAAiB,UAC9B;EACJ,MAAM,iBAAiB,KAAK,IAAI,GAAG,oBAAoB,eAAe;EAEtE,IAAI,YAA2B;EAC/B,MAAM,kBAAkB,OAAO,iBAAiB;GAC9C,cAAc,IAAI;GAElB,YAAY,OAAO,iBAAiB;IAClC,UAAU,UAAU;GACtB,GAAG,eAAe;EACpB,GAAG,cAAc;EAEjB,aAAa;GACX,OAAO,aAAa,eAAe;GACnC,IAAI,cAAc,MAChB,OAAO,aAAa,SAAS;EAEjC;CACF,GAAG,CAAC,SAAS,IAAI,CAAC;CAElB,OACE,qBAAC,iBAAD;EACE,GAAI;EACJ,WAAW,oBAAoB,aAAa;EAC5C,mBAAiB;EAHnB,UAAA;GAKE,oBAAC,WAAD,EAAY,SAAoB,CAAA;GAC/B,UAAU,aACT,oBAAC,MAAD;IACE,IAAG;IACH,OAAM;IACN,SAAQ;IACR,UAAS;IACT,OAAM;IAEN,UAAA,oBAAC,SAAD,EAAS,MAAK,IAAK,CAAA;GACf,CAAA;GAGP,UAAU,UACT,qBAAC,MAAD;IACE,IAAG;IACH,OAAM;IACN,SAAQ;IACR,UAAS;IACT,OAAM;IACN,KAAI;IANN,UAAA,CAQE,oBAAC,WAAD;KACE,OAAM;KACN,QAAO;KACP,OAAO,EAAE,YAAY,OAAO;KAC5B,WAAW,oBAAoB,wBAAwB;IACxD,CAAA,GACD,oBAAC,MAAD;KAAM,WAAW,oBAAoB,wBAAwB;KAC3D,UAAA,oBAAC,aAAD;MACE,gBAAe;MACf,IAAG;MACH,aAAY;KACb,CAAA;IACG,CAAA,CACF;;EAEO;;AAErB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workos-inc/widgets",
3
- "version": "1.20.2-next.1792439904014",
3
+ "version": "1.20.2-next.1792451814155",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -53,7 +53,7 @@ function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
53
53
  * Accepts callback refs and RefObject(s)
54
54
  */
55
55
  function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
56
- // eslint-disable-next-line react-hooks/exhaustive-deps
56
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
57
57
  return React.useCallback(composeRefs(...refs), refs);
58
58
  }
59
59
 
@@ -1,6 +1,6 @@
1
1
  import { Callout, Flex, Text } from "@radix-ui/themes";
2
2
  import type { PropsWithChildren } from "react";
3
- import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { useCallback, useRef, useState } from "react";
4
4
  import { useElevatedAccessToken } from "../api/api-provider.js";
5
5
  import { useClientGraphqlMutation } from "../api/client-graphql/hooks.js";
6
6
  import { useMe } from "./api/me.js";
@@ -24,12 +24,19 @@ export function ElevatedAccess({
24
24
  const [authenticationChallengeId, setAuthenticationChallengeId] =
25
25
  useState<string>();
26
26
 
27
- const prevAccessToken = useRef(elevatedAccess);
28
- const hasCalledOnVerified = useRef(false);
27
+ const [{ prevAccessToken, hasTokenExpired }, setPrevAccessState] = useState({
28
+ prevAccessToken: elevatedAccess,
29
+ hasTokenExpired: false,
30
+ });
31
+ if (elevatedAccess !== prevAccessToken) {
32
+ setPrevAccessState({
33
+ prevAccessToken: elevatedAccess,
34
+ hasTokenExpired:
35
+ hasTokenExpired || (!!prevAccessToken && !elevatedAccess),
36
+ });
37
+ }
29
38
 
30
- useEffect(() => {
31
- prevAccessToken.current = elevatedAccess;
32
- }, [elevatedAccess]);
39
+ const hasCalledOnVerified = useRef(false);
33
40
 
34
41
  const handleVerificationSuccess = useCallback(
35
42
  (elevatedAccessToken: string) => {
@@ -51,10 +58,6 @@ export function ElevatedAccess({
51
58
  }
52
59
 
53
60
  if (!authenticationChallengeId) {
54
- // FIXME: This should be refactored
55
- // eslint-disable-next-line react-hooks/refs
56
- const hasTokenExpired = !!prevAccessToken.current;
57
-
58
61
  return (
59
62
  <SendVerificationEmailForm
60
63
  type={type}
@@ -10,12 +10,15 @@ interface OptContextType {
10
10
  readOnly?: boolean;
11
11
  state?: "valid" | "invalid";
12
12
  onEnterPressed: () => void;
13
- onChildAdd: (input: HTMLInputElement) => void;
14
13
  onCharChange: (char: string, index: number) => void;
15
- allChildrenAdded: boolean;
14
+ onChildAdd: (element: HTMLInputElement | null, index: number) => void;
16
15
  }
17
16
 
18
17
  const OtpContext = React.createContext<OptContextType | undefined>(undefined);
18
+ OtpContext.displayName = "OtpContext";
19
+
20
+ const OtpIndexContext = React.createContext<number>(-1);
21
+ OtpIndexContext.displayName = "OtpIndexContext";
19
22
 
20
23
  type OtpRootProps = React.ComponentPropsWithoutRef<typeof Grid> & {
21
24
  id?: string;
@@ -43,8 +46,6 @@ export const Root = React.forwardRef<HTMLInputElement, OtpRootProps>(
43
46
  forwardedRef,
44
47
  ) {
45
48
  const [lastCharIndex, setLastCharIndex] = React.useState<number>(0);
46
- const [allChildrenAdded, setAllChildrenAdded] =
47
- React.useState<boolean>(false);
48
49
  const childCount = React.Children.count(children);
49
50
 
50
51
  // Internal array state is the source of truth so that empty slots between
@@ -75,7 +76,7 @@ export const Root = React.forwardRef<HTMLInputElement, OtpRootProps>(
75
76
  );
76
77
 
77
78
  const hiddenInputRef = React.useRef<HTMLInputElement>(null);
78
- const childrenRefs = React.useRef<HTMLInputElement[]>([]);
79
+ const childrenRefs = React.useRef<(HTMLInputElement | null)[]>([]);
79
80
 
80
81
  const attemptAutoSubmit = React.useCallback(
81
82
  (enterPressed = false) => {
@@ -97,19 +98,10 @@ export const Root = React.forwardRef<HTMLInputElement, OtpRootProps>(
97
98
  );
98
99
 
99
100
  const handleChildAdd = React.useCallback(
100
- (input: HTMLInputElement) => {
101
- if (input) {
102
- input.dataset.index = `${childrenRefs.current.length}`;
103
- childrenRefs.current.push(input);
104
- } else {
105
- childrenRefs.current.pop();
106
- }
107
-
108
- if (childrenRefs.current.length === childCount) {
109
- setAllChildrenAdded(true);
110
- }
101
+ (element: HTMLInputElement | null, index: number) => {
102
+ childrenRefs.current[index] = element;
111
103
  },
112
- [childCount],
104
+ [],
113
105
  );
114
106
 
115
107
  const handleCharChange = React.useCallback(
@@ -124,24 +116,23 @@ export const Root = React.forwardRef<HTMLInputElement, OtpRootProps>(
124
116
  [setValue],
125
117
  );
126
118
 
127
- const otpContext = React.useMemo(
128
- () => ({
129
- value,
130
- readOnly,
131
- state,
132
- allChildrenAdded,
133
- onEnterPressed: handleEnterPressed,
134
- onChildAdd: handleChildAdd,
135
- onCharChange: handleCharChange,
136
- }),
119
+ const otpContext = React.useMemo<OptContextType>(
120
+ () =>
121
+ ({
122
+ value,
123
+ readOnly,
124
+ state,
125
+ onEnterPressed: handleEnterPressed,
126
+ onCharChange: handleCharChange,
127
+ onChildAdd: handleChildAdd,
128
+ }) satisfies OptContextType,
137
129
  [
138
130
  value,
139
- allChildrenAdded,
140
131
  readOnly,
141
132
  state,
142
133
  handleEnterPressed,
143
- handleChildAdd,
144
134
  handleCharChange,
135
+ handleChildAdd,
145
136
  ],
146
137
  );
147
138
 
@@ -170,7 +161,11 @@ export const Root = React.forwardRef<HTMLInputElement, OtpRootProps>(
170
161
  childrenRefs.current?.[index]?.focus();
171
162
  }}
172
163
  >
173
- {children}
164
+ {React.Children.map(children, (child, index) => (
165
+ <OtpIndexContext.Provider value={index}>
166
+ {child}
167
+ </OtpIndexContext.Provider>
168
+ ))}
174
169
  <input
175
170
  ref={useComposedRefs(forwardedRef, hiddenInputRef)}
176
171
  defaultValue={value.join("")}
@@ -194,16 +189,16 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
194
189
  forwardedRef,
195
190
  ) {
196
191
  const otpContext = useOptContext();
197
- const inputRef = React.useRef<HTMLInputElement>(null);
198
- const composedInputRef = useComposedRefs(
199
- forwardedRef,
200
- inputRef,
201
- otpContext.onChildAdd,
192
+ const { onChildAdd } = otpContext;
193
+ const index = React.useContext(OtpIndexContext);
194
+ const inputRef = React.useCallback(
195
+ (input: HTMLInputElement | null) => {
196
+ onChildAdd(input, index);
197
+ },
198
+ [onChildAdd, index],
202
199
  );
200
+ const composedInputRef = useComposedRefs(forwardedRef, inputRef);
203
201
 
204
- // FIXME: This should be refactored
205
- // eslint-disable-next-line react-hooks/refs
206
- const index = Number(inputRef.current?.dataset.index ?? -1);
207
202
  const char = otpContext.value[index] ?? "";
208
203
 
209
204
  return (
@@ -230,7 +225,6 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
230
225
  // Update the value if it matches the input pattern (number only) or is empty (backspace)
231
226
  if (event.target.validity.valid || event.target.value === "") {
232
227
  const char = event.target.value;
233
- const index = Number(event.target.dataset.index ?? -1);
234
228
  otpContext.onCharChange(char, index);
235
229
  focusSibling(event.currentTarget, { back: char === "" });
236
230
  }
@@ -258,7 +252,6 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
258
252
 
259
253
  const useOptContext = () => {
260
254
  const optContext = React.useContext(OtpContext);
261
-
262
255
  if (!optContext) {
263
256
  throw new Error(
264
257
  "OtpInput compound components cannot be rendered outside the OtpRoot component",
@@ -25,9 +25,25 @@ export function SaveButton({
25
25
  onDone,
26
26
  ...props
27
27
  }: SaveButtonProps) {
28
- const [state, setState] = useState<"idle" | "loading" | "done">(
29
- loading ? "loading" : done ? "done" : "idle",
30
- );
28
+ // Whether the spinner has been visible for at least SAVING_TIMEOUT_MS, so a
29
+ // fast response doesn't flash it for a single frame.
30
+ const [minElapsed, setMinElapsed] = useState(!!done);
31
+ const [prevLoading, setPrevLoading] = useState(loading);
32
+ if (loading !== prevLoading) {
33
+ setPrevLoading(loading);
34
+ if (loading) {
35
+ setMinElapsed(false);
36
+ }
37
+ }
38
+
39
+ const state = loading
40
+ ? "loading"
41
+ : done
42
+ ? minElapsed
43
+ ? "done"
44
+ : "loading"
45
+ : "idle";
46
+
31
47
  const loadingStartTime = useRef<number | null>(null);
32
48
  const onDoneRef = useRef(onDone);
33
49
  const ButtonComponent = asChild ? Slot : Button;
@@ -38,37 +54,33 @@ export function SaveButton({
38
54
 
39
55
  useEffect(() => {
40
56
  if (loading) {
41
- // FIXME: This should be refactored
42
- // eslint-disable-next-line react-hooks/set-state-in-effect
43
- setState("loading");
44
57
  loadingStartTime.current = Date.now();
45
- } else if (done) {
46
- const currentTime = Date.now();
47
- const loadingDuration = loadingStartTime.current
48
- ? currentTime - loadingStartTime.current
49
- : 0;
58
+ return;
59
+ }
60
+ if (!done) {
61
+ return;
62
+ }
50
63
 
51
- // If loading lasted less than 500 ms, wait for the remaining time
52
- const remainingDelay = Math.max(0, SAVING_TIMEOUT_MS - loadingDuration);
64
+ const loadingDuration = loadingStartTime.current
65
+ ? Date.now() - loadingStartTime.current
66
+ : 0;
67
+ const remainingDelay = Math.max(0, SAVING_TIMEOUT_MS - loadingDuration);
53
68
 
54
- let doneTimer: number | null = null;
55
- const savedTimer = window.setTimeout(() => {
56
- setState("done");
69
+ let doneTimer: number | null = null;
70
+ const minElapsedTimer = window.setTimeout(() => {
71
+ setMinElapsed(true);
57
72
 
58
- doneTimer = window.setTimeout(() => {
59
- onDoneRef.current?.();
60
- }, DONE_TIMEOUT_MS);
61
- }, remainingDelay);
73
+ doneTimer = window.setTimeout(() => {
74
+ onDoneRef.current?.();
75
+ }, DONE_TIMEOUT_MS);
76
+ }, remainingDelay);
62
77
 
63
- return () => {
64
- window.clearTimeout(savedTimer);
65
- if (doneTimer !== null) {
66
- window.clearTimeout(doneTimer);
67
- }
68
- };
69
- } else if (!loading && !done) {
70
- setState("idle");
71
- }
78
+ return () => {
79
+ window.clearTimeout(minElapsedTimer);
80
+ if (doneTimer !== null) {
81
+ window.clearTimeout(doneTimer);
82
+ }
83
+ };
72
84
  }, [loading, done]);
73
85
 
74
86
  return (