2025-12-24 17:52:22 +08:00
|
|
|
import { BRANDING_NAME } from '@lobechat/business-const';
|
2026-01-22 19:54:08 +08:00
|
|
|
import { Alert, Button, Flexbox, Icon, Input, Skeleton, Text } from '@lobehub/ui';
|
2025-12-20 21:44:57 +08:00
|
|
|
import { Divider, Form } from 'antd';
|
|
|
|
|
import type { FormInstance, InputRef } from 'antd';
|
2026-01-22 19:54:08 +08:00
|
|
|
import { createStaticStyles } from 'antd-style';
|
2025-12-20 21:44:57 +08:00
|
|
|
import { ChevronRight, Mail } from 'lucide-react';
|
|
|
|
|
import { useEffect, useRef } from 'react';
|
|
|
|
|
import { Trans, useTranslation } from 'react-i18next';
|
|
|
|
|
|
2026-01-23 23:57:08 +08:00
|
|
|
import AuthIcons from '@/components/AuthIcons';
|
2025-12-20 21:44:57 +08:00
|
|
|
import { PRIVACY_URL, TERMS_URL } from '@/const/url';
|
|
|
|
|
|
|
|
|
|
import AuthCard from '../../../../features/AuthCard';
|
|
|
|
|
|
2026-01-22 19:54:08 +08:00
|
|
|
const styles = createStaticStyles(({ css, cssVar }) => ({
|
|
|
|
|
setPasswordLink: css`
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
color: ${cssVar.colorPrimary};
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
`,
|
|
|
|
|
}));
|
|
|
|
|
|
2025-12-20 21:44:57 +08:00
|
|
|
export const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
|
|
export const USERNAME_REGEX = /^\w+$/;
|
|
|
|
|
|
|
|
|
|
export interface SignInEmailStepProps {
|
2026-01-31 18:25:22 +08:00
|
|
|
disableEmailPassword?: boolean;
|
2025-12-20 21:44:57 +08:00
|
|
|
form: FormInstance<{ email: string }>;
|
2026-01-22 19:54:08 +08:00
|
|
|
isSocialOnly: boolean;
|
2025-12-20 21:44:57 +08:00
|
|
|
loading: boolean;
|
|
|
|
|
oAuthSSOProviders: string[];
|
|
|
|
|
onCheckUser: (values: { email: string }) => Promise<void>;
|
2026-01-22 19:54:08 +08:00
|
|
|
onSetPassword: () => void;
|
2025-12-20 21:44:57 +08:00
|
|
|
onSocialSignIn: (provider: string) => void;
|
|
|
|
|
serverConfigInit: boolean;
|
|
|
|
|
socialLoading: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const SignInEmailStep = ({
|
2026-01-31 18:25:22 +08:00
|
|
|
disableEmailPassword,
|
2025-12-20 21:44:57 +08:00
|
|
|
form,
|
2026-01-22 19:54:08 +08:00
|
|
|
isSocialOnly,
|
2025-12-20 21:44:57 +08:00
|
|
|
loading,
|
|
|
|
|
oAuthSSOProviders,
|
|
|
|
|
serverConfigInit,
|
|
|
|
|
socialLoading,
|
|
|
|
|
onCheckUser,
|
2026-01-22 19:54:08 +08:00
|
|
|
onSetPassword,
|
2025-12-20 21:44:57 +08:00
|
|
|
onSocialSignIn,
|
|
|
|
|
}: SignInEmailStepProps) => {
|
|
|
|
|
const { t } = useTranslation('auth');
|
|
|
|
|
const emailInputRef = useRef<InputRef>(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
emailInputRef.current?.focus();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const divider = (
|
|
|
|
|
<Divider>
|
|
|
|
|
<Text fontSize={12} type={'secondary'}>
|
|
|
|
|
{t('betterAuth.signin.orContinueWith')}
|
|
|
|
|
</Text>
|
|
|
|
|
</Divider>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const getProviderLabel = (provider: string) => {
|
|
|
|
|
const normalized = provider
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
.replaceAll(/(^|[_-])([a-z])/g, (_, __, c) => c.toUpperCase());
|
|
|
|
|
const normalizedKey = normalized.replaceAll(/[^\dA-Za-z]/g, '');
|
|
|
|
|
const key = `betterAuth.signin.continueWith${normalizedKey}`;
|
|
|
|
|
return t(key, { defaultValue: `Continue with ${normalized}` });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const footer = (
|
|
|
|
|
<Text fontSize={13} type={'secondary'}>
|
|
|
|
|
<Trans
|
|
|
|
|
components={{
|
|
|
|
|
privacy: (
|
|
|
|
|
<a
|
|
|
|
|
href={PRIVACY_URL}
|
|
|
|
|
style={{ color: 'inherit', cursor: 'pointer', textDecoration: 'underline' }}
|
|
|
|
|
>
|
|
|
|
|
{t('footer.terms')}
|
|
|
|
|
</a>
|
|
|
|
|
),
|
|
|
|
|
terms: (
|
|
|
|
|
<a
|
|
|
|
|
href={TERMS_URL}
|
|
|
|
|
style={{ color: 'inherit', cursor: 'pointer', textDecoration: 'underline' }}
|
|
|
|
|
>
|
|
|
|
|
{t('footer.privacy')}
|
|
|
|
|
</a>
|
|
|
|
|
),
|
|
|
|
|
}}
|
|
|
|
|
i18nKey={'footer.agreement'}
|
|
|
|
|
ns={'auth'}
|
|
|
|
|
/>
|
|
|
|
|
</Text>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<AuthCard
|
|
|
|
|
footer={footer}
|
|
|
|
|
subtitle={t('signin.subtitle', { appName: BRANDING_NAME })}
|
2026-01-31 00:50:11 +08:00
|
|
|
title={'Agent teammates that grow with you'}
|
2025-12-20 21:44:57 +08:00
|
|
|
>
|
|
|
|
|
{!serverConfigInit && (
|
|
|
|
|
<Flexbox gap={12}>
|
|
|
|
|
<Skeleton.Button active block size="large" />
|
|
|
|
|
<Skeleton.Button active block size="large" />
|
|
|
|
|
{divider}
|
|
|
|
|
</Flexbox>
|
|
|
|
|
)}
|
|
|
|
|
{serverConfigInit && oAuthSSOProviders.length > 0 && (
|
|
|
|
|
<Flexbox gap={12}>
|
|
|
|
|
{oAuthSSOProviders.map((provider) => (
|
|
|
|
|
<Button
|
|
|
|
|
block
|
|
|
|
|
icon={
|
|
|
|
|
<Icon
|
|
|
|
|
icon={AuthIcons(provider, 18)}
|
|
|
|
|
style={{
|
|
|
|
|
left: 12,
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
top: 13,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
}
|
|
|
|
|
key={provider}
|
|
|
|
|
loading={socialLoading === provider}
|
|
|
|
|
onClick={() => onSocialSignIn(provider)}
|
|
|
|
|
size="large"
|
|
|
|
|
>
|
|
|
|
|
{getProviderLabel(provider)}
|
|
|
|
|
</Button>
|
|
|
|
|
))}
|
2026-01-31 18:25:22 +08:00
|
|
|
{!disableEmailPassword && divider}
|
2025-12-20 21:44:57 +08:00
|
|
|
</Flexbox>
|
|
|
|
|
)}
|
2026-01-31 18:25:22 +08:00
|
|
|
{serverConfigInit && disableEmailPassword && oAuthSSOProviders.length === 0 && (
|
|
|
|
|
<Alert description={t('betterAuth.signin.ssoOnlyNoProviders')} showIcon type="warning" />
|
|
|
|
|
)}
|
|
|
|
|
{!disableEmailPassword && (
|
|
|
|
|
<Form
|
|
|
|
|
form={form}
|
|
|
|
|
layout="vertical"
|
|
|
|
|
onFinish={(values) => onCheckUser(values as { email: string })}
|
2025-12-20 21:44:57 +08:00
|
|
|
>
|
2026-01-31 18:25:22 +08:00
|
|
|
<Form.Item
|
|
|
|
|
name="email"
|
|
|
|
|
rules={[
|
|
|
|
|
{ message: t('betterAuth.errors.emailRequired'), required: true },
|
|
|
|
|
{
|
|
|
|
|
validator: (_, value) => {
|
|
|
|
|
if (!value) return Promise.resolve();
|
|
|
|
|
const trimmedValue = (value as string).trim();
|
|
|
|
|
if (EMAIL_REGEX.test(trimmedValue) || USERNAME_REGEX.test(trimmedValue)) {
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
}
|
|
|
|
|
return Promise.reject(new Error(t('betterAuth.errors.emailInvalid')));
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
]}
|
|
|
|
|
style={{ marginBottom: 0 }}
|
|
|
|
|
>
|
|
|
|
|
<Input
|
|
|
|
|
placeholder={t('betterAuth.signin.emailPlaceholder')}
|
|
|
|
|
prefix={
|
|
|
|
|
<Icon
|
|
|
|
|
icon={Mail}
|
|
|
|
|
style={{
|
|
|
|
|
marginInline: 6,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
}
|
|
|
|
|
ref={emailInputRef}
|
|
|
|
|
size="large"
|
|
|
|
|
style={{
|
|
|
|
|
padding: 6,
|
|
|
|
|
}}
|
|
|
|
|
suffix={
|
|
|
|
|
<Button
|
|
|
|
|
icon={ChevronRight}
|
|
|
|
|
loading={loading}
|
|
|
|
|
onClick={() => form.submit()}
|
|
|
|
|
title={t('betterAuth.signin.nextStep')}
|
|
|
|
|
variant={'filled'}
|
|
|
|
|
/>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Form>
|
|
|
|
|
)}
|
2026-01-22 19:54:08 +08:00
|
|
|
{isSocialOnly && (
|
|
|
|
|
<Alert
|
|
|
|
|
description={
|
|
|
|
|
<>
|
|
|
|
|
{t('betterAuth.signin.socialOnlyHint')}{' '}
|
|
|
|
|
<a className={styles.setPasswordLink} onClick={onSetPassword}>
|
|
|
|
|
{t('betterAuth.signin.setPassword')}
|
|
|
|
|
</a>
|
|
|
|
|
</>
|
|
|
|
|
}
|
|
|
|
|
showIcon
|
|
|
|
|
style={{ marginTop: 12 }}
|
|
|
|
|
type="info"
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2025-12-20 21:44:57 +08:00
|
|
|
</AuthCard>
|
|
|
|
|
);
|
|
|
|
|
};
|