From 31aed0517648a5a5be2510af0286e50ad960315c Mon Sep 17 00:00:00 2001 From: Michael Canova Date: Tue, 9 Dec 2025 18:21:24 -0500 Subject: [PATCH 01/21] First steps for connecting to orcid --- app/author/[id]/page.tsx | 7 ++- components/Orcid/OrcidSyncBanner.tsx | 42 +++++++++++++++++ components/Orcid/lib/hooks/useConnectOrcid.ts | 23 ++++++++++ .../Orcid/lib/services/orcid.service.ts | 6 +++ .../About/ProfileInformationForm/index.tsx | 45 +++++++++++++------ types/authorProfile.ts | 2 + 6 files changed, 110 insertions(+), 15 deletions(-) create mode 100644 components/Orcid/OrcidSyncBanner.tsx create mode 100644 components/Orcid/lib/hooks/useConnectOrcid.ts create mode 100644 components/Orcid/lib/services/orcid.service.ts diff --git a/app/author/[id]/page.tsx b/app/author/[id]/page.tsx index 7f51833d3..2e107b0d3 100644 --- a/app/author/[id]/page.tsx +++ b/app/author/[id]/page.tsx @@ -18,7 +18,7 @@ import AuthorProfile from './components/AuthorProfile'; import { useAuthorPublications } from '@/hooks/usePublications'; import { transformPublicationToFeedEntry } from '@/types/publication'; import PinnedFundraise from './components/PinnedFundraise'; - +import { OrcidSyncBanner } from '@/components/Orcid/OrcidSyncBanner'; function toNumberOrNull(value: any): number | null { if (value === '' || value === null || value === undefined) return null; const num = Number(value); @@ -293,8 +293,13 @@ export default function AuthorProfilePage({ params }: { params: Promise<{ id: st return ; } + const isOwnProfile = + currentUser?.authorProfile?.id && user.authorProfile.id === currentUser.authorProfile.id; + const orcidConnected = user.authorProfile.orcidConnected ?? false; + return ( <> + {isOwnProfile && !orcidConnected && } diff --git a/components/Orcid/OrcidSyncBanner.tsx b/components/Orcid/OrcidSyncBanner.tsx new file mode 100644 index 000000000..2b6d4065b --- /dev/null +++ b/components/Orcid/OrcidSyncBanner.tsx @@ -0,0 +1,42 @@ +'use client'; + +import { RefreshCw } from 'lucide-react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faOrcid } from '@fortawesome/free-brands-svg-icons'; +import { Button } from '@/components/ui/Button'; +import { useConnectOrcid } from '@/components/Orcid/lib/hooks/useConnectOrcid'; + +export function OrcidSyncBanner() { + const { connect, isConnecting } = useConnectOrcid(); + + return ( +
+
+
+
+ +
+ +
+

+ Connect your ORCID iD to auto-sync authorship +

+

+ Securely sync publications from ORCID into your ResearchHub profile. +

+
+
+ + +
+
+ ); +} diff --git a/components/Orcid/lib/hooks/useConnectOrcid.ts b/components/Orcid/lib/hooks/useConnectOrcid.ts new file mode 100644 index 000000000..f11793d81 --- /dev/null +++ b/components/Orcid/lib/hooks/useConnectOrcid.ts @@ -0,0 +1,23 @@ +import { useState, useCallback } from 'react'; +import { connectOrcidAccount } from '@/components/Orcid/lib/services/orcid.service'; +import { extractApiErrorMessage } from '@/services/lib/serviceUtils'; +import toast from 'react-hot-toast'; + +export function useConnectOrcid() { + const [isConnecting, setIsConnecting] = useState(false); + + const connect = useCallback(async () => { + setIsConnecting(true); + try { + await connectOrcidAccount(); + } catch (error) { + toast.error(extractApiErrorMessage(error, 'Failed to connect ORCID account')); + setIsConnecting(false); + } + }, []); + + return { + connect, + isConnecting, + }; +} diff --git a/components/Orcid/lib/services/orcid.service.ts b/components/Orcid/lib/services/orcid.service.ts new file mode 100644 index 000000000..e70ef82cf --- /dev/null +++ b/components/Orcid/lib/services/orcid.service.ts @@ -0,0 +1,6 @@ +import { ApiClient } from '@/services/client'; + +export async function connectOrcidAccount(returnUrl = window.location.href): Promise { + const { auth_url } = await ApiClient.post('/api/orcid/connect', { return_to: returnUrl }); + window.location.href = auth_url; +} diff --git a/components/profile/About/ProfileInformationForm/index.tsx b/components/profile/About/ProfileInformationForm/index.tsx index 8e50ed415..4745fa04f 100644 --- a/components/profile/About/ProfileInformationForm/index.tsx +++ b/components/profile/About/ProfileInformationForm/index.tsx @@ -25,6 +25,7 @@ import toast from 'react-hot-toast'; import { Accordion, AccordionItem } from '@/components/ui/Accordion'; import { faUpload, faGraduationCap, faShareNodes, faUser } from '@fortawesome/pro-light-svg-icons'; import Icon from '@/components/ui/icons/Icon'; +import { useConnectOrcid } from '@/components/Orcid/lib/hooks/useConnectOrcid'; interface ProfileInformationFormProps { onSubmit: (data: ProfileInformationFormValues) => void; @@ -52,20 +53,22 @@ export function ProfileInformationForm({ icon: , label: 'Google Scholar Profile URL', }, - orcid_id: { - icon: , - label: 'ORCID URL', - }, - twitter: { icon: , label: 'X (Twitter) Profile URL', }, + orcid_id: { + icon: , + label: 'ORCID URL', + }, } as const; const { user } = useUser(); const authorProfile = user?.authorProfile; + const orcidConnected = authorProfile?.orcidConnected ?? false; + + const { connect: connectOrcid, isConnecting: isConnectingOrcid } = useConnectOrcid(); const methods = useForm({ resolver: zodResolver(getProfileInformationSchema({ fields })), @@ -249,6 +252,8 @@ export function ProfileInformationForm({
{(Object.keys(socialLinkMeta) as Array).map((key) => { const meta = socialLinkMeta[key]; + const isOrcid = key === 'orcid_id'; + return (
- + {isOrcid && !orcidConnected ? ( + + ) : ( + + )}
); })} diff --git a/types/authorProfile.ts b/types/authorProfile.ts index af6427f2a..6df59f738 100644 --- a/types/authorProfile.ts +++ b/types/authorProfile.ts @@ -38,6 +38,7 @@ export interface AuthorProfile { linkedin?: string | null; googleScholar?: string | null; orcidId?: string | null; + orcidConnected?: boolean; isClaimed: boolean; isVerified: boolean; hIndex?: number; @@ -88,6 +89,7 @@ export const transformAuthorProfile = createTransformer((raw linkedin: raw.linkedin || undefined, googleScholar: raw.google_scholar || undefined, orcidId: raw.orcid_id || undefined, + orcidConnected: raw.orcid_connected || false, isClaimed: isClaimed, hIndex: raw.h_index || undefined, i10Index: raw.i10_index || undefined, From 7cf7b10c35153d486e0ee242bd28e729660d0a5f Mon Sep 17 00:00:00 2001 From: Michael Canova Date: Wed, 10 Dec 2025 16:17:52 -0500 Subject: [PATCH 02/21] [ORCID connection] Cleanup, adding Connection page for callback instead of directly returning to backend and optimization --- app/auth/orcid/callback/page.tsx | 92 +++++++++++++++++++ app/author/[id]/components/AuthorProfile.tsx | 6 +- components/Orcid/OrcidSyncBanner.tsx | 7 +- components/Orcid/lib/hooks/useConnectOrcid.ts | 2 +- .../Orcid/lib/services/orcid.service.ts | 30 +++++- 5 files changed, 128 insertions(+), 9 deletions(-) create mode 100644 app/auth/orcid/callback/page.tsx diff --git a/app/auth/orcid/callback/page.tsx b/app/auth/orcid/callback/page.tsx new file mode 100644 index 000000000..c14cd0c60 --- /dev/null +++ b/app/auth/orcid/callback/page.tsx @@ -0,0 +1,92 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { Loader2 } from 'lucide-react'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faOrcid } from '@fortawesome/free-brands-svg-icons'; +import { processOrcidCallback } from '@/components/Orcid/lib/services/orcid.service'; +import { extractApiErrorMessage } from '@/services/lib/serviceUtils'; +import { Button } from '@/components/ui/Button'; +import { PageLayout } from '@/app/layouts/PageLayout'; +import toast from 'react-hot-toast'; + +type CallbackStatus = 'idle' | 'processing' | 'error'; + +export default function OrcidCallbackPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const [status, setStatus] = useState('idle'); + const [error, setError] = useState(null); + + useEffect(() => { + if (status !== 'idle') return; + + const code = searchParams.get('code'); + const oauthState = searchParams.get('state'); + + if (!code || !oauthState) { + setError('Missing authorization code or state parameter'); + setStatus('error'); + return; + } + + setStatus('processing'); + router.replace('/auth/orcid/callback', { scroll: false }); + + const handleCallback = async () => { + try { + const response = await processOrcidCallback(code, oauthState); + + if (response.success && response.authorId) { + toast.success('ORCID account connected.'); + router.replace(`/author/${response.authorId}`); + } else { + throw new Error('Failed to connect ORCID.'); + } + } catch (err) { + setError(extractApiErrorMessage(err, 'Failed to connect ORCID.')); + setStatus('error'); + } + }; + + handleCallback(); + }, [status, searchParams, router]); + + return ( + +
+
+
+
+ +
+ + {status !== 'error' && ( + <> +

Connecting ORCID

+

+ Please wait while we connect your ORCID account... +

+ + + )} + + {status === 'error' && ( + <> +

Connection Failed

+

{error}

+ + + )} +
+
+
+
+ ); +} diff --git a/app/author/[id]/components/AuthorProfile.tsx b/app/author/[id]/components/AuthorProfile.tsx index a68a5efd8..22ac95e1c 100644 --- a/app/author/[id]/components/AuthorProfile.tsx +++ b/app/author/[id]/components/AuthorProfile.tsx @@ -230,10 +230,12 @@ const AuthorProfile: React.FC = ({ author, refetchAuthorInfo /> } - href={author.orcidId} + href={author.orcidConnected ? author.orcidId : null} label="ORCID" className={ - author.orcidId ? '[&>svg]:text-[#A6CE39] [&>svg]:hover:text-[#82A629] px-0' : 'px-0' + author.orcidConnected + ? '[&>svg]:text-[#A6CE39] [&>svg]:hover:text-[#82A629] px-0' + : 'px-0' } /> +
- +
@@ -30,8 +30,7 @@ export function OrcidSyncBanner() { + ); +} diff --git a/components/Orcid/OrcidSyncBanner.tsx b/components/Orcid/OrcidSyncBanner.tsx index a26f94e6b..6f6247a3f 100644 --- a/components/Orcid/OrcidSyncBanner.tsx +++ b/components/Orcid/OrcidSyncBanner.tsx @@ -1,40 +1,43 @@ 'use client'; -import { RefreshCw } from 'lucide-react'; +import { X } from 'lucide-react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faOrcid } from '@fortawesome/free-brands-svg-icons'; -import { Button } from '@/components/ui/Button'; -import { useConnectOrcid } from '@/components/Orcid/lib/hooks/useConnectOrcid'; +import { OrcidConnectButton } from '@/components/Orcid/OrcidConnectButton'; +import { useDismissableFeature } from '@/hooks/useDismissableFeature'; export function OrcidSyncBanner() { - const { connect, isConnecting } = useConnectOrcid(); + const { isDismissed, dismissFeature } = useDismissableFeature('orcid_sync_banner'); + + if (isDismissed) { + return null; + } return ( -
-
-
+
+ + +
+
-

- Connect your ORCID iD to auto-sync authorship -

-

- Securely sync publications from ORCID into your ResearchHub profile. +

Sync your ORCID account

+

+ Sync your ORCID publications with your ResearchHub profile

- +
); diff --git a/components/Orcid/lib/hooks/useConnectOrcid.ts b/components/Orcid/lib/hooks/useConnectOrcid.ts index 609fed10d..eacb2aa66 100644 --- a/components/Orcid/lib/hooks/useConnectOrcid.ts +++ b/components/Orcid/lib/hooks/useConnectOrcid.ts @@ -12,6 +12,7 @@ export function useConnectOrcid() { await connectOrcidAccount(); } catch (error) { toast.error(extractApiErrorMessage(error, 'Failed to connect ORCID.')); + } finally { setIsConnecting(false); } }, []); diff --git a/components/Orcid/lib/services/orcid.service.ts b/components/Orcid/lib/services/orcid.service.ts index 77a97a340..c2e6a8554 100644 --- a/components/Orcid/lib/services/orcid.service.ts +++ b/components/Orcid/lib/services/orcid.service.ts @@ -1,9 +1,20 @@ +'use server'; + import { ApiClient } from '@/services/client'; import { ID } from '@/types/root'; +import { redirect } from 'next/navigation'; export async function connectOrcidAccount(): Promise { - const { auth_url } = await ApiClient.post<{ auth_url: string }>('/api/orcid/connect/'); - window.location.href = auth_url; + try { + const { auth_url } = await ApiClient.post<{ auth_url: string }>('/api/orcid/connect/'); + + if (!auth_url) { + throw new Error('Could not connect to ORCID. Please try again.'); + } + redirect(auth_url); + } catch (error) { + throw error; + } } interface ApiOrcidCallbackResponse { diff --git a/components/profile/About/ProfileInformationForm/index.tsx b/components/profile/About/ProfileInformationForm/index.tsx index 4745fa04f..f9a767a6b 100644 --- a/components/profile/About/ProfileInformationForm/index.tsx +++ b/components/profile/About/ProfileInformationForm/index.tsx @@ -25,7 +25,7 @@ import toast from 'react-hot-toast'; import { Accordion, AccordionItem } from '@/components/ui/Accordion'; import { faUpload, faGraduationCap, faShareNodes, faUser } from '@fortawesome/pro-light-svg-icons'; import Icon from '@/components/ui/icons/Icon'; -import { useConnectOrcid } from '@/components/Orcid/lib/hooks/useConnectOrcid'; +import { OrcidConnectButton } from '@/components/Orcid/OrcidConnectButton'; interface ProfileInformationFormProps { onSubmit: (data: ProfileInformationFormValues) => void; @@ -68,8 +68,6 @@ export function ProfileInformationForm({ const authorProfile = user?.authorProfile; const orcidConnected = authorProfile?.orcidConnected ?? false; - const { connect: connectOrcid, isConnecting: isConnectingOrcid } = useConnectOrcid(); - const methods = useForm({ resolver: zodResolver(getProfileInformationSchema({ fields })), mode: 'onChange', @@ -264,14 +262,7 @@ export function ProfileInformationForm({ className="text-gray-500" /> {isOrcid && !orcidConnected ? ( - + ) : ( = ({ label="Google Scholar" />
); diff --git a/components/menus/UserMenu.tsx b/components/menus/UserMenu.tsx index 73426509a..c461ac6bb 100644 --- a/components/menus/UserMenu.tsx +++ b/components/menus/UserMenu.tsx @@ -4,7 +4,6 @@ import { User as UserIcon, LogOut, BadgeCheck, Bell, Shield, UserPlus } from 'lu import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faBookmark } from '@fortawesome/free-regular-svg-icons'; import { faPen } from '@fortawesome/free-solid-svg-icons'; -import { faOrcid } from '@fortawesome/free-brands-svg-icons'; import { useState, useEffect } from 'react'; import type { User } from '@/types/user'; import VerificationBanner from '@/components/banners/VerificationBanner'; @@ -18,7 +17,6 @@ import { AuthSharingService } from '@/services/auth-sharing.service'; import { navigateToAuthorProfile } from '@/utils/navigation'; import { Button } from '@/components/ui/Button'; import { useVerification } from '@/contexts/VerificationContext'; -import { useConnectOrcid } from '@/components/Orcid/lib/hooks/useConnectOrcid'; import { useOrcidCallback } from '@/components/Orcid/lib/hooks/useOrcidCallback'; interface UserMenuProps { @@ -44,8 +42,6 @@ export default function UserMenu({ const [internalMenuOpen, setInternalMenuOpen] = useState(false); const [isMobile, setIsMobile] = useState(false); const { openVerificationModal } = useVerification(); - const { connect: connectOrcid, isConnecting: isConnectingOrcid } = useConnectOrcid(); - const isOrcidConnected = user.authorProfile?.isOrcidConnected ?? false; useOrcidCallback(); // Use controlled or uncontrolled menu state @@ -223,23 +219,6 @@ export default function UserMenu({
- {!isOrcidConnected && ( - - )} - {!user.isVerified && (
- {!isOrcidConnected && ( - { - connectOrcid(); - setMenuOpenState(false); - }} - className="w-full px-4 py-2" - > -
- - - {isConnectingOrcid ? 'Connecting...' : 'Connect to ORCID'} - -
-
- )} - {!user.isVerified && (
From 07c62559f3fbcd7055622dadfa19027a07352b5b Mon Sep 17 00:00:00 2001 From: Michael Canova Date: Fri, 19 Dec 2025 12:11:47 -0500 Subject: [PATCH 11/21] [ORCID Integration] More Small Cleanup --- components/menus/UserMenu.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/menus/UserMenu.tsx b/components/menus/UserMenu.tsx index c461ac6bb..5e3e38ba9 100644 --- a/components/menus/UserMenu.tsx +++ b/components/menus/UserMenu.tsx @@ -17,7 +17,6 @@ import { AuthSharingService } from '@/services/auth-sharing.service'; import { navigateToAuthorProfile } from '@/utils/navigation'; import { Button } from '@/components/ui/Button'; import { useVerification } from '@/contexts/VerificationContext'; -import { useOrcidCallback } from '@/components/Orcid/lib/hooks/useOrcidCallback'; interface UserMenuProps { user: User; @@ -43,7 +42,6 @@ export default function UserMenu({ const [isMobile, setIsMobile] = useState(false); const { openVerificationModal } = useVerification(); - useOrcidCallback(); // Use controlled or uncontrolled menu state const menuOpenState = isMenuOpen !== undefined ? isMenuOpen : internalMenuOpen; const setMenuOpenState = (open: boolean) => { From d50705024a468973ad230279099dfa99e230c58e Mon Sep 17 00:00:00 2001 From: Michael Canova Date: Fri, 19 Dec 2025 12:13:38 -0500 Subject: [PATCH 12/21] [ORCID Integration] Removing extra white line spacing --- components/menus/UserMenu.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/components/menus/UserMenu.tsx b/components/menus/UserMenu.tsx index 5e3e38ba9..a417441cd 100644 --- a/components/menus/UserMenu.tsx +++ b/components/menus/UserMenu.tsx @@ -41,7 +41,6 @@ export default function UserMenu({ const [internalMenuOpen, setInternalMenuOpen] = useState(false); const [isMobile, setIsMobile] = useState(false); const { openVerificationModal } = useVerification(); - // Use controlled or uncontrolled menu state const menuOpenState = isMenuOpen !== undefined ? isMenuOpen : internalMenuOpen; const setMenuOpenState = (open: boolean) => { From 5f629a88c955c4cf24282357415b59d8d79d8321 Mon Sep 17 00:00:00 2001 From: Michael Canova Date: Fri, 19 Dec 2025 13:48:22 -0500 Subject: [PATCH 13/21] [ORCID Integration] Optimizations, adding orcid color scheme to tailwind, other cleanup --- app/author/[id]/components/Moderation.tsx | 2 +- app/author/[id]/page.tsx | 14 ++++++----- app/styles/colors.ts | 7 ++++++ components/Orcid/OrcidConnectButton.tsx | 16 ++++++------- components/Orcid/OrcidSyncBanner.tsx | 23 ++++++++++++------- components/Orcid/lib/hooks/useConnectOrcid.ts | 15 ++++++++---- .../Orcid/lib/hooks/useOrcidCallback.ts | 19 ++++++++++----- tailwind.config.ts | 1 + 8 files changed, 63 insertions(+), 34 deletions(-) diff --git a/app/author/[id]/components/Moderation.tsx b/app/author/[id]/components/Moderation.tsx index 7f89525b7..ff13d3425 100644 --- a/app/author/[id]/components/Moderation.tsx +++ b/app/author/[id]/components/Moderation.tsx @@ -242,7 +242,7 @@ export default function Moderation({ userId, authorId, refetchAuthorInfo }: Mode {userDetails.isSuspended ? 'Yes' : 'No'}
- ORCID Verified? + ORCID Connected? {userDetails.isOrcidConnected ? 'Yes' : 'No'}
diff --git a/app/author/[id]/page.tsx b/app/author/[id]/page.tsx index d84f20ceb..c8d715fad 100644 --- a/app/author/[id]/page.tsx +++ b/app/author/[id]/page.tsx @@ -19,6 +19,7 @@ import { useAuthorPublications } from '@/hooks/usePublications'; import { transformPublicationToFeedEntry } from '@/types/publication'; import PinnedFundraise from './components/PinnedFundraise'; import { OrcidSyncBanner } from '@/components/Orcid/OrcidSyncBanner'; +import { useOrcidCallback } from '@/components/Orcid/lib/hooks/useOrcidCallback'; function toNumberOrNull(value: any): number | null { if (value === '' || value === null || value === undefined) return null; const num = Number(value); @@ -255,8 +256,8 @@ export default function AuthorProfilePage({ params }: { params: Promise<{ id: st const { isLoading: isUserLoading, error: userError } = useUser(); const authorId = toNumberOrNull(resolvedParams.id); const [{ author: user, isLoading, error }, refetchAuthorInfo] = useAuthorInfo(authorId); - const { user: currentUser } = useUser(); - // Determine if current user is a hub editor + const { user: currentUser, refreshUser } = useUser(); + useOrcidCallback({ onSuccess: refreshUser }); const isHubEditor = !!currentUser?.authorProfile?.isHubEditor; const [{ achievements, isLoading: isAchievementsLoading, error: achievementsError }] = useAuthorAchievements(authorId); @@ -291,13 +292,14 @@ export default function AuthorProfilePage({ params }: { params: Promise<{ id: st return ; } - const isOwnProfile = - currentUser?.authorProfile?.id && user.authorProfile.id === currentUser.authorProfile.id; - const isOrcidConnected = currentUser?.authorProfile?.isOrcidConnected ?? false; + const isOwnProfile = Boolean( + currentUser?.authorProfile?.id && user.authorProfile.id === currentUser.authorProfile.id + ); + const isOrcidConnected = Boolean(currentUser?.authorProfile?.isOrcidConnected); return ( <> - {isOwnProfile && !isOrcidConnected && } + diff --git a/app/styles/colors.ts b/app/styles/colors.ts index 3c3bd9b95..91b86ec29 100644 --- a/app/styles/colors.ts +++ b/app/styles/colors.ts @@ -39,4 +39,11 @@ export const colors = { 900: '#1e3a8a', 950: '#172554', }, + orcid: { + 50: '#F5FAEB', // Pale lemon background + 100: '#E3F3C1', // Light green + 200: '#DCEEC4', // Border green + 500: '#A6CE39', // ORCID brand green + 600: '#82A629', // Darker green for hover + }, }; diff --git a/components/Orcid/OrcidConnectButton.tsx b/components/Orcid/OrcidConnectButton.tsx index 080586326..cb18fbfd1 100644 --- a/components/Orcid/OrcidConnectButton.tsx +++ b/components/Orcid/OrcidConnectButton.tsx @@ -2,14 +2,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faOrcid } from '@fortawesome/free-brands-svg-icons'; +import { Loader2 } from 'lucide-react'; import { Button, ButtonProps } from '@/components/ui/Button'; import { useConnectOrcid } from '@/components/Orcid/lib/hooks/useConnectOrcid'; -interface OrcidConnectButtonProps { - readonly variant?: ButtonProps['variant']; - readonly size?: ButtonProps['size']; - readonly className?: string; -} +type OrcidConnectButtonProps = Pick; export function OrcidConnectButton({ variant = 'outlined', @@ -26,10 +23,11 @@ export function OrcidConnectButton({ size={size} className={className} > - + {isConnecting ? ( + + ) : ( + + )} {isConnecting ? 'Connecting...' : 'Connect ORCID'} ); diff --git a/components/Orcid/OrcidSyncBanner.tsx b/components/Orcid/OrcidSyncBanner.tsx index d6d3dcf77..df0cdadd2 100644 --- a/components/Orcid/OrcidSyncBanner.tsx +++ b/components/Orcid/OrcidSyncBanner.tsx @@ -7,20 +7,27 @@ import { OrcidConnectButton } from '@/components/Orcid/OrcidConnectButton'; import { useDismissableFeature } from '@/hooks/useDismissableFeature'; import { Button } from '@/components/ui/Button'; -export function OrcidSyncBanner() { - const { isDismissed, dismissFeature } = useDismissableFeature('orcid_sync_banner'); +const FEATURE_NAME = 'orcid_sync_banner'; - if (isDismissed) { +interface OrcidSyncBannerProps { + readonly isOwnProfile: boolean; + readonly isOrcidConnected: boolean; +} + +export function OrcidSyncBanner({ isOwnProfile, isOrcidConnected }: OrcidSyncBannerProps) { + const { isDismissed, dismissFeature, dismissStatus } = useDismissableFeature(FEATURE_NAME); + + if (!isOwnProfile || isOrcidConnected || dismissStatus !== 'checked' || isDismissed) { return null; } return ( -
+
-
-
+
+

Sync your ORCID account

-

+

Sync your ORCID publications with your ResearchHub profile

@@ -49,7 +49,7 @@ export function OrcidSyncBanner({ isOwnProfile, isOrcidConnected }: OrcidSyncBan
From 7ea517cd36c7f6000444b5622a1801c1573124bb Mon Sep 17 00:00:00 2001 From: Michael Canova Date: Fri, 19 Dec 2025 14:46:38 -0500 Subject: [PATCH 18/21] [ORCID Integration] Updating error messaging for ORCID connection failures --- components/Orcid/lib/hooks/useConnectOrcid.ts | 2 +- .../Orcid/lib/services/orcid.service.ts | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/components/Orcid/lib/hooks/useConnectOrcid.ts b/components/Orcid/lib/hooks/useConnectOrcid.ts index dbafb2ce0..80c2d2585 100644 --- a/components/Orcid/lib/hooks/useConnectOrcid.ts +++ b/components/Orcid/lib/hooks/useConnectOrcid.ts @@ -20,7 +20,7 @@ export function useConnectOrcid() { setIsConnecting(true); await connectOrcidAccount(url.toString()); } catch (error) { - toast.error(extractApiErrorMessage(error, 'Failed to connect to ORCID')); + toast.error(extractApiErrorMessage(error, 'Unable to connect to ORCID.')); } finally { setIsConnecting(false); } diff --git a/components/Orcid/lib/services/orcid.service.ts b/components/Orcid/lib/services/orcid.service.ts index 6a1cb61cd..23d0f969c 100644 --- a/components/Orcid/lib/services/orcid.service.ts +++ b/components/Orcid/lib/services/orcid.service.ts @@ -4,13 +4,20 @@ import { ApiClient } from '@/services/client'; import { redirect } from 'next/navigation'; export async function connectOrcidAccount(returnUrl: string): Promise { - const { auth_url } = await ApiClient.post<{ auth_url: string }>('/api/orcid/connect/', { - return_url: returnUrl, - }); + let authUrl: string; - if (!auth_url) { - throw new Error('Could not connect to ORCID'); + try { + const response = await ApiClient.post<{ auth_url: string }>('/api/orcid/connect/', { + return_url: returnUrl, + }); + authUrl = response.auth_url; + } catch { + throw new Error('Unable to connect to ORCID.'); } - redirect(auth_url); + if (!authUrl) { + throw new Error('Unable to connect to ORCID.'); + } + + redirect(authUrl); } From ec74e8943cdd70a66ce4feae19ca78190ffd63d0 Mon Sep 17 00:00:00 2001 From: Michael Canova Date: Fri, 19 Dec 2025 15:01:48 -0500 Subject: [PATCH 19/21] [ORCID Integration] Optimizing connection setup for potential errors --- components/Orcid/lib/hooks/useConnectOrcid.ts | 13 ++++++----- .../Orcid/lib/hooks/useOrcidCallback.ts | 3 +-- .../Orcid/lib/services/orcid.service.ts | 22 ++++++++----------- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/components/Orcid/lib/hooks/useConnectOrcid.ts b/components/Orcid/lib/hooks/useConnectOrcid.ts index 80c2d2585..cb78d0d06 100644 --- a/components/Orcid/lib/hooks/useConnectOrcid.ts +++ b/components/Orcid/lib/hooks/useConnectOrcid.ts @@ -16,12 +16,13 @@ export function useConnectOrcid() { url.search = query; } - try { - setIsConnecting(true); - await connectOrcidAccount(url.toString()); - } catch (error) { - toast.error(extractApiErrorMessage(error, 'Unable to connect to ORCID.')); - } finally { + setIsConnecting(true); + const result = await connectOrcidAccount(url.toString()); + + if (result.success) { + globalThis.location.href = result.authUrl; + } else { + toast.error(extractApiErrorMessage(result.error, 'Unable to connect to ORCID.')); setIsConnecting(false); } }, [pathname, searchParams]); diff --git a/components/Orcid/lib/hooks/useOrcidCallback.ts b/components/Orcid/lib/hooks/useOrcidCallback.ts index 4322cf9ff..37cf27a52 100644 --- a/components/Orcid/lib/hooks/useOrcidCallback.ts +++ b/components/Orcid/lib/hooks/useOrcidCallback.ts @@ -12,11 +12,10 @@ interface UseOrcidCallbackOptions { readonly onSuccess?: () => void; } -export function useOrcidCallback(options?: UseOrcidCallbackOptions) { +export function useOrcidCallback({ onSuccess }: UseOrcidCallbackOptions = {}) { const pathname = usePathname(); const router = useRouter(); const searchParams = useSearchParams(); - const { onSuccess } = options || {}; const orcidSuccess = searchParams.get('orcid_connected') === 'true'; const orcidError = searchParams.get('orcid_error'); diff --git a/components/Orcid/lib/services/orcid.service.ts b/components/Orcid/lib/services/orcid.service.ts index 23d0f969c..264b0a9cf 100644 --- a/components/Orcid/lib/services/orcid.service.ts +++ b/components/Orcid/lib/services/orcid.service.ts @@ -1,23 +1,19 @@ -'use server'; - import { ApiClient } from '@/services/client'; -import { redirect } from 'next/navigation'; -export async function connectOrcidAccount(returnUrl: string): Promise { - let authUrl: string; +type ConnectResult = { success: true; authUrl: string } | { success: false; error: string }; +export async function connectOrcidAccount(returnUrl: string): Promise { try { const response = await ApiClient.post<{ auth_url: string }>('/api/orcid/connect/', { return_url: returnUrl, }); - authUrl = response.auth_url; - } catch { - throw new Error('Unable to connect to ORCID.'); - } - if (!authUrl) { - throw new Error('Unable to connect to ORCID.'); - } + if (!response.auth_url) { + return { success: false, error: 'Unable to connect to ORCID.' }; + } - redirect(authUrl); + return { success: true, authUrl: response.auth_url }; + } catch { + return { success: false, error: 'Unable to connect to ORCID.' }; + } } From 359283e843f4f336c7ff3d10a8f1589a93107d94 Mon Sep 17 00:00:00 2001 From: Michael Canova Date: Mon, 5 Jan 2026 16:57:39 -0500 Subject: [PATCH 20/21] [ORCID Integration] Feedback style updates --- components/Orcid/OrcidConnectButton.tsx | 4 +++- components/Orcid/OrcidSyncBanner.tsx | 4 ++-- components/profile/About/ProfileInformationForm/index.tsx | 6 +++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/components/Orcid/OrcidConnectButton.tsx b/components/Orcid/OrcidConnectButton.tsx index 3cebe00c8..464245b32 100644 --- a/components/Orcid/OrcidConnectButton.tsx +++ b/components/Orcid/OrcidConnectButton.tsx @@ -10,12 +10,14 @@ interface OrcidConnectButtonProps { readonly variant?: ButtonProps['variant']; readonly size?: ButtonProps['size']; readonly className?: string; + readonly showIcon?: boolean; } export function OrcidConnectButton({ variant = 'outlined', size = 'default', className, + showIcon = true, }: OrcidConnectButtonProps) { const { connect, isConnecting } = useConnectOrcid(); @@ -30,7 +32,7 @@ export function OrcidConnectButton({ {isConnecting ? ( ) : ( - + showIcon && )} {isConnecting ? 'Connecting...' : 'Connect ORCID'} diff --git a/components/Orcid/OrcidSyncBanner.tsx b/components/Orcid/OrcidSyncBanner.tsx index 107730690..cb5e6b225 100644 --- a/components/Orcid/OrcidSyncBanner.tsx +++ b/components/Orcid/OrcidSyncBanner.tsx @@ -40,8 +40,8 @@ export function OrcidSyncBanner({ isOwnProfile, isOrcidConnected }: OrcidSyncBan
-

Sync your ORCID account

-

+

Connect your ORCID account

+

Sync your ORCID publications with your ResearchHub profile

diff --git a/components/profile/About/ProfileInformationForm/index.tsx b/components/profile/About/ProfileInformationForm/index.tsx index 462ad2b87..7d66d4210 100644 --- a/components/profile/About/ProfileInformationForm/index.tsx +++ b/components/profile/About/ProfileInformationForm/index.tsx @@ -262,7 +262,11 @@ export function ProfileInformationForm({ className="text-gray-500" /> {isOrcid && !isOrcidConnected ? ( - + ) : ( Date: Mon, 5 Jan 2026 19:40:35 -0500 Subject: [PATCH 21/21] [ORCID Integration] Adding border to updated Edit Profile button --- components/profile/About/ProfileInformationForm/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/profile/About/ProfileInformationForm/index.tsx b/components/profile/About/ProfileInformationForm/index.tsx index 7d66d4210..308e6087b 100644 --- a/components/profile/About/ProfileInformationForm/index.tsx +++ b/components/profile/About/ProfileInformationForm/index.tsx @@ -263,7 +263,7 @@ export function ProfileInformationForm({ /> {isOrcid && !isOrcidConnected ? (