fix: harden merge UI and improve error handling

- MergeAccounts: guard mutationFn against null token/userId
- MergeAccounts: call checkAdminStatus after successful merge
- MergeAccounts: add fallback when success but tokens null
- MergeAccounts: fix ErrorState showing "expired" for all errors
- MergeAccounts: fix formatCountdown dead ternary and hardcoded English
- MergeAccounts: add staleTime/refetchOnWindowFocus to preview query
- MergeAccounts: remove manual useCallback (React Compiler handles it)
- LinkOAuthCallback: add toast feedback on all error/success paths
- ConnectedAccounts: use invalidateQueries instead of refetch
- ConnectedAccounts: add error state rendering
- ConnectedAccounts/LinkOAuthCallback: share sessionStorage key constants
- i18n: add missing profile.accounts.linking key to zh and fa locales
This commit is contained in:
Fringg
2026-03-04 07:47:36 +03:00
parent 93f97d45be
commit 58cf1e3b50
5 changed files with 53 additions and 22 deletions

View File

@@ -1,5 +1,5 @@
import { useTranslation } from 'react-i18next';
import { useQuery, useMutation } from '@tanstack/react-query';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { motion } from 'framer-motion';
import { authApi } from '../api/auth';
import { useToast } from '../components/Toast';
@@ -7,6 +7,7 @@ import { Card } from '@/components/data-display/Card';
import { Button } from '@/components/primitives/Button';
import { staggerContainer, staggerItem } from '@/components/motion/transitions';
import OAuthProviderIcon from '../components/OAuthProviderIcon';
import { LINK_OAUTH_STATE_KEY, LINK_OAUTH_PROVIDER_KEY } from './LinkOAuthCallback';
import type { LinkedProvider } from '../types';
const OAUTH_PROVIDERS = ['google', 'yandex', 'discord', 'vk'];
@@ -78,8 +79,9 @@ function LoadingSkeleton() {
export default function ConnectedAccounts() {
const { t } = useTranslation();
const { showToast } = useToast();
const queryClient = useQueryClient();
const { data, isLoading, refetch } = useQuery({
const { data, isLoading, isError } = useQuery({
queryKey: ['linked-providers'],
queryFn: () => authApi.getLinkedProviders(),
});
@@ -87,7 +89,7 @@ export default function ConnectedAccounts() {
const unlinkMutation = useMutation({
mutationFn: (provider: string) => authApi.unlinkProvider(provider),
onSuccess: () => {
refetch();
queryClient.invalidateQueries({ queryKey: ['linked-providers'] });
showToast({
type: 'success',
message: t('profile.accounts.unlinkSuccess'),
@@ -111,8 +113,8 @@ export default function ConnectedAccounts() {
const handleLink = async (provider: string) => {
try {
const { authorize_url, state } = await authApi.linkProviderInit(provider);
sessionStorage.setItem('link_oauth_state', state);
sessionStorage.setItem('link_oauth_provider', provider);
sessionStorage.setItem(LINK_OAUTH_STATE_KEY, state);
sessionStorage.setItem(LINK_OAUTH_PROVIDER_KEY, provider);
window.location.href = authorize_url;
} catch {
showToast({
@@ -150,6 +152,15 @@ export default function ConnectedAccounts() {
</motion.div>
)}
{/* Error state */}
{isError && (
<motion.div variants={staggerItem}>
<Card>
<p className="text-center text-dark-400">{t('common.error')}</p>
</Card>
</motion.div>
)}
{/* Provider cards */}
{data?.providers.map((provider) => (
<motion.div key={provider.provider} variants={staggerItem}>