(\n () => ({mutedWordsDialogControl, signinDialogControl}),\n [mutedWordsDialogControl, signinDialogControl],\n )\n\n return (\n {children}\n )\n}\n","import {useCallback} from 'react'\nimport {useLightboxControls} from './lightbox'\nimport {useModalControls} from './modals'\nimport {useComposerControls} from './shell/composer'\nimport {useSetDrawerOpen} from './shell/drawer-open'\nimport {useDialogStateControlContext} from '#/state/dialogs'\n\n/**\n * returns true if something was closed\n * (used by the android hardware back btn)\n */\nexport function useCloseAnyActiveElement() {\n const {closeLightbox} = useLightboxControls()\n const {closeModal} = useModalControls()\n const {closeComposer} = useComposerControls()\n const {closeAllDialogs} = useDialogStateControlContext()\n const setDrawerOpen = useSetDrawerOpen()\n return useCallback(() => {\n if (closeLightbox()) {\n return true\n }\n if (closeModal()) {\n return true\n }\n if (closeComposer()) {\n return true\n }\n if (closeAllDialogs()) {\n return true\n }\n setDrawerOpen(false)\n return false\n }, [closeLightbox, closeModal, closeComposer, setDrawerOpen, closeAllDialogs])\n}\n\n/**\n * used to clear out any modals, eg for a navigation\n */\nexport function useCloseAllActiveElements() {\n const {closeLightbox} = useLightboxControls()\n const {closeAllModals} = useModalControls()\n const {closeComposer} = useComposerControls()\n const {closeAllDialogs: closeAlfDialogs} = useDialogStateControlContext()\n const setDrawerOpen = useSetDrawerOpen()\n return useCallback(() => {\n closeLightbox()\n closeAllModals()\n closeComposer()\n closeAlfDialogs()\n setDrawerOpen(false)\n }, [\n closeLightbox,\n closeAllModals,\n closeComposer,\n closeAlfDialogs,\n setDrawerOpen,\n ])\n}\n","import EventEmitter from 'eventemitter3'\n\ntype UnlistenFn = () => void\n\nconst emitter = new EventEmitter()\n\n// a \"soft reset\" typically means scrolling to top and loading latest\n// but it can depend on the screen\nexport function emitSoftReset() {\n emitter.emit('soft-reset')\n}\nexport function listenSoftReset(fn: () => void): UnlistenFn {\n emitter.on('soft-reset', fn)\n return () => emitter.off('soft-reset', fn)\n}\n\nexport function emitSessionDropped() {\n emitter.emit('session-dropped')\n}\nexport function listenSessionDropped(fn: () => void): UnlistenFn {\n emitter.on('session-dropped', fn)\n return () => emitter.off('session-dropped', fn)\n}\n\nexport function emitPostCreated() {\n emitter.emit('post-created')\n}\nexport function listenPostCreated(fn: () => void): UnlistenFn {\n emitter.on('post-created', fn)\n return () => emitter.off('post-created', fn)\n}\n","export function cleanError(str: any): string {\n if (!str) {\n return ''\n }\n if (typeof str !== 'string') {\n str = str.toString()\n }\n if (isNetworkError(str)) {\n return 'Unable to connect. Please check your internet connection and try again.'\n }\n if (str.includes('Upstream Failure')) {\n return 'The server appears to be experiencing issues. Please try again in a few moments.'\n }\n if (str.includes('Bad token scope')) {\n return 'This feature is not available while using an App Password. Please sign in with your main password.'\n }\n if (str.startsWith('Error: ')) {\n return str.slice('Error: '.length)\n }\n return str\n}\n\nexport function isNetworkError(e: unknown) {\n const str = String(e)\n return (\n str.includes('Abort') ||\n str.includes('Network request failed') ||\n str.includes('Failed to fetch')\n )\n}\n","import {isNetworkError} from 'lib/strings/errors'\n\nexport async function retry(\n retries: number,\n cond: (err: any) => boolean,\n fn: () => Promise
,\n): Promise
{\n let lastErr\n while (retries > 0) {\n try {\n return await fn()\n } catch (e: any) {\n lastErr = e\n if (cond(e)) {\n retries--\n continue\n }\n throw e\n }\n }\n throw lastErr\n}\n\nexport async function networkRetry
(\n retries: number,\n fn: () => Promise
,\n): Promise
{\n return retry(retries, isNetworkError, fn)\n}\n","const NOW = 5\nconst MINUTE = 60\nconst HOUR = MINUTE * 60\nconst DAY = HOUR * 24\nconst MONTH_30 = DAY * 30\nconst MONTH = DAY * 30.41675 // This results in 365.001 days in a year, which is close enough for nearly all cases\nexport function ago(date: number | string | Date): string {\n let ts: number\n if (typeof date === 'string') {\n ts = Number(new Date(date))\n } else if (date instanceof Date) {\n ts = Number(date)\n } else {\n ts = date\n }\n const diffSeconds = Math.floor((Date.now() - ts) / 1e3)\n if (diffSeconds < NOW) {\n return `now`\n } else if (diffSeconds < MINUTE) {\n return `${diffSeconds}s`\n } else if (diffSeconds < HOUR) {\n return `${Math.floor(diffSeconds / MINUTE)}m`\n } else if (diffSeconds < DAY) {\n return `${Math.floor(diffSeconds / HOUR)}h`\n } else if (diffSeconds < MONTH_30) {\n return `${Math.round(diffSeconds / DAY)}d`\n } else {\n let months = diffSeconds / MONTH\n if (months % 1 >= 0.9) {\n months = Math.ceil(months)\n } else {\n months = Math.floor(months)\n }\n\n if (months < 12) {\n return `${months}mo`\n } else {\n return new Date(ts).toLocaleDateString()\n }\n }\n}\n\nexport function niceDate(date: number | string | Date) {\n const d = new Date(date)\n return `${d.toLocaleDateString('en-us', {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n })} at ${d.toLocaleTimeString(undefined, {\n hour: 'numeric',\n minute: '2-digit',\n })}`\n}\n\nexport function getAge(birthDate: Date): number {\n var today = new Date()\n var age = today.getFullYear() - birthDate.getFullYear()\n var m = today.getMonth() - birthDate.getMonth()\n if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {\n age--\n }\n return age\n}\n","import AsyncStorage from '@react-native-async-storage/async-storage'\n\nconst PREFIX = 'agent-labelers'\n\nexport async function saveLabelers(did: string, value: string[]) {\n await AsyncStorage.setItem(`${PREFIX}:${did}`, JSON.stringify(value))\n}\n\nexport async function readLabelers(did: string): Promise {\n const rawData = await AsyncStorage.getItem(`${PREFIX}:${did}`)\n return rawData ? JSON.parse(rawData) : undefined\n}\n","import {BSKY_LABELER_DID, BskyAgent} from '@atproto/api'\n\nimport {IS_TEST_USER} from '#/lib/constants'\nimport {readLabelers} from './agent-config'\nimport {SessionAccount} from './types'\n\nexport function configureModerationForGuest() {\n // This global mutation is *only* OK because this code is only relevant for testing.\n // Don't add any other global behavior here!\n switchToBskyAppLabeler()\n}\n\nexport async function configureModerationForAccount(\n agent: BskyAgent,\n account: SessionAccount,\n) {\n // This global mutation is *only* OK because this code is only relevant for testing.\n // Don't add any other global behavior here!\n switchToBskyAppLabeler()\n if (IS_TEST_USER(account.handle)) {\n await trySwitchToTestAppLabeler(agent)\n }\n\n // The code below is actually relevant to production (and isn't global).\n const labelerDids = await readLabelers(account.did).catch(_ => {})\n if (labelerDids) {\n agent.configureLabelersHeader(\n labelerDids.filter(did => did !== BSKY_LABELER_DID),\n )\n } else {\n // If there are no headers in the storage, we'll not send them on the initial requests.\n // If we wanted to fix this, we could block on the preferences query here.\n }\n}\n\nfunction switchToBskyAppLabeler() {\n BskyAgent.configure({appLabelers: [BSKY_LABELER_DID]})\n}\n\nasync function trySwitchToTestAppLabeler(agent: BskyAgent) {\n const did = (\n await agent\n .resolveHandle({handle: 'mod-authority.test'})\n .catch(_ => undefined)\n )?.data.did\n if (did) {\n console.warn('USING TEST ENV MODERATION')\n BskyAgent.configure({appLabelers: [did]})\n }\n}\n","export function isObj(v: unknown): v is Record {\n return !!v && typeof v === 'object'\n}\n\nexport function hasProp(\n data: object,\n prop: K,\n): data is Record {\n return prop in data\n}\n\nexport function isStrArray(v: unknown): v is string[] {\n return Array.isArray(v) && v.every(item => typeof item === 'string')\n}\n","import {jwtDecode} from 'jwt-decode'\n\nimport {hasProp} from '#/lib/type-guards'\nimport {logger} from '#/logger'\nimport * as persisted from '#/state/persisted'\nimport {SessionAccount} from './types'\n\nexport function readLastActiveAccount() {\n const {currentAccount, accounts} = persisted.get('session')\n return accounts.find(a => a.did === currentAccount?.did)\n}\n\nexport function isSignupQueued(accessJwt: string | undefined) {\n if (accessJwt) {\n const sessData = jwtDecode(accessJwt)\n return (\n hasProp(sessData, 'scope') &&\n sessData.scope === 'com.atproto.signupQueued'\n )\n }\n return false\n}\n\nexport function isSessionExpired(account: SessionAccount) {\n try {\n if (account.accessJwt) {\n const decoded = jwtDecode(account.accessJwt)\n if (decoded.exp) {\n const didExpire = Date.now() >= decoded.exp * 1000\n return didExpire\n }\n }\n } catch (e) {\n logger.error(`session: could not decode jwt`)\n }\n return true\n}\n","import {AtpSessionData, AtpSessionEvent, BskyAgent} from '@atproto/api'\nimport {TID} from '@atproto/common-web'\n\nimport {networkRetry} from '#/lib/async/retry'\nimport {\n DISCOVER_SAVED_FEED,\n IS_PROD_SERVICE,\n PUBLIC_BSKY_SERVICE,\n TIMELINE_SAVED_FEED,\n} from '#/lib/constants'\nimport {tryFetchGates} from '#/lib/statsig/statsig'\nimport {getAge} from '#/lib/strings/time'\nimport {logger} from '#/logger'\nimport {\n configureModerationForAccount,\n configureModerationForGuest,\n} from './moderation'\nimport {SessionAccount} from './types'\nimport {isSessionExpired, isSignupQueued} from './util'\n\nexport function createPublicAgent() {\n configureModerationForGuest() // Side effect but only relevant for tests\n return new BskyAgent({service: PUBLIC_BSKY_SERVICE})\n}\n\nexport async function createAgentAndResume(\n storedAccount: SessionAccount,\n onSessionChange: (\n agent: BskyAgent,\n did: string,\n event: AtpSessionEvent,\n ) => void,\n) {\n const agent = new BskyAgent({service: storedAccount.service})\n if (storedAccount.pdsUrl) {\n agent.pdsUrl = agent.api.xrpc.uri = new URL(storedAccount.pdsUrl)\n }\n const gates = tryFetchGates(storedAccount.did, 'prefer-low-latency')\n const moderation = configureModerationForAccount(agent, storedAccount)\n const prevSession: AtpSessionData = {\n // Sorted in the same property order as when returned by BskyAgent (alphabetical).\n accessJwt: storedAccount.accessJwt ?? '',\n did: storedAccount.did,\n email: storedAccount.email,\n emailAuthFactor: storedAccount.emailAuthFactor,\n emailConfirmed: storedAccount.emailConfirmed,\n handle: storedAccount.handle,\n refreshJwt: storedAccount.refreshJwt ?? '',\n }\n if (isSessionExpired(storedAccount)) {\n await networkRetry(1, () => agent.resumeSession(prevSession))\n } else {\n agent.session = prevSession\n if (!storedAccount.signupQueued) {\n // Intentionally not awaited to unblock the UI:\n networkRetry(3, () => agent.resumeSession(prevSession)).catch(\n (e: any) => {\n logger.error(`networkRetry failed to resume session`, {\n status: e?.status || 'unknown',\n // this field name is ignored by Sentry scrubbers\n safeMessage: e?.message || 'unknown',\n })\n\n throw e\n },\n )\n }\n }\n\n return prepareAgent(agent, gates, moderation, onSessionChange)\n}\n\nexport async function createAgentAndLogin(\n {\n service,\n identifier,\n password,\n authFactorToken,\n }: {\n service: string\n identifier: string\n password: string\n authFactorToken?: string\n },\n onSessionChange: (\n agent: BskyAgent,\n did: string,\n event: AtpSessionEvent,\n ) => void,\n) {\n const agent = new BskyAgent({service})\n await agent.login({identifier, password, authFactorToken})\n\n const account = agentToSessionAccountOrThrow(agent)\n const gates = tryFetchGates(account.did, 'prefer-fresh-gates')\n const moderation = configureModerationForAccount(agent, account)\n return prepareAgent(agent, moderation, gates, onSessionChange)\n}\n\nexport async function createAgentAndCreateAccount(\n {\n service,\n email,\n password,\n handle,\n birthDate,\n inviteCode,\n verificationPhone,\n verificationCode,\n }: {\n service: string\n email: string\n password: string\n handle: string\n birthDate: Date\n inviteCode?: string\n verificationPhone?: string\n verificationCode?: string\n },\n onSessionChange: (\n agent: BskyAgent,\n did: string,\n event: AtpSessionEvent,\n ) => void,\n) {\n const agent = new BskyAgent({service})\n await agent.createAccount({\n email,\n password,\n handle,\n inviteCode,\n verificationPhone,\n verificationCode,\n })\n const account = agentToSessionAccountOrThrow(agent)\n const gates = tryFetchGates(account.did, 'prefer-fresh-gates')\n const moderation = configureModerationForAccount(agent, account)\n if (!account.signupQueued) {\n /*dont await*/ agent.upsertProfile(_existing => {\n return {\n displayName: '',\n // HACKFIX\n // creating a bunch of identical profile objects is breaking the relay\n // tossing this unspecced field onto it to reduce the size of the problem\n // -prf\n createdAt: new Date().toISOString(),\n }\n })\n }\n\n // Not awaited so that we can still get into onboarding.\n // This is OK because we won't let you toggle adult stuff until you set the date.\n if (IS_PROD_SERVICE(service)) {\n try {\n networkRetry(1, async () => {\n await agent.setPersonalDetails({birthDate: birthDate.toISOString()})\n await agent.overwriteSavedFeeds([\n {\n ...DISCOVER_SAVED_FEED,\n id: TID.nextStr(),\n },\n {\n ...TIMELINE_SAVED_FEED,\n id: TID.nextStr(),\n },\n ])\n\n if (getAge(birthDate) < 18) {\n await agent.api.com.atproto.repo.putRecord({\n repo: account.did,\n collection: 'chat.bsky.actor.declaration',\n rkey: 'self',\n record: {\n $type: 'chat.bsky.actor.declaration',\n allowIncoming: 'none',\n },\n })\n }\n })\n } catch (e: any) {\n logger.error(e, {\n context: `session: createAgentAndCreateAccount failed to save personal details and feeds`,\n })\n }\n } else {\n agent.setPersonalDetails({birthDate: birthDate.toISOString()})\n }\n\n return prepareAgent(agent, gates, moderation, onSessionChange)\n}\n\nasync function prepareAgent(\n agent: BskyAgent,\n // Not awaited in the calling code so we can delay blocking on them.\n gates: Promise,\n moderation: Promise,\n onSessionChange: (\n agent: BskyAgent,\n did: string,\n event: AtpSessionEvent,\n ) => void,\n) {\n // There's nothing else left to do, so block on them here.\n await Promise.all([gates, moderation])\n\n // Now the agent is ready.\n const account = agentToSessionAccountOrThrow(agent)\n agent.setPersistSessionHandler(event => {\n onSessionChange(agent, account.did, event)\n })\n return {agent, account}\n}\n\nexport function agentToSessionAccountOrThrow(agent: BskyAgent): SessionAccount {\n const account = agentToSessionAccount(agent)\n if (!account) {\n throw Error('Expected an active session')\n }\n return account\n}\n\nexport function agentToSessionAccount(\n agent: BskyAgent,\n): SessionAccount | undefined {\n if (!agent.session) {\n return undefined\n }\n return {\n service: agent.service.toString(),\n did: agent.session.did,\n handle: agent.session.handle,\n email: agent.session.email,\n emailConfirmed: agent.session.emailConfirmed || false,\n emailAuthFactor: agent.session.emailAuthFactor || false,\n refreshJwt: agent.session.refreshJwt,\n accessJwt: agent.session.accessJwt,\n signupQueued: isSignupQueued(agent.session.accessJwt),\n // @ts-expect-error TODO remove when backend is ready\n status: agent.session.status,\n pdsUrl: agent.pdsUrl?.toString(),\n }\n}\n","import {AtpSessionEvent} from '@atproto/api'\n\nimport {createPublicAgent} from './agent'\nimport {SessionAccount} from './types'\n\n// A hack so that the reducer can't read anything from the agent.\n// From the reducer's point of view, it should be a completely opaque object.\ntype OpaqueBskyAgent = {\n readonly service: URL\n readonly api: unknown\n readonly app: unknown\n readonly com: unknown\n}\n\ntype AgentState = {\n readonly agent: OpaqueBskyAgent\n readonly did: string | undefined\n}\n\nexport type State = {\n readonly accounts: SessionAccount[]\n readonly currentAgentState: AgentState\n needsPersist: boolean // Mutated in an effect.\n}\n\nexport type Action =\n | {\n type: 'received-agent-event'\n agent: OpaqueBskyAgent\n accountDid: string\n refreshedAccount: SessionAccount | undefined\n sessionEvent: AtpSessionEvent\n }\n | {\n type: 'switched-to-account'\n newAgent: OpaqueBskyAgent\n newAccount: SessionAccount\n }\n | {\n type: 'removed-account'\n accountDid: string\n }\n | {\n type: 'logged-out'\n }\n | {\n type: 'synced-accounts'\n syncedAccounts: SessionAccount[]\n syncedCurrentDid: string | undefined\n }\n\nfunction createPublicAgentState(): AgentState {\n return {\n agent: createPublicAgent(),\n did: undefined,\n }\n}\n\nexport function getInitialState(persistedAccounts: SessionAccount[]): State {\n return {\n accounts: persistedAccounts,\n currentAgentState: createPublicAgentState(),\n needsPersist: false,\n }\n}\n\nexport function reducer(state: State, action: Action): State {\n switch (action.type) {\n case 'received-agent-event': {\n const {agent, accountDid, refreshedAccount, sessionEvent} = action\n if (\n refreshedAccount === undefined &&\n agent !== state.currentAgentState.agent\n ) {\n // If the session got cleared out (e.g. due to expiry or network error) but\n // this account isn't the active one, don't clear it out at this time.\n // This way, if the problem is transient, it'll work on next resume.\n return state\n }\n if (sessionEvent === 'network-error') {\n // Don't change stored accounts but kick to the choose account screen.\n return {\n accounts: state.accounts,\n currentAgentState: createPublicAgentState(),\n needsPersist: true,\n }\n }\n const existingAccount = state.accounts.find(a => a.did === accountDid)\n if (\n !existingAccount ||\n JSON.stringify(existingAccount) === JSON.stringify(refreshedAccount)\n ) {\n // Fast path without a state update.\n return state\n }\n return {\n accounts: state.accounts.map(a => {\n if (a.did === accountDid) {\n if (refreshedAccount) {\n return refreshedAccount\n } else {\n return {\n ...a,\n // If we didn't receive a refreshed account, clear out the tokens.\n accessJwt: undefined,\n refreshJwt: undefined,\n }\n }\n } else {\n return a\n }\n }),\n currentAgentState: refreshedAccount\n ? state.currentAgentState\n : createPublicAgentState(), // Log out if expired.\n needsPersist: true,\n }\n }\n case 'switched-to-account': {\n const {newAccount, newAgent} = action\n return {\n accounts: [\n newAccount,\n ...state.accounts.filter(a => a.did !== newAccount.did),\n ],\n currentAgentState: {\n did: newAccount.did,\n agent: newAgent,\n },\n needsPersist: true,\n }\n }\n case 'removed-account': {\n const {accountDid} = action\n return {\n accounts: state.accounts.filter(a => a.did !== accountDid),\n currentAgentState:\n state.currentAgentState.did === accountDid\n ? createPublicAgentState() // Log out if removing the current one.\n : state.currentAgentState,\n needsPersist: true,\n }\n }\n case 'logged-out': {\n return {\n accounts: state.accounts.map(a => ({\n ...a,\n // Clear tokens for *every* account (this is a hard logout).\n refreshJwt: undefined,\n accessJwt: undefined,\n })),\n currentAgentState: createPublicAgentState(),\n needsPersist: true,\n }\n }\n case 'synced-accounts': {\n const {syncedAccounts, syncedCurrentDid} = action\n return {\n accounts: syncedAccounts,\n currentAgentState:\n syncedCurrentDid === state.currentAgentState.did\n ? state.currentAgentState\n : createPublicAgentState(), // Log out if different user.\n needsPersist: false, // Synced from another tab. Don't persist to avoid cycles.\n }\n }\n }\n}\n","import React from 'react'\nimport {AtpSessionEvent, BskyAgent} from '@atproto/api'\n\nimport {track} from '#/lib/analytics/analytics'\nimport {logEvent} from '#/lib/statsig/statsig'\nimport {isWeb} from '#/platform/detection'\nimport * as persisted from '#/state/persisted'\nimport {useCloseAllActiveElements} from '#/state/util'\nimport {useGlobalDialogsControlContext} from '#/components/dialogs/Context'\nimport {IS_DEV} from '#/env'\nimport {emitSessionDropped} from '../events'\nimport {\n agentToSessionAccount,\n createAgentAndCreateAccount,\n createAgentAndLogin,\n createAgentAndResume,\n} from './agent'\nimport {getInitialState, reducer} from './reducer'\n\nexport {isSignupQueued} from './util'\nexport type {SessionAccount} from '#/state/session/types'\nimport {SessionApiContext, SessionStateContext} from '#/state/session/types'\n\nconst StateContext = React.createContext({\n accounts: [],\n currentAccount: undefined,\n hasSession: false,\n})\n\nconst AgentContext = React.createContext(null)\n\nconst ApiContext = React.createContext({\n createAccount: async () => {},\n login: async () => {},\n logout: async () => {},\n resumeSession: async () => {},\n removeAccount: () => {},\n})\n\nexport function Provider({children}: React.PropsWithChildren<{}>) {\n const cancelPendingTask = useOneTaskAtATime()\n const [state, dispatch] = React.useReducer(reducer, null, () =>\n getInitialState(persisted.get('session').accounts),\n )\n\n const onAgentSessionChange = React.useCallback(\n (agent: BskyAgent, accountDid: string, sessionEvent: AtpSessionEvent) => {\n const refreshedAccount = agentToSessionAccount(agent) // Mutable, so snapshot it right away.\n if (sessionEvent === 'expired' || sessionEvent === 'create-failed') {\n emitSessionDropped()\n }\n dispatch({\n type: 'received-agent-event',\n agent,\n refreshedAccount,\n accountDid,\n sessionEvent,\n })\n },\n [],\n )\n\n const createAccount = React.useCallback(\n async params => {\n const signal = cancelPendingTask()\n track('Try Create Account')\n logEvent('account:create:begin', {})\n const {agent, account} = await createAgentAndCreateAccount(\n params,\n onAgentSessionChange,\n )\n\n if (signal.aborted) {\n return\n }\n dispatch({\n type: 'switched-to-account',\n newAgent: agent,\n newAccount: account,\n })\n track('Create Account')\n logEvent('account:create:success', {})\n },\n [onAgentSessionChange, cancelPendingTask],\n )\n\n const login = React.useCallback(\n async (params, logContext) => {\n const signal = cancelPendingTask()\n const {agent, account} = await createAgentAndLogin(\n params,\n onAgentSessionChange,\n )\n\n if (signal.aborted) {\n return\n }\n dispatch({\n type: 'switched-to-account',\n newAgent: agent,\n newAccount: account,\n })\n track('Sign In', {resumedSession: false})\n logEvent('account:loggedIn', {logContext, withPassword: true})\n },\n [onAgentSessionChange, cancelPendingTask],\n )\n\n const logout = React.useCallback(\n logContext => {\n cancelPendingTask()\n dispatch({\n type: 'logged-out',\n })\n logEvent('account:loggedOut', {logContext})\n },\n [cancelPendingTask],\n )\n\n const resumeSession = React.useCallback(\n async storedAccount => {\n const signal = cancelPendingTask()\n const {agent, account} = await createAgentAndResume(\n storedAccount,\n onAgentSessionChange,\n )\n\n if (signal.aborted) {\n return\n }\n dispatch({\n type: 'switched-to-account',\n newAgent: agent,\n newAccount: account,\n })\n },\n [onAgentSessionChange, cancelPendingTask],\n )\n\n const removeAccount = React.useCallback(\n account => {\n cancelPendingTask()\n dispatch({\n type: 'removed-account',\n accountDid: account.did,\n })\n },\n [cancelPendingTask],\n )\n\n React.useEffect(() => {\n if (state.needsPersist) {\n state.needsPersist = false\n persisted.write('session', {\n accounts: state.accounts,\n currentAccount: state.accounts.find(\n a => a.did === state.currentAgentState.did,\n ),\n })\n }\n }, [state])\n\n React.useEffect(() => {\n return persisted.onUpdate(() => {\n const synced = persisted.get('session')\n dispatch({\n type: 'synced-accounts',\n syncedAccounts: synced.accounts,\n syncedCurrentDid: synced.currentAccount?.did,\n })\n const syncedAccount = synced.accounts.find(\n a => a.did === synced.currentAccount?.did,\n )\n if (syncedAccount && syncedAccount.refreshJwt) {\n if (syncedAccount.did !== state.currentAgentState.did) {\n resumeSession(syncedAccount)\n } else {\n // @ts-ignore we checked for `refreshJwt` above\n state.currentAgentState.agent.session = syncedAccount\n }\n }\n })\n }, [state, resumeSession])\n\n const stateContext = React.useMemo(\n () => ({\n accounts: state.accounts,\n currentAccount: state.accounts.find(\n a => a.did === state.currentAgentState.did,\n ),\n hasSession: !!state.currentAgentState.did,\n }),\n [state],\n )\n\n const api = React.useMemo(\n () => ({\n createAccount,\n login,\n logout,\n resumeSession,\n removeAccount,\n }),\n [createAccount, login, logout, resumeSession, removeAccount],\n )\n\n // @ts-ignore\n if (IS_DEV && isWeb) window.agent = state.currentAgentState.agent\n\n const agent = state.currentAgentState.agent as BskyAgent\n const currentAgentRef = React.useRef(agent)\n React.useEffect(() => {\n if (currentAgentRef.current !== agent) {\n // Read the previous value and immediately advance the pointer.\n const prevAgent = currentAgentRef.current\n currentAgentRef.current = agent\n // We never reuse agents so let's fully neutralize the previous one.\n // This ensures it won't try to consume any refresh tokens.\n prevAgent.session = undefined\n prevAgent.setPersistSessionHandler(undefined)\n }\n }, [agent])\n\n return (\n \n \n {children}\n \n \n )\n}\n\nfunction useOneTaskAtATime() {\n const abortController = React.useRef(null)\n const cancelPendingTask = React.useCallback(() => {\n if (abortController.current) {\n abortController.current.abort()\n }\n abortController.current = new AbortController()\n return abortController.current.signal\n }, [])\n return cancelPendingTask\n}\n\nexport function useSession() {\n return React.useContext(StateContext)\n}\n\nexport function useSessionApi() {\n return React.useContext(ApiContext)\n}\n\nexport function useRequireAuth() {\n const {hasSession} = useSession()\n const closeAll = useCloseAllActiveElements()\n const {signinDialogControl} = useGlobalDialogsControlContext()\n\n return React.useCallback(\n (fn: () => void) => {\n if (hasSession) {\n fn()\n } else {\n closeAll()\n signinDialogControl.open()\n }\n },\n [hasSession, signinDialogControl, closeAll],\n )\n}\n\nexport function useAgent(): BskyAgent {\n const agent = React.useContext(AgentContext)\n if (!agent) {\n throw Error('useAgent() must be below .')\n }\n return agent\n}\n","export function timeout(ms: number): Promise {\n return new Promise(r => setTimeout(r, ms))\n}\n","import React from 'react'\nimport {Platform} from 'react-native'\nimport {AppState, AppStateStatus} from 'react-native'\nimport {sha256} from 'js-sha256'\nimport {Statsig, StatsigProvider} from 'statsig-react-native-expo'\n\nimport {logger} from '#/logger'\nimport {isWeb} from '#/platform/detection'\nimport * as persisted from '#/state/persisted'\nimport {BUNDLE_DATE, BUNDLE_IDENTIFIER, IS_TESTFLIGHT} from 'lib/app-info'\nimport {useSession} from '../../state/session'\nimport {timeout} from '../async/timeout'\nimport {useNonReactiveCallback} from '../hooks/useNonReactiveCallback'\nimport {LogEvents} from './events'\nimport {Gate} from './gates'\n\ntype StatsigUser = {\n userID: string | undefined\n // TODO: Remove when enough users have custom.platform:\n platform: 'ios' | 'android' | 'web'\n custom: {\n // This is the place where we can add our own stuff.\n // Fields here have to be non-optional to be visible in the UI.\n platform: 'ios' | 'android' | 'web'\n bundleIdentifier: string\n bundleDate: number\n refSrc: string\n refUrl: string\n appLanguage: string\n contentLanguages: string[]\n }\n}\n\nlet refSrc = ''\nlet refUrl = ''\nif (isWeb && typeof window !== 'undefined') {\n const params = new URLSearchParams(window.location.search)\n refSrc = params.get('ref_src') ?? ''\n refUrl = decodeURIComponent(params.get('ref_url') ?? '')\n}\n\nexport type {LogEvents}\n\nfunction createStatsigOptions(prefetchUsers: StatsigUser[]) {\n return {\n environment: {\n tier:\n process.env.NODE_ENV === 'development'\n ? 'development'\n : IS_TESTFLIGHT\n ? 'staging'\n : 'production',\n },\n // Don't block on waiting for network. The fetched config will kick in on next load.\n // This ensures the UI is always consistent and doesn't update mid-session.\n // Note this makes cold load (no local storage) and private mode return `false` for all gates.\n initTimeoutMs: 1,\n // Get fresh flags for other accounts as well, if any.\n prefetchUsers,\n }\n}\n\ntype FlatJSONRecord = Record<\n string,\n | string\n | number\n | boolean\n | null\n | undefined\n // Technically not scalar but Statsig will stringify it which works for us:\n | string[]\n>\n\nlet getCurrentRouteName: () => string | null | undefined = () => null\n\nexport function attachRouteToLogEvents(\n getRouteName: () => string | null | undefined,\n) {\n getCurrentRouteName = getRouteName\n}\n\nexport function toClout(n: number | null | undefined): number | undefined {\n if (n == null) {\n return undefined\n } else {\n return Math.max(0, Math.round(Math.log(n)))\n }\n}\n\nconst DOWNSAMPLED_EVENTS: Set = new Set([\n 'router:navigate:sampled',\n 'state:background:sampled',\n 'state:foreground:sampled',\n 'home:feedDisplayed:sampled',\n 'feed:endReached:sampled',\n 'feed:refresh:sampled',\n])\nconst isDownsampledSession = Math.random() < 0.9 // 90% likely\n\nexport function logEvent(\n eventName: E & string,\n rawMetadata: LogEvents[E] & FlatJSONRecord,\n) {\n try {\n if (\n process.env.NODE_ENV === 'development' &&\n eventName.endsWith(':sampled') &&\n !DOWNSAMPLED_EVENTS.has(eventName)\n ) {\n logger.error(\n 'Did you forget to add ' + eventName + ' to DOWNSAMPLED_EVENTS?',\n )\n }\n\n if (isDownsampledSession && DOWNSAMPLED_EVENTS.has(eventName)) {\n return\n }\n const fullMetadata = {\n ...rawMetadata,\n } as Record // Statsig typings are unnecessarily strict here.\n fullMetadata.routeName = getCurrentRouteName() ?? '(Uninitialized)'\n if (Statsig.initializeCalled()) {\n Statsig.logEvent(eventName, null, fullMetadata)\n }\n } catch (e) {\n // A log should never interrupt the calling code, whatever happens.\n logger.error('Failed to log an event', {message: e})\n }\n}\n\n// We roll our own cache in front of Statsig because it is a singleton\n// and it's been difficult to get it to behave in a predictable way.\n// Our own cache ensures consistent evaluation within a single session.\nconst GateCache = React.createContext