diff --git a/src/api/auth.ts b/src/api/auth.ts index 4e7db24..86e4f42 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -87,7 +87,10 @@ export const authApi = { message: string; email?: string; merge_required?: boolean; - merge_token?: string; + // 'email_code' means the email belongs to another account: a confirmation + // code was mailed to it and must be verified before a merge token is issued. + merge_verification?: 'email_code'; + merge_token?: string | null; }> => { const response = await apiClient.post('/cabinet/auth/email/register', { email, @@ -97,6 +100,14 @@ export const authApi = { return response.data; }, + // Confirm an email account merge with the code sent to the existing account. + verifyEmailMerge: async ( + code: string, + ): Promise<{ message: string; merge_required?: boolean; merge_token?: string }> => { + const response = await apiClient.post('/cabinet/auth/email/merge/verify', { code }); + return response.data; + }, + registerEmailStandalone: async (data: { email: string; password: string; diff --git a/src/locales/en.json b/src/locales/en.json index b36979e..b81f0de 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -4595,7 +4595,12 @@ }, "backToAccounts": "Back to accounts" }, - "emailMergePasswordRequired": "This email already belongs to another account. To merge the accounts, enter that account's password." + "emailMergePasswordRequired": "This email already belongs to another account. To merge the accounts, enter that account's password.", + "emailMergeCodeSent": "A confirmation code was sent to that email.", + "emailMergeCodeDescription": "This email already belongs to another account. We sent a code to it — enter it to confirm ownership and merge the accounts.", + "emailMergeCodeLabel": "Code from the email", + "emailMergeConfirm": "Confirm and merge", + "emailMergeCodeInvalid": "Invalid or expired code" }, "theme": { "colors": "Theme Colors", diff --git a/src/locales/fa.json b/src/locales/fa.json index e189012..640e433 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -4140,7 +4140,12 @@ }, "backToAccounts": "بازگشت به فهرست حساب‌ها" }, - "emailMergePasswordRequired": "این ایمیل قبلاً به حساب دیگری تعلق دارد. برای ادغام حساب‌ها، رمز عبور آن حساب را وارد کنید." + "emailMergePasswordRequired": "این ایمیل قبلاً به حساب دیگری تعلق دارد. برای ادغام حساب‌ها، رمز عبور آن حساب را وارد کنید.", + "emailMergeCodeSent": "کد تأیید به آن ایمیل ارسال شد.", + "emailMergeCodeDescription": "این ایمیل قبلاً به حساب دیگری تعلق دارد. کدی به آن ارسال کردیم — برای تأیید مالکیت و ادغام حساب‌ها آن را وارد کنید.", + "emailMergeCodeLabel": "کد ارسال‌شده در ایمیل", + "emailMergeConfirm": "تأیید و ادغام", + "emailMergeCodeInvalid": "کد نامعتبر یا منقضی‌شده" }, "theme": { "colors": "رنگ‌های تم", diff --git a/src/locales/ru.json b/src/locales/ru.json index 2a9899d..3ceed9a 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -5147,7 +5147,12 @@ }, "backToAccounts": "К списку аккаунтов" }, - "emailMergePasswordRequired": "Этот email уже привязан к другому аккаунту. Чтобы объединить аккаунты, введите пароль от него." + "emailMergePasswordRequired": "Этот email уже привязан к другому аккаунту. Чтобы объединить аккаунты, введите пароль от него.", + "emailMergeCodeSent": "Код подтверждения отправлен на тот email.", + "emailMergeCodeDescription": "Этот email уже привязан к другому аккаунту. Мы отправили на него код — введите его, чтобы подтвердить владение и объединить аккаунты.", + "emailMergeCodeLabel": "Код из письма", + "emailMergeConfirm": "Подтвердить и объединить", + "emailMergeCodeInvalid": "Неверный или просроченный код" }, "theme": { "colors": "Цвета темы", diff --git a/src/locales/zh.json b/src/locales/zh.json index 557fb7c..7a78ee1 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -4139,7 +4139,12 @@ }, "backToAccounts": "返回账户列表" }, - "emailMergePasswordRequired": "该邮箱已属于另一个账户。要合并账户,请输入该账户的密码。" + "emailMergePasswordRequired": "该邮箱已属于另一个账户。要合并账户,请输入该账户的密码。", + "emailMergeCodeSent": "确认码已发送至该邮箱。", + "emailMergeCodeDescription": "该邮箱已属于另一个账户。我们已向其发送验证码——请输入以确认所有权并合并账户。", + "emailMergeCodeLabel": "邮件中的验证码", + "emailMergeConfirm": "确认并合并", + "emailMergeCodeInvalid": "验证码无效或已过期" }, "theme": { "colors": "主题颜色", diff --git a/src/pages/ConnectedAccounts.tsx b/src/pages/ConnectedAccounts.tsx index 0d81f42..78c3456 100644 --- a/src/pages/ConnectedAccounts.tsx +++ b/src/pages/ConnectedAccounts.tsx @@ -318,6 +318,10 @@ export default function ConnectedAccounts() { const [emailConfirmPassword, setEmailConfirmPassword] = useState(''); const [emailError, setEmailError] = useState(null); const [emailSuccess, setEmailSuccess] = useState(null); + // Email-merge confirmation: the target email belongs to another account, so a + // one-time code was mailed to it; verifying the code yields the merge token. + const [emailMergeCodePending, setEmailMergeCodePending] = useState(false); + const [emailMergeCode, setEmailMergeCode] = useState(''); const setUser = useAuthStore((state) => state.setUser); const { data: emailAuthConfig } = useQuery({ @@ -396,6 +400,15 @@ export default function ConnectedAccounts() { navigate(`/merge/${response.merge_token}`, { replace: true }); return; } + // The email belongs to another account: a one-time code was mailed to it. + // Switch to the code step; verifying it yields the merge token. + if (response.merge_required && response.merge_verification === 'email_code') { + setEmailMergeCodePending(true); + setEmailMergeCode(''); + setEmailSuccess(t('profile.emailMergeCodeSent')); + setEmailError(null); + return; + } setEmailSuccess(t('profile.emailSent')); setEmailError(null); setEmailValue(''); @@ -425,6 +438,36 @@ export default function ConnectedAccounts() { }, }); + const verifyEmailMergeMutation = useMutation({ + mutationFn: (code: string) => authApi.verifyEmailMerge(code), + onSuccess: (response) => { + if (response.merge_token) { + navigate(`/merge/${response.merge_token}`, { replace: true }); + } + }, + onError: (err: { response?: { data?: { detail?: string } } }) => { + setEmailError(err.response?.data?.detail || t('profile.emailMergeCodeInvalid')); + }, + }); + + const handleVerifyMergeCode = (e: React.SyntheticEvent) => { + e.preventDefault(); + setEmailError(null); + const code = emailMergeCode.trim(); + if (!/^\d{6}$/.test(code)) { + setEmailError(t('profile.emailMergeCodeInvalid')); + return; + } + verifyEmailMergeMutation.mutate(code); + }; + + const cancelEmailMerge = () => { + setEmailMergeCodePending(false); + setEmailMergeCode(''); + setEmailError(null); + setEmailSuccess(null); + }; + const handleEmailSubmit = (e: React.SyntheticEvent) => { e.preventDefault(); setEmailError(null); @@ -685,68 +728,117 @@ export default function ConnectedAccounts() { >

- {t('profile.linkEmailDescription')} + {emailMergeCodePending + ? t('profile.emailMergeCodeDescription') + : t('profile.linkEmailDescription')}

-
-
- - setEmailValue(e.target.value)} - placeholder="email@example.com" - className="input" - autoComplete="email" - /> -
-
- - setEmailPassword(e.target.value)} - placeholder={t('profile.passwordPlaceholder')} - className="input" - autoComplete="new-password" - /> -

