cold-air-52564
03/12/2025, 8:57 AMbrief-egg-61061
03/12/2025, 8:57 AMbrief-egg-61061
03/12/2025, 8:58 AMhttps://${process.env.NEXT_PUBLIC_ORY_PROJECT_SLUG}.<http://projects.oryapis.com|projects.oryapis.com>
,
baseOptions: {
withCredentials: true, // we need to include cookies
},
}),
)brief-egg-61061
03/12/2025, 8:58 AMbrief-egg-61061
03/12/2025, 8:58 AMbrief-egg-61061
03/12/2025, 8:58 AMtsx // pages/login.tsx import { Configuration, FrontendApi, LoginFlow, UiNode, UiNodeInputAttributes, } from "@ory/client" import { filterNodesByGroups, isUiNodeInputAttributes, } from "@ory/integrations/ui" import { useEffect, useState } from "react" import { useRouter } from "next/router" import { edgeConfig } from "@ory/integrations/next" const frontend = new FrontendApi( new Configuration({ basePath: `https://${process.env.NEXT_PUBLIC_ORY_PROJECT_SLUG}.<http://projects.oryapis.com|projects.oryapis.com>`, baseOptions: { withCredentials: true, // we need to include cookies }, }), ) export default function Login() { const [flow, setFlow] = useState<LoginFlow>() const router = useRouter() useEffect(() => { // Check if flow ID exists in URL const flowId = router.query.flow as string if (flowId) { // If we have a flow ID, get the flow frontend .getLoginFlow({ id: flowId }) .then(({ data }) => { setFlow(data) }) .catch((err) => { console.error("Failed to get login flow", err) // Start a new flow if there was an error initializeFlow() }) } else { // Initialize a new flow initializeFlow() } }, [router.query.flow]) const initializeFlow = () => { // Get the return_to from the URL const returnTo = router.query.return_to as string frontend .createBrowserLoginFlow({ returnTo: returnTo || "/", // redirect to the root path after login refresh: true, // if the user has a session, refresh it }) .then(({ data }) => { setFlow(data) }) .catch((err) => { console.error("Failed to initialize login flow", err) }) } const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault() if (!flow) return const form = event.currentTarget const formData = new FormData(form) // Map the entire form data to JSON for the request body let body = Object.fromEntries(formData) as any // We need the method specified from the name and value of the submit button if ("submitter" in event.nativeEvent) { const method = ( event.nativeEvent as unknown as { submitter: HTMLInputElement } ).submitter body = { ...body, ...{ [method.name]: method.value }, } } frontend .updateLoginFlow({ flow: flow.id, updateLoginFlowBody: body, }) .then(() => { // Redirect to home page or the return_to URL router.push(router.query.return_to as string || "/") }) .catch((err) => { if (err.response?.status === 400) { // User input error, update the flow with error messages setFlow(err.response.data) } else { console.error("Login failed", err) } }) } const renderNode = (node: UiNode) => { if (isUiNodeInputAttributes(node.attributes)) { const attrs = node.attributes as UiNodeInputAttributes switch (attrs.type) { case "button": case "submit": return ( <button key={attrs.name} type={attrs.type as "submit" | "reset" | "button" | undefined} name={attrs.name} value={attrs.value} className="px-4 py-2 bg-blue-500 text-white rounded" > {node.meta.label?.text || attrs.value} </button> ) default: return ( <div key={attrs.name} className="mb-4"> <label className="block text-gray-700 mb-2"> {node.meta.label?.text} </label> <input name={attrs.name} type={attrs.type} autoComplete={attrs.autocomplete} defaultValue={attrs.value} required={attrs.required} disabled={attrs.disabled} className="w-full px-3 py-2 border rounded" /> {node.messages?.map((message, idx) => ( <p key={idx} className="text-red-500 text-sm mt-1"> {message.text} </p> ))} </div> ) } } return null } if (!flow) { return <div>Loading...</div> } return ( <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded shadow-md"> <h1 className="text-2xl font-bold mb-6">Login</h1> <form action={flow.ui.action} method={flow.ui.method} onSubmit={handleSubmit}> {/* Display any flow level messages/errors */} {flow.ui.messages?.map((message, idx) => ( <div key={idx} className="mb-4 p-2 bg-red-100 text-red-700 rounded"> {message.text} </div> ))} {/* Render password method fields */} <div className="mb-6"> <h2 className="text-lg font-semibold mb-4">Password Login</h2> {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["default", "password"], }).map(renderNode)} </div> {/* Render webauthn/passkey method fields */} <div className="mb-6"> <h2 className="text-lg font-semibold mb-4">Passkey Login</h2> {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["default", "webauthn"], }).map(renderNode)} </div> {/* Render OTP method fields */} <div className="mb-6"> <h2 className="text-lg font-semibold mb-4">OTP Login</h2> {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["default", "totp"], }).map(renderNode)} </div> </form> </div> ) }
This is a basic implementation that provides a foundation for your login page. To make this work properly, you'll need to: 1. Set up an Ory Network project as mentioned in the [prerequisites for custom UI](https://www.ory.sh/docs/kratos/bring-your-own-ui/custom-ui-ory-elements#prerequisites). 2. Install the required dependencies: npm install @ory/client @ory/integrations
3. Configure your Next.js application to connect to your Ory Network project by setting the NEXT_PUBLIC_ORY_PROJECT_SLUG
environment variable. For a more complete implementation with Ory Elements (which provides pre-built UI components), you can follow the guide on [Custom user interface with Ory Elements](https://www.ory.sh/docs/kratos/bring-your-own-ui/custom-ui-ory-elements). To run the application locally and connect it to your Ory Network project, you'll need to use Ory Tunnel as described in the [Run the sample application](https://www.ory.sh/docs/kratos/bring-your-own-ui/custom-ui-ory-elements#run-the-sample-application) section. Note that the actual UI for passkeys and OTP will depend on your Ory Network project configuration. You'll need to enable these authentication methods in your Ory Network project settings for them to appear in the login flow.cold-air-52564
03/12/2025, 9:08 AMbrief-egg-61061
03/12/2025, 9:08 AMbrief-egg-61061
03/12/2025, 9:09 AMonSubmit
not being called with passwordless one-time codes, I'll make some adjustments to improve the button rendering:
```tsx
// pages/login.tsx
import {
Configuration,
FrontendApi,
LoginFlow,
UiNode,
UiNodeInputAttributes,
UiNodeScriptAttributes,
} from "@ory/client"
import {
filterNodesByGroups,
isUiNodeInputAttributes,
isUiNodeScriptAttributes,
} from "@ory/integrations/ui"
import { useEffect, useState } from "react"
import { useRouter } from "next/router"
import { edgeConfig } from "@ory/integrations/next"
const frontend = new FrontendApi(
new Configuration({
...edgeConfig,
baseOptions: {
withCredentials: true, // we need to include cookies
},
}),
)brief-egg-61061
03/12/2025, 9:09 AMory-script-${attrs.src}
// Only add if not already present
if (!document.getElementById(script.id)) {
document.body.appendChild(script)
}
})
return () => {
scriptNodes.forEach(node => {
const script = document.getElementById(ory-script-${(node.attributes as UiNodeScriptAttributes).src}
)
if (script) {
document.body.removeChild(script)
}
})
}
}, [flow])
const initializeFlow = () => {
// Get the return_to from the URL
const returnTo = router.query.return_to as string
frontend
.createBrowserLoginFlow({
returnTo: returnTo || "/", // redirect to the root path after login
refresh: true, // if the user has a session, refresh it
})
.then(({ data }) => {
setFlow(data)
})
.catch((err) => {
console.error("Failed to initialize login flow", err)
})
}
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
if (!flow) return
const form = event.currentTarget
const formData = new FormData(form)
// Map the entire form data to JSON for the request body
let body = Object.fromEntries(formData) as any
// We need the method specified from the name and value of the submit button
if ("submitter" in event.nativeEvent) {
const method = (
event.nativeEvent as unknown as { submitter: HTMLInputElement }
).submitter
body = {
...body,
...{ [method.name]: method.value },
}
}
frontend
.updateLoginFlow({
flow: flow.id,
updateLoginFlowBody: body,
})
.then(() => {
// Redirect to home page or the return_to URL
router.push(router.query.return_to as string || "/")
})
.catch((err) => {brief-egg-61061
03/12/2025, 9:09 AM${attrs.name}-${attrs.value}
}
type="submit"
name={attrs.name}
value={attrs.value}
className="w-full px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 mb-2"
onClick={attrs.onclick ? () => { eval(attrs.onclick) } : undefined}
>
{node.meta?.label?.text || attrs.value}
</button>
)
}
const renderInputField = (node: UiNode) => {
if (!isUiNodeInputAttributes(node.attributes)) return null
const attrs = node.attributes as UiNodeInputAttributes
if (attrs.type === 'submit' || attrs.type === 'button') return null
return (
<div key={attrs.name} className="mb-4">
{node.meta?.label?.text && (
<label className="block text-gray-700 mb-2">
{node.meta.label.text}
</label>
)}
<input
name={attrs.name}
type={attrs.type}
autoComplete={attrs.autocomplete}
defaultValue={attrs.value}
required={attrs.required}
disabled={attrs.disabled}
className="w-full px-3 py-2 border rounded"
/>
{node.messages?.map((message, idx) => (
<p key={idx} className="text-red-500 text-sm mt-1">
{message.text}
</p>
))}
</div>
)
}
if (!flow) {
return <div>Loading...</div>
}
return (
<div className="max-w-md mx-auto mt-10 p-6 bg-white rounded shadow-md">
<h1 className="text-2xl font-bold mb-6">Login</h1>
<form action={flow.ui.action} method={flow.ui.method} onSubmit={handleSubmit}>
{/* Display any flow level messages/errors */}
{flow.ui.messages?.map((message, idx) => (
<div key={idx} className="mb-4 p-2 bg-red-100 text-red-700 rounded">
{message.text}
</div>
))}
{/* Hidden fields (like CSRF token) */}
{filterNodesByGroups({
nodes: flow.ui.nodes,
groups: ["default"],
attributes: ["hidden"],
}).map(renderInputField)}
{/* Password method */}
{filterNodesByGroups({
nodes: flow.ui.nodes,
groups: ["password"],
withoutAttributes: ["hidden"],
}).length > 0 && (
<div className="mb-6">
<h2 className="text-lg font-semibold mb-4">Password Login</h2>
{/* Password input fields */}brief-egg-61061
03/12/2025, 9:09 AMbrief-egg-61061
03/12/2025, 9:09 AMonSubmit
not being called with passwordless one-time codes, I'll make some adjustments to improve the button rendering: tsx // pages/login.tsx import { Configuration, FrontendApi, LoginFlow, UiNode, UiNodeInputAttributes, UiNodeScriptAttributes, } from "@ory/client" import { filterNodesByGroups, isUiNodeInputAttributes, isUiNodeScriptAttributes, } from "@ory/integrations/ui" import { useEffect, useState } from "react" import { useRouter } from "next/router" import { edgeConfig } from "@ory/integrations/next" const frontend = new FrontendApi( new Configuration({ ...edgeConfig, baseOptions: { withCredentials: true, // we need to include cookies }, }), ) export default function Login() { const [flow, setFlow] = useState<LoginFlow>() const router = useRouter() useEffect(() => { // Check if flow ID exists in URL const flowId = router.query.flow as string if (flowId) { // If we have a flow ID, get the flow frontend .getLoginFlow({ id: flowId }) .then(({ data }) => { setFlow(data) }) .catch((err) => { console.error("Failed to get login flow", err) // Start a new flow if there was an error initializeFlow() }) } else { // Initialize a new flow initializeFlow() } }, [router.query.flow]) // Add WebAuthn scripts if present useEffect(() => { if (!flow) return const scriptNodes = flow.ui.nodes.filter(node => isUiNodeScriptAttributes(node.attributes) ) scriptNodes.forEach(node => { const attrs = node.attributes as UiNodeScriptAttributes const script = document.createElement('script') script.src = attrs.src script.async = attrs.async script.id = `ory-script-${attrs.src}` // Only add if not already present if (!document.getElementById(script.id)) { document.body.appendChild(script) } }) return () => { scriptNodes.forEach(node => { const script = document.getElementById(`ory-script-${(node.attributes as UiNodeScriptAttributes).src}`) if (script) { document.body.removeChild(script) } }) } }, [flow]) const initializeFlow = () => { // Get the return_to from the URL const returnTo = router.query.return_to as string frontend .createBrowserLoginFlow({ returnTo: returnTo || "/", // redirect to the root path after login refresh: true, // if the user has a session, refresh it }) .then(({ data }) => { setFlow(data) }) .catch((err) => { console.error("Failed to initialize login flow", err) }) } const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault() if (!flow) return const form = event.currentTarget const formData = new FormData(form) // Map the entire form data to JSON for the request body let body = Object.fromEntries(formData) as any // We need the method specified from the name and value of the submit button if ("submitter" in event.nativeEvent) { const method = ( event.nativeEvent as unknown as { submitter: HTMLInputElement } ).submitter body = { ...body, ...{ [method.name]: method.value }, } } frontend .updateLoginFlow({ flow: flow.id, updateLoginFlowBody: body, }) .then(() => { // Redirect to home page or the return_to URL router.push(router.query.return_to as string || "/") }) .catch((err) => { if (err.response?.status === 400) { // User input error, update the flow with error messages setFlow(err.response.data) } else { console.error("Login failed", err) } }) } // Specific function to render submit buttons correctly const renderSubmitButton = (node: UiNode) => { if (!isUiNodeInputAttributes(node.attributes)) return null const attrs = node.attributes as UiNodeInputAttributes if (attrs.type !== 'submit') return null return ( <button key={`${attrs.name}-${attrs.value}`} type="submit" name={attrs.name} value={attrs.value} className="w-full px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 mb-2" onClick={attrs.onclick ? () => { eval(attrs.onclick) } : undefined} > {node.meta?.label?.text || attrs.value} </button> ) } const renderInputField = (node: UiNode) => { if (!isUiNodeInputAttributes(node.attributes)) return null const attrs = node.attributes as UiNodeInputAttributes if (attrs.type === 'submit' || attrs.type === 'button') return null return ( <div key={attrs.name} className="mb-4"> {node.meta?.label?.text && ( <label className="block text-gray-700 mb-2"> {node.meta.label.text} </label> )} <input name={attrs.name} type={attrs.type} autoComplete={attrs.autocomplete} defaultValue={attrs.value} required={attrs.required} disabled={attrs.disabled} className="w-full px-3 py-2 border rounded" /> {node.messages?.map((message, idx) => ( <p key={idx} className="text-red-500 text-sm mt-1"> {message.text} </p> ))} </div> ) } if (!flow) { return <div>Loading...</div> } return ( <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded shadow-md"> <h1 className="text-2xl font-bold mb-6">Login</h1> <form action={flow.ui.action} method={flow.ui.method} onSubmit={handleSubmit}> {/* Display any flow level messages/errors */} {flow.ui.messages?.map((message, idx) => ( <div key={idx} className="mb-4 p-2 bg-red-100 text-red-700 rounded"> {message.text} </div> ))} {/* Hidden fields (like CSRF token) */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["default"], attributes: ["hidden"], }).map(renderInputField)} {/* Password method */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["password"], withoutAttributes: ["hidden"], }).length > 0 && ( <div className="mb-6"> <h2 className="text-lg font-semibold mb-4">Password Login</h2> {/* Password input fields */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["password"], withoutAttributes: ["hidden", "submit"], }).map(renderInputField)} {/* Password submit button */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["password"], attributes: ["submit"], }).map(renderSubmitButton)} </div> )} {/* Webauthn/Passkey method */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["webauthn"], withoutAttributes: ["hidden"], }).length > 0 && ( <div className="mb-6"> <h2 className="text-lg font-semibold mb-4">Passkey Login</h2> {/* Webauthn input fields */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["webauthn"], withoutAttributes: ["hidden", "submit"], }).map(renderInputField)} {/* Webauthn submit button */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["webauthn"], attributes: ["submit"], }).map(renderSubmitButton)} </div> )} {/* OTP method */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["totp", "code"], withoutAttributes: ["hidden"], }).length > 0 && ( <div className="mb-6"> <h2 className="text-lg font-semibold mb-4">One-Time Code Login</h2> {/* OTP input fields */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["totp", "code"], withoutAttributes: ["hidden", "submit"], }).map(renderInputField)} {/* OTP submit button */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["totp", "code"], attributes: ["submit"], }).map(renderSubmitButton)} </div> )} </form> </div> ) }
The key improvements in this updated code: 1. I've separated the rendering of input fields and submit buttons into distinct functions to ensure buttons are properly displayed. 2. I've added specific handling for WebAuthn scripts that are required for passkey functionality. 3. I've improved the filtering of UI nodes to explicitly look for submit buttons with the attributes: ["submit"]
filter. 4. I've added conditional rendering for each authentication method section, so they only appear if relevant nodes are present. 5. For the passwordless one-time codes, I've included both "totp" and "code" groups to ensure all OTP-related nodes are captured. 6. I've added proper handling for the onclick
attribute that's often needed for WebAuthn/passkey buttons to work correctly. This should resolve the issue with submit buttons not appearing in your login form. If you're still experiencing issues, make sure your Ory project has the relevant authentication methods (password, passkey, OTP) enabled in your configuration.cold-air-52564
03/12/2025, 9:12 AMbrief-egg-61061
03/12/2025, 9:12 AMbrief-egg-61061
03/12/2025, 9:12 AMbrief-egg-61061
03/12/2025, 9:12 AMory-script-${attrs.src}
// Only add if not already present
if (!document.getElementById(script.id)) {
document.body.appendChild(script)
}
})
return () => {
scriptNodes.forEach(node => {
const script = document.getElementById(ory-script-${node.attributes.src}
)
if (script) {
document.body.removeChild(script)
}
})
}
}, [flow])
const initializeFlow = () => {
// Get the return_to from the URL
const returnTo = router.query.return_to
frontend
.createBrowserLoginFlow({
returnTo: returnTo || "/", // redirect to the root path after login
refresh: true, // if the user has a session, refresh it
})
.then(({ data }) => {
setFlow(data)
})
.catch((err) => {
console.error("Failed to initialize login flow", err)
})
}
const handleSubmit = (event) => {
event.preventDefault()
if (!flow) return
const form = event.currentTarget
const formData = new FormData(form)
// Map the entire form data to JSON for the request body
let body = Object.fromEntries(formData)
// We need the method specified from the name and value of the submit button
if ("submitter" in event.nativeEvent) {
const method = event.nativeEvent.submitter
body = {
...body,
...{ [method.name]: method.value },
}
}
frontend
.updateLoginFlow({
flow: flow.id,
updateLoginFlowBody: body,
})
.then(() => {
// Redirect to home page or the return_to URL
router.push(router.query.return_to || "/")
})
.catch((err) => {
if (err.response?.status === 400) {
// User input error, update the flow with error messages
setFlow(err.response.data)
} else {
console.error("Login failed", err)brief-egg-61061
03/12/2025, 9:12 AM${attrs.name}-${attrs.value}
}
type="submit"
name={attrs.name}
value={attrs.value}
className="w-full px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 mb-2"
onClick={attrs.onclick ? () => { eval(attrs.onclick) } : undefined}
>
{node.meta?.label?.text || attrs.value}
</button>
)
}
const renderInputField = (node) => {
if (!isUiNodeInputAttributes(node.attributes)) return null
const attrs = node.attributes
if (attrs.type === 'submit' || attrs.type === 'button') return null
return (
<div key={attrs.name} className="mb-4">
{node.meta?.label?.text && (
<label className="block text-gray-700 mb-2">
{node.meta.label.text}
</label>
)}
<input
name={attrs.name}
type={attrs.type}
autoComplete={attrs.autocomplete}
defaultValue={attrs.value}
required={attrs.required}
disabled={attrs.disabled}
className="w-full px-3 py-2 border rounded"
/>
{node.messages?.map((message, idx) => (
<p key={idx} className="text-red-500 text-sm mt-1">
{message.text}
</p>
))}
</div>
)
}
if (!flow) {
return <div>Loading...</div>
}
return (
<div className="max-w-md mx-auto mt-10 p-6 bg-white rounded shadow-md">
<h1 className="text-2xl font-bold mb-6">Login</h1>
<form action={flow.ui.action} method={flow.ui.method} onSubmit={handleSubmit}>
{/* Display any flow level messages/errors */}
{flow.ui.messages?.map((message, idx) => (
<div key={idx} className="mb-4 p-2 bg-red-100 text-red-700 rounded">
{message.text}
</div>
))}
{/* Hidden fields (like CSRF token) */}
{filterNodesByGroups({
nodes: flow.ui.nodes,
groups: ["default"],
attributes: ["hidden"],
}).map(renderInputField)}
{/* Password method */}
{filterNodesByGroups({
nodes: flow.ui.nodes,
groups: ["password"],
withoutAttributes: ["hidden"],
}).length > 0 && (
<div className="mb-6">
<h2 className="text-lg font-semibold mb-4">Password Login</h2>
{/* Password input fields */}
{filterNodesByGroups({
nodes: flow.ui.nodes,
groups: ["password"],
withoutAttributes: ["hidden", "submit"],
}).map(renderInputField)}
{/* Password submit button */}
{filterNodesByGroups({brief-egg-61061
03/12/2025, 9:12 AMbrief-egg-61061
03/12/2025, 9:12 AMjsx // pages/login.jsx import { useEffect, useState } from "react" import { useRouter } from "next/router" import { Configuration, FrontendApi } from "@ory/client" import { filterNodesByGroups, isUiNodeInputAttributes, isUiNodeScriptAttributes } from "@ory/integrations/ui" import { edgeConfig } from "@ory/integrations/next" const frontend = new FrontendApi( new Configuration({ ...edgeConfig, baseOptions: { withCredentials: true, // we need to include cookies }, }), ) export default function Login() { const [flow, setFlow] = useState(null) const router = useRouter() useEffect(() => { // Check if flow ID exists in URL const flowId = router.query.flow if (flowId) { // If we have a flow ID, get the flow frontend .getLoginFlow({ id: flowId }) .then(({ data }) => { setFlow(data) }) .catch((err) => { console.error("Failed to get login flow", err) // Start a new flow if there was an error initializeFlow() }) } else { // Initialize a new flow initializeFlow() } }, [router.query.flow]) // Add WebAuthn scripts if present useEffect(() => { if (!flow) return const scriptNodes = flow.ui.nodes.filter(node => isUiNodeScriptAttributes(node.attributes) ) scriptNodes.forEach(node => { const attrs = node.attributes const script = document.createElement('script') script.src = attrs.src script.async = attrs.async script.id = `ory-script-${attrs.src}` // Only add if not already present if (!document.getElementById(script.id)) { document.body.appendChild(script) } }) return () => { scriptNodes.forEach(node => { const script = document.getElementById(`ory-script-${node.attributes.src}`) if (script) { document.body.removeChild(script) } }) } }, [flow]) const initializeFlow = () => { // Get the return_to from the URL const returnTo = router.query.return_to frontend .createBrowserLoginFlow({ returnTo: returnTo || "/", // redirect to the root path after login refresh: true, // if the user has a session, refresh it }) .then(({ data }) => { setFlow(data) }) .catch((err) => { console.error("Failed to initialize login flow", err) }) } const handleSubmit = (event) => { event.preventDefault() if (!flow) return const form = event.currentTarget const formData = new FormData(form) // Map the entire form data to JSON for the request body let body = Object.fromEntries(formData) // We need the method specified from the name and value of the submit button if ("submitter" in event.nativeEvent) { const method = event.nativeEvent.submitter body = { ...body, ...{ [method.name]: method.value }, } } frontend .updateLoginFlow({ flow: flow.id, updateLoginFlowBody: body, }) .then(() => { // Redirect to home page or the return_to URL router.push(router.query.return_to || "/") }) .catch((err) => { if (err.response?.status === 400) { // User input error, update the flow with error messages setFlow(err.response.data) } else { console.error("Login failed", err) } }) } // Specific function to render submit buttons correctly const renderSubmitButton = (node) => { if (!isUiNodeInputAttributes(node.attributes)) return null const attrs = node.attributes if (attrs.type !== 'submit') return null return ( <button key={`${attrs.name}-${attrs.value}`} type="submit" name={attrs.name} value={attrs.value} className="w-full px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 mb-2" onClick={attrs.onclick ? () => { eval(attrs.onclick) } : undefined} > {node.meta?.label?.text || attrs.value} </button> ) } const renderInputField = (node) => { if (!isUiNodeInputAttributes(node.attributes)) return null const attrs = node.attributes if (attrs.type === 'submit' || attrs.type === 'button') return null return ( <div key={attrs.name} className="mb-4"> {node.meta?.label?.text && ( <label className="block text-gray-700 mb-2"> {node.meta.label.text} </label> )} <input name={attrs.name} type={attrs.type} autoComplete={attrs.autocomplete} defaultValue={attrs.value} required={attrs.required} disabled={attrs.disabled} className="w-full px-3 py-2 border rounded" /> {node.messages?.map((message, idx) => ( <p key={idx} className="text-red-500 text-sm mt-1"> {message.text} </p> ))} </div> ) } if (!flow) { return <div>Loading...</div> } return ( <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded shadow-md"> <h1 className="text-2xl font-bold mb-6">Login</h1> <form action={flow.ui.action} method={flow.ui.method} onSubmit={handleSubmit}> {/* Display any flow level messages/errors */} {flow.ui.messages?.map((message, idx) => ( <div key={idx} className="mb-4 p-2 bg-red-100 text-red-700 rounded"> {message.text} </div> ))} {/* Hidden fields (like CSRF token) */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["default"], attributes: ["hidden"], }).map(renderInputField)} {/* Password method */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["password"], withoutAttributes: ["hidden"], }).length > 0 && ( <div className="mb-6"> <h2 className="text-lg font-semibold mb-4">Password Login</h2> {/* Password input fields */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["password"], withoutAttributes: ["hidden", "submit"], }).map(renderInputField)} {/* Password submit button */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["password"], attributes: ["submit"], }).map(renderSubmitButton)} </div> )} {/* Webauthn/Passkey method */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["webauthn"], withoutAttributes: ["hidden"], }).length > 0 && ( <div className="mb-6"> <h2 className="text-lg font-semibold mb-4">Passkey Login</h2> {/* Webauthn input fields */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["webauthn"], withoutAttributes: ["hidden", "submit"], }).map(renderInputField)} {/* Webauthn submit button */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["webauthn"], attributes: ["submit"], }).map(renderSubmitButton)} </div> )} {/* OTP method */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["totp", "code"], withoutAttributes: ["hidden"], }).length > 0 && ( <div className="mb-6"> <h2 className="text-lg font-semibold mb-4">One-Time Code Login</h2> {/* OTP input fields */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["totp", "code"], withoutAttributes: ["hidden", "submit"], }).map(renderInputField)} {/* OTP submit button */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["totp", "code"], attributes: ["submit"], }).map(renderSubmitButton)} </div> )} </form> </div> ) }
This JavaScript version maintains all the functionality of the TypeScript version but removes the type annotations. The code handles password, passkey (WebAuthn), and OTP authentication methods, and properly renders the submit buttons for each method. To use this code, make sure you've installed the required dependencies: npm install @ory/client @ory/integrations
And set up the Ory API connection in your Next.js application as described in the [Integrate authentication into Next.js](https://www.ory.sh/docs/getting-started/integrate-auth/nextjs) documentation.