diff --git a/src/locales/en.json b/src/locales/en.json
index bd995ecb..abc58575 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -20,7 +20,7 @@
"identities.messages.1010005": "Verify",
"identities.messages.1010006": "Authentication code",
"identities.messages.1010007": "Backup recovery code",
- "identities.messages.1010008": "Use security key",
+ "identities.messages.1010008": "Sign in with hardware key",
"identities.messages.1010009": "Use Authenticator",
"identities.messages.1010010": "Use backup recovery code",
"identities.messages.1010011": "Continue with security key",
@@ -131,7 +131,7 @@
"identities.messages.4070006": "The verification code is invalid or has already been used. Please try again.",
"identities.messages.5000001": "{reason}",
"login.forgot-password": "Forgot password?",
- "login.logged-in-as-label": "You're logged in as:",
+ "login.logged-in-as-label": "You are using:",
"login.logout-button": "Logout",
"login.logout-label": "Something's not working?",
"login.registration-button": "Sign up",
diff --git a/src/react-components/ory/helpers/user-auth-form.tsx b/src/react-components/ory/helpers/user-auth-form.tsx
index 52990a6f..2d85a3e7 100644
--- a/src/react-components/ory/helpers/user-auth-form.tsx
+++ b/src/react-components/ory/helpers/user-auth-form.tsx
@@ -69,19 +69,9 @@ export const UserAuthForm = ({
formFilterOverride,
className,
...props
-}: UserAuthFormProps): JSX.Element => (
-
-
+ >
)
diff --git a/src/react-components/ory/sections/logged-info.tsx b/src/react-components/ory/sections/logged-info.tsx
index bac55532..d3306fb7 100644
--- a/src/react-components/ory/sections/logged-info.tsx
+++ b/src/react-components/ory/sections/logged-info.tsx
@@ -24,7 +24,7 @@ export const LoggedInInfo = ({ flow }: IdentifierInfoProps) => {
diff --git a/src/react-components/ory/sections/login-section.tsx b/src/react-components/ory/sections/login-section.tsx
index 820c679c..f7f73de3 100644
--- a/src/react-components/ory/sections/login-section.tsx
+++ b/src/react-components/ory/sections/login-section.tsx
@@ -5,46 +5,81 @@ import { FormattedMessage } from "react-intl"
import { gridStyle } from "../../../theme"
import { ButtonLink, CustomHref } from "../../button-link"
import { FilterFlowNodes } from "../helpers/filter-flow-nodes"
-import { hasPassword } from "../helpers/utils"
+import { hasPassword, hasIdentifierFirst } from "../helpers/utils"
+import { SelfServiceFlow } from "../helpers/types"
export interface LoginSectionProps {
nodes: UiNode[]
forgotPasswordURL?: CustomHref | string
}
+export const IdentifierFirstLoginSection = (
+ flow: SelfServiceFlow,
+): JSX.Element | null => {
+ const nodes = flow.ui.nodes
+ return hasIdentifierFirst(nodes) ? (
+
+
+
+
+ ) : null
+}
+
export const LoginSection = ({
nodes,
forgotPasswordURL,
}: LoginSectionProps): JSX.Element | null => {
return hasPassword(nodes) ? (
-
-
+ <>
+
+
+
+
+ {forgotPasswordURL && (
+
+
+
+ )}
+
- {forgotPasswordURL && (
-
-
-
- )}
-
-
+ >
) : null
}
diff --git a/src/react-components/ory/sections/lookup-secret-settings-section.tsx b/src/react-components/ory/sections/lookup-secret-settings-section.tsx
index 123f926f..72fb9be2 100644
--- a/src/react-components/ory/sections/lookup-secret-settings-section.tsx
+++ b/src/react-components/ory/sections/lookup-secret-settings-section.tsx
@@ -21,7 +21,7 @@ export const LookupSecretSettingsSection = ({
return hasLookupSecret(flow.ui.nodes) ? (
{
nodes: flow.ui.nodes,
groups: "oidc",
withoutDefaultGroup: true,
- excludeAttributes: "submit",
+ excludeAttributeTypes: "submit",
}).length > 0
return hasOidc(flow.ui.nodes) ? (
@@ -24,7 +24,7 @@ export const OIDCSection = (flow: SelfServiceFlow): JSX.Element | null => {
nodes: flow.ui.nodes,
groups: "oidc",
withoutDefaultGroup: true,
- excludeAttributes: "submit",
+ excludeAttributeTypes: ["submit"],
}}
/>
@@ -35,6 +35,7 @@ export const OIDCSection = (flow: SelfServiceFlow): JSX.Element | null => {
nodes: flow.ui.nodes,
groups: "oidc",
attributes: "submit",
+ withoutDefaultGroup: true,
}}
/>
diff --git a/src/react-components/ory/sections/passkey-settings-section.tsx b/src/react-components/ory/sections/passkey-settings-section.tsx
index edea2749..52b48ddd 100644
--- a/src/react-components/ory/sections/passkey-settings-section.tsx
+++ b/src/react-components/ory/sections/passkey-settings-section.tsx
@@ -21,9 +21,8 @@ export const PasskeySettingsSection = ({
return hasPasskey(flow.ui.nodes) ? (
-
{
return hasWebauthn(flow.ui.nodes) ? (
-
-
+ <>
+
+
+ >
+ ) : null
+}
+
+export const PasskeySection = (flow: SelfServiceFlow): JSX.Element | null => {
+ return hasPasskey(flow.ui.nodes) ? (
+ <>
-
- ) : null
-}
-
-export const PasskeySection = (flow: SelfServiceFlow): JSX.Element | null => {
- return hasPasskey(flow.ui.nodes) ? (
-
-
+ >
) : null
}
@@ -63,16 +85,26 @@ export const PasskeyLoginSection = (
flow: SelfServiceFlow,
): JSX.Element | null => {
return hasPasskey(flow.ui.nodes) ? (
-
+ <>
-
+
+
+
+ >
) : null
}
@@ -81,27 +113,36 @@ export const PasswordlessLoginSection = (
): JSX.Element | null => {
if (hasWebauthn(flow.ui.nodes)) {
return (
-
-
+ >
)
}
diff --git a/src/react-components/ory/sections/profile-section.tsx b/src/react-components/ory/sections/profile-section.tsx
index b053664a..6d66ce8d 100644
--- a/src/react-components/ory/sections/profile-section.tsx
+++ b/src/react-components/ory/sections/profile-section.tsx
@@ -17,7 +17,7 @@ export const ProfileSettingsSection = ({
return (
+
+ ) : null
+}
+
+export const ProfileLoginSection = (
+ flow: SelfServiceFlow,
+): JSX.Element | null => {
+ return hasProfile(flow.ui.nodes) ? (
+
+
+
diff --git a/src/react-components/ory/sections/registration-section.tsx b/src/react-components/ory/sections/registration-section.tsx
index 19b9bd3c..0d6ad7b3 100644
--- a/src/react-components/ory/sections/registration-section.tsx
+++ b/src/react-components/ory/sections/registration-section.tsx
@@ -19,7 +19,7 @@ export const RegistrationSection = ({
filter={{
nodes: nodes,
groups: ["password"],
- excludeAttributes: "submit,hidden",
+ excludeAttributeTypes: "submit,hidden",
}}
/>
@@ -27,7 +27,7 @@ export const RegistrationSection = ({
filter={{
nodes: nodes,
groups: ["password"],
- excludeAttributes: "hidden",
+ excludeAttributeTypes: "hidden",
attributes: "submit",
}}
/>
diff --git a/src/react-components/ory/sections/totp-settings-section.tsx b/src/react-components/ory/sections/totp-settings-section.tsx
index f483f763..ba4a77f2 100644
--- a/src/react-components/ory/sections/totp-settings-section.tsx
+++ b/src/react-components/ory/sections/totp-settings-section.tsx
@@ -21,7 +21,7 @@ export const TOTPSettingsSection = ({
return hasTotp(flow.ui.nodes) ? (
{
/>,
)
- await expect(component).toContainText("You're logged in as:")
+ await expect(component).toContainText("You are using:")
await expect(component).toContainText("johndoe@acme.com")
})
@@ -416,7 +416,5 @@ test("ory auth card login two factor confirmation", async ({ mount }) => {
expect(identifier).not.toBeNull()
expect(String(identifier.value)).toContain("@ory.sh")
- await expect(component).toContainText(
- `You're logged in as:${identifier.value}`,
- )
+ await expect(component).toContainText(`You are using:${identifier.value}`)
})
diff --git a/src/react-components/ory/user-auth-card.tsx b/src/react-components/ory/user-auth-card.tsx
index fed3aaf1..f679c4fb 100644
--- a/src/react-components/ory/user-auth-card.tsx
+++ b/src/react-components/ory/user-auth-card.tsx
@@ -33,7 +33,10 @@ import {
import { AuthCodeSection } from "./sections/auth-code-section"
import { LinkSection } from "./sections/link-section"
import { LoggedInInfo } from "./sections/logged-info"
-import { LoginSection } from "./sections/login-section"
+import {
+ LoginSection,
+ IdentifierFirstLoginSection,
+} from "./sections/login-section"
import { OIDCSection } from "./sections/oidc-section"
import {
PasskeyLoginSection,
@@ -42,7 +45,10 @@ import {
PasswordlessSection,
} from "./sections/passwordless-section"
import { RegistrationSection } from "./sections/registration-section"
-import { ProfileRegistrationSection } from "./sections/profile-section"
+import {
+ ProfileLoginSection,
+ ProfileRegistrationSection,
+} from "./sections/profile-section"
export interface LoginSectionAdditionalProps {
forgotPasswordURL?: CustomHref | string
@@ -78,28 +84,29 @@ export type UserAuthCardProps = {
includeScripts?: boolean
className?: string
} & UserAuthFormAdditionalProps &
- (
- | {
- flow: LoginFlow
- flowType: "login"
- additionalProps?: LoginSectionAdditionalProps
- }
- | {
- flow: RegistrationFlow
- flowType: "registration"
- additionalProps?: RegistrationSectionAdditionalProps
- }
- | {
- flow: RecoveryFlow
- flowType: "recovery"
- additionalProps?: RecoverySectionAdditionalProps
- }
- | {
- flow: VerificationFlow
- flowType: "verification"
- additionalProps?: VerificationSectionAdditionalProps
- }
- )
+ FlowProps
+
+type FlowProps =
+ | {
+ flow: LoginFlow
+ flowType: "login"
+ additionalProps?: LoginSectionAdditionalProps
+ }
+ | {
+ flow: RegistrationFlow
+ flowType: "registration"
+ additionalProps?: RegistrationSectionAdditionalProps
+ }
+ | {
+ flow: RecoveryFlow
+ flowType: "recovery"
+ additionalProps?: RecoverySectionAdditionalProps
+ }
+ | {
+ flow: VerificationFlow
+ flowType: "verification"
+ additionalProps?: VerificationSectionAdditionalProps
+ }
/**
* UserAuthCard renders a login, registration, verification or recovery flow
@@ -108,31 +115,33 @@ export type UserAuthCardProps = {
* @returns JSX.Element
*/
export const UserAuthCard = ({
- flow,
title,
subtitle,
- flowType,
additionalProps,
cardImage,
onSubmit,
includeScripts,
className,
+ flow,
+ flowType,
}: UserAuthCardProps): JSX.Element => {
+ // Safe, because we know that the props are of the correct type
+ const flowProps = { flow, flowType, additionalProps } as FlowProps
const intl = useIntl()
if (includeScripts) {
- useScriptNodes({ nodes: flow.ui.nodes })
+ useScriptNodes({ nodes: flowProps.flow.ui.nodes })
}
if (!title) {
- switch (flowType) {
+ switch (flowProps.flowType) {
case "login":
- if (flow.refresh) {
+ if (flowProps.flow.refresh) {
title = intl.formatMessage({
id: "login.title-refresh",
defaultMessage: "Confirm it's you",
})
- } else if (flow.requested_aal === "aal2") {
+ } else if (flowProps.flow.requested_aal === "aal2") {
title = intl.formatMessage({
id: "login.title-aal2",
defaultMessage: "Two-Factor Authentication",
@@ -165,9 +174,9 @@ export const UserAuthCard = ({
}
}
if (!subtitle) {
- switch (flowType) {
+ switch (flowProps.flowType) {
case "login":
- if (flow.oauth2_login_request) {
+ if (flowProps.flow.oauth2_login_request) {
subtitle = intl.formatMessage(
{
id: "login.subtitle-oauth2",
@@ -175,14 +184,14 @@ export const UserAuthCard = ({
},
{
clientName:
- flow.oauth2_login_request.client?.client_name ??
- flow.oauth2_login_request.client?.client_uri,
+ flowProps.flow.oauth2_login_request.client?.client_name ??
+ flowProps.flow.oauth2_login_request.client?.client_uri,
},
)
}
break
case "registration":
- if (flow.oauth2_login_request) {
+ if (flowProps.flow.oauth2_login_request) {
subtitle = intl.formatMessage(
{
id: "registration.subtitle-oauth2",
@@ -190,8 +199,8 @@ export const UserAuthCard = ({
},
{
clientName:
- flow.oauth2_login_request.client?.client_name ??
- flow.oauth2_login_request.client?.client_uri,
+ flowProps.flow.oauth2_login_request.client?.client_name ??
+ flowProps.flow.oauth2_login_request.client?.client_uri,
},
)
}
@@ -204,6 +213,7 @@ export const UserAuthCard = ({
let $code: JSX.Element | null = null
let $passwordlessWebauthn: JSX.Element | null = null
let $passkey: JSX.Element | null = null
+ let $twoStep: JSX.Element | null = null
let $profile: JSX.Element | null = null
let message: MessageSectionProps | null = null
@@ -221,20 +231,20 @@ export const UserAuthCard = ({
// we want the login section to handle passwordless as well when we have a 2FA screen.
const canShowPasswordless = () =>
!!$passwordlessWebauthn &&
- (!isLoggedIn(flow as LoginFlow) || flowType === "registration")
+ (!isLoggedIn(flow as LoginFlow) || flowProps.flowType === "registration")
// passkey can be shown if the user is not logged in (e.g. exclude 2FA screen) or if the flow is a registration flow.
// we want the login section to handle passwordless as well when we have a 2FA screen.
const canShowPasskey = () =>
!!$passkey &&
- (!isLoggedIn(flow as LoginFlow) || flowType === "registration")
+ (!isLoggedIn(flow as LoginFlow) || flowProps.flowType === "registration")
- const canShowProfile = () => !!$profile && flowType === "registration"
+ const canShowProfile = () => !!$profile && hasProfile(flow.ui.nodes)
// the current flow is a two factor flow if the user is logged in and has any of the second factor methods enabled.
const isTwoFactor = () =>
- flowType === "login" &&
- isLoggedIn(flow) &&
+ flowProps.flowType === "login" &&
+ isLoggedIn(flowProps.flow) &&
(hasTotp(flow.ui.nodes) ||
hasWebauthn(flow.ui.nodes) ||
hasPasskey(flow.ui.nodes) ||
@@ -307,7 +317,7 @@ export const UserAuthCard = ({
nodes: flow.ui.nodes,
groups: "totp",
withoutDefaultGroup: true,
- excludeAttributes: "submit",
+ excludeAttributeTypes: "submit",
}}
/>
)) // only map the divider if the index is greater than 0 - more than one flow
- switch (flowType) {
+ switch (flowProps.flowType) {
case "login":
$passwordlessWebauthn = PasswordlessLoginSection(flow)
$passkey = PasskeyLoginSection(flow)
+ $twoStep = IdentifierFirstLoginSection(flow)
$oidc = OIDCSection(flow)
+ $profile = ProfileLoginSection(flow)
$code = AuthCodeSection({ nodes: flow.ui.nodes })
$flow = LoginSection({
@@ -363,7 +375,7 @@ export const UserAuthCard = ({
...additionalProps,
})
- if (isLoggedIn(flow) && additionalProps?.logoutURL) {
+ if (isLoggedIn(flowProps.flow) && flowProps.additionalProps?.logoutURL) {
message = {
text: intl.formatMessage({
id: "login.logout-label",
@@ -374,9 +386,9 @@ export const UserAuthCard = ({
defaultMessage: "Logout",
}),
dataTestId: "logout-link",
- url: additionalProps.logoutURL,
+ url: flowProps.additionalProps.logoutURL,
}
- } else if (additionalProps?.signupURL) {
+ } else if (flowProps.additionalProps?.signupURL) {
message = {
text: intl.formatMessage({
id: "login.registration-label",
@@ -386,7 +398,7 @@ export const UserAuthCard = ({
id: "login.registration-button",
defaultMessage: "Sign up",
}),
- url: additionalProps.signupURL,
+ url: flowProps.additionalProps.signupURL,
dataTestId: "signup-link",
}
}
@@ -400,13 +412,13 @@ export const UserAuthCard = ({
$flow = RegistrationSection({
nodes: flow.ui.nodes,
})
- if (additionalProps?.loginURL) {
+ if (flowProps.additionalProps?.loginURL) {
message = {
text: intl.formatMessage({
id: "registration.login-label",
defaultMessage: "Already have an account?",
}),
- url: additionalProps.loginURL,
+ url: flowProps.additionalProps.loginURL,
buttonText: intl.formatMessage({
id: "registration.login-button",
defaultMessage: "Sign in",
@@ -420,7 +432,7 @@ export const UserAuthCard = ({
$flow = LinkSection({
nodes: flow.ui.nodes,
})
- if (additionalProps?.loginURL) {
+ if (flowProps.additionalProps?.loginURL) {
message = {
text: intl.formatMessage({
id: "recovery.login-label",
@@ -430,7 +442,7 @@ export const UserAuthCard = ({
id: "recovery.login-button",
defaultMessage: "Sign in",
}),
- url: additionalProps.loginURL,
+ url: flowProps.additionalProps.loginURL,
dataTestId: "cta-link",
}
}
@@ -439,7 +451,7 @@ export const UserAuthCard = ({
$flow = LinkSection({
nodes: flow.ui.nodes,
})
- if (additionalProps?.signupURL) {
+ if (flowProps.additionalProps?.signupURL) {
message = {
text: intl.formatMessage({
id: "verification.registration-label",
@@ -449,7 +461,7 @@ export const UserAuthCard = ({
id: "verification.registration-button",
defaultMessage: "Sign up",
}),
- url: additionalProps.signupURL,
+ url: flowProps.additionalProps.signupURL,
dataTestId: "cta-link",
}
}
@@ -467,41 +479,73 @@ export const UserAuthCard = ({
}
image={cardImage}
- data-testid={`${flowType}-auth-card`}
+ data-testid={`${flowProps.flowType}-auth-card`}
>
{subtitle &&
{subtitle}}
+
+
{$oidc && (
<>
-
-
+
{$oidc}
+
>
)}
+
+ {$twoStep && (
+ <>
+
+ {$twoStep}
+
+ >
+ )}
+
+ {canShowPasskey() && (
+ <>
+
+ {$passkey}
+
+ >
+ )}
+
{$code && (
<>
-
-
+
{$code}
>
)}
+
{$flow && !isTwoFactor() && (
<>
-
{$flow}
- {showLoggedAccount && }
>
)}
+
{isTwoFactor() && (
<>
{twoFactorFlows()}
- {showLoggedAccount && }
- >
- )}
-
- {canShowPasskey() && (
- <>
-
-
- {$passkey}
-
>
)}
{canShowPasswordless() && (
<>
-
)}
- {$profile && (
+ {canShowProfile() && (
<>
-
-
+
{$profile}
>
)}
+ {showLoggedAccount && }
+
{message && MessageSection(message)}
diff --git a/src/react-components/tests/translations.spec.ts b/src/react-components/tests/translations.spec.ts
index 924c0d7d..c794e5ba 100644
--- a/src/react-components/tests/translations.spec.ts
+++ b/src/react-components/tests/translations.spec.ts
@@ -35,11 +35,9 @@ test("language keys and templates match", async ({ templates }) => {
}
await test.step("Checking template strings", () => {
- Object.entries(supportedLanguages).forEach(([language, translation]) => {
- console.log(`Checking ${language} template strings`)
+ Object.entries(supportedLanguages).forEach(([, translation]) => {
Object.entries(templates).forEach(([key, templateStrings]) => {
for (const templateString of templateStrings) {
- console.log(`Checking ${language} ${key} ${templateString}`)
expect(
translation[key as keyof typeof supportedLanguages.en],
).toContain(templateString)
diff --git a/src/ui/index.test.ts b/src/ui/index.test.ts
index d6d396cd..ad7c448a 100644
--- a/src/ui/index.test.ts
+++ b/src/ui/index.test.ts
@@ -251,7 +251,7 @@ describe("generic helpers", () => {
opts: {
groups: "webauthn",
withoutDefaultGroup: true,
- excludeAttributes: "script",
+ excludeAttributeTypes: "script",
},
expected: [
{
diff --git a/src/ui/index.ts b/src/ui/index.ts
index c86286df..94f1a5c5 100644
--- a/src/ui/index.ts
+++ b/src/ui/index.ts
@@ -125,7 +125,8 @@ export interface FilterNodesByGroups {
withoutDefaultGroup?: boolean
attributes?: string[] | string
withoutDefaultAttributes?: boolean
- excludeAttributes?: string[] | string
+ excludeAttributeTypes?: string[] | string
+ excludeAttributeValues?: string[] | string
}
/**
@@ -146,14 +147,15 @@ export const filterNodesByGroups = ({
withoutDefaultGroup,
attributes,
withoutDefaultAttributes,
- excludeAttributes,
+ excludeAttributeTypes,
+ excludeAttributeValues,
}: FilterNodesByGroups) => {
const search = (s: string[] | string | undefined) =>
typeof s === "string" ? s.split(",") : s
return nodes.filter(({ group, attributes: attr }) => {
// if we have not specified any group or attribute filters, return all nodes
- if (!groups && !attributes && !excludeAttributes) return true
+ if (!groups && !attributes && !excludeAttributeTypes) return true
const g = search(groups) ?? []
if (!withoutDefaultGroup) {
@@ -174,16 +176,33 @@ export const filterNodesByGroups = ({
}
// filter the attributes to exclude
- const ea = search(excludeAttributes) ?? []
+ const eat = search(excludeAttributeTypes) ?? []
+ const eav = search(excludeAttributeValues) ?? []
const filterGroup = groups ? g.includes(group) : true
const filterAttributes = attributes
? a.includes(getNodeInputType(attr))
: true
- const filterExcludeAttributes = excludeAttributes
- ? !ea.includes(getNodeInputType(attr))
+ const filterExcludeAttributeTypes = excludeAttributeTypes
+ ? !eat.includes(getNodeInputType(attr))
+ : true
+ const filterExcludeAttributeValue = excludeAttributeValues
+ ? !eav.includes(getNodeInputValue(attr))
: true
- return filterGroup && filterAttributes && filterExcludeAttributes
+ return (
+ filterGroup &&
+ filterAttributes &&
+ filterExcludeAttributeTypes &&
+ filterExcludeAttributeValue
+ )
})
}
+
+export const getNodeInputValue = (attr?: UiNodeAttributes) => {
+ if (!attr) return ""
+ if (isUiNodeInputAttributes(attr)) {
+ return String(attr.value)
+ }
+ return ""
+}