fix: double-click guard on link, wall-clock timer, blur cleanup

- Add linkingProvider state to prevent double-click on Link button
- Use wall-clock based countdown timer to prevent drift
- Clean up onBlur setTimeout ref on unmount
This commit is contained in:
Fringg
2026-03-04 16:05:24 +03:00
parent fba4481799
commit 8ad0500cc8
2 changed files with 31 additions and 14 deletions

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { motion } from 'framer-motion';
@@ -42,6 +42,14 @@ export default function ConnectedAccounts() {
const queryClient = useQueryClient();
const [confirmingUnlink, setConfirmingUnlink] = useState<string | null>(null);
const [linkingProvider, setLinkingProvider] = useState<string | null>(null);
const blurTimeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
useEffect(() => {
return () => {
if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
};
}, []);
const { data, isLoading, isError } = useQuery({
queryKey: ['linked-providers'],
@@ -76,6 +84,8 @@ export default function ConnectedAccounts() {
};
const handleLink = async (provider: string) => {
if (linkingProvider) return;
setLinkingProvider(provider);
try {
const { authorize_url, state } = await authApi.linkProviderInit(provider);
sessionStorage.setItem(LINK_OAUTH_STATE_KEY, state);
@@ -86,6 +96,7 @@ export default function ConnectedAccounts() {
type: 'error',
message: t('profile.accounts.linkError'),
});
setLinkingProvider(null);
}
};
@@ -160,7 +171,7 @@ export default function ConnectedAccounts() {
onClick={() => handleUnlink(provider.provider)}
onBlur={() => {
// Delay so click on the same button fires before blur resets state
setTimeout(() => {
blurTimeoutRef.current = setTimeout(() => {
setConfirmingUnlink((cur) => (cur === provider.provider ? null : cur));
}, 150);
}}
@@ -176,6 +187,8 @@ export default function ConnectedAccounts() {
<Button
variant="primary"
size="sm"
disabled={linkingProvider !== null}
loading={linkingProvider === provider.provider}
onClick={() => handleLink(provider.provider)}
>
{t('profile.accounts.link')}