Skip to content

Commit ac45715

Browse files
committed
refactor: enhance social provider button and login forms with improved structure and functionality
1 parent e64db6f commit ac45715

File tree

14 files changed

+263
-65
lines changed

14 files changed

+263
-65
lines changed

src/common/SocialProviderButton/index.tsx

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
import React from "react";
22

3-
export interface SocialProviderButtonProps {
4-
providerName: string;
5-
icon: React.ReactNode;
3+
interface SocialProviderButtonProps {
64
onClick: () => void;
5+
displayName: string;
6+
iconComponent: React.ReactNode | null;
77
disabled?: boolean;
88
className?: string;
99
}
1010

1111
const SocialProviderButton: React.FC<SocialProviderButtonProps> = ({
12-
providerName,
13-
icon,
1412
onClick,
13+
displayName,
14+
iconComponent,
1515
disabled = false,
1616
className = "",
1717
}) => {
18+
const dataTestId = `social-provider-button-${displayName.toLowerCase().replace(/\s+/g, "-")}`;
19+
1820
const baseStyles =
1921
"flex items-center justify-start w-full max-w-[320px] h-[52px] py-[14px] px-[16px] border rounded gap-x-4 focus:outline-none transition-colors duration-150 ease-in-out focus:ring-4 focus:ring-primary/15";
2022

@@ -28,13 +30,18 @@ const SocialProviderButton: React.FC<SocialProviderButtonProps> = ({
2830
<button
2931
type="button"
3032
onClick={onClick}
31-
disabled={disabled}
32-
className={`${baseStyles} ${disabled ? disabledStyles : enabledStyles}
33-
${className}`}
34-
aria-label={`Continue with ${providerName}`}
33+
className={`${baseStyles} ${disabled ? disabledStyles : enabledStyles} ${className}`}
34+
data-testid={dataTestId}
35+
title={`Continue with ${displayName}`}
3536
>
36-
{icon}
37-
<span className="font-normal text-base">{`Continue with ${providerName}`}</span>
37+
{iconComponent && (
38+
<span className="mr-3 w-5 h-5 flex items-center justify-center flex-shrink-0">
39+
{iconComponent}
40+
</span>
41+
)}
42+
<span className="overflow-hidden whitespace-nowrap text-ellipsis font-normal text-base">
43+
Continue with {displayName}
44+
</span>
3845
</button>
3946
);
4047
};

src/mock-data/login-id.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@
4242
"name": "google-oauth2",
4343
"strategy": "google"
4444
},
45+
{
46+
"name": "hugging-face",
47+
"strategy": "oauth2"
48+
},
49+
{
50+
"name": "klarna-social-connection",
51+
"strategy": "oauth2"
52+
},
53+
{
54+
"name": "digitalocean",
55+
"strategy": "oauth2"
56+
},
4557
{
4658
"name": "linkedin",
4759
"strategy": "linkedin"

src/mock-data/login-password.json

Lines changed: 4 additions & 0 deletions
Large diffs are not rendered by default.

src/mock-data/login.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@
4242
"name": "google-oauth2",
4343
"strategy": "google"
4444
},
45+
{
46+
"name": "hugging-face",
47+
"strategy": "oauth2"
48+
},
49+
{
50+
"name": "klarna-social-connection",
51+
"strategy": "oauth2"
52+
},
53+
{
54+
"name": "digitalocean",
55+
"strategy": "oauth2"
56+
},
4557
{
4658
"name": "linkedin",
4759
"strategy": "linkedin"

src/screens/login-id/components/AlternativeLogins.tsx

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import React from "react";
22
import Separator from "@/common/Separator";
33
import SocialProviderButton from "@/common/SocialProviderButton";
4+
import { getSocialProviderDetails } from "@/utils/socialUtils";
5+
import type { SocialConnection } from "@/utils/socialUtils";
46
import { getIcon } from "@/utils/iconUtils";
57
import { useLoginIdManager } from "../hooks/useLoginIdManager";
68

@@ -9,8 +11,8 @@ const AlternativeLogins: React.FC = () => {
911
const { loginIdInstance, handleSocialLogin, handlePasskeyLogin } =
1012
useLoginIdManager();
1113

12-
const alternateConnections =
13-
loginIdInstance?.transaction?.alternateConnections;
14+
const alternateConnections = loginIdInstance?.transaction
15+
?.alternateConnections as SocialConnection[] | undefined;
1416
const isPasskeyAvailable = !!loginIdInstance?.screen?.data?.passkey;
1517

1618
const showSeparator =
@@ -25,24 +27,23 @@ const AlternativeLogins: React.FC = () => {
2527
{isPasskeyAvailable && (
2628
<SocialProviderButton
2729
key="passkey"
28-
providerName={"Passkey"}
29-
icon={getIcon("passkey")}
30+
displayName={"Passkey"}
31+
iconComponent={getIcon("passkey")}
3032
onClick={() => handlePasskeyLogin()}
3133
/>
3234
)}
33-
{alternateConnections?.map((connection: any) => (
34-
<SocialProviderButton
35-
key={connection.name}
36-
providerName={
37-
connection.strategy
38-
? connection.strategy?.charAt(0)?.toUpperCase() +
39-
connection.strategy?.slice(1)
40-
: connection.name
41-
}
42-
icon={getIcon(connection.name)}
43-
onClick={() => handleSocialLogin(connection.name)}
44-
/>
45-
))}
35+
{alternateConnections?.map((connection) => {
36+
const { displayName, iconComponent } =
37+
getSocialProviderDetails(connection);
38+
return (
39+
<SocialProviderButton
40+
key={connection.name}
41+
displayName={displayName}
42+
iconComponent={iconComponent}
43+
onClick={() => handleSocialLogin(connection.name)}
44+
/>
45+
);
46+
})}
4647
</div>
4748
</>
4849
);

src/screens/login-id/components/IdentifierForm.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Button from "@/common/Button";
33
import CaptchaBox from "@/common/CaptchaBox";
44
import FormField from "@/common/FormField";
55
import { getFieldError } from "@/utils/errorUtils";
6+
import { rebaseLinkToCurrentOrigin } from "@/utils/urlUtils";
67
import { useLoginIdManager } from "../hooks/useLoginIdManager";
78
import { useLoginIdForm } from "../hooks/useLoginIdForm";
89
import type { SdkError } from "@/utils/errorUtils";
@@ -24,6 +25,12 @@ const IdentifierForm: React.FC = () => {
2425
handleLoginId(identifier, captcha);
2526
};
2627

28+
const originalResetPasswordLink =
29+
loginIdInstance?.screen?.links?.reset_password;
30+
const localizedResetPasswordLink = rebaseLinkToCurrentOrigin(
31+
originalResetPasswordLink,
32+
);
33+
2734
return (
2835
<form onSubmit={onLoginIdSubmit} className="space-y-4">
2936
<FormField
@@ -65,9 +72,9 @@ const IdentifierForm: React.FC = () => {
6572
/>
6673
)}
6774
<div className="text-left">
68-
{loginIdInstance?.screen?.links?.reset_password && (
75+
{localizedResetPasswordLink && (
6976
<a
70-
href={loginIdInstance?.screen?.links?.reset_password}
77+
href={localizedResetPasswordLink}
7178
className="text-sm text-link font-bold hover:text-link/80 focus:bg-link/15 focus:rounded p-1"
7279
>
7380
Forgot Password?

src/screens/login-password/components/LoginPasswordForm.tsx

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,51 @@ import Button from "@/common/Button";
33
import PasswordInput from "@/common/PasswordInput";
44
import type { SdkError } from "@/utils/errorUtils";
55
import { getFieldError } from "@/utils/errorUtils";
6+
import { rebaseLinkToCurrentOrigin } from "@/utils/urlUtils";
67
import { useLoginPasswordManager } from "../hooks/userLoginPasswordManager";
78
import { useLoginPasswordForm } from "../hooks/useLoginPasswordForm";
89
import FormField from "@/common/FormField";
10+
import CaptchaBox from "@/common/CaptchaBox";
911

1012
const LoginForm: React.FC = () => {
1113
const { loginPasswordInstance, username, editIdentifierLink, handleLogin } =
1214
useLoginPasswordManager();
13-
const { passwordRef, getFormValues } = useLoginPasswordForm();
15+
const { passwordRef, captchaRef, getFormValues } = useLoginPasswordForm();
16+
17+
const captchaImage = loginPasswordInstance?.screen?.captchaImage;
18+
const isCaptchaAvailable = loginPasswordInstance?.screen?.isCaptchaAvailable;
1419

1520
const sdkErrors: SdkError[] = (loginPasswordInstance?.transaction?.errors ||
1621
[]) as SdkError[];
1722

1823
const onLoginClick = (e: React.FormEvent) => {
1924
e.preventDefault();
20-
const { password } = getFormValues();
21-
handleLogin({ username: username || "", password });
25+
const { password, captcha } = getFormValues();
26+
handleLogin({
27+
username: username || "",
28+
password,
29+
captcha: isCaptchaAvailable ? captcha : undefined,
30+
});
2231
};
2332

33+
const localizedEditIdentifierLink =
34+
rebaseLinkToCurrentOrigin(editIdentifierLink);
35+
const originalResetPasswordLink =
36+
loginPasswordInstance?.screen?.links?.reset_password;
37+
const localizedResetPasswordLink = rebaseLinkToCurrentOrigin(
38+
originalResetPasswordLink,
39+
);
40+
2441
return (
2542
<form onSubmit={onLoginClick} className="space-y-4">
2643
<FormField
2744
className="mb-4 w-full"
2845
labelProps={{
2946
children: `Username or Email address*`,
30-
htmlFor: "email-login",
47+
htmlFor: "email-login-lp",
3148
}}
3249
inputProps={{
33-
id: "email-login",
50+
id: "email-login-lp",
3451
name: "email",
3552
type: "email",
3653
value: username,
@@ -39,12 +56,15 @@ const LoginForm: React.FC = () => {
3956
disabled: true,
4057
}}
4158
inputIcon={
42-
<a
43-
href={editIdentifierLink}
44-
className="text-sm text-link font-bold hover:text-link/80 focus:bg-link/15 focus:rounded p-1 px-3"
45-
>
46-
Edit
47-
</a>
59+
localizedEditIdentifierLink && (
60+
<a
61+
href={localizedEditIdentifierLink}
62+
className="text-sm text-link font-bold hover:text-link/80 focus:bg-link/15 focus:rounded p-1 px-3"
63+
data-testid="edit-identifier"
64+
>
65+
Edit
66+
</a>
67+
)
4868
}
4969
/>
5070

@@ -62,11 +82,27 @@ const LoginForm: React.FC = () => {
6282
error={getFieldError("password", sdkErrors)}
6383
/>
6484

85+
{isCaptchaAvailable && captchaImage && (
86+
<CaptchaBox
87+
className="mb-4"
88+
id="captcha-input-login-password"
89+
label="Enter the code shown above"
90+
imageUrl={captchaImage}
91+
inputProps={{
92+
ref: captchaRef,
93+
required: isCaptchaAvailable,
94+
maxLength: 15,
95+
}}
96+
error={getFieldError("captcha", sdkErrors)}
97+
/>
98+
)}
99+
65100
<div className="mt-6 text-left">
66-
{loginPasswordInstance?.screen?.links?.reset_password && (
101+
{localizedResetPasswordLink && (
67102
<a
68-
href={loginPasswordInstance.screen.links.reset_password}
103+
href={localizedResetPasswordLink}
69104
className="text-sm text-link font-bold hover:text-link/80 focus:bg-link/15 focus:rounded p-1"
105+
data-testid="forgot-password"
70106
>
71107
Forgot password?
72108
</a>

src/screens/login-password/hooks/useLoginPasswordForm.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@ export const useLoginPasswordForm = () => {
44
const passwordRef = useRef<HTMLInputElement>(
55
null,
66
) as React.RefObject<HTMLInputElement>;
7+
const captchaRef = useRef<HTMLInputElement>(
8+
null,
9+
) as React.RefObject<HTMLInputElement>;
710

811
const getFormValues = () => ({
912
password: passwordRef.current?.value ?? "",
13+
captcha: captchaRef.current?.value ?? "",
1014
});
1115

1216
return {
1317
passwordRef,
18+
captchaRef,
1419
getFormValues,
1520
};
1621
};

src/screens/login-password/hooks/userLoginPasswordManager.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,15 @@ export const useLoginPasswordManager = () => {
99
loginPasswordInstance.screen.editIdentifierLink || "";
1010
const username = loginPasswordInstance.screen.data?.username || "";
1111

12-
const handleLogin = (options: { username: string; password: string }) => {
12+
const handleLogin = (options: {
13+
username: string;
14+
password: string;
15+
captcha?: string;
16+
}) => {
1317
const payload = {
1418
username: options.username,
1519
password: options.password,
20+
captcha: options.captcha,
1621
};
1722
executeSafely(
1823
`Login password with options: ${JSON.stringify(payload)}`,

src/screens/login/components/AlternativeConnections.tsx

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import React from "react";
22
import Separator from "@/common/Separator";
33
import SocialProviderButton from "@/common/SocialProviderButton";
4-
import { getIcon } from "@/utils/iconUtils";
4+
import { getSocialProviderDetails } from "@/utils/socialUtils";
5+
import type { SocialConnection } from "@/utils/socialUtils";
56
import { useLoginManager } from "../hooks/useLoginManager";
67

78
const AlternativeConnections: React.FC = () => {
89
const { loginInstance, handleSocialLogin } = useLoginManager();
9-
const alternateConnections = loginInstance?.transaction?.alternateConnections;
10+
const alternateConnections = loginInstance?.transaction
11+
?.alternateConnections as SocialConnection[] | undefined;
1012

1113
if (!alternateConnections || alternateConnections.length === 0) {
1214
return null;
@@ -15,20 +17,19 @@ const AlternativeConnections: React.FC = () => {
1517
return (
1618
<>
1719
<Separator text="OR" />
18-
<div className="space-y-3">
19-
{alternateConnections.map((connection: any) => (
20-
<SocialProviderButton
21-
key={connection.name}
22-
providerName={
23-
connection.strategy
24-
? connection.strategy?.charAt(0)?.toUpperCase() +
25-
connection.strategy?.slice(1)
26-
: connection.name
27-
}
28-
icon={getIcon(connection.name)}
29-
onClick={() => handleSocialLogin(connection.name)}
30-
/>
31-
))}
20+
<div className="space-y-3 mt-4">
21+
{alternateConnections.map((connection) => {
22+
const { displayName, iconComponent } =
23+
getSocialProviderDetails(connection);
24+
return (
25+
<SocialProviderButton
26+
key={connection.name}
27+
displayName={displayName}
28+
iconComponent={iconComponent}
29+
onClick={() => handleSocialLogin(connection.name)}
30+
/>
31+
);
32+
})}
3233
</div>
3334
</>
3435
);

0 commit comments

Comments
 (0)