2023-10-21 13:32:37 -07:00
import React , { ErrorInfo , useRef , useState } from 'react' ;
import { ErrorBoundary } from 'react-error-boundary' ;
2020-03-27 13:59:38 -07:00
import { FormattedMessage } from 'react-intl' ;
2022-01-10 14:25:06 -08:00
2023-10-21 13:32:37 -07:00
import { NODE_ENV } from 'soapbox/build-config' ;
2022-11-25 09:04:11 -08:00
import { HStack , Text , Stack } from 'soapbox/components/ui' ;
2023-10-21 13:32:37 -07:00
import { useSoapboxConfig } from 'soapbox/hooks' ;
2023-10-21 12:18:56 -07:00
import { captureSentryException } from 'soapbox/sentry' ;
2022-11-15 12:42:22 -08:00
import KVStore from 'soapbox/storage/kv-store' ;
2022-02-23 09:42:04 -08:00
import sourceCode from 'soapbox/utils/code' ;
2023-09-15 12:07:59 -07:00
import { unregisterSW } from 'soapbox/utils/sw' ;
2020-03-27 13:59:38 -07:00
2022-07-22 10:30:16 -07:00
import SiteLogo from './site-logo' ;
2023-10-21 13:32:37 -07:00
interface ISiteErrorBoundary {
2023-10-02 11:54:02 -07:00
children : React.ReactNode ;
2023-01-10 15:03:15 -08:00
}
2020-03-27 13:59:38 -07:00
2023-10-21 13:32:37 -07:00
/** Application-level error boundary. Fills the whole screen. */
const SiteErrorBoundary : React.FC < ISiteErrorBoundary > = ( { children } ) = > {
const { links } = useSoapboxConfig ( ) ;
const textarea = useRef < HTMLTextAreaElement > ( null ) ;
2020-03-27 13:59:38 -07:00
2023-10-21 13:32:37 -07:00
const [ error , setError ] = useState < any > ( ) ;
const [ componentStack , setComponentStack ] = useState < any > ( ) ;
const [ browser , setBrowser ] = useState < Bowser.Parser.Parser > ( ) ;
2021-09-08 13:45:44 -07:00
2023-10-21 13:32:37 -07:00
const isProduction = NODE_ENV === 'production' ;
const errorText = error + componentStack ;
2021-09-11 16:29:43 -07:00
2023-10-21 13:32:37 -07:00
const clearCookies : React.MouseEventHandler = ( e ) = > {
2021-04-22 11:57:47 -07:00
localStorage . clear ( ) ;
sessionStorage . clear ( ) ;
2022-04-20 08:46:49 -07:00
KVStore . clear ( ) ;
2022-04-19 14:30:10 -07:00
if ( 'serviceWorker' in navigator ) {
e . preventDefault ( ) ;
2023-09-15 12:07:59 -07:00
unregisterSW ( ) . then ( goHome ) . catch ( goHome ) ;
2022-04-19 14:30:10 -07:00
}
2023-01-05 09:55:08 -08:00
} ;
2021-04-22 11:57:47 -07:00
2023-10-21 13:32:37 -07:00
const handleCopy : React.MouseEventHandler = ( ) = > {
if ( ! textarea . current ) return ;
2020-03-27 13:59:38 -07:00
2023-10-21 13:32:37 -07:00
textarea . current . select ( ) ;
textarea . current . setSelectionRange ( 0 , 99999 ) ;
2022-03-21 11:09:01 -07:00
2023-10-21 13:32:37 -07:00
document . execCommand ( 'copy' ) ;
} ;
2021-07-05 16:08:04 -07:00
2023-10-21 13:32:37 -07:00
function handleError ( error : Error , info : ErrorInfo ) {
setError ( error ) ;
setComponentStack ( info . componentStack ) ;
2022-03-21 11:09:01 -07:00
2023-10-21 13:32:37 -07:00
captureSentryException ( error , {
tags : {
// Allow page crashes to be easily searched in Sentry.
ErrorBoundary : 'yes' ,
} ,
} ) ;
2022-03-21 11:09:01 -07:00
2023-10-21 13:32:37 -07:00
import ( 'bowser' )
. then ( ( { default : Bowser } ) = > setBrowser ( Bowser . getParser ( window . navigator . userAgent ) ) )
. catch ( ( ) = > { } ) ;
}
2022-03-21 11:09:01 -07:00
2023-10-21 13:32:37 -07:00
function goHome() {
location . href = '/' ;
}
2022-03-21 11:09:01 -07:00
2023-10-21 13:32:37 -07:00
const fallback = (
< div className = 'flex h-screen flex-col bg-white pb-12 pt-16 dark:bg-primary-900' >
< main className = 'mx-auto flex w-full max-w-7xl grow flex-col justify-center px-4 sm:px-6 lg:px-8' >
< div className = 'flex shrink-0 justify-center' >
< a href = '/' className = 'inline-flex' >
< SiteLogo alt = 'Logo' className = 'h-12 w-auto cursor-pointer' / >
< / a >
< / div >
< div className = 'py-8' >
< div className = 'mx-auto max-w-xl space-y-2 text-center' >
< h1 className = 'text-3xl font-extrabold tracking-tight text-gray-900 dark:text-gray-500 sm:text-4xl' >
< FormattedMessage id = 'alert.unexpected.message' defaultMessage = 'Something went wrong.' / >
< / h1 >
< p className = 'text-lg text-gray-700 dark:text-gray-600' >
< FormattedMessage
id = 'alert.unexpected.body'
defaultMessage = "We're sorry for the interruption. If the problem persists, please reach out to our support team. You may also try to {clearCookies} (this will log you out)."
values = { {
clearCookies : (
< a href = '/' onClick = { clearCookies } className = 'text-primary-600 hover:underline dark:text-accent-blue' >
< FormattedMessage
id = 'alert.unexpected.clear_cookies'
defaultMessage = 'clear cookies and browser data'
/ >
< / a >
) ,
} }
/ >
< / p >
< Text theme = 'muted' >
< Text weight = 'medium' tag = 'span' theme = 'muted' > { sourceCode . displayName } : < / Text >
{ ' ' } { sourceCode . version }
< / Text >
< div className = 'mt-10' >
< a href = '/' className = 'text-base font-medium text-primary-600 hover:underline dark:text-accent-blue' >
< FormattedMessage id = 'alert.unexpected.return_home' defaultMessage = 'Return Home' / >
{ ' ' }
< span className = 'inline-block rtl:rotate-180' aria-hidden = 'true' > & rarr ; < / span >
< / a >
2022-03-21 11:09:01 -07:00
< / div >
< / div >
2023-10-10 18:29:31 -07:00
2023-10-21 13:32:37 -07:00
{ ! isProduction && (
< div className = 'mx-auto max-w-lg space-y-4 py-16' >
{ errorText && (
< textarea
ref = { textarea }
className = 'block h-48 w-full rounded-md border-gray-300 bg-gray-100 p-4 font-mono text-gray-900 shadow-sm focus:border-primary-500 focus:ring-2 focus:ring-primary-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 sm:text-sm'
value = { errorText }
onClick = { handleCopy }
dir = 'ltr'
readOnly
/ >
) }
{ browser && (
< Stack >
< Text weight = 'semibold' > < FormattedMessage id = 'alert.unexpected.browser' defaultMessage = 'Browser' / > < / Text >
< Text theme = 'muted' > { browser . getBrowserName ( ) } { browser . getBrowserVersion ( ) } < / Text >
< / Stack >
) }
< / div >
) }
< / div >
< / main >
< footer className = 'mx-auto w-full max-w-7xl shrink-0 px-4 sm:px-6 lg:px-8' >
< HStack justifyContent = 'center' space = { 4 } element = 'nav' >
{ links . get ( 'status' ) && (
< >
< a href = { links . get ( 'status' ) } className = 'text-sm font-medium text-gray-700 hover:underline dark:text-gray-600' >
< FormattedMessage id = 'alert.unexpected.links.status' defaultMessage = 'Status' / >
< / a >
< / >
) }
{ links . get ( 'help' ) && (
< >
< span className = 'inline-block border-l border-gray-300' aria-hidden = 'true' / >
< a href = { links . get ( 'help' ) } className = 'text-sm font-medium text-gray-700 hover:underline dark:text-gray-600' >
< FormattedMessage id = 'alert.unexpected.links.help' defaultMessage = 'Help Center' / >
< / a >
< / >
) }
{ links . get ( 'support' ) && (
< >
< span className = 'inline-block border-l border-gray-300' aria-hidden = 'true' / >
< a href = { links . get ( 'support' ) } className = 'text-sm font-medium text-gray-700 hover:underline dark:text-gray-600' >
< FormattedMessage id = 'alert.unexpected.links.support' defaultMessage = 'Support' / >
< / a >
< / >
) }
< / HStack >
< / footer >
< / div >
) ;
return (
< ErrorBoundary fallback = { fallback } onError = { handleError } >
{ children }
< / ErrorBoundary >
) ;
} ;
export default SiteErrorBoundary ;