This commit is contained in:
Hailey
2025-07-18 13:53:36 -07:00
parent e4625c9014
commit 063a29c3d0
16 changed files with 171 additions and 171 deletions
+1
View File
@@ -6,3 +6,4 @@ EXPO_PUBLIC_LOG_LEVEL=debug
EXPO_PUBLIC_LOG_DEBUG=
EXPO_PUBLIC_BUNDLE_IDENTIFIER=
EXPO_PUBLIC_BUNDLE_DATE=0
EXPO_PUBLIC_USE_OAUTH="false"
+1 -1
View File
@@ -5,7 +5,7 @@
"redirect_uris": [
"https://bsky.hailey.at/auth/web/callback"
],
"scope": "atproto transition:generic transition:email",
"scope": "atproto transition:generic transition:email transition:chat.bsky",
"token_endpoint_auth_method": "none",
"response_types": [
"code"
@@ -5,7 +5,7 @@
"redirect_uris": [
"at.hailey.bsky:/auth/native/callback"
],
"scope": "atproto transition:generic transition:email",
"scope": "atproto transition:generic transition:email transition:chat.bsky",
"token_endpoint_auth_method": "none",
"response_types": [
"code"
+3 -7
View File
@@ -93,7 +93,7 @@ beginResolveGeolocation()
function InnerApp() {
const [isReady, setIsReady] = React.useState(false)
const {currentAccount} = useSession()
const {resumeSession, resumeSessionOauth} = useSessionApi()
const {resumeSession} = useSessionApi()
const theme = useColorModeTheme()
const {_} = useLingui()
const hasCheckedReferrer = useStarterPackEntry()
@@ -103,11 +103,7 @@ function InnerApp() {
async function onLaunch(account?: SessionAccount) {
try {
if (account) {
if (true) {
await resumeSessionOauth(account)
} else {
await resumeSession(account)
}
await resumeSession(account)
} else {
await tryFetchGates(undefined, 'prefer-fresh-gates')
}
@@ -119,7 +115,7 @@ function InnerApp() {
}
const account = readLastActiveAccount()
onLaunch(account)
}, [resumeSession, resumeSessionOauth])
}, [resumeSession])
useEffect(() => {
return listenSessionDropped(() => {
+3 -7
View File
@@ -72,7 +72,7 @@ beginResolveGeolocation()
function InnerApp() {
const [isReady, setIsReady] = React.useState(false)
const {currentAccount} = useSession()
const {resumeSession, resumeSessionOauth} = useSessionApi()
const {resumeSession} = useSessionApi()
const theme = useColorModeTheme()
const {_} = useLingui()
const hasCheckedReferrer = useStarterPackEntry()
@@ -82,11 +82,7 @@ function InnerApp() {
async function onLaunch(account?: SessionAccount) {
try {
if (account) {
if (true) {
await resumeSessionOauth(account)
} else {
await resumeSession(account)
}
await resumeSession(account)
}
} catch (e) {
logger.error(`session: resumeSession failed`, {message: e})
@@ -96,7 +92,7 @@ function InnerApp() {
}
const account = readLastActiveAccount()
onLaunch(account)
}, [resumeSession, resumeSessionOauth])
}, [resumeSession])
useEffect(() => {
return listenSessionDropped(() => {
+5 -8
View File
@@ -7,13 +7,14 @@ import {isWeb} from '#/platform/detection'
import {type SessionAccount, useSessionApi} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import * as Toast from '#/view/com/util/Toast'
import {USE_OAUTH} from '../app-info'
import {logEvent} from '../statsig/statsig'
import {type LogEvents} from '../statsig/statsig'
export function useAccountSwitcher() {
const [pendingDid, setPendingDid] = useState<string | null>(null)
const {_} = useLingui()
const {resumeSession, resumeSessionOauth} = useSessionApi()
const {resumeSession} = useSessionApi()
const {requestSwitchToAccount} = useLoggedOutViewControls()
const onPressSwitchAccount = useCallback(
@@ -28,7 +29,7 @@ export function useAccountSwitcher() {
try {
setPendingDid(account.did)
// TODO: this should be checking if it is an oauth session
if (true || account.accessJwt) {
if (USE_OAUTH || account.accessJwt) {
if (isWeb) {
// We're switching accounts, which remounts the entire app.
// On mobile, this gets us Home, but on the web we also need reset the URL.
@@ -37,11 +38,7 @@ export function useAccountSwitcher() {
// So we change the URL ourselves. The navigator will pick it up on remount.
history.pushState(null, '', '/')
}
if (true) {
await resumeSessionOauth(account)
} else {
await resumeSession(account)
}
await resumeSession(account)
logEvent('account:loggedIn', {logContext, withPassword: false})
Toast.show(_(msg`Signed in as @${account.handle}`))
} else {
@@ -64,7 +61,7 @@ export function useAccountSwitcher() {
setPendingDid(null)
}
},
[_, resumeSession, resumeSessionOauth, requestSwitchToAccount, pendingDid],
[_, resumeSession, requestSwitchToAccount, pendingDid],
)
return {onPressSwitchAccount, pendingDid}
+7 -4
View File
@@ -6,7 +6,7 @@ import {useSessionApi} from '#/state/session'
import {getWebOAuthClient} from '#/state/session/oauth-web-client'
export function AuthCallback() {
const {loginOauth} = useSessionApi()
const {login} = useSessionApi()
const navigation = useNavigation<NavigationProp>()
// TODO: handle errors, loading state, etc...
@@ -18,10 +18,13 @@ export function AuthCallback() {
;(async () => {
const client = getWebOAuthClient()
const res = await client.callback(params)
await loginOauth(res.session, 'LoginForm') // TODO: right context?
navigation.navigate('Home')
await login(
{service: '', identifier: '', password: '', oauthSession: res.session},
'LoginForm',
)
navigation.replace('Home')
})()
}, [loginOauth, navigation])
}, [login, navigation])
return null
}
+4 -8
View File
@@ -3,6 +3,7 @@ import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {USE_OAUTH} from '#/lib/app-info'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {type SessionAccount, useSession, useSessionApi} from '#/state/session'
@@ -24,7 +25,7 @@ export const ChooseAccountForm = ({
const [pendingDid, setPendingDid] = React.useState<string | null>(null)
const {_} = useLingui()
const {currentAccount} = useSession()
const {resumeSession, resumeSessionOauth} = useSessionApi()
const {resumeSession} = useSessionApi()
const {setShowLoggedOut} = useLoggedOutViewControls()
const onSelect = React.useCallback(
@@ -34,7 +35,7 @@ export const ChooseAccountForm = ({
return
}
// TODO: this should be checking if it is an oauth session
if (!true || !account.accessJwt) {
if (!USE_OAUTH && !account.accessJwt) {
// Move to login form.
onSelectAccount(account)
return
@@ -46,11 +47,7 @@ export const ChooseAccountForm = ({
}
try {
setPendingDid(account.did)
if (true) {
await resumeSessionOauth(account)
} else {
await resumeSession(account)
}
await resumeSession(account)
logEvent('account:loggedIn', {
logContext: 'ChooseAccountForm',
withPassword: false,
@@ -69,7 +66,6 @@ export const ChooseAccountForm = ({
[
currentAccount,
resumeSession,
resumeSessionOauth,
pendingDid,
onSelectAccount,
setShowLoggedOut,
+14 -5
View File
@@ -13,6 +13,7 @@ import {
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {USE_OAUTH} from '#/lib/app-info'
import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
import {isNetworkError} from '#/lib/strings/errors'
import {cleanError} from '#/lib/strings/errors'
@@ -53,14 +54,14 @@ interface LoginFormProps {
}
export function LoginForm(props: LoginFormProps) {
if (true) {
return <OAuthLoginForm {...props} />
if (USE_OAUTH) {
return <OAuthLoginFormInner {...props} />
} else {
return <LoginFormInner {...props} />
}
}
function OAuthLoginForm({
function OAuthLoginFormInner({
error,
initialHandle,
onPressBack,
@@ -70,7 +71,7 @@ function OAuthLoginForm({
const [isProcessing, setIsProcessing] = React.useState(false)
const identifierValueRef = useRef<string>(initialHandle || '')
const {loginOauth} = useSessionApi()
const {login} = useSessionApi()
const onPressNext = async () => {
setIsProcessing(true)
@@ -86,7 +87,15 @@ function OAuthLoginForm({
const client = getNativeOAuthClient()
const res = await client.signIn(identifierValueRef.current)
if (res.status === 'success') {
await loginOauth(res.session, 'LoginForm')
await login(
{
service: '',
identifier: '',
password: '',
oauthSession: res.session,
},
'LoginForm',
)
} else {
logger.error(`Invalid OAuth status: ${res.status}`)
setError(_(msg`An error occurred during authentication.`))
-57
View File
@@ -1,12 +1,9 @@
import {
Agent,
type AtpSessionData,
type AtpSessionEvent,
BskyAgent,
} from '@atproto/api'
import {type OutputSchema} from '@atproto/api/dist/client/types/com/atproto/server/getSession'
import {TID} from '@atproto/common-web'
import {type OAuthSession} from '@atproto/oauth-client-browser'
import {networkRetry} from '#/lib/async/retry'
import {
@@ -26,7 +23,6 @@ import {
configureModerationForAccount,
configureModerationForGuest,
} from './moderation'
import {BSKY_OAUTH_CLIENT} from './oauth-web-client'
import {type SessionAccount} from './types'
import {isSessionExpired, isSignupQueued} from './util'
@@ -72,22 +68,6 @@ export async function createAgentAndResume(
return agent.prepare(gates, moderation, onSessionChange)
}
export async function createAgentOauth(session: OAuthSession) {
const agent = new Agent(session)
const account = await oauthAgentAndSessionToSessionAccountOrThrow(
agent,
session,
)
tryFetchGates(account.did, 'prefer-fresh-gates')
configureModerationForAccount(agent, account)
return {agent, account}
}
export async function resumeAgentOauth(account: SessionAccount) {
const session = await BSKY_OAUTH_CLIENT.restore(account.did)
return await createAgentOauth(session)
}
export async function createAgentAndLogin(
{
service,
@@ -207,17 +187,6 @@ export async function createAgentAndCreateAccount(
return agent.prepare(gates, moderation, onSessionChange)
}
export async function oauthAgentAndSessionToSessionAccountOrThrow(
agent: Agent,
session: OAuthSession,
): Promise<SessionAccount> {
const account = await oauthAgentAndSessionToSessionAccount(agent, session)
if (!account) {
throw Error('Expected an active session')
}
return account
}
export function agentToSessionAccountOrThrow(agent: BskyAgent): SessionAccount {
const account = agentToSessionAccount(agent)
if (!account) {
@@ -226,32 +195,6 @@ export function agentToSessionAccountOrThrow(agent: BskyAgent): SessionAccount {
return account
}
export async function oauthAgentAndSessionToSessionAccount(
agent: Agent,
session: OAuthSession,
): Promise<SessionAccount | undefined> {
let data: OutputSchema
try {
const res = await agent.com.atproto.server.getSession()
data = res.data
} catch (e: any) {
logger.error(e)
return undefined
}
return {
service: session.serverMetadata.issuer,
did: session.did,
handle: data.handle,
email: data.email,
emailConfirmed: data.emailConfirmed,
emailAuthFactor: data.emailAuthFactor,
active: data.active,
status: data.status,
pdsUrl: session.serverMetadata.issuer,
isSelfHosted: !session.server.issuer.startsWith(BSKY_SERVICE), // TODO: is this entryway?
}
}
export function agentToSessionAccount(
agent: BskyAgent,
): SessionAccount | undefined {
+41 -64
View File
@@ -12,8 +12,6 @@ import {
createAgentAndCreateAccount,
createAgentAndLogin,
createAgentAndResume,
createAgentOauth,
resumeAgentOauth,
sessionAccountToSession,
} from './agent'
import {getInitialState, reducer} from './reducer'
@@ -21,11 +19,18 @@ import {getInitialState, reducer} from './reducer'
export {isSignupQueued} from './util'
import {addSessionDebugLog} from './logging'
export type {SessionAccount} from '#/state/session/types'
import {USE_OAUTH} from '#/lib/app-info'
import {logger} from '#/logger'
import {
type SessionAccount,
type SessionApiContext,
type SessionStateContext,
} from '#/state/session/types'
import {
type OauthBskyAppAgent,
oauthCreateAgent,
oauthResumeSession,
} from './oauth-agent'
const StateContext = React.createContext<SessionStateContext>({
accounts: [],
@@ -38,11 +43,9 @@ const AgentContext = React.createContext<BskyAgent | null>(null)
const ApiContext = React.createContext<SessionApiContext>({
createAccount: async () => {},
login: async () => {},
loginOauth: async () => {},
logoutCurrentAccount: async () => {},
logoutEveryAccount: async () => {},
resumeSession: async () => {},
resumeSessionOauth: async () => {},
removeAccount: () => {},
})
@@ -98,15 +101,25 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const login = React.useCallback<SessionApiContext['login']>(
async (params, logContext) => {
addSessionDebugLog({type: 'method:start', method: 'login'})
let agentAccount: {
agent: OauthBskyAppAgent | BskyAppAgent
account: SessionAccount
}
const signal = cancelPendingTask()
const {agent, account} = await createAgentAndLogin(
params,
onAgentSessionChange,
)
if (params.oauthSession) {
agentAccount = await oauthCreateAgent(params.oauthSession)
} else {
agentAccount = await createAgentAndLogin(params, onAgentSessionChange)
}
if (signal.aborted) {
return
}
const {agent, account} = agentAccount
dispatch({
type: 'switched-to-account',
newAgent: agent,
@@ -122,28 +135,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
[onAgentSessionChange, cancelPendingTask],
)
const loginOauth = React.useCallback<SessionApiContext['loginOauth']>(
async (session, logContext) => {
const signal = cancelPendingTask()
const {agent, account} = await createAgentOauth(session)
if (signal.aborted) {
return
}
dispatch({
type: 'switched-to-account',
newAgent: agent,
newAccount: account,
})
logger.metric(
'account:loggedIn',
{logContext, withPassword: true},
{statsig: true},
)
addSessionDebugLog({type: 'method:end', method: 'login', account})
},
[cancelPendingTask],
)
const logoutCurrentAccount = React.useCallback<
SessionApiContext['logoutEveryAccount']
>(
@@ -190,14 +181,27 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
account: storedAccount,
})
const signal = cancelPendingTask()
const {agent, account} = await createAgentAndResume(
storedAccount,
onAgentSessionChange,
)
let agentAccount: {
agent: OauthBskyAppAgent | BskyAppAgent
account: SessionAccount
}
if (USE_OAUTH) {
agentAccount = await oauthResumeSession(storedAccount)
} else {
agentAccount = await createAgentAndResume(
storedAccount,
onAgentSessionChange,
)
}
const {agent, account} = agentAccount
if (signal.aborted) {
return
}
dispatch({
type: 'switched-to-account',
newAgent: agent,
@@ -208,25 +212,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
[onAgentSessionChange, cancelPendingTask],
)
const resumeSessionOauth = React.useCallback<
SessionApiContext['resumeSessionOauth']
>(
async storedAccount => {
const signal = cancelPendingTask()
const {agent, account} = await resumeAgentOauth(storedAccount)
if (signal.aborted) {
return
}
dispatch({
type: 'switched-to-account',
newAgent: agent,
newAccount: account,
})
addSessionDebugLog({type: 'method:end', method: 'resumeSession', account})
},
[cancelPendingTask],
)
const removeAccount = React.useCallback<SessionApiContext['removeAccount']>(
account => {
addSessionDebugLog({
@@ -271,13 +256,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
a => a.did === synced.currentAccount?.did,
)
// TODO: this should be checking if it is an oauth session
if (syncedAccount && (true || syncedAccount?.refreshJwt)) {
if (syncedAccount && (USE_OAUTH || syncedAccount?.refreshJwt)) {
if (syncedAccount.did !== state.currentAgentState.did) {
if (true) {
resumeSessionOauth(syncedAccount)
} else {
resumeSession(syncedAccount)
}
resumeSession(syncedAccount)
} else {
const agent = state.currentAgentState.agent as BskyAgent
const prevSession = agent.session
@@ -291,7 +272,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}
}
})
}, [state, resumeSession, resumeSessionOauth])
}, [state, resumeSession])
const stateContext = React.useMemo(
() => ({
@@ -308,21 +289,17 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
() => ({
createAccount,
login,
loginOauth,
logoutCurrentAccount,
logoutEveryAccount,
resumeSession,
resumeSessionOauth,
removeAccount,
}),
[
createAccount,
login,
loginOauth,
logoutCurrentAccount,
logoutEveryAccount,
resumeSession,
resumeSessionOauth,
removeAccount,
],
)
+1 -2
View File
@@ -3,7 +3,6 @@ import {type Agent, BSKY_LABELER_DID, BskyAgent} from '@atproto/api'
import {IS_TEST_USER} from '#/lib/constants'
import {configureAdditionalModerationAuthorities} from './additional-moderation-authorities'
import {readLabelers} from './agent-config'
import {type SessionAccount} from './types'
export function configureModerationForGuest() {
// This global mutation is *only* OK because this code is only relevant for testing.
@@ -14,7 +13,7 @@ export function configureModerationForGuest() {
export async function configureModerationForAccount(
agent: Agent | BskyAgent,
account: SessionAccount,
account: {did: string; handle?: string},
) {
// This global mutation is *only* OK because this code is only relevant for testing.
// Don't add any other global behavior here!
+87
View File
@@ -0,0 +1,87 @@
import {Agent} from '@atproto/api'
import {type OutputSchema} from '@atproto/api/dist/client/types/com/atproto/server/getSession'
import {type OAuthSession} from '@atproto/oauth-client-browser'
import {BSKY_SERVICE} from '#/lib/constants'
import {tryFetchGates} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {configureModerationForAccount} from './moderation'
import {BSKY_OAUTH_CLIENT} from './oauth-web-client'
import {type SessionAccount} from './types'
export async function oauthCreateAgent(session: OAuthSession) {
const agent = new OauthBskyAppAgent(session)
const gates = tryFetchGates(session.did, 'prefer-fresh-gates')
const moderation = configureModerationForAccount(agent, session)
return agent.prepare(gates, moderation)
}
export async function oauthResumeSession(account: SessionAccount) {
const session = await BSKY_OAUTH_CLIENT.restore(account.did)
return await oauthCreateAgent(session)
}
export async function oauthAgentAndSessionToSessionAccountOrThrow(
agent: Agent,
session: OAuthSession,
): Promise<SessionAccount> {
const account = await oauthAgentAndSessionToSessionAccount(agent, session)
if (!account) {
throw Error('Expected an active session')
}
return account
}
export async function oauthAgentAndSessionToSessionAccount(
agent: Agent,
session: OAuthSession,
): Promise<SessionAccount | undefined> {
let data: OutputSchema
try {
const res = await agent.com.atproto.server.getSession()
data = res.data
} catch (e: any) {
logger.error(e)
return undefined
}
return {
service: session.serverMetadata.issuer,
did: session.did,
handle: data.handle,
email: data.email,
emailConfirmed: data.emailConfirmed,
emailAuthFactor: data.emailAuthFactor,
active: data.active,
status: data.status,
pdsUrl: session.serverMetadata.issuer,
isSelfHosted: !session.server.issuer.startsWith(BSKY_SERVICE), // TODO: is this entryway?
}
}
export class OauthBskyAppAgent extends Agent {
#session: OAuthSession
#account?: SessionAccount
constructor(session: OAuthSession) {
super(session)
this.#session = session
}
async prepare(gates: Promise<void>, moderation: Promise<void>) {
// we have to await account fetching, since we need a separate call to getSession. this doesn't get returned in the
// OAuthSession itself, unlike the old agent
const account = await oauthAgentAndSessionToSessionAccountOrThrow(
this,
this.#session,
)
this.#account = account
await Promise.all([gates, moderation])
return {account, agent: this}
}
// does nothing, but aligning with BskyAppAgent
dispose() {}
}
+1 -1
View File
@@ -9,7 +9,7 @@ export function createNativeOAuthClient() {
client_name: 'Bluesky Native App (Hailey Demo)',
client_uri: 'https://bsky.hailey.at',
redirect_uris: ['at.hailey.bsky:/auth/native/callback'],
scope: 'atproto transition:generic transition:email',
scope: 'atproto transition:generic transition:email transition:chat.bsky',
token_endpoint_auth_method: 'none',
response_types: ['code'],
grant_types: ['authorization_code', 'refresh_token'],
+1 -1
View File
@@ -14,7 +14,7 @@ export function createWebOAuthClient() {
client_name: 'Bluesky (Hailey Demo)',
client_uri: 'https://bsky.hailey.at',
redirect_uris: ['https://bsky.hailey.at/auth/web/callback'],
scope: 'atproto transition:generic transition:email',
scope: 'atproto transition:generic transition:email transition:chat.bsky',
token_endpoint_auth_method: 'none',
response_types: ['code'],
grant_types: ['authorization_code', 'refresh_token'],
+1 -5
View File
@@ -31,13 +31,10 @@ export type SessionApiContext = {
identifier: string
password: string
authFactorToken?: string | undefined
oauthSession?: OAuthSession
},
logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void>
loginOauth: (
session: OAuthSession,
logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void>
logoutCurrentAccount: (
logContext: LogEvents['account:loggedOut']['logContext'],
) => void
@@ -45,6 +42,5 @@ export type SessionApiContext = {
logContext: LogEvents['account:loggedOut']['logContext'],
) => void
resumeSession: (account: SessionAccount) => Promise<void>
resumeSessionOauth: (account: SessionAccount) => Promise<void>
removeAccount: (account: SessionAccount) => void
}