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 {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
import {useGate} from '#/lib/statsig/statsig'
import {isNetworkError} from '#/lib/strings/errors'
import {cleanError} from '#/lib/strings/errors'
import {createFullHandle} from '#/lib/strings/handles'
import {logger} from '#/logger'
import {isWeb} from '#/platform/detection'
import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
import {useSessionApi} from '#/state/session'
import {getNativeOAuthClient, getWebOAuthClient} from '#/state/session/oauth'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {atoms as a, useTheme} from '#/alf'
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 {Text} from '#/components/Typography'
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
@@ -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 [isProcessing, setIsProcessing] = React.useState(false)
const identifierValueRef = useRef<string>(initialHandle || '')
const {loginOauth} = useSessionApi()
const onPressNext = async () => {
setIsProcessing(true)
if (isWeb) {
@@ -74,8 +81,14 @@ function OAuthLoginForm({error, initialHandle, onPressBack}: LoginFormProps) {
} else {
const client = getNativeOAuthClient()
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 (
+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 {type OAuthSession} from '@atproto/oauth-client-browser'
import {networkRetry} from '#/lib/async/retry'
import {
@@ -19,10 +26,9 @@ import {
configureModerationForAccount,
configureModerationForGuest,
} from './moderation'
import {SessionAccount} from './types'
import {isSessionExpired, isSignupQueued} from './util'
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() {
configureModerationForGuest() // Side effect but only relevant for tests
@@ -66,6 +72,22 @@ 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,
@@ -185,6 +207,17 @@ 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) {
@@ -193,6 +226,32 @@ 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 {
+49
View File
@@ -12,6 +12,8 @@ import {
createAgentAndCreateAccount,
createAgentAndLogin,
createAgentAndResume,
createAgentOauth,
resumeAgentOauth,
sessionAccountToSession,
} from './agent'
import {getInitialState, reducer} from './reducer'
@@ -36,9 +38,11 @@ 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: () => {},
})
@@ -118,6 +122,28 @@ 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']
>(
@@ -182,6 +208,25 @@ 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({
@@ -258,17 +303,21 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
() => ({
createAccount,
login,
loginOauth,
logoutCurrentAccount,
logoutEveryAccount,
resumeSession,
resumeSessionOauth,
removeAccount,
}),
[
createAccount,
login,
loginOauth,
logoutCurrentAccount,
logoutEveryAccount,
resumeSession,
resumeSessionOauth,
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 {configureAdditionalModerationAuthorities} from './additional-moderation-authorities'
import {readLabelers} from './agent-config'
import {SessionAccount} from './types'
import {type SessionAccount} from './types'
export function configureModerationForGuest() {
// 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(
agent: BskyAgent,
agent: Agent | BskyAgent,
account: SessionAccount,
) {
// 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]})
}
async function trySwitchToTestAppLabeler(agent: BskyAgent) {
async function trySwitchToTestAppLabeler(agent: Agent | BskyAgent) {
const did = (
await agent
.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 {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.
// From the reducer's point of view, it should be a completely opaque object.
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 app: 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 PersistedAccount} from '#/state/persisted'
@@ -32,6 +34,10 @@ export type SessionApiContext = {
},
logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void>
loginOauth: (
session: OAuthSession,
logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void>
logoutCurrentAccount: (
logContext: LogEvents['account:loggedOut']['logContext'],
) => void
@@ -39,5 +45,6 @@ export type SessionApiContext = {
logContext: LogEvents['account:loggedOut']['logContext'],
) => void
resumeSession: (account: SessionAccount) => Promise<void>
resumeSessionOauth: (account: SessionAccount) => Promise<void>
removeAccount: (account: SessionAccount) => void
}