import '#/logger/sentry/setup' // must be near top
import '#/view/icons'
import './style.css'
import {Fragment, useEffect, useState} from 'react'
import {KeyboardProvider as KeyboardControllerProvider} from 'react-native-keyboard-controller'
import {SafeAreaProvider} from 'react-native-safe-area-context'
import {useLingui} from '@lingui/react/macro'
import * as Sentry from '@sentry/react-native'
import {Provider as HotkeysProvider} from '#/lib/hotkeys'
import {SafeAreaOverride} from '#/lib/pwa-safe-area'
import {QueryProvider} from '#/lib/react-query'
import {ThemeProvider} from '#/lib/ThemeContext'
import {Provider as TranslateOnDeviceProvider} from '#/lib/translation'
import I18nProvider from '#/locale/i18nProvider'
import {logger} from '#/logger'
import {Provider as A11yProvider} from '#/state/a11y'
import {
prefetchAppConfig,
Provider as AppConfigProvider,
} from '#/state/appConfig'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs'
import {Provider as EmailVerificationProvider} from '#/state/email-verification'
import {listenSessionDropped} from '#/state/events'
import {Provider as HomeBadgeProvider} from '#/state/home-badge'
import {Provider as LightboxStateProvider} from '#/state/lightbox'
import {MessagesProvider} from '#/state/messages'
import {Provider as ModalStateProvider} from '#/state/modals'
import {init as initPersistedState} from '#/state/persisted'
import {Provider as PrefsStateProvider} from '#/state/preferences'
import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts'
import {Provider as UnreadNotifsProvider} from '#/state/queries/notifications/unread'
import {Provider as ServiceConfigProvider} from '#/state/service-config'
import {
Provider as SessionProvider,
type SessionAccount,
useSession,
useSessionApi,
} from '#/state/session'
import {getWebOAuthClient} from '#/state/session/oauth-web-client'
import {readLastActiveAccount} from '#/state/session/util'
import {Provider as ShellStateProvider} from '#/state/shell'
import {Provider as ComposerProvider} from '#/state/shell/composer'
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
import {Provider as OnboardingProvider} from '#/state/shell/onboarding'
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies'
import {Shell} from '#/view/shell/index'
import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {Provider as ContextMenuProvider} from '#/components/ContextMenu'
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs'
import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay'
import {Provider as PortalProvider} from '#/components/Portal'
import {Provider as ActiveVideoProvider} from '#/components/Post/Embed/VideoEmbed/ActiveVideoWebContext'
import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbed/VideoVolumeContext'
import * as Toast from '#/components/Toast'
import {ToastOutlet} from '#/components/Toast'
import {
prefetchAgeAssuranceConfig,
Provider as AgeAssuranceV2Provider,
} from '#/ageAssurance'
import {
AnalyticsContext,
AnalyticsFeaturesContext,
features,
setupDeviceId,
} from '#/analytics'
import {
prefetchLiveEvents,
Provider as LiveEventsProvider,
} from '#/features/liveEvents/context'
import * as Geo from '#/geolocation'
import {Splash} from '#/Splash'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder'
// For local development: the OAuth loopback spec requires IP-based origins
// (127.0.0.1), not "localhost". The auth server redirects to 127.0.0.1, but
// IndexedDB is per-origin, so PKCE state stored on "localhost" is unreachable
// from "127.0.0.1". Redirect immediately so both signIn() and the callback
// use the same origin.
if (typeof window !== 'undefined' && window.location.hostname === 'localhost') {
const url = new URL(window.location.href)
url.hostname = '127.0.0.1'
window.location.replace(url.href)
}
function hasOAuthCallbackParams(): boolean {
// OAuth callback params come in the hash fragment (response_mode=fragment)
// or query string. Check both for "state" + ("code" or "error").
const hash = new URLSearchParams(window.location.hash.slice(1))
const query = new URLSearchParams(window.location.search)
const params = hash.has('state') ? hash : query
return params.has('state') && (params.has('code') || params.has('error'))
}
/**
* Begin geolocation ASAP
*/
void Geo.resolve()
void prefetchAgeAssuranceConfig()
void prefetchLiveEvents()
void prefetchAppConfig()
function InnerApp() {
const [isReady, setIsReady] = useState(false)
const {currentAccount} = useSession()
const {resumeSession, login} = useSessionApi()
const theme = useColorModeTheme()
const {t: l} = useLingui()
const hasCheckedReferrer = useStarterPackEntry()
// init
useEffect(() => {
// Safety valve: if onLaunch hangs (e.g. stale IndexedDB blocking an
// upgrade, or a never-settling promise), the app will still load after
// this timeout fires.
const safetyTimeout = setTimeout(() => {
logger.warn('session: onLaunch safety timeout fired, forcing ready state')
setIsReady(true)
}, 15_000)
async function onLaunch(account?: SessionAccount) {
try {
// Check for OAuth callback params first (loopback redirects to /)
if (hasOAuthCallbackParams()) {
const client = getWebOAuthClient()
const result = await client.init()
if (result?.session) {
await login(
{
service: '',
identifier: '',
password: '',
oauthSession: result.session,
},
'LoginForm',
)
// Clear hash fragment after processing
window.history.replaceState(null, '', window.location.pathname)
return
}
}
if (account) {
await resumeSession(account)
} else {
await features.init
}
} catch (e) {
logger.error('session: resumeSession failed', {message: e})
} finally {
clearTimeout(safetyTimeout)
setIsReady(true)
}
}
const account = readLastActiveAccount()
void onLaunch(account)
}, [resumeSession, login])
useEffect(() => {
return listenSessionDropped(() => {
Toast.show(l`Sorry! Your session expired. Please sign in again.`, {
type: 'info',
})
})
}, [l])
return (
{/* LabelDefsProvider MUST come before ModerationOptsProvider */}
)
}
function App() {
const [isReady, setIsReady] = useState(false)
useEffect(() => {
void Promise.all([initPersistedState(), Geo.resolve(), setupDeviceId]).then(
() => setIsReady(true),
)
}, [])
if (!isReady) {
return null
}
/*
* NOTE: only nothing here can depend on other data or session state, since
* that is set up in the InnerApp component above.
*/
return (
)
}
export default Sentry.wrap(App)