Skip to content

Commit 69ce0a1

Browse files
committed
chore: css and sdk fixes for login screen
1 parent 967425f commit 69ce0a1

File tree

8 files changed

+185
-74
lines changed

8 files changed

+185
-74
lines changed

src/common/Input/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
4646
} else if (forceFocusStyle) {
4747
borderAndFocusStyles = "border-link ring-1 ring-link";
4848
} else {
49-
borderAndFocusStyles = "border-gray-mid focus:ring-1 focus:ring-link focus:border-link";
49+
borderAndFocusStyles =
50+
"border-gray-mid focus:ring-1 focus:ring-link focus:border-link";
5051
}
5152

5253
return (

src/common/Label/index.tsx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,36 @@ export interface LabelProps
66
htmlFor: string;
77
className?: string;
88
isError?: boolean;
9+
forceApplyFocusStyle?: boolean; //Especially for password input icon button
910
}
1011

1112
const Label: React.FC<LabelProps> = ({
1213
children,
1314
htmlFor,
1415
className,
1516
isError,
17+
forceApplyFocusStyle,
1618
...rest
1719
}) => {
18-
const unfloatedTextColor = isError ? "text-error" : "text-text-secondary";
19-
const baseLabelStyles =
20-
`absolute left-3 top-1/2 -translate-y-1/2 ${unfloatedTextColor} transition-all duration-200 ease-in-out pointer-events-none origin-[0]`;
20+
const unfloatedTextColor = isError
21+
? "text-error"
22+
: forceApplyFocusStyle
23+
? "!text-link"
24+
: "text-text-secondary";
2125

22-
// Determine floated text color based on isError
23-
const floatedTextColor = isError ? "!text-error" : "text-link";
26+
const baseLabelStyles = `absolute left-3 top-1/2 -translate-y-1/2 ${unfloatedTextColor} transition-all duration-200 ease-in-out pointer-events-none origin-[0]`;
27+
28+
const floatedTextColorForFilledOrForced = isError
29+
? "!text-error"
30+
: "!text-link";
2431

25-
// Styles for when the label is floated (input has focus or value)
26-
// Includes moving up, scaling down, changing color, and adding a background to cut through the input border
2732
const floatedLabelStyles =
28-
// Base floating anatomy (scale, position, z-index)
2933
"peer-focus:scale-75 peer-focus:-translate-y-[1.18rem] peer-focus:top-2 peer-focus:z-10 " +
34+
(isError ? "peer-focus:!text-error " : "peer-focus:!text-link ") +
3035
"peer-[.is-forced-focus]:scale-75 peer-[.is-forced-focus]:-translate-y-[1.18rem] peer-[.is-forced-focus]:top-2 peer-[.is-forced-focus]:z-10 " +
36+
`peer-[.is-forced-focus]:${floatedTextColorForFilledOrForced} ` +
3137
"peer-[:not(:placeholder-shown)]:scale-75 peer-[:not(:placeholder-shown)]:-translate-y-[1.18rem] peer-[:not(:placeholder-shown)]:top-2 peer-[:not(:placeholder-shown)]:z-10 " +
32-
// Apply determined text color to all floated states
33-
`peer-focus:${floatedTextColor} ` +
34-
`peer-[.is-forced-focus]:${floatedTextColor} ` +
35-
`peer-[:not(:placeholder-shown)]:${floatedTextColor} ` +
36-
// Background for cut-through effect (common to all floated states)
38+
`peer-[:not(:placeholder-shown)]:${floatedTextColorForFilledOrForced} ` +
3739
"peer-focus:bg-background-widget peer-focus:px-2 " +
3840
"peer-[.is-forced-focus]:bg-background-widget peer-[.is-forced-focus]:px-2 " +
3941
"peer-[:not(:placeholder-shown)]:bg-background-widget peer-[:not(:placeholder-shown)]:px-2";

src/common/PasswordInput/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const PasswordInput: React.FC<PasswordInputProps> = ({
5151
children: label,
5252
htmlFor: generatedId,
5353
className: labelClassName,
54+
forceApplyFocusStyle: isIconButtonFocused,
5455
};
5556

5657
const formFieldInputProps: InputProps = {

src/index.css

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,5 @@ body {
4141
margin: 0;
4242
background-color: var(--color-background-page);
4343
color: var(--color-text-default);
44-
padding-top: 3rem;
45-
padding-bottom: 3rem;
4644
min-height: 100vh;
47-
display: flex;
48-
justify-content: center;
49-
align-items: center;
5045
}

src/mock-data/login-password.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"code": "IN",
3838
"prefix": "91"
3939
},
40+
<<<<<<< HEAD
4041
"alternate_connections": [
4142
{
4243
"name": "google-oauth2",
@@ -45,6 +46,18 @@
4546
{
4647
"name": "linkedin",
4748
"strategy": "linkedin"
49+
=======
50+
"errors": [
51+
{
52+
"code": "invalid-captcha",
53+
"message": "Solve the challenge question to verify you are not a robot.",
54+
"field": "captcha"
55+
},
56+
{
57+
"code": "wrong-email-username-credentials",
58+
"message": "Incorrect email address, username, or password",
59+
"field": "password"
60+
>>>>>>> 6d648a5 (chore: css and sdk fixes for login screen)
4861
}
4962
],
5063
"connection": {

src/screens/login/index.tsx

Lines changed: 61 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,68 @@
1+
import React from "react";
2+
13
import Button from "@/common/Button";
2-
import FormField from "@/common/FormField";
4+
import CaptchaBox from "@/common/CaptchaBox";
35
import Card from "@/common/Card";
4-
import PasswordInput from "@/common/PasswordInput";
6+
import FormField from "@/common/FormField";
57
import Logo from "@/common/Logo";
6-
import CaptchaBox from "@/common/CaptchaBox";
8+
import PasswordInput from "@/common/PasswordInput";
79
import Separator from "@/common/Separator";
8-
import { useLoginManager } from "./hooks/useLoginManager";
9-
import { useLoginForm } from "./hooks/useLoginForm";
1010
import SocialProviderButton from "@/common/SocialProviderButton";
11-
import { getIcon } from "@/utils/iconUtils";
12-
import Alert from "@/common/Alert";
11+
1312
import { BrandingProvider } from "@/context/BrandingProvider";
13+
import { getIcon } from "@/utils/iconUtils";
14+
import { getSdkErrorForField } from "@/utils/errorUtils";
15+
import { useLoginForm } from "./hooks/useLoginForm";
16+
import { useLoginManager } from "./hooks/useLoginManager";
17+
import type { SdkError } from "@/utils/errorUtils";
1418

1519
const LoginScreen: React.FC = () => {
1620
const { handleLogin, handleSocialLogin, loginInstance } = useLoginManager();
1721
const { usernameRef, passwordRef, captchaRef, getFormValues } =
1822
useLoginForm();
1923

20-
const onLoginClick = (e: React.FormEvent) => {
21-
e.preventDefault();
22-
const { username, password, captcha } = getFormValues();
23-
handleLogin(username, password, captcha);
24-
};
25-
26-
// CAPTCHA related variables
2724
const texts = loginInstance?.screen?.texts || {};
25+
// IMP: This is a to set the page title dynamically
26+
const pageTitle = texts?.pageTitle || "Login";
27+
document.title = pageTitle;
28+
29+
const sdkErrors: SdkError[] = (loginInstance?.transaction?.errors ||
30+
[]) as SdkError[];
2831
const isCaptchaAvailable = !!loginInstance?.screen?.captcha;
2932
const captchaImage = loginInstance?.screen?.captcha?.image || "";
3033
const captchaLabelText =
3134
(texts.captchaCodePlaceholder || "Enter the code shown above") + "*";
3235

33-
// Prepare global messages for the Alert component
34-
// Attempt to get screen messages, default to empty array if undefined
35-
const screenMessages: { type: string; text: string }[] =
36-
(loginInstance?.screen as any)?.messages || [];
37-
const firstErrorMessage = screenMessages.find(
38-
(m: { type: string; text: string }) => m.type === "error",
39-
);
36+
const onLoginClick = (e: React.FormEvent) => {
37+
e.preventDefault();
38+
const { username, password, captcha } = getFormValues();
39+
handleLogin(username, password, captcha);
40+
};
4041

41-
// Additionally, check for a general pageError from the loginInstance
42-
const pageError = (loginInstance as any)?.pageError;
43-
let errorMessageToShow: string | undefined = firstErrorMessage?.text;
44-
if (!errorMessageToShow && typeof pageError === "string" && pageError) {
45-
errorMessageToShow = pageError;
46-
}
47-
// TODO: Potentially handle other message types (warning, info, success) or multiple messages.
42+
const getFieldError = (fieldName: string): string | undefined => {
43+
return getSdkErrorForField(fieldName, sdkErrors);
44+
};
4845

4946
return (
5047
<BrandingProvider screenInstance={loginInstance}>
51-
<div className="min-h-screen flex items-center justify-center p-4">
48+
<div className="min-h-screen flex items-center justify-center px-10 py-20">
49+
{/* Parent Card */}
5250
<Card className="w-full max-w-[400px]">
51+
{/* Header section */}
5352
<Logo imageClassName="h-13" />
5453
<h1 className="text-2xl font-normal text-center text-text-default mt-6 mb-4">
55-
{loginInstance?.screen?.texts?.title || "Welcome"}
54+
{loginInstance?.screen?.texts?.title}
5655
</h1>
5756
<p className="text-center text-text-default text-sm mb-4">
58-
{loginInstance?.screen?.texts?.description || "Log in to continue."}
57+
{loginInstance?.screen?.texts?.description}
5958
</p>
6059

61-
{errorMessageToShow && (
62-
<Alert
63-
type="error"
64-
message={errorMessageToShow}
65-
title={
66-
loginInstance?.screen?.texts?.alertListTitle ||
67-
texts.titleLoginError ||
68-
"Login Error"
69-
}
70-
className="mb-4"
71-
/>
72-
)}
73-
60+
{/* Login form */}
7461
<form onSubmit={onLoginClick} className="space-y-4">
7562
<FormField
7663
className="mb-4"
7764
labelProps={{
78-
children: `${loginInstance?.screen?.texts?.phoneOrUsernameOrEmailPlaceholder || "Phone or Username or Email"}*`,
65+
children: `${loginInstance?.screen?.texts?.usernameOrEmailPlaceholder}*`,
7966
htmlFor: "email-login",
8067
}}
8168
inputProps={{
@@ -85,18 +72,21 @@ const LoginScreen: React.FC = () => {
8572
ref: usernameRef,
8673
placeholder: "\u00A0",
8774
autoComplete: "email",
75+
required: true,
8876
}}
89-
error="this is a dummy error message for testing"
77+
error={getFieldError("username") || getFieldError("email")}
9078
/>
9179

9280
<PasswordInput
9381
className="mb-4"
94-
label={`${loginInstance?.screen?.texts?.passwordPlaceholder || "Password"}*`}
82+
label={`${texts.passwordPlaceholder || "Password"}*`}
9583
name="password"
9684
inputProps={{
9785
ref: passwordRef,
9886
autoComplete: "current-password",
87+
required: true,
9988
}}
89+
error={getFieldError("password")}
10090
/>
10191

10292
{isCaptchaAvailable && captchaImage && (
@@ -107,33 +97,49 @@ const LoginScreen: React.FC = () => {
10797
imageUrl={captchaImage}
10898
inputProps={{
10999
ref: captchaRef,
100+
required: isCaptchaAvailable,
110101
}}
111102
imageClassName="h-16"
103+
error={getFieldError("captcha")}
112104
/>
113105
)}
114106
<div className="mt-6 text-left">
115-
<Button variant="link" size="sm" className="p-1 font-bold">
116-
{loginInstance?.screen?.texts?.forgotPasswordText ||
117-
"Forgot password?"}
118-
</Button>
107+
{loginInstance?.screen?.links?.reset_password &&
108+
loginInstance?.screen?.texts?.forgotPasswordText && (
109+
<a
110+
href={loginInstance.screen.links.reset_password}
111+
className="text-sm text-link hover:text-link-hover active:text-link-pressed font-bold p-1"
112+
>
113+
{loginInstance.screen.texts.forgotPasswordText}
114+
</a>
115+
)}
119116
</div>
117+
118+
{/* Login button */}
120119
<Button type="submit" fullWidth>
121-
{loginInstance?.screen?.texts?.buttonText || "Continue"}
120+
{loginInstance?.screen?.texts?.buttonText}
122121
</Button>
123122
</form>
124123

124+
{/* Footer text */}
125125
<div className="mt-4 text-left">
126126
<span className="text-sm">
127127
{loginInstance?.screen?.texts?.dontHaveAccountText ||
128128
"Don't have an account?"}
129129
</span>{" "}
130-
<Button variant="link" size="sm" className="px-1">
131-
{loginInstance?.screen?.texts?.signUpText || "Sign up"}
132-
</Button>
130+
{loginInstance?.screen?.links?.signup && (
131+
<a
132+
href={loginInstance.screen.links.signup}
133+
className="text-sm font-bold text-link hover:text-link-hover active:text-link-pressed px-1"
134+
>
135+
{loginInstance.screen.texts?.signUpText || "Sign up"}
136+
</a>
137+
)}
133138
</div>
134139

135140
<Separator text="OR" />
136141

142+
{/* Social login buttons */}
137143
<div className="space-y-3">
138144
{loginInstance?.transaction?.alternateConnections?.map(
139145
(connection) => (

src/utils/errorUtils.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Represents a generic SDK error object.
3+
*/
4+
export interface SdkError {
5+
code?: string;
6+
message: string;
7+
field?: string;
8+
[key: string]: any; // Allow other properties
9+
}
10+
11+
/**
12+
* Finds and returns the error message for a specific field from a list of SDK errors.
13+
* @param fieldName The name of the field to find an error for.
14+
* @param sdkErrors An array of SDK error objects.
15+
* @returns The error message string if found, otherwise undefined.
16+
*/
17+
export const getSdkErrorForField = (
18+
fieldName: string,
19+
sdkErrors: SdkError[] = [],
20+
): string | undefined => {
21+
if (!Array.isArray(sdkErrors)) {
22+
return undefined;
23+
}
24+
const error = sdkErrors.find((err) => err.field === fieldName);
25+
return error?.message;
26+
};

src/utils/placeholderUtils.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Get the appropriate placeholder text based on allowed identifiers.
3+
* @param allowedIdentifiers Array of allowed identifier types (e.g., ["email"], ["username", "email"])
4+
* @param texts Text configuration object from screen instance (e.g., loginInstance.screen.texts)
5+
* @param appendAsterisk Whether to append an asterisk (*) to the placeholder (defaults to true)
6+
* @returns Appropriate placeholder text string.
7+
*/
8+
export const getDynamicPlaceholder = (
9+
allowedIdentifiers: string[] = [],
10+
texts: Record<string, string> = {},
11+
appendAsterisk: boolean = true,
12+
): string => {
13+
// Ensure texts is an object, even if undefined from SDK
14+
const safeTexts = texts || {};
15+
16+
let placeholderKey = "default";
17+
const sortedIdentifiers = [...new Set(allowedIdentifiers)].sort().join(","); // Deduplicate and sort for a consistent key
18+
19+
if (allowedIdentifiers.length === 0) {
20+
// Fallback if no identifiers specified, though usually there will be.
21+
placeholderKey = "default";
22+
} else if (sortedIdentifiers === "email,phone,username") {
23+
placeholderKey = safeTexts.phoneOrUsernameOrEmailPlaceholder
24+
? "phoneOrUsernameOrEmailPlaceholder"
25+
: "default";
26+
} else if (sortedIdentifiers === "email,phone") {
27+
placeholderKey = safeTexts.phoneOrEmailPlaceholder
28+
? "phoneOrEmailPlaceholder"
29+
: "default";
30+
} else if (sortedIdentifiers === "phone,username") {
31+
placeholderKey = safeTexts.phoneOrUsernamePlaceholder
32+
? "phoneOrUsernamePlaceholder"
33+
: "default";
34+
} else if (sortedIdentifiers === "email,username") {
35+
placeholderKey = safeTexts.usernameOrEmailPlaceholder
36+
? "usernameOrEmailPlaceholder"
37+
: "default";
38+
} else if (sortedIdentifiers === "email") {
39+
placeholderKey = safeTexts.emailPlaceholder
40+
? "emailPlaceholder"
41+
: "default";
42+
} else if (sortedIdentifiers === "phone") {
43+
placeholderKey = safeTexts.phonePlaceholder
44+
? "phonePlaceholder"
45+
: "default";
46+
} else if (sortedIdentifiers === "username") {
47+
placeholderKey = safeTexts.usernameOnlyPlaceholder
48+
? "usernameOnlyPlaceholder"
49+
: "default";
50+
}
51+
52+
let placeholderText =
53+
safeTexts[placeholderKey] ||
54+
safeTexts.phoneOrUsernameOrEmailPlaceholder ||
55+
"Phone or Username or Email"; // Ultimate fallback
56+
57+
if (
58+
placeholderKey === "default" &&
59+
safeTexts.phoneOrUsernameOrEmailPlaceholder
60+
) {
61+
placeholderText = safeTexts.phoneOrUsernameOrEmailPlaceholder;
62+
} else if (placeholderKey === "default") {
63+
placeholderText = "Login ID"; // A more generic default if no specific placeholders are found
64+
}
65+
66+
return appendAsterisk ? `${placeholderText}*` : placeholderText;
67+
};

0 commit comments

Comments
 (0)