diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 4ee9fbf69..b8981b66a 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -10,7 +10,7 @@ import { import { IconExternalLink, isExternalLinkImplied, - isNewTabImplied, + useIsNewTabImplied, } from './Link.utils'; interface LinkProperties extends DesignSystemReactLinkProperties { @@ -38,7 +38,8 @@ export function Link({ let icon; // Open link in new tab - const openInNewTab = isNewTab ?? isNewTabImplied(href); + const isNewTabImplied = useIsNewTabImplied(href); + const openInNewTab = isNewTab ?? isNewTabImplied; if (openInNewTab) otherProperties.target = '_blank'; // Treat as an External link diff --git a/src/components/Link.utils.tsx b/src/components/Link.utils.tsx index 7b368fbf9..6864b49bc 100644 --- a/src/components/Link.utils.tsx +++ b/src/components/Link.utils.tsx @@ -1,26 +1,28 @@ +import useSblAuth from 'api/useSblAuth'; import { Icon } from 'design-system-react'; import type { ReactElement } from 'react'; +import { useLocation } from 'react-router-dom'; -// Link to specific regulation -// Ex: /rules-policy/regulations/1002/109/#a-1-ii -const regsLinkPattern = /\/rules-policy\/regulations\/\d+\/\d+\/#.+/; - -// Link to specific FIG subsection -// Ex: /small-business-lending/filing-instructions-guide/2024-guide/#4.4.1 -const figLinkPattern = /\/filing-instructions-guide\/\d{4}-guide\/#.+/; - -/** - * Programmatically determine if a link is external to the CFPB sphere of websites - */ -export const isExternalLinkImplied = (targetUrl: string): boolean => { +// Parse URL string into URL object +const parseURL = (url: string): URL | false => { let parsed; try { - parsed = new URL(targetUrl); + parsed = new URL(url); } catch { return false; // Relative targets will fail parsing (ex. '/home') } + return parsed; +}; + +/** + * Programmatically determine if a link is external to the CFPB sphere of websites + */ +export const isExternalLinkImplied = (targetUrl: string): boolean => { + const parsed = parseURL(targetUrl); + if (!parsed) return false; // Invalid URL or Relative URL (ex. '/home') + const externalProtocols = ['http', 'tel:', 'sms:', 'ftp:']; if (externalProtocols.includes(parsed.protocol)) return true; @@ -46,7 +48,54 @@ export function IconExternalLink(): ReactElement { ); } -export function isNewTabImplied(href: string | undefined): boolean { +// Determine if the the target href should open in a new tab +export function useIsNewTabImplied(href: string | undefined): boolean { + const { ...auth } = useSblAuth(); + const { pathname } = useLocation(); + + // No link to open if (!href) return false; - return regsLinkPattern.test(href) || figLinkPattern.test(href); + + // User is NOT logged-in to Login.gov / Keycloak + if (!auth.isAuthenticated) return false; + + // User is on a "Complete user profile" page, which qualifies as "Not logged-in" + // (Once associated with an LEI, users are always redirected away from these pages) + if (pathname.includes('/profile/complete')) return false; + + // Login.gov account page opens in same tab + if (href === 'https://secure.login.gov/account/') return false; + + // Open a new tab when the user is logged-in but visiting an unauthenticated page + const isCollectionAndReportingRequirements = href.endsWith( + '/compliance/compliance-resources/', + ); + const isFinalRule = href.endsWith('/rules-policy/final-rules/'); + const isPaperworkReductionAct = href.endsWith( + '/paperwork-reduction-act-notice', + ); + const isUnauthenticatedHomepage = href.toString() === '/'; + const isViewPrivacyNotice = href.endsWith('/privacy-notice'); + + if ( + isViewPrivacyNotice || + isPaperworkReductionAct || + isFinalRule || + isCollectionAndReportingRequirements || + isUnauthenticatedHomepage + ) + return true; + + // Open in the same tab if the target is within our app + const parsed = parseURL(href); + if (!parsed) return false; + + const isTargetWithinOurAppDomain = new RegExp( + `([\\S]*\\.)?(${window.location.host})`, + ).test(parsed.host); + + if (isTargetWithinOurAppDomain) return false; + + // All other links will open in a new tab + return true; }