Skip to content

Commit 2f9d93c

Browse files
anusha-c18natalian98
authored andcommitted
feat: add password error styles to input text
1 parent 7d76ad5 commit 2f9d93c

File tree

6 files changed

+138
-82
lines changed

6 files changed

+138
-82
lines changed

ui/api/kratos.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Configuration, FrontendApi } from "@ory/client";
1+
import { Configuration, FrontendApi, UpdateLoginFlowBody, LoginFlow } from "@ory/client";
22

33
export const kratos = new FrontendApi(
44
new Configuration({
@@ -9,3 +9,33 @@ export const kratos = new FrontendApi(
99
},
1010
})
1111
);
12+
13+
type IdentifierFirstResponse = { redirect_to: string } | LoginFlow;
14+
15+
export async function loginIdentifierFirst(
16+
flowId: string,
17+
values: UpdateLoginFlowBody,
18+
method: string,
19+
flow?: { id?: string; return_to?: string }
20+
) {
21+
const res = await fetch(
22+
`/api/kratos/self-service/login/id-first?flow=${encodeURIComponent(flowId)}`,
23+
{
24+
method: "POST",
25+
headers: {
26+
"Content-Type": "application/json",
27+
},
28+
body: JSON.stringify({
29+
...values,
30+
method,
31+
flow: String(flow?.id),
32+
}),
33+
},
34+
);
35+
36+
if (!res.ok) {
37+
throw new Error(await res.text());
38+
}
39+
40+
return (await res.json()) as IdentifierFirstResponse;
41+
}

ui/components/NodeInputPassword.tsx

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getNodeLabel } from "@ory/integrations/ui";
2-
import React, { FC, useState } from "react";
2+
import React, { FC, useState, ChangeEvent, KeyboardEvent } from "react";
33
import { NodeInputProps } from "./helpers";
44
import PasswordToggle from "./PasswordToggle";
55

@@ -30,6 +30,19 @@ export const NodeInputPassword: FC<NodeInputProps> = ({
3030
}
3131
};
3232

