diff --git a/.cursor/rules/components.mdc b/.cursor/rules/components.mdc index 521b54f..4cfc431 100644 --- a/.cursor/rules/components.mdc +++ b/.cursor/rules/components.mdc @@ -328,10 +328,9 @@ import { combineClassNames } from "@/submodules/javascript-functions/general"; ```typescript // pages/settings.tsx diff --git a/pages/_app.tsx b/pages/_app.tsx index 16dfdb6..1bf6972 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -22,7 +22,7 @@ function MyApp({ Component, pageProps }: AppProps) { -
+
diff --git a/pages/error.tsx b/pages/error.tsx new file mode 100644 index 0000000..ac0b94d --- /dev/null +++ b/pages/error.tsx @@ -0,0 +1,27 @@ +import type { NextPage } from "next" +import Head from "next/head" +import { KernLogo } from "@/pkg/ui/Icons" + +const ErrorPage: NextPage = () => { + + return ( + <> + + Error + + +
+ +
+

An error occurred

+ +

Please contact support if the problem persists.

+
+
+
+
+ + ) +} + +export default ErrorPage \ No newline at end of file diff --git a/pages/invite.tsx b/pages/invite.tsx new file mode 100644 index 0000000..0a4095c --- /dev/null +++ b/pages/invite.tsx @@ -0,0 +1,83 @@ +import { RecoveryFlow, UpdateRecoveryFlowBody } from "@ory/client" +import type { NextPage } from "next" +import Head from "next/head" +import { useRouter } from "next/router" +import { useEffect, useState } from "react" + +import { Flow } from "../pkg" +import { handleFlowError } from "../pkg/errors" +import ory from "../pkg/sdk" +import { KernLogo } from "@/pkg/ui/Icons" + +const InvitePage: NextPage = () => { + const router = useRouter() + const { flow: flowId } = router.query + + const [flow, setFlow] = useState(null) + + useEffect(() => { + if (!router.isReady || !flowId) return + ory + .getRecoveryFlow({ id: String(flowId) }) + .then(({ data }) => { + data.ui.nodes.forEach((node) => { + const attrs = node.attributes as { name?: string } + if (attrs.name === "code") { + node.meta.label = { text: "Enter the code from your email", id: 0, type: "info" } + } + if (attrs.name === "email") { + node.meta.label = { text: "Your email address", id: 0, type: "info" } + } + }) + setFlow(data) + }) + .catch((err) => handleFlowError(router, "recovery", setFlow)(err)) + }, [router.isReady, flowId]) + + const onSubmit = (values: UpdateRecoveryFlowBody) => { + if (!flow?.id) return + return router + .push(`/invite?flow=${flow?.id}`, undefined, { shallow: true }) + .then(() => + ory + .updateRecoveryFlow({ + flow: String(flow?.id), + updateRecoveryFlowBody: values, + }) + .then(({ data }) => { + setFlow(data) + }) + .catch(handleFlowError(router, "recovery", setFlow)) + .catch((err: any) => { + if (err.response?.status === 400) { + setFlow(err.response?.data) + return + } + throw err + }) + ) + } + + if (!flow) return null + + return ( + <> + + Accept Invitation + + +
+ +
+

Accept Your Invitation

+ + +
+
+
+
+ + ) +} + +export default InvitePage \ No newline at end of file diff --git a/pages/login.tsx b/pages/login.tsx index e38623f..7dea621 100644 --- a/pages/login.tsx +++ b/pages/login.tsx @@ -132,7 +132,7 @@ const Login: NextPage = () => {

Sign in to your account

Or - Register account - + Register account - no credit card required!

@@ -159,9 +159,9 @@ const Login: NextPage = () => { { !isAccLinkageRequested ? <> - {displayMailForm ? Forgot your password? : null} + {displayMailForm ? Forgot your password? : null} - : Go back to login + : Go back to login }
diff --git a/pages/recovery.tsx b/pages/recovery.tsx index ed6b8fb..cd47d9f 100644 --- a/pages/recovery.tsx +++ b/pages/recovery.tsx @@ -1,5 +1,4 @@ import { RecoveryFlow, UpdateRecoveryFlowBody } from "@ory/client" -import { AxiosError } from "axios" import type { NextPage } from "next" import Head from "next/head" import { useRouter } from "next/router" @@ -11,99 +10,99 @@ import ory from "../pkg/sdk" import { KernLogo } from "@/pkg/ui/Icons" const Recovery: NextPage = () => { - const [initialFlow, setInitialFlow]: any = useState() - const [changedFlow, setChangedFlow] = useState() - - // Get ?flow=... from the URL const router = useRouter() - const { flow: flowId, return_to: returnTo } = router.query + const { flow: flowId } = router.query - useEffect(() => { - // If the router is not ready yet, or we already have a flow, do nothing. - if (!router.isReady || initialFlow) { - return - } + const [flow, setFlow] = useState() - // If ?flow=.. was in the URL, we fetch it - if (flowId) { - ory - .getRecoveryFlow({ id: String(flowId) }) - .then(({ data }) => { - setInitialFlow(data) - }) - .catch(handleFlowError(router, "recovery", setInitialFlow)) - return - } + useEffect(() => { + if (!router.isReady) return - // Otherwise we initialize it - ory - .createBrowserRecoveryFlow() - .then(({ data }) => { - setInitialFlow(data) - }) - .catch(handleFlowError(router, "recovery", setInitialFlow)) - .catch((err: AxiosError) => { - // If the previous handler did not catch the error it's most likely a form validation error - if (err.response?.status === 400) { - // Yup, it is! - setInitialFlow(err.response?.data) - return + const fetchFlow = async () => { + try { + let data: RecoveryFlow + if (flowId) { + const res = await ory.getRecoveryFlow({ id: String(flowId) }) + data = res.data + } else { + const res = await ory.createBrowserRecoveryFlow({ + returnTo: "/settings", + }) + data = res.data } - return Promise.reject(err) - }) - }, [flowId, router, router.isReady, returnTo, initialFlow]) + data.ui.nodes.forEach((node) => { + const attrs = node.attributes as { name?: string; value?: string; disabled?: boolean; type?: string; required?: boolean } + if (attrs.name === "code") { + node.meta.label = { + text: "Enter your recovery code", + id: 0, + type: "info", + } + attrs.required = false + } + if (attrs.name === "email") { + node.meta.label = { text: "Your email address", id: 0, type: "info" } + } + if (attrs.type === "button" || (attrs.name === "method" && attrs.value === "link")) { + attrs.disabled = false + } + }) - useEffect(() => { - if (!initialFlow) return - initialFlow.ui.nodes[1].meta.label = { text: "Email address", id: 0, type: "info" } - if (initialFlow.ui.nodes[2].meta.label) { - initialFlow.ui.nodes[2].meta.label.text = "Send reset code to mail" + setFlow(data) + } catch (err: any) { + handleFlowError(router, "recovery", setFlow)(err) + } } - setChangedFlow(initialFlow) - }, [initialFlow]) + + fetchFlow() + }, [router.isReady, flowId]) const onSubmit = (values: UpdateRecoveryFlowBody) => router - // On submission, add the flow ID to the URL but do not navigate. This prevents the user loosing - // his data when she/he reloads the page. - .push(`/recovery?flow=${initialFlow?.id}`, undefined, { shallow: true }) + .push(`${router.pathname}?flow=${flow?.id}`, undefined, { + shallow: true, + }) .then(() => ory .updateRecoveryFlow({ - flow: String(initialFlow?.id), + flow: String(flow?.id), updateRecoveryFlowBody: values, }) .then(({ data }) => { - // Form submission was successful, show the message to the user! - setInitialFlow(data) + setFlow(data) }) - .catch(handleFlowError(router, "recovery", setInitialFlow)) - .catch((err: AxiosError) => { - switch (err.response?.status) { - case 400: - // Status code 400 implies the form validation had an error - setInitialFlow(err.response?.data) - return + .catch(handleFlowError(router, "recovery", setFlow)) + .catch((err: any) => { + if (err.response?.status === 400) { + setFlow(err.response?.data) + return } - throw err - }), + }) ) + if (!flow) return null + return ( <> Recovery - +
-
-

Recover your account

- +
+

+ Recover your account +

+ + +
@@ -113,4 +112,4 @@ const Recovery: NextPage = () => { ) } -export default Recovery +export default Recovery \ No newline at end of file diff --git a/pages/registration.tsx b/pages/registration.tsx index 1df3d75..1760444 100644 --- a/pages/registration.tsx +++ b/pages/registration.tsx @@ -117,7 +117,7 @@ const Registration: NextPage = () => {
diff --git a/pages/settings.tsx b/pages/settings.tsx index 6fb3a57..bb2143f 100644 --- a/pages/settings.tsx +++ b/pages/settings.tsx @@ -329,7 +329,7 @@ const Settings: NextPage = () => { ) : (<> )}
-
diff --git a/pages/verify.tsx b/pages/verify.tsx index 28c0f1e..bb1e12d 100644 --- a/pages/verify.tsx +++ b/pages/verify.tsx @@ -1,128 +1,93 @@ import { VerificationFlow, UpdateVerificationFlowBody } from "@ory/client" -import { AxiosError } from "axios" import type { NextPage } from "next" import Head from "next/head" import { useRouter } from "next/router" import { useEffect, useState } from "react" import { Flow } from "../pkg" +import { handleFlowError } from "../pkg/errors" import ory from "../pkg/sdk" import { KernLogo } from "@/pkg/ui/Icons" const Verification: NextPage = () => { - const [initialFlow, setInitialFlow] = useState() - const [changedFlow, setChangedFlow] = useState() - - // Get ?flow=... from the URL const router = useRouter() - const { flow: flowId, return_to: returnTo } = router.query + const { flow: flowId, state: flowState } = router.query - useEffect(() => { - // If the router is not ready yet, or we already have a flow, do nothing. - if (!router.isReady || initialFlow) { - return - } - - // If ?flow=.. was in the URL, we fetch it - if (flowId) { - ory - .getVerificationFlow({ id: String(flowId) }) - .then(({ data }) => { - setInitialFlow(data) - }) - .catch((err: AxiosError) => { - switch (err.response?.status) { - case 410: - // Status code 410 means the request has expired - so let's load a fresh flow! - case 403: - // Status code 403 implies some other issue (e.g. CSRF) - let's reload! - return router.push("/verify") - } + const [flow, setFlow] = useState(null) - throw err - }) + useEffect(() => { + if (!router.isReady) return + if (!flowId) { + router.replace("/error") return } - - // Otherwise we initialize it ory - .createBrowserVerificationFlow({ - returnTo: returnTo ? String(returnTo) : undefined, - }) + .getVerificationFlow({ id: String(flowId) }) .then(({ data }) => { - setInitialFlow(data) - }) - .catch((err: AxiosError) => { - switch (err.response?.status) { - case 400: - // Status code 400 implies the user is already signed in - return router.push("/") + if (flowState === "success") { + const returnTo = (data as VerificationFlow & { return_to?: string }).return_to + window.location.href = returnTo || "/cognition" + return } - - throw err + data.ui.nodes.forEach((node) => { + const attrs = node.attributes as { name?: string; value?: string } + if (attrs.name === "code") { + node.meta.label = { text: "Enter the code from your email", id: 0, type: "info" } + attrs.value = "" + } + }) + setFlow(data) }) - }, [flowId, router, router.isReady, returnTo, initialFlow]) - - useEffect(() => { - if (!initialFlow) return - initialFlow.ui.nodes[1].meta.label = { text: "Email address", id: 0, type: "info" } - setChangedFlow(initialFlow) - }, [initialFlow]) + .catch((err) => handleFlowError(router, "verification", setFlow)(err)) + }, [router.isReady, flowId, flowState]) - const onSubmit = async (values: UpdateVerificationFlowBody) => { - await router - // On submission, add the flow ID to the URL but do not navigate. This prevents the user loosing - // their data when they reload the page. - .push(`/verify?flow=${initialFlow?.id}`, undefined, { shallow: true }) - - ory - .updateVerificationFlow({ - flow: String(initialFlow?.id), - updateVerificationFlowBody: values, + const onSubmit = (values: UpdateVerificationFlowBody) => + router + .push(`${router.pathname}?flow=${flow?.id}`, undefined, { + shallow: true, }) - .then(({ data }) => { - // Form submission was successful, show the message to the user! - setInitialFlow(data) - }) - .catch((err: AxiosError) => { - switch (err.response?.status) { - case 400: - // Status code 400 implies the form validation had an error - setInitialFlow(err.response?.data as VerificationFlow | undefined) - return - case 410: - const newFlowID = (err.response.data as any).use_flow_id - router - // On submission, add the flow ID to the URL but do not navigate. This prevents the user loosing - // their data when they reload the page. - .push(`/verify?flow=${newFlowID}`, undefined, { - shallow: true, - }) - - ory - .getVerificationFlow({ id: newFlowID }) - .then(({ data }) => setInitialFlow(data)) - return - } + .then(() => + ory + .updateVerificationFlow({ + flow: String(flow?.id), + updateVerificationFlowBody: values, + }) + .then(({ data }) => { + const returnTo = (data as VerificationFlow & { return_to?: string }).return_to + window.location.href = returnTo || "/cognition" + }) + .catch((err: any) => { + if (err.response?.data?.error?.id === "browser_location_change_required") { + window.location.href = "/cognition" + return + } + return handleFlowError(router, "verification", setFlow)(err) + }) + .catch((err: any) => { + if (err.response?.status === 400) { + setFlow(err.response?.data) + return + } + throw err + }) + ) - throw err - }) - } + if (!flow) return null return ( <> - Verification - + Verify +
-

Verify your account

- - +

+ Verify your account +

+ +
@@ -131,4 +96,4 @@ const Verification: NextPage = () => { ) } -export default Verification +export default Verification \ No newline at end of file diff --git a/pkg/ui/Messages.tsx b/pkg/ui/Messages.tsx index bcb4000..e7ef5fd 100644 --- a/pkg/ui/Messages.tsx +++ b/pkg/ui/Messages.tsx @@ -13,7 +13,7 @@ export const Message = ({ message }: MessageProps) => { return ( - + {displayMessage(message, language)} diff --git a/pkg/ui/NodeAnchor.tsx b/pkg/ui/NodeAnchor.tsx index 71f96e0..8178cb7 100644 --- a/pkg/ui/NodeAnchor.tsx +++ b/pkg/ui/NodeAnchor.tsx @@ -9,7 +9,6 @@ interface Props { export const NodeAnchor = ({ node, attributes }: Props) => { return ( diff --git a/pkg/ui/NodeInputSubmit.tsx b/pkg/ui/NodeInputSubmit.tsx index 1c79504..b71a1f5 100644 --- a/pkg/ui/NodeInputSubmit.tsx +++ b/pkg/ui/NodeInputSubmit.tsx @@ -37,6 +37,7 @@ export function NodeInputSubmit({ name={attributes.name} value={attributes.value || ""} disabled={attributes.disabled || disabled} + formNoValidate={node.group === "link" || /resend/i.test(getNodeLabel(node))} > {buttonName} diff --git a/pkg/ui/NodeScript.tsx b/pkg/ui/NodeScript.tsx index 1a47b67..4c9f0b4 100644 --- a/pkg/ui/NodeScript.tsx +++ b/pkg/ui/NodeScript.tsx @@ -11,7 +11,6 @@ export const NodeScript = ({ attributes }: Props) => { const script = document.createElement("script") script.async = true - script.setAttribute("data-testid", `node/script/${attributes.id}`) script.src = attributes.src script.async = attributes.async script.crossOrigin = attributes.crossorigin diff --git a/pkg/ui/NodeText.tsx b/pkg/ui/NodeText.tsx index 82639f3..1dc2ccf 100644 --- a/pkg/ui/NodeText.tsx +++ b/pkg/ui/NodeText.tsx @@ -17,28 +17,21 @@ const Content = ({ node, attributes }: Props) => { // This text node contains lookup secrets. Let's make them a bit more beautiful! const secrets = (attributes.text.context as any).secrets.map( (text: UiText, k: number) => ( -
+
{/* Used lookup_secret has ID 1050014 */} {text.id === 1050014 ? "Used" : text.text}
), ) return ( -
+
{secrets}
) } return ( -
+
) @@ -47,7 +40,7 @@ const Content = ({ node, attributes }: Props) => { export const NodeText = ({ node, attributes }: Props) => { return ( <> -

+

{node.meta?.label?.text}