maybe working login...

This commit is contained in:
Hailey
2025-07-18 00:00:38 -07:00
parent aa2b1f0eec
commit 25d831c12e
6 changed files with 144 additions and 16 deletions
+18 -5
View File
@@ -14,12 +14,15 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useRequestNotificationsPermission} from '#/lib/notifications/notifications' import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
import {useGate} from '#/lib/statsig/statsig'
import {isNetworkError} from '#/lib/strings/errors' import {isNetworkError} from '#/lib/strings/errors'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {createFullHandle} from '#/lib/strings/handles' import {createFullHandle} from '#/lib/strings/handles'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs' import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
import {useSessionApi} from '#/state/session' import {useSessionApi} from '#/state/session'
import {getNativeOAuthClient, getWebOAuthClient} from '#/state/session/oauth'
import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -32,9 +35,6 @@ import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticke
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {FormContainer} from './FormContainer' import {FormContainer} from './FormContainer'
import {useGate} from '#/lib/statsig/statsig'
import {isWeb} from '#/platform/detection'
import {getNativeOAuthClient, getWebOAuthClient} from '#/state/session/oauth'
type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
@@ -61,11 +61,18 @@ export function LoginForm(props: LoginFormProps) {
} }
} }
function OAuthLoginForm({error, initialHandle, onPressBack}: LoginFormProps) { function OAuthLoginForm({
error,
initialHandle,
onPressBack,
setError,
}: LoginFormProps) {
const {_} = useLingui() const {_} = useLingui()
const [isProcessing, setIsProcessing] = React.useState(false) const [isProcessing, setIsProcessing] = React.useState(false)
const identifierValueRef = useRef<string>(initialHandle || '') const identifierValueRef = useRef<string>(initialHandle || '')
const {loginOauth} = useSessionApi()
const onPressNext = async () => { const onPressNext = async () => {
setIsProcessing(true) setIsProcessing(true)
if (isWeb) { if (isWeb) {
@@ -74,8 +81,14 @@ function OAuthLoginForm({error, initialHandle, onPressBack}: LoginFormProps) {
} else { } else {
const client = getNativeOAuthClient() const client = getNativeOAuthClient()
const res = await client.signIn(identifierValueRef.current) const res = await client.signIn(identifierValueRef.current)
// redirect after result if (res.status === 'success') {
await loginOauth(res.session, 'LoginForm')
} else {
logger.error(`Invalid OAuth status: ${res.status}`)
setError(_(msg`An error occurred during authentication.`))
}
} }
setIsProcessing(false)
} }
return ( return (
+63 -4
View File
@@ -1,5 +1,12 @@
import {AtpSessionData, AtpSessionEvent, BskyAgent} from '@atproto/api' 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 {TID} from '@atproto/common-web'
import {type OAuthSession} from '@atproto/oauth-client-browser'
import {networkRetry} from '#/lib/async/retry' import {networkRetry} from '#/lib/async/retry'
import { import {
@@ -19,10 +26,9 @@ import {
configureModerationForAccount, configureModerationForAccount,
configureModerationForGuest, configureModerationForGuest,
} from './moderation' } from './moderation'
import {SessionAccount} from './types'
import {isSessionExpired, isSignupQueued} from './util'
import {BSKY_OAUTH_CLIENT} from './oauth' import {BSKY_OAUTH_CLIENT} from './oauth'
import {ExpoOAuthClient} from 'expo-atproto-auth' import {type SessionAccount} from './types'
import {isSessionExpired, isSignupQueued} from './util'
export function createPublicAgent() { export function createPublicAgent() {
configureModerationForGuest() // Side effect but only relevant for tests configureModerationForGuest() // Side effect but only relevant for tests
@@ -66,6 +72,22 @@ export async function createAgentAndResume(
return agent.prepare(gates, moderation, onSessionChange) 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( export async function createAgentAndLogin(
{ {
service, service,
@@ -185,6 +207,17 @@ export async function createAgentAndCreateAccount(
return agent.prepare(gates, moderation, onSessionChange) 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 { export function agentToSessionAccountOrThrow(agent: BskyAgent): SessionAccount {
const account = agentToSessionAccount(agent) const account = agentToSessionAccount(agent)
if (!account) { if (!account) {
@@ -193,6 +226,32 @@ export function agentToSessionAccountOrThrow(agent: BskyAgent): SessionAccount {
return account 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( export function agentToSessionAccount(
agent: BskyAgent, agent: BskyAgent,
): SessionAccount | undefined { ): SessionAccount | undefined {
+49
View File
@@ -12,6 +12,8 @@ import {
createAgentAndCreateAccount, createAgentAndCreateAccount,
createAgentAndLogin, createAgentAndLogin,
createAgentAndResume, createAgentAndResume,
createAgentOauth,
resumeAgentOauth,
sessionAccountToSession, sessionAccountToSession,
} from './agent' } from './agent'
import {getInitialState, reducer} from './reducer' import {getInitialState, reducer} from './reducer'
@@ -36,9 +38,11 @@ const AgentContext = React.createContext<BskyAgent | null>(null)
const ApiContext = React.createContext<SessionApiContext>({ const ApiContext = React.createContext<SessionApiContext>({
createAccount: async () => {}, createAccount: async () => {},
login: async () => {}, login: async () => {},
loginOauth: async () => {},
logoutCurrentAccount: async () => {}, logoutCurrentAccount: async () => {},
logoutEveryAccount: async () => {}, logoutEveryAccount: async () => {},
resumeSession: async () => {}, resumeSession: async () => {},
resumeSessionOauth: async () => {},
removeAccount: () => {}, removeAccount: () => {},
}) })
@@ -118,6 +122,28 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
[onAgentSessionChange, cancelPendingTask], [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< const logoutCurrentAccount = React.useCallback<
SessionApiContext['logoutEveryAccount'] SessionApiContext['logoutEveryAccount']
>( >(
@@ -182,6 +208,25 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
[onAgentSessionChange, cancelPendingTask], [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']>( const removeAccount = React.useCallback<SessionApiContext['removeAccount']>(
account => { account => {
addSessionDebugLog({ addSessionDebugLog({
@@ -258,17 +303,21 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
() => ({ () => ({
createAccount, createAccount,
login, login,
loginOauth,
logoutCurrentAccount, logoutCurrentAccount,
logoutEveryAccount, logoutEveryAccount,
resumeSession, resumeSession,
resumeSessionOauth,
removeAccount, removeAccount,
}), }),
[ [
createAccount, createAccount,
login, login,
loginOauth,
logoutCurrentAccount, logoutCurrentAccount,
logoutEveryAccount, logoutEveryAccount,
resumeSession, resumeSession,
resumeSessionOauth,
removeAccount, removeAccount,
], ],
) )
+4 -4
View File
@@ -1,9 +1,9 @@
import {BSKY_LABELER_DID, BskyAgent} from '@atproto/api' import {type Agent, BSKY_LABELER_DID, BskyAgent} from '@atproto/api'
import {IS_TEST_USER} from '#/lib/constants' import {IS_TEST_USER} from '#/lib/constants'
import {configureAdditionalModerationAuthorities} from './additional-moderation-authorities' import {configureAdditionalModerationAuthorities} from './additional-moderation-authorities'
import {readLabelers} from './agent-config' import {readLabelers} from './agent-config'
import {SessionAccount} from './types' import {type SessionAccount} from './types'
export function configureModerationForGuest() { export function configureModerationForGuest() {
// This global mutation is *only* OK because this code is only relevant for testing. // This global mutation is *only* OK because this code is only relevant for testing.
@@ -13,7 +13,7 @@ export function configureModerationForGuest() {
} }
export async function configureModerationForAccount( export async function configureModerationForAccount(
agent: BskyAgent, agent: Agent | BskyAgent,
account: SessionAccount, account: SessionAccount,
) { ) {
// This global mutation is *only* OK because this code is only relevant for testing. // This global mutation is *only* OK because this code is only relevant for testing.
@@ -41,7 +41,7 @@ function switchToBskyAppLabeler() {
BskyAgent.configure({appLabelers: [BSKY_LABELER_DID]}) BskyAgent.configure({appLabelers: [BSKY_LABELER_DID]})
} }
async function trySwitchToTestAppLabeler(agent: BskyAgent) { async function trySwitchToTestAppLabeler(agent: Agent | BskyAgent) {
const did = ( const did = (
await agent await agent
.resolveHandle({handle: 'mod-authority.test'}) .resolveHandle({handle: 'mod-authority.test'})
+3 -3
View File
@@ -1,13 +1,13 @@
import {AtpSessionEvent} from '@atproto/api' import {type AtpSessionEvent} from '@atproto/api'
import {createPublicAgent} from './agent' import {createPublicAgent} from './agent'
import {wrapSessionReducerForLogging} from './logging' import {wrapSessionReducerForLogging} from './logging'
import {SessionAccount} from './types' import {type SessionAccount} from './types'
// A hack so that the reducer can't read anything from the agent. // A hack so that the reducer can't read anything from the agent.
// From the reducer's point of view, it should be a completely opaque object. // From the reducer's point of view, it should be a completely opaque object.
type OpaqueBskyAgent = { type OpaqueBskyAgent = {
readonly service: URL readonly service?: URL | undefined // TODO: do we need service at all? what are implications if we rm?
readonly api: unknown readonly api: unknown
readonly app: unknown readonly app: unknown
readonly com: unknown readonly com: unknown
+7
View File
@@ -1,3 +1,5 @@
import {type OAuthSession} from '@atproto/oauth-client-browser'
import {type LogEvents} from '#/lib/statsig/statsig' import {type LogEvents} from '#/lib/statsig/statsig'
import {type PersistedAccount} from '#/state/persisted' import {type PersistedAccount} from '#/state/persisted'
@@ -32,6 +34,10 @@ export type SessionApiContext = {
}, },
logContext: LogEvents['account:loggedIn']['logContext'], logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void> ) => Promise<void>
loginOauth: (
session: OAuthSession,
logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void>
logoutCurrentAccount: ( logoutCurrentAccount: (
logContext: LogEvents['account:loggedOut']['logContext'], logContext: LogEvents['account:loggedOut']['logContext'],
) => void ) => void
@@ -39,5 +45,6 @@ export type SessionApiContext = {
logContext: LogEvents['account:loggedOut']['logContext'], logContext: LogEvents['account:loggedOut']['logContext'],
) => void ) => void
resumeSession: (account: SessionAccount) => Promise<void> resumeSession: (account: SessionAccount) => Promise<void>
resumeSessionOauth: (account: SessionAccount) => Promise<void>
removeAccount: (account: SessionAccount) => void removeAccount: (account: SessionAccount) => void
} }