From 5cd51a17dd157958426447d3ab171f8f0c717cfe Mon Sep 17 00:00:00 2001 From: Lina Date: Wed, 25 Feb 2026 12:01:12 +0100 Subject: [PATCH 1/5] Test codes --- pages/invite.tsx | 82 +++++++++++++++++++++++++ pages/recovery.tsx | 138 ++++++++++++++++++++--------------------- pages/verify.tsx | 150 +++++++++++++++------------------------------ 3 files changed, 201 insertions(+), 169 deletions(-) create mode 100644 pages/invite.tsx diff --git a/pages/invite.tsx b/pages/invite.tsx new file mode 100644 index 0000000..cd0dcb8 --- /dev/null +++ b/pages/invite.tsx @@ -0,0 +1,82 @@ +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) => + router + .push(`/invite?flow=${flow?.id}`, undefined, { shallow: true }) + .then(() => + ory + .updateRecoveryFlow({ + flow: String(flow?.id), + updateRecoveryFlowBody: values, + }) + .then(({ data }) => { + setFlow(data) + if (data.state === "passed_challenge") { + router.push("/set-password?invite=true") + } + }) + .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/recovery.tsx b/pages/recovery.tsx index ed6b8fb..abf36cd 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,106 +10,107 @@ 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, invite } = router.query + const isInvite = invite === "true" - 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: isInvite ? "/set-password?invite=true" : undefined, + }) + 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 } + if (attrs.name === "code") { + node.meta.label = { + text: isInvite ? "Enter the code from your email" : "Enter your recovery code", + id: 0, + type: "info", + } + } + if (attrs.name === "email") { + node.meta.label = { text: "Your email address", id: 0, type: "info" } + } + }) - 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, isInvite]) 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}${isInvite ? "&invite=true" : ""}`, 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) + if (data.state === "passed_challenge") { + router.push(isInvite ? "/set-password?invite=true" : "/") + } }) - .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 - + {isInvite ? "Accept Invitation" : "Recover Account"} +
+
-

Recover your account

- - +

+ {isInvite ? "Accept Your Invitation" : "Recover your account"} +

+ + + + {!isInvite && ( + + )}
-
-
) } -export default Recovery +export default Recovery \ No newline at end of file diff --git a/pages/verify.tsx b/pages/verify.tsx index 28c0f1e..857486d 100644 --- a/pages/verify.tsx +++ b/pages/verify.tsx @@ -1,134 +1,84 @@ 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 - - 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") - } - - throw err - }) - return - } - - // Otherwise we initialize it - ory - .createBrowserVerificationFlow({ - returnTo: returnTo ? String(returnTo) : undefined, - }) - .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("/") - } + const { flow: flowId, invite } = router.query + const isInvite = invite === "true" - throw err - }) - }, [flowId, router, router.isReady, returnTo, initialFlow]) + const [flow, setFlow] = useState(null) useEffect(() => { - if (!initialFlow) return - initialFlow.ui.nodes[1].meta.label = { text: "Email address", id: 0, type: "info" } - setChangedFlow(initialFlow) - }, [initialFlow]) - - 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 }) - + if (!router.isReady || !flowId) return ory - .updateVerificationFlow({ - flow: String(initialFlow?.id), - updateVerificationFlowBody: values, - }) + .getVerificationFlow({ id: String(flowId) }) .then(({ data }) => { - // Form submission was successful, show the message to the user! - setInitialFlow(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" } + } + }) + setFlow(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 - } + .catch((err) => handleFlowError(router, "verification", setFlow)(err)) + }, [router.isReady, flowId]) - throw err + const onSubmit = (values: UpdateVerificationFlowBody) => + router + .push(`${router.pathname}?flow=${flow?.id}${isInvite ? "&invite=true" : ""}`, undefined, { + shallow: true, }) - } + .then(() => + ory + .updateVerificationFlow({ + flow: String(flow?.id), + updateVerificationFlowBody: values, + }) + .then(({ data }) => { + setFlow(data) + if (data.state === "passed_challenge") { + router.push(isInvite ? "/set-password?invite=true" : "/") + } + }) + .catch(handleFlowError(router, "verification", setFlow)) + .catch((err: any) => { + if (err.response?.status === 400) { + setFlow(err.response?.data) + return + } + throw err + }) + ) + + if (!flow) return null return ( <> - Verification - + {isInvite ? "Accept Invitation" : "Verify"} +
-

Verify your account

- - +

+ {isInvite ? "Accept Your Invitation" : "Verify your account"} +

+ +
-
-
) } -export default Verification +export default Verification \ No newline at end of file From ca43d22211e1dbb5183e4cfc22ba83483ac9eaf2 Mon Sep 17 00:00:00 2001 From: Lina Date: Wed, 25 Feb 2026 17:31:27 +0100 Subject: [PATCH 2/5] Verification page improvements --- pages/verify.tsx | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/pages/verify.tsx b/pages/verify.tsx index 857486d..9c690e3 100644 --- a/pages/verify.tsx +++ b/pages/verify.tsx @@ -11,8 +11,7 @@ import { KernLogo } from "@/pkg/ui/Icons" const Verification: NextPage = () => { const router = useRouter() - const { flow: flowId, invite } = router.query - const isInvite = invite === "true" + const { flow: flowId, state: flowState } = router.query const [flow, setFlow] = useState(null) @@ -21,20 +20,26 @@ const Verification: NextPage = () => { ory .getVerificationFlow({ id: String(flowId) }) .then(({ data }) => { + if (flowState === "success") { + const returnTo = (data as VerificationFlow & { return_to?: string }).return_to + window.location.href = returnTo || "/cognition" + return + } data.ui.nodes.forEach((node) => { - const attrs = node.attributes as { name?: string } + 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) }) .catch((err) => handleFlowError(router, "verification", setFlow)(err)) - }, [router.isReady, flowId]) + }, [router.isReady, flowId, flowState]) const onSubmit = (values: UpdateVerificationFlowBody) => router - .push(`${router.pathname}?flow=${flow?.id}${isInvite ? "&invite=true" : ""}`, undefined, { + .push(`${router.pathname}?flow=${flow?.id}`, undefined, { shallow: true, }) .then(() => @@ -44,12 +49,16 @@ const Verification: NextPage = () => { updateVerificationFlowBody: values, }) .then(({ data }) => { - setFlow(data) - if (data.state === "passed_challenge") { - router.push(isInvite ? "/set-password?invite=true" : "/") + 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(handleFlowError(router, "verification", setFlow)) .catch((err: any) => { if (err.response?.status === 400) { setFlow(err.response?.data) @@ -64,19 +73,21 @@ const Verification: NextPage = () => { return ( <> - {isInvite ? "Accept Invitation" : "Verify"} + Verify

- {isInvite ? "Accept Your Invitation" : "Verify your account"} + Verify your account

+
+
) } From 67fc9d83b3942bd6b4761b194d42a6b1d7883a92 Mon Sep 17 00:00:00 2001 From: Lina Date: Thu, 26 Feb 2026 10:47:30 +0100 Subject: [PATCH 3/5] Recovery improvements --- pages/recovery.tsx | 41 +++++++++++++++++++------------------- pkg/ui/NodeInputButton.tsx | 1 + pkg/ui/NodeInputSubmit.tsx | 1 + 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/pages/recovery.tsx b/pages/recovery.tsx index abf36cd..cd47d9f 100644 --- a/pages/recovery.tsx +++ b/pages/recovery.tsx @@ -11,8 +11,7 @@ import { KernLogo } from "@/pkg/ui/Icons" const Recovery: NextPage = () => { const router = useRouter() - const { flow: flowId, invite } = router.query - const isInvite = invite === "true" + const { flow: flowId } = router.query const [flow, setFlow] = useState() @@ -27,23 +26,27 @@ const Recovery: NextPage = () => { data = res.data } else { const res = await ory.createBrowserRecoveryFlow({ - returnTo: isInvite ? "/set-password?invite=true" : undefined, + returnTo: "/settings", }) data = res.data } data.ui.nodes.forEach((node) => { - const attrs = node.attributes as { name?: string } + const attrs = node.attributes as { name?: string; value?: string; disabled?: boolean; type?: string; required?: boolean } if (attrs.name === "code") { node.meta.label = { - text: isInvite ? "Enter the code from your email" : "Enter your recovery code", + 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 + } }) setFlow(data) @@ -53,11 +56,11 @@ const Recovery: NextPage = () => { } fetchFlow() - }, [router.isReady, flowId, isInvite]) + }, [router.isReady, flowId]) const onSubmit = (values: UpdateRecoveryFlowBody) => router - .push(`${router.pathname}?flow=${flow?.id}${isInvite ? "&invite=true" : ""}`, undefined, { + .push(`${router.pathname}?flow=${flow?.id}`, undefined, { shallow: true, }) .then(() => @@ -68,9 +71,6 @@ const Recovery: NextPage = () => { }) .then(({ data }) => { setFlow(data) - if (data.state === "passed_challenge") { - router.push(isInvite ? "/set-password?invite=true" : "/") - } }) .catch(handleFlowError(router, "recovery", setFlow)) .catch((err: any) => { @@ -87,28 +87,27 @@ const Recovery: NextPage = () => { return ( <> - {isInvite ? "Accept Invitation" : "Recover Account"} + Recovery
- -
+

- {isInvite ? "Accept Your Invitation" : "Recover your account"} + Recover your account

- {!isInvite && ( - - )} +
+
+
) } diff --git a/pkg/ui/NodeInputButton.tsx b/pkg/ui/NodeInputButton.tsx index 3460520..80811e2 100644 --- a/pkg/ui/NodeInputButton.tsx +++ b/pkg/ui/NodeInputButton.tsx @@ -37,6 +37,7 @@ export function NodeInputButton({ }} value={attributes.value || ""} disabled={attributes.disabled || disabled} + formNoValidate > {getNodeLabel(node)} 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} From a9c3082148083ce978aab75c7eeab5ab4a688200 Mon Sep 17 00:00:00 2001 From: Lina Date: Thu, 26 Feb 2026 12:13:44 +0100 Subject: [PATCH 4/5] Invite users improvements --- pages/invite.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pages/invite.tsx b/pages/invite.tsx index cd0dcb8..e35fff5 100644 --- a/pages/invite.tsx +++ b/pages/invite.tsx @@ -75,6 +75,8 @@ const InvitePage: NextPage = () => {
+
+
) } From 77444be983837afa4dbe8e65575c2bd17b0100f8 Mon Sep 17 00:00:00 2001 From: Lina Date: Fri, 27 Feb 2026 08:35:13 +0100 Subject: [PATCH 5/5] PR comments --- .cursor/rules/components.mdc | 7 +++---- pages/_app.tsx | 2 +- pages/error.tsx | 27 +++++++++++++++++++++++++++ pages/invite.tsx | 9 ++++----- pages/login.tsx | 6 +++--- pages/registration.tsx | 2 +- pages/settings.tsx | 2 +- pages/verify.tsx | 6 +++++- pkg/ui/Messages.tsx | 2 +- pkg/ui/NodeAnchor.tsx | 1 - pkg/ui/NodeImage.tsx | 1 - pkg/ui/NodeScript.tsx | 1 - pkg/ui/NodeText.tsx | 15 ++++----------- 13 files changed, 50 insertions(+), 31 deletions(-) create mode 100644 pages/error.tsx 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 index e35fff5..0a4095c 100644 --- a/pages/invite.tsx +++ b/pages/invite.tsx @@ -34,8 +34,9 @@ const InvitePage: NextPage = () => { .catch((err) => handleFlowError(router, "recovery", setFlow)(err)) }, [router.isReady, flowId]) - const onSubmit = (values: UpdateRecoveryFlowBody) => - router + const onSubmit = (values: UpdateRecoveryFlowBody) => { + if (!flow?.id) return + return router .push(`/invite?flow=${flow?.id}`, undefined, { shallow: true }) .then(() => ory @@ -45,9 +46,6 @@ const InvitePage: NextPage = () => { }) .then(({ data }) => { setFlow(data) - if (data.state === "passed_challenge") { - router.push("/set-password?invite=true") - } }) .catch(handleFlowError(router, "recovery", setFlow)) .catch((err: any) => { @@ -58,6 +56,7 @@ const InvitePage: NextPage = () => { throw err }) ) + } if (!flow) return null 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/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 9c690e3..bb1e12d 100644 --- a/pages/verify.tsx +++ b/pages/verify.tsx @@ -16,7 +16,11 @@ const Verification: NextPage = () => { const [flow, setFlow] = useState(null) useEffect(() => { - if (!router.isReady || !flowId) return + if (!router.isReady) return + if (!flowId) { + router.replace("/error") + return + } ory .getVerificationFlow({ id: String(flowId) }) .then(({ data }) => { 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 (