Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

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>

authored by samuel.fm

Eric Bailey and committed by
GitHub
8fdcc3ee b4938bc9

+98 -65
+22 -43
src/components/dialogs/EmailDialog/data/useAccountEmailState.ts
··· 1 - import {useCallback, useEffect, useState} from 'react' 2 - import {useQuery, useQueryClient} from '@tanstack/react-query' 1 + import {useEffect, useMemo, useState} from 'react' 2 + import {useQuery} from '@tanstack/react-query' 3 3 4 - import {useAgent} from '#/state/session' 4 + import {useAgent, useSessionApi} from '#/state/session' 5 5 import {emitEmailVerified} from '#/components/dialogs/EmailDialog/events' 6 6 7 7 export type AccountEmailState = { ··· 11 11 12 12 export const accountEmailStateQueryKey = ['accountEmailState'] as const 13 13 14 - export function useInvalidateAccountEmailState() { 15 - const qc = useQueryClient() 16 - 17 - return useCallback(() => { 18 - return qc.invalidateQueries({ 19 - queryKey: accountEmailStateQueryKey, 20 - }) 21 - }, [qc]) 22 - } 23 - 24 - export function useUpdateAccountEmailStateQueryCache() { 25 - const qc = useQueryClient() 26 - 27 - return useCallback( 28 - (data: AccountEmailState) => { 29 - return qc.setQueriesData( 30 - { 31 - queryKey: accountEmailStateQueryKey, 32 - }, 33 - data, 34 - ) 35 - }, 36 - [qc], 37 - ) 38 - } 39 - 40 14 export function useAccountEmailState() { 41 15 const agent = useAgent() 16 + const {partialRefreshSession} = useSessionApi() 42 17 const [prevIsEmailVerified, setPrevEmailIsVerified] = useState( 43 18 !!agent.session?.emailConfirmed, 44 19 ) 45 - const fallbackData: AccountEmailState = { 46 - isEmailVerified: !!agent.session?.emailConfirmed, 47 - email2FAEnabled: !!agent.session?.emailAuthFactor, 48 - } 49 - const query = useQuery<AccountEmailState>({ 20 + const state: AccountEmailState = useMemo( 21 + () => ({ 22 + isEmailVerified: !!agent.session?.emailConfirmed, 23 + email2FAEnabled: !!agent.session?.emailAuthFactor, 24 + }), 25 + [agent.session], 26 + ) 27 + 28 + /** 29 + * Only here to refetch on focus, when necessary 30 + */ 31 + useQuery({ 50 32 enabled: !!agent.session, 51 - refetchOnWindowFocus: true, 33 + /** 34 + * Only refetch if the email verification s incomplete. 35 + */ 36 + refetchOnWindowFocus: !prevIsEmailVerified, 52 37 queryKey: accountEmailStateQueryKey, 53 38 queryFn: async () => { 54 - // will also trigger updates to `#/state/session` data 55 - const {data} = await agent.resumeSession(agent.session!) 56 - return { 57 - isEmailVerified: !!data.emailConfirmed, 58 - email2FAEnabled: !!data.emailAuthFactor, 59 - } 39 + await partialRefreshSession() 40 + return null 60 41 }, 61 42 }) 62 - 63 - const state = query.data ?? fallbackData 64 43 65 44 /* 66 45 * This will emit `n` times for each instance of this hook. So the listeners
+2 -8
src/components/dialogs/EmailDialog/data/useConfirmEmail.ts
··· 1 1 import {useMutation} from '@tanstack/react-query' 2 2 3 3 import {useAgent, useSession} from '#/state/session' 4 - import {useUpdateAccountEmailStateQueryCache} from '#/components/dialogs/EmailDialog/data/useAccountEmailState' 5 4 6 5 export function useConfirmEmail() { 7 6 const agent = useAgent() 8 7 const {currentAccount} = useSession() 9 - const updateAccountEmailStateQueryCache = 10 - useUpdateAccountEmailStateQueryCache() 11 8 12 9 return useMutation({ 13 10 mutationFn: async ({token}: {token: string}) => { ··· 19 16 email: currentAccount.email, 20 17 token: token.trim(), 21 18 }) 22 - const {data} = await agent.resumeSession(agent.session!) 23 - updateAccountEmailStateQueryCache({ 24 - isEmailVerified: !!data.emailConfirmed, 25 - email2FAEnabled: !!data.emailAuthFactor, 26 - }) 19 + // will update session state at root of app 20 + await agent.resumeSession(agent.session!) 27 21 }, 28 22 }) 29 23 }
+2 -8
src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts
··· 1 1 import {useMutation} from '@tanstack/react-query' 2 2 3 3 import {useAgent, useSession} from '#/state/session' 4 - import {useUpdateAccountEmailStateQueryCache} from '#/components/dialogs/EmailDialog/data/useAccountEmailState' 5 4 6 5 export function useManageEmail2FA() { 7 6 const agent = useAgent() 8 7 const {currentAccount} = useSession() 9 - const updateAccountEmailStateQueryCache = 10 - useUpdateAccountEmailStateQueryCache() 11 8 12 9 return useMutation({ 13 10 mutationFn: async ({ ··· 25 22 emailAuthFactor: enabled, 26 23 token, 27 24 }) 28 - const {data} = await agent.resumeSession(agent.session!) 29 - updateAccountEmailStateQueryCache({ 30 - isEmailVerified: !!data.emailConfirmed, 31 - email2FAEnabled: !!data.emailAuthFactor, 32 - }) 25 + // will update session state at root of app 26 + await agent.resumeSession(agent.session!) 33 27 }, 34 28 }) 35 29 }
+3 -3
src/lib/api/feed/home.ts
··· 1 - import {AppBskyFeedDefs, BskyAgent} from '@atproto/api' 1 + import {type AppBskyFeedDefs, type BskyAgent} from '@atproto/api' 2 2 3 3 import {PROD_DEFAULT_FEED} from '#/lib/constants' 4 4 import {CustomFeedAPI} from './custom' 5 5 import {FollowingFeedAPI} from './following' 6 - import {FeedAPI, FeedAPIResponse} from './types' 6 + import {type FeedAPI, type FeedAPIResponse} from './types' 7 7 8 8 // HACK 9 9 // the feed API does not include any facilities for passing down ··· 93 93 } 94 94 } 95 95 96 - if (this.usingDiscover) { 96 + if (this.usingDiscover && !__DEV__) { 97 97 const res = await this.discover.fetch({cursor, limit}) 98 98 returnCursor = res.cursor 99 99 posts = posts.concat(res.feed)
+21 -1
src/state/session/index.tsx
··· 40 40 logoutEveryAccount: async () => {}, 41 41 resumeSession: async () => {}, 42 42 removeAccount: () => {}, 43 + partialRefreshSession: async () => {}, 43 44 }) 44 45 45 46 export function Provider({children}: React.PropsWithChildren<{}>) { ··· 119 120 ) 120 121 121 122 const logoutCurrentAccount = React.useCallback< 122 - SessionApiContext['logoutEveryAccount'] 123 + SessionApiContext['logoutCurrentAccount'] 123 124 >( 124 125 logContext => { 125 126 addSessionDebugLog({type: 'method:start', method: 'logout'}) ··· 182 183 [onAgentSessionChange, cancelPendingTask], 183 184 ) 184 185 186 + const partialRefreshSession = React.useCallback< 187 + SessionApiContext['partialRefreshSession'] 188 + >(async () => { 189 + const agent = state.currentAgentState.agent as BskyAppAgent 190 + const signal = cancelPendingTask() 191 + const {data} = await agent.com.atproto.server.getSession() 192 + if (signal.aborted) return 193 + dispatch({ 194 + type: 'partial-refresh-session', 195 + accountDid: agent.session!.did, 196 + patch: { 197 + emailConfirmed: data.emailConfirmed, 198 + emailAuthFactor: data.emailAuthFactor, 199 + }, 200 + }) 201 + }, [state, cancelPendingTask]) 202 + 185 203 const removeAccount = React.useCallback<SessionApiContext['removeAccount']>( 186 204 account => { 187 205 addSessionDebugLog({ ··· 262 280 logoutEveryAccount, 263 281 resumeSession, 264 282 removeAccount, 283 + partialRefreshSession, 265 284 }), 266 285 [ 267 286 createAccount, ··· 270 289 logoutEveryAccount, 271 290 resumeSession, 272 291 removeAccount, 292 + partialRefreshSession, 273 293 ], 274 294 ) 275 295
+40 -2
src/state/session/reducer.ts
··· 1 - import {AtpSessionEvent} from '@atproto/api' 1 + import {type AtpSessionEvent, type BskyAgent} from '@atproto/api' 2 2 3 3 import {createPublicAgent} from './agent' 4 4 import {wrapSessionReducerForLogging} from './logging' 5 - import {SessionAccount} from './types' 5 + import {type SessionAccount} from './types' 6 6 7 7 // A hack so that the reducer can't read anything from the agent. 8 8 // From the reducer's point of view, it should be a completely opaque object. ··· 51 51 type: 'synced-accounts' 52 52 syncedAccounts: SessionAccount[] 53 53 syncedCurrentDid: string | undefined 54 + } 55 + | { 56 + type: 'partial-refresh-session' 57 + accountDid: string 58 + patch: Pick<SessionAccount, 'emailConfirmed' | 'emailAuthFactor'> 54 59 } 55 60 56 61 function createPublicAgentState(): AgentState { ··· 178 183 ? state.currentAgentState 179 184 : createPublicAgentState(), // Log out if different user. 180 185 needsPersist: false, // Synced from another tab. Don't persist to avoid cycles. 186 + } 187 + } 188 + case 'partial-refresh-session': { 189 + const {accountDid, patch} = action 190 + const agent = state.currentAgentState.agent as BskyAgent 191 + 192 + /* 193 + * Only mutating values that are safe. Be very careful with this. 194 + */ 195 + if (agent.session) { 196 + agent.session.emailConfirmed = 197 + patch.emailConfirmed ?? agent.session.emailConfirmed 198 + agent.session.emailAuthFactor = 199 + patch.emailAuthFactor ?? agent.session.emailAuthFactor 200 + } 201 + 202 + return { 203 + ...state, 204 + currentAgentState: { 205 + ...state.currentAgentState, 206 + agent, 207 + }, 208 + accounts: state.accounts.map(a => { 209 + if (a.did === accountDid) { 210 + return { 211 + ...a, 212 + emailConfirmed: patch.emailConfirmed ?? a.emailConfirmed, 213 + emailAuthFactor: patch.emailAuthFactor ?? a.emailAuthFactor, 214 + } 215 + } 216 + return a 217 + }), 218 + needsPersist: true, 181 219 } 182 220 } 183 221 }
+8
src/state/session/types.ts
··· 40 40 ) => void 41 41 resumeSession: (account: SessionAccount) => Promise<void> 42 42 removeAccount: (account: SessionAccount) => void 43 + /** 44 + * Calls `getSession` and updates select fields on the current account and 45 + * `BskyAgent`. This is an alternative to `resumeSession`, which updates 46 + * current account/agent using the `persistSessionHandler`, but is more load 47 + * bearing. This patches in updates without causing any side effects via 48 + * `persistSessionHandler`. 49 + */ 50 + partialRefreshSession: () => Promise<void> 43 51 }