33+
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
34+
setPassword(e.target.value);
35+
void setValue(e.target.value);
36+
};
37+
38+
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
39+
if (e.key === "Enter") {
40+
e.preventDefault();
41+
e.stopPropagation();
42+
void dispatchSubmit(e, "password");
43+
}
44+
};
45+
3346
return (
3447
<PasswordToggle
3548
tabIndex={2}
@@ -50,17 +63,8 @@ export const NodeInputPassword: FC<NodeInputProps> = ({
5063
disabled={disabled}
5164
placeholder="Your Password"
5265
error={getError()}
53-
onChange={(e) => {
54-
setPassword(e.target.value);
55-
void setValue(e.target.value);
56-
}}
57-
onKeyDown={(e) => {
58-
if (e.key === "Enter") {
59-
e.preventDefault();
60-
e.stopPropagation();
61-
void dispatchSubmit(e, "password");
62-
}
63-
}}
66+
onChange={handleChange}
67+
onKeyDown={handleKeyDown}
6468
/>
6569
);
6670
};

ui/components/Password.tsx

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { FC, useEffect, useState } from "react";
1+
import React, { FC, useEffect, useState, useCallback } from "react";
22
import PasswordCheck from "./PasswordCheck";
33
import PasswordToggle from "./PasswordToggle";
44

@@ -24,6 +24,19 @@ function useDebounce<T>(value: T, delay: number): T {
2424
return debounced;
2525
}
2626

27+
const validateCheck = (check: PasswordCheckType, value: string): boolean => {
28+
switch (check) {
29+
case "lowercase":
30+
return /[a-z]/.test(value);
31+
case "uppercase":
32+
return /[A-Z]/.test(value);
33+
case "number":
34+
return /\d/.test(value);
35+
case "length":
36+
return value.length >= 8;
37+
}
38+
};
39+
2740
const Password: FC<Props> = ({
2841
checks,
2942
password,
@@ -39,19 +52,6 @@ const Password: FC<Props> = ({
3952
const debouncedPassword = useDebounce(password, DEBOUNCE_DURATION);
4053
const debouncedConfirmation = useDebounce(confirmation, DEBOUNCE_DURATION);
4154

42-
const validateCheck = (check: PasswordCheckType, value: string): boolean => {
43-
switch (check) {
44-
case "lowercase":
45-
return /[a-z]/.test(value);
46-
case "uppercase":
47-
return /[A-Z]/.test(value);
48-
case "number":
49-
return /\d/.test(value);
50-
case "length":
51-
return value.length >= 8;
52-
}
53-
};
54-
5555
const getStatus = (check: PasswordCheckType) => {
5656
if (!hasTouched || !debouncedPassword) return "neutral";
5757
if (validateCheck(check, debouncedPassword)) return "success";
@@ -71,6 +71,14 @@ const Password: FC<Props> = ({
7171
}
7272
}, [computedValid, setValid]);
7373

74+
const handlePasswordChange = useCallback(
75+
(e: React.ChangeEvent<HTMLInputElement>) => {
76+
if (!hasTouched) setHasTouched(true);
77+
setPassword(e.target.value);
78+
},
79+
[hasTouched, setPassword],
80+
);
81+
7482
return (
7583
<>
7684
<PasswordToggle
@@ -79,12 +87,10 @@ const Password: FC<Props> = ({
7987
label={label}
8088
placeholder="Your password"
8189
onBlur={() => setHasBlurred(true)}
82-
onChange={(e) => {
83-
if (!hasTouched) setHasTouched(true);
84-
setPassword(e.target.value);
85-
}}
90+
onChange={handlePasswordChange}
8691
value={password}
8792
help={checks.length > 0 && "Password must contain"}
93+
className={isCheckFailed ? "password-error" : ""}
8894
/>
8995
<div className="password-checks">
9096
{checks.map((check) => {

ui/components/PasswordCheck.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ type Props = {
88
status: "success" | "error" | "neutral";
99
};
1010

11+
const STATUSES_WITH_INFO_ICON = ["error", "neutral"];
12+
1113
const PasswordCheck: FC<Props> = ({ check, status }) => {
1214
const getMessage = () => {
1315
switch (check) {
@@ -26,15 +28,16 @@ const PasswordCheck: FC<Props> = ({ check, status }) => {
2628
<div
2729
className={classNames({
2830
"is-success": status === "success",
29-
"is-error": status === "error",
3031
})}
3132
>
3233
<p
3334
className={classNames("p-form-validation__message", {
34-
"is-neutral u-text--muted": status === "neutral",
35+
"is-neutral u-text--muted": STATUSES_WITH_INFO_ICON.includes(status),
3536
})}
3637
>
37-
{status === "neutral" && <Icon name="information" />}
38+
{STATUSES_WITH_INFO_ICON.includes(status) && (
39+
<Icon name="information" />
40+
)}
3841
{getMessage()}
3942
</p>
4043
</div>

ui/pages/login.tsx

Lines changed: 48 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { useEffect, useState, useCallback } from "react";
1212
import React from "react";
1313
import { handleFlowError } from "../util/handleFlowError";
1414
import { Flow } from "../components/Flow";
15-
import { kratos } from "../api/kratos";
15+
import { kratos, loginIdentifierFirst } from "../api/kratos";
1616
import { FlowResponse } from "./consent";
1717
import PageLayout from "../components/PageLayout";
1818
import { replaceAuthLabel } from "../util/replaceAuthLabel";
@@ -35,7 +35,34 @@ type AppConfig = {
3535
oidc_webauthn_sequencing_enabled?: boolean;
3636
};
3737

38-
type IdentifierFirstResponse = { redirect_to: string } | LoginFlow;
38+
const getTitleSuffix = (reqName: string, reqDomain: string) => {
39+
if (reqName && reqDomain) {
40+
return ` to ${reqName} on ${reqDomain}`;
41+
}
42+
if (reqName) {
43+
return ` to ${reqName}`;
44+
}
45+
if (reqDomain) {
46+
return ` to ${reqDomain}`;
47+
}
48+
return "";
49+
};
50+
51+
const resolveLoginTitle = (
52+
isIdentifierFirst: boolean,
53+
isAuthCode: UiNode | undefined,
54+
titleSuffix: string,
55+
) => {
56+
if (isIdentifierFirst) {
57+
return "Sign in";
58+
}
59+
60+
if (isAuthCode) {
61+
return "Verify your identity";
62+
}
63+
64+
return `Sign in${titleSuffix}`;
65+
};
3966

4067
const Login: NextPage = () => {
4168
const [flow, setFlow] = useState<LoginFlow>();
@@ -114,7 +141,7 @@ const Login: NextPage = () => {
114141
if (login_challenge) {
115142
return undefined;
116143
}
117-
return window.location.pathname.replace("login", "manage_details");
144+
return window.location.pathname;
118145
};
119146

120147
// Otherwise we initialize it
@@ -135,9 +162,18 @@ const Login: NextPage = () => {
135162
return;
136163
}
137164

138-
await router.replace(`/ui/login?flow=${data.id}`, undefined, {
139-
shallow: true,
140-
});
165+
await router.replace(
166+
{
167+
pathname: "/ui/login",
168+
query: {
169+
...router.query,
170+
flow: data.id,
171+
},
172+
},
173+
undefined,
174+
{ shallow: true },
175+
);
176+
141177
setFlow(data);
142178
})
143179
.catch(handleFlowError("login", setFlow))
@@ -189,26 +225,7 @@ const Login: NextPage = () => {
189225
if (method === "identifier_first") {
190226
const flowId = String(flow?.id);
191227

192-
return fetch(
193-
`/api/kratos/self-service/login/id-first?flow=${encodeURIComponent(flowId)}`,
194-
{
195-
method: "POST",
196-
headers: {
197-
"Content-Type": "application/json",
198-
},
199-
body: JSON.stringify({
200-
...values,
201-
method,
202-
flow: String(flow?.id),
203-
}),
204-
},
205-
)
206-
.then(async (res) => {
207-
if (!res.ok) {
208-
throw new Error(await res.text());
209-
}
210-
return (await res.json()) as IdentifierFirstResponse;
211-
})
228+
loginIdentifierFirst(flowId, values, method, flow)
212229
.then((data) => {
213230
if ("redirect_to" in data) {
214231
window.location.href = data.redirect_to;
@@ -272,29 +289,12 @@ const Login: NextPage = () => {
272289
},
273290
[flow, router],
274291
);
275-
const reqName = flow?.oauth2_login_request?.client?.client_name;
292+
const reqName = flow?.oauth2_login_request?.client?.client_name ?? "";
276293
const reqDomain = flow?.oauth2_login_request?.client?.client_uri
277294
? new URL(flow.oauth2_login_request.client.client_uri).hostname
278295
: "";
279-
280-
const getTitleSuffix = () => {
281-
if (reqName && reqDomain) {
282-
return ` to ${reqName} on ${reqDomain}`;
283-
}
284-
if (reqName) {
285-
return ` to ${reqName}`;
286-
}
287-
if (reqDomain) {
288-
return ` to ${reqDomain}`;
289-
}
290-
return "";
291-
};
292-
293-
const title = isIdentifierFirst
294-
? "Sign in"
295-
: isAuthCode
296-
? "Verify your identity"
297-
: `Sign in${getTitleSuffix()}`;
296+
const titleSuffix = getTitleSuffix(reqName, reqDomain);
297+
const title = resolveLoginTitle(isIdentifierFirst, isAuthCode, titleSuffix);
298298

299299
const filterFlow = (flow: LoginFlow | undefined): LoginFlow => {
300300
if (!flow) {
@@ -472,7 +472,8 @@ const Login: NextPage = () => {
472472
<>
473473
{isWebauthn && isSequencedLogin && (
474474
<p className="u-text--muted">
475-
Additional authentication needed to get access {getTitleSuffix()}
475+
Additional authentication needed to get access{" "}
476+
{getTitleSuffix(reqName, reqDomain)}
476477
</p>
477478
)}
478479
{flow ? (

ui/static/sass/styles.scss

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,20 @@ hr {
190190
top: 41%;
191191
}
192192
}
193-
}
194193

194+
.password-error {
195+
background-color: var(--vf-color-background-negative-active);
196+
border-bottom-color: var(--vf-color-border-negative);
197+
198+
&:hover {
199+
background-color: var(--vf-color-background-negative-hover);
200+
}
201+
202+
&:focus {
203+
outline-color: #c7162b;
204+
}
205+
}
206+
}
195207

196208
.is-success .p-form-validation__input,
197209
.is-success .p-form-validation__input:hover,

0 commit comments

Comments
 (0)