mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
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:
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
@@ -42,6 +42,14 @@ export default function ConnectedAccounts() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const [confirmingUnlink, setConfirmingUnlink] = useState<string | null>(null);
|
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({
|
const { data, isLoading, isError } = useQuery({
|
||||||
queryKey: ['linked-providers'],
|
queryKey: ['linked-providers'],
|
||||||
@@ -76,6 +84,8 @@ export default function ConnectedAccounts() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleLink = async (provider: string) => {
|
const handleLink = async (provider: string) => {
|
||||||
|
if (linkingProvider) return;
|
||||||
|
setLinkingProvider(provider);
|
||||||
try {
|
try {
|
||||||
const { authorize_url, state } = await authApi.linkProviderInit(provider);
|
const { authorize_url, state } = await authApi.linkProviderInit(provider);
|
||||||
sessionStorage.setItem(LINK_OAUTH_STATE_KEY, state);
|
sessionStorage.setItem(LINK_OAUTH_STATE_KEY, state);
|
||||||
@@ -86,6 +96,7 @@ export default function ConnectedAccounts() {
|
|||||||
type: 'error',
|
type: 'error',
|
||||||
message: t('profile.accounts.linkError'),
|
message: t('profile.accounts.linkError'),
|
||||||
});
|
});
|
||||||
|
setLinkingProvider(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -160,7 +171,7 @@ export default function ConnectedAccounts() {
|
|||||||
onClick={() => handleUnlink(provider.provider)}
|
onClick={() => handleUnlink(provider.provider)}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
// Delay so click on the same button fires before blur resets state
|
// Delay so click on the same button fires before blur resets state
|
||||||
setTimeout(() => {
|
blurTimeoutRef.current = setTimeout(() => {
|
||||||
setConfirmingUnlink((cur) => (cur === provider.provider ? null : cur));
|
setConfirmingUnlink((cur) => (cur === provider.provider ? null : cur));
|
||||||
}, 150);
|
}, 150);
|
||||||
}}
|
}}
|
||||||
@@ -176,6 +187,8 @@ export default function ConnectedAccounts() {
|
|||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
disabled={linkingProvider !== null}
|
||||||
|
loading={linkingProvider === provider.provider}
|
||||||
onClick={() => handleLink(provider.provider)}
|
onClick={() => handleLink(provider.provider)}
|
||||||
>
|
>
|
||||||
{t('profile.accounts.link')}
|
{t('profile.accounts.link')}
|
||||||
|
|||||||
@@ -357,22 +357,26 @@ export default function MergeAccounts() {
|
|||||||
// If both have subs — null until user picks
|
// If both have subs — null until user picks
|
||||||
}, [data, selectedUserId]);
|
}, [data, selectedUserId]);
|
||||||
|
|
||||||
// Countdown timer
|
// Countdown timer (wall-clock based to avoid drift)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
setExpiresIn(data.expires_in_seconds);
|
const startTime = Date.now();
|
||||||
|
const totalSeconds = data.expires_in_seconds;
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const tick = () => {
|
||||||
setExpiresIn((prev) => {
|
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
||||||
if (prev <= 1) {
|
const remaining = totalSeconds - elapsed;
|
||||||
clearInterval(interval);
|
if (remaining <= 0) {
|
||||||
setIsExpired(true);
|
setExpiresIn(0);
|
||||||
return 0;
|
setIsExpired(true);
|
||||||
}
|
clearInterval(interval);
|
||||||
return prev - 1;
|
} else {
|
||||||
});
|
setExpiresIn(remaining);
|
||||||
}, 1000);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tick();
|
||||||
|
const interval = setInterval(tick, 1000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user