Replace resumeSession with getSession in the email check (#8670)
* replace resumeSession with getSession * copy lil type tweak from the other PR * Add partialRefreshSession to session API context, use session state to infer state further down tree * Review --------- Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import {useCallback, useEffect, useState} from 'react'
|
import {useEffect, useMemo, useState} from 'react'
|
||||||
import {useQuery, useQueryClient} from '@tanstack/react-query'
|
import {useQuery} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent, useSessionApi} from '#/state/session'
|
||||||
import {emitEmailVerified} from '#/components/dialogs/EmailDialog/events'
|
import {emitEmailVerified} from '#/components/dialogs/EmailDialog/events'
|
||||||
|
|
||||||
export type AccountEmailState = {
|
export type AccountEmailState = {
|
||||||
@@ -11,57 +11,36 @@ export type AccountEmailState = {
|
|||||||
|
|
||||||
export const accountEmailStateQueryKey = ['accountEmailState'] as const
|
export const accountEmailStateQueryKey = ['accountEmailState'] as const
|
||||||
|
|
||||||
export function useInvalidateAccountEmailState() {
|
|
||||||
const qc = useQueryClient()
|
|
||||||
|
|
||||||
return useCallback(() => {
|
|
||||||
return qc.invalidateQueries({
|
|
||||||
queryKey: accountEmailStateQueryKey,
|
|
||||||
})
|
|
||||||
}, [qc])
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useUpdateAccountEmailStateQueryCache() {
|
|
||||||
const qc = useQueryClient()
|
|
||||||
|
|
||||||
return useCallback(
|
|
||||||
(data: AccountEmailState) => {
|
|
||||||
return qc.setQueriesData(
|
|
||||||
{
|
|
||||||
queryKey: accountEmailStateQueryKey,
|
|
||||||
},
|
|
||||||
data,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
[qc],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useAccountEmailState() {
|
export function useAccountEmailState() {
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
|
const {partialRefreshSession} = useSessionApi()
|
||||||
const [prevIsEmailVerified, setPrevEmailIsVerified] = useState(
|
const [prevIsEmailVerified, setPrevEmailIsVerified] = useState(
|
||||||
!!agent.session?.emailConfirmed,
|
!!agent.session?.emailConfirmed,
|
||||||
)
|
)
|
||||||
const fallbackData: AccountEmailState = {
|
const state: AccountEmailState = useMemo(
|
||||||
isEmailVerified: !!agent.session?.emailConfirmed,
|
() => ({
|
||||||
email2FAEnabled: !!agent.session?.emailAuthFactor,
|
isEmailVerified: !!agent.session?.emailConfirmed,
|
||||||
}
|
email2FAEnabled: !!agent.session?.emailAuthFactor,
|
||||||
const query = useQuery<AccountEmailState>({
|
}),
|
||||||
|
[agent.session],
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only here to refetch on focus, when necessary
|
||||||
|
*/
|
||||||
|
useQuery({
|
||||||
enabled: !!agent.session,
|
enabled: !!agent.session,
|
||||||
refetchOnWindowFocus: true,
|
/**
|
||||||
|
* Only refetch if the email verification s incomplete.
|
||||||
|
*/
|
||||||
|
refetchOnWindowFocus: !prevIsEmailVerified,
|
||||||
queryKey: accountEmailStateQueryKey,
|
queryKey: accountEmailStateQueryKey,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
// will also trigger updates to `#/state/session` data
|
await partialRefreshSession()
|
||||||
const {data} = await agent.resumeSession(agent.session!)
|
return null
|
||||||
return {
|
|
||||||
isEmailVerified: !!data.emailConfirmed,
|
|
||||||
email2FAEnabled: !!data.emailAuthFactor,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const state = query.data ?? fallbackData
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This will emit `n` times for each instance of this hook. So the listeners
|
* This will emit `n` times for each instance of this hook. So the listeners
|
||||||
* all use `once` to prevent multiple handlers firing.
|
* all use `once` to prevent multiple handlers firing.
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import {useMutation} from '@tanstack/react-query'
|
import {useMutation} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
import {useUpdateAccountEmailStateQueryCache} from '#/components/dialogs/EmailDialog/data/useAccountEmailState'
|
|
||||||
|
|
||||||
export function useConfirmEmail() {
|
export function useConfirmEmail() {
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const updateAccountEmailStateQueryCache =
|
|
||||||
useUpdateAccountEmailStateQueryCache()
|
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({token}: {token: string}) => {
|
mutationFn: async ({token}: {token: string}) => {
|
||||||
@@ -19,11 +16,8 @@ export function useConfirmEmail() {
|
|||||||
email: currentAccount.email,
|
email: currentAccount.email,
|
||||||
token: token.trim(),
|
token: token.trim(),
|
||||||
})
|
})
|
||||||
const {data} = await agent.resumeSession(agent.session!)
|
// will update session state at root of app
|
||||||
updateAccountEmailStateQueryCache({
|
await agent.resumeSession(agent.session!)
|
||||||
isEmailVerified: !!data.emailConfirmed,
|
|
||||||
email2FAEnabled: !!data.emailAuthFactor,
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import {useMutation} from '@tanstack/react-query'
|
import {useMutation} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
import {useUpdateAccountEmailStateQueryCache} from '#/components/dialogs/EmailDialog/data/useAccountEmailState'
|
|
||||||
|
|
||||||
export function useManageEmail2FA() {
|
export function useManageEmail2FA() {
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const updateAccountEmailStateQueryCache =
|
|
||||||
useUpdateAccountEmailStateQueryCache()
|
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({
|
mutationFn: async ({
|
||||||
@@ -25,11 +22,8 @@ export function useManageEmail2FA() {
|
|||||||
emailAuthFactor: enabled,
|
emailAuthFactor: enabled,
|
||||||
token,
|
token,
|
||||||
})
|
})
|
||||||
const {data} = await agent.resumeSession(agent.session!)
|
// will update session state at root of app
|
||||||
updateAccountEmailStateQueryCache({
|
await agent.resumeSession(agent.session!)
|
||||||
isEmailVerified: !!data.emailConfirmed,
|
|
||||||
email2FAEnabled: !!data.emailAuthFactor,
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {AppBskyFeedDefs, BskyAgent} from '@atproto/api'
|
import {type AppBskyFeedDefs, type BskyAgent} from '@atproto/api'
|
||||||
|
|
||||||
import {PROD_DEFAULT_FEED} from '#/lib/constants'
|
import {PROD_DEFAULT_FEED} from '#/lib/constants'
|
||||||
import {CustomFeedAPI} from './custom'
|
import {CustomFeedAPI} from './custom'
|
||||||
import {FollowingFeedAPI} from './following'
|
import {FollowingFeedAPI} from './following'
|
||||||
import {FeedAPI, FeedAPIResponse} from './types'
|
import {type FeedAPI, type FeedAPIResponse} from './types'
|
||||||
|
|
||||||
// HACK
|
// HACK
|
||||||
// the feed API does not include any facilities for passing down
|
// the feed API does not include any facilities for passing down
|
||||||
@@ -93,7 +93,7 @@ export class HomeFeedAPI implements FeedAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.usingDiscover) {
|
if (this.usingDiscover && !__DEV__) {
|
||||||
const res = await this.discover.fetch({cursor, limit})
|
const res = await this.discover.fetch({cursor, limit})
|
||||||
returnCursor = res.cursor
|
returnCursor = res.cursor
|
||||||
posts = posts.concat(res.feed)
|
posts = posts.concat(res.feed)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const ApiContext = React.createContext<SessionApiContext>({
|
|||||||
logoutEveryAccount: async () => {},
|
logoutEveryAccount: async () => {},
|
||||||
resumeSession: async () => {},
|
resumeSession: async () => {},
|
||||||
removeAccount: () => {},
|
removeAccount: () => {},
|
||||||
|
partialRefreshSession: async () => {},
|
||||||
})
|
})
|
||||||
|
|
||||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
@@ -119,7 +120,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const logoutCurrentAccount = React.useCallback<
|
const logoutCurrentAccount = React.useCallback<
|
||||||
SessionApiContext['logoutEveryAccount']
|
SessionApiContext['logoutCurrentAccount']
|
||||||
>(
|
>(
|
||||||
logContext => {
|
logContext => {
|
||||||
addSessionDebugLog({type: 'method:start', method: 'logout'})
|
addSessionDebugLog({type: 'method:start', method: 'logout'})
|
||||||
@@ -182,6 +183,23 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
[onAgentSessionChange, cancelPendingTask],
|
[onAgentSessionChange, cancelPendingTask],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const partialRefreshSession = React.useCallback<
|
||||||
|
SessionApiContext['partialRefreshSession']
|
||||||
|
>(async () => {
|
||||||
|
const agent = state.currentAgentState.agent as BskyAppAgent
|
||||||
|
const signal = cancelPendingTask()
|
||||||
|
const {data} = await agent.com.atproto.server.getSession()
|
||||||
|
if (signal.aborted) return
|
||||||
|
dispatch({
|
||||||
|
type: 'partial-refresh-session',
|
||||||
|
accountDid: agent.session!.did,
|
||||||
|
patch: {
|
||||||
|
emailConfirmed: data.emailConfirmed,
|
||||||
|
emailAuthFactor: data.emailAuthFactor,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, [state, cancelPendingTask])
|
||||||
|
|
||||||
const removeAccount = React.useCallback<SessionApiContext['removeAccount']>(
|
const removeAccount = React.useCallback<SessionApiContext['removeAccount']>(
|
||||||
account => {
|
account => {
|
||||||
addSessionDebugLog({
|
addSessionDebugLog({
|
||||||
@@ -262,6 +280,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
logoutEveryAccount,
|
logoutEveryAccount,
|
||||||
resumeSession,
|
resumeSession,
|
||||||
removeAccount,
|
removeAccount,
|
||||||
|
partialRefreshSession,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
createAccount,
|
createAccount,
|
||||||
@@ -270,6 +289,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||||||
logoutEveryAccount,
|
logoutEveryAccount,
|
||||||
resumeSession,
|
resumeSession,
|
||||||
removeAccount,
|
removeAccount,
|
||||||
|
partialRefreshSession,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {AtpSessionEvent} from '@atproto/api'
|
import {type AtpSessionEvent, type BskyAgent} 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.
|
||||||
@@ -52,6 +52,11 @@ export type Action =
|
|||||||
syncedAccounts: SessionAccount[]
|
syncedAccounts: SessionAccount[]
|
||||||
syncedCurrentDid: string | undefined
|
syncedCurrentDid: string | undefined
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'partial-refresh-session'
|
||||||
|
accountDid: string
|
||||||
|
patch: Pick<SessionAccount, 'emailConfirmed' | 'emailAuthFactor'>
|
||||||
|
}
|
||||||
|
|
||||||
function createPublicAgentState(): AgentState {
|
function createPublicAgentState(): AgentState {
|
||||||
return {
|
return {
|
||||||
@@ -180,6 +185,39 @@ let reducer = (state: State, action: Action): State => {
|
|||||||
needsPersist: false, // Synced from another tab. Don't persist to avoid cycles.
|
needsPersist: false, // Synced from another tab. Don't persist to avoid cycles.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case 'partial-refresh-session': {
|
||||||
|
const {accountDid, patch} = action
|
||||||
|
const agent = state.currentAgentState.agent as BskyAgent
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Only mutating values that are safe. Be very careful with this.
|
||||||
|
*/
|
||||||
|
if (agent.session) {
|
||||||
|
agent.session.emailConfirmed =
|
||||||
|
patch.emailConfirmed ?? agent.session.emailConfirmed
|
||||||
|
agent.session.emailAuthFactor =
|
||||||
|
patch.emailAuthFactor ?? agent.session.emailAuthFactor
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
currentAgentState: {
|
||||||
|
...state.currentAgentState,
|
||||||
|
agent,
|
||||||
|
},
|
||||||
|
accounts: state.accounts.map(a => {
|
||||||
|
if (a.did === accountDid) {
|
||||||
|
return {
|
||||||
|
...a,
|
||||||
|
emailConfirmed: patch.emailConfirmed ?? a.emailConfirmed,
|
||||||
|
emailAuthFactor: patch.emailAuthFactor ?? a.emailAuthFactor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}),
|
||||||
|
needsPersist: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reducer = wrapSessionReducerForLogging(reducer)
|
reducer = wrapSessionReducerForLogging(reducer)
|
||||||
|
|||||||
@@ -40,4 +40,12 @@ export type SessionApiContext = {
|
|||||||
) => void
|
) => void
|
||||||
resumeSession: (account: SessionAccount) => Promise<void>
|
resumeSession: (account: SessionAccount) => Promise<void>
|
||||||
removeAccount: (account: SessionAccount) => void
|
removeAccount: (account: SessionAccount) => void
|
||||||
|
/**
|
||||||
|
* Calls `getSession` and updates select fields on the current account and
|
||||||
|
* `BskyAgent`. This is an alternative to `resumeSession`, which updates
|
||||||
|
* current account/agent using the `persistSessionHandler`, but is more load
|
||||||
|
* bearing. This patches in updates without causing any side effects via
|
||||||
|
* `persistSessionHandler`.
|
||||||
|
*/
|
||||||
|
partialRefreshSession: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user