{t('profile.passwordHint')}

-
-
- - setEmailConfirmPassword(e.target.value)} - placeholder={t('profile.confirmPasswordPlaceholder')} - className="input" - autoComplete="new-password" - /> -
- - {emailError && ( -
- {emailError} + {emailMergeCodePending ? ( + +
+ + setEmailMergeCode(e.target.value.replace(/\D/g, ''))} + placeholder="000000" + className="input tracking-[0.5em]" + autoComplete="one-time-code" + />
- )} - {emailSuccess && ( -
- {emailSuccess} + {emailError && ( +
+ {emailError} +
+ )} + {emailSuccess && ( +
+ {emailSuccess} +
+ )} + + + + ) : ( +
+
+ + setEmailValue(e.target.value)} + placeholder="email@example.com" + className="input" + autoComplete="email" + /> +
+
+ + setEmailPassword(e.target.value)} + placeholder={t('profile.passwordPlaceholder')} + className="input" + autoComplete="new-password" + /> +

+ {t('profile.passwordHint')} +

+
+
+ + setEmailConfirmPassword(e.target.value)} + placeholder={t('profile.confirmPasswordPlaceholder')} + className="input" + autoComplete="new-password" + />
- )} - -
+ {emailError && ( +
+ {emailError} +
+ )} + {emailSuccess && ( +
+ {emailSuccess} +
+ )} + + + + )}
)}