First pass at a session handler (#1850)
* First pass at a session handler * TODOs * Fix recursion * Couple more things * Add back resume session concept * Handle ready * Cleanup of initial loading states * Handle init failure * Cleanup * Remove account * Add updateCurrentAccount * Remove log * Cleanup * Integrate removeAccount * Add hasSession * Add to App.native, harden migration * Use effect to persist datazio/stable
parent
664e7a91a9
commit
625cbc435f
|
@ -26,6 +26,12 @@ import {Provider as ModalStateProvider} from 'state/modals'
|
||||||
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
|
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
|
||||||
import {Provider as InvitesStateProvider} from 'state/invites'
|
import {Provider as InvitesStateProvider} from 'state/invites'
|
||||||
import {Provider as PrefsStateProvider} from 'state/preferences'
|
import {Provider as PrefsStateProvider} from 'state/preferences'
|
||||||
|
import {
|
||||||
|
Provider as SessionProvider,
|
||||||
|
useSession,
|
||||||
|
useSessionApi,
|
||||||
|
} from 'state/session'
|
||||||
|
import * as persisted from '#/state/persisted'
|
||||||
import {i18n} from '@lingui/core'
|
import {i18n} from '@lingui/core'
|
||||||
import {I18nProvider} from '@lingui/react'
|
import {I18nProvider} from '@lingui/react'
|
||||||
import {messages} from './locale/locales/en/messages'
|
import {messages} from './locale/locales/en/messages'
|
||||||
|
@ -36,6 +42,8 @@ SplashScreen.preventAutoHideAsync()
|
||||||
|
|
||||||
const InnerApp = observer(function AppImpl() {
|
const InnerApp = observer(function AppImpl() {
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
|
const {isInitialLoad} = useSession()
|
||||||
|
const {resumeSession} = useSessionApi()
|
||||||
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
|
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
)
|
)
|
||||||
|
@ -52,10 +60,17 @@ const InnerApp = observer(function AppImpl() {
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const account = persisted.get('session').currentAccount
|
||||||
|
resumeSession(account)
|
||||||
|
}, [resumeSession])
|
||||||
|
|
||||||
// show nothing prior to init
|
// show nothing prior to init
|
||||||
if (!rootStore) {
|
if (!rootStore || isInitialLoad) {
|
||||||
|
// TODO add a loading state
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ThemeProvider theme={colorMode}>
|
<ThemeProvider theme={colorMode}>
|
||||||
|
@ -88,6 +103,7 @@ function App() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<SessionProvider>
|
||||||
<ShellStateProvider>
|
<ShellStateProvider>
|
||||||
<PrefsStateProvider>
|
<PrefsStateProvider>
|
||||||
<MutedThreadsProvider>
|
<MutedThreadsProvider>
|
||||||
|
@ -99,6 +115,7 @@ function App() {
|
||||||
</MutedThreadsProvider>
|
</MutedThreadsProvider>
|
||||||
</PrefsStateProvider>
|
</PrefsStateProvider>
|
||||||
</ShellStateProvider>
|
</ShellStateProvider>
|
||||||
|
</SessionProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,8 +24,16 @@ import {Provider as ModalStateProvider} from 'state/modals'
|
||||||
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
|
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
|
||||||
import {Provider as InvitesStateProvider} from 'state/invites'
|
import {Provider as InvitesStateProvider} from 'state/invites'
|
||||||
import {Provider as PrefsStateProvider} from 'state/preferences'
|
import {Provider as PrefsStateProvider} from 'state/preferences'
|
||||||
|
import {
|
||||||
|
Provider as SessionProvider,
|
||||||
|
useSession,
|
||||||
|
useSessionApi,
|
||||||
|
} from 'state/session'
|
||||||
|
import * as persisted from '#/state/persisted'
|
||||||
|
|
||||||
const InnerApp = observer(function AppImpl() {
|
const InnerApp = observer(function AppImpl() {
|
||||||
|
const {isInitialLoad} = useSession()
|
||||||
|
const {resumeSession} = useSessionApi()
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
|
const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
|
@ -38,10 +46,16 @@ const InnerApp = observer(function AppImpl() {
|
||||||
analytics.init(store)
|
analytics.init(store)
|
||||||
})
|
})
|
||||||
dynamicActivate(defaultLocale) // async import of locale data
|
dynamicActivate(defaultLocale) // async import of locale data
|
||||||
}, [])
|
}, [resumeSession])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const account = persisted.get('session').currentAccount
|
||||||
|
resumeSession(account)
|
||||||
|
}, [resumeSession])
|
||||||
|
|
||||||
// show nothing prior to init
|
// show nothing prior to init
|
||||||
if (!rootStore) {
|
if (!rootStore || isInitialLoad) {
|
||||||
|
// TODO add a loading state
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,6 +91,7 @@ function App() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<SessionProvider>
|
||||||
<ShellStateProvider>
|
<ShellStateProvider>
|
||||||
<PrefsStateProvider>
|
<PrefsStateProvider>
|
||||||
<MutedThreadsProvider>
|
<MutedThreadsProvider>
|
||||||
|
@ -88,6 +103,7 @@ function App() {
|
||||||
</MutedThreadsProvider>
|
</MutedThreadsProvider>
|
||||||
</PrefsStateProvider>
|
</PrefsStateProvider>
|
||||||
</ShellStateProvider>
|
</ShellStateProvider>
|
||||||
|
</SessionProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {migrate} from '#/state/persisted/legacy'
|
||||||
import * as store from '#/state/persisted/store'
|
import * as store from '#/state/persisted/store'
|
||||||
import BroadcastChannel from '#/state/persisted/broadcast'
|
import BroadcastChannel from '#/state/persisted/broadcast'
|
||||||
|
|
||||||
export type {Schema} from '#/state/persisted/schema'
|
export type {Schema, PersistedAccount} from '#/state/persisted/schema'
|
||||||
export {defaults} from '#/state/persisted/schema'
|
export {defaults} from '#/state/persisted/schema'
|
||||||
|
|
||||||
const broadcast = new BroadcastChannel('BSKY_BROADCAST_CHANNEL')
|
const broadcast = new BroadcastChannel('BSKY_BROADCAST_CHANNEL')
|
||||||
|
@ -50,7 +50,9 @@ export async function write<K extends keyof Schema>(
|
||||||
await store.write(_state)
|
await store.write(_state)
|
||||||
// must happen on next tick, otherwise the tab will read stale storage data
|
// must happen on next tick, otherwise the tab will read stale storage data
|
||||||
setTimeout(() => broadcast.postMessage({event: UPDATE_EVENT}), 0)
|
setTimeout(() => broadcast.postMessage({event: UPDATE_EVENT}), 0)
|
||||||
logger.debug(`persisted state: wrote root state to storage`)
|
logger.debug(`persisted state: wrote root state to storage`, {
|
||||||
|
updatedKey: key,
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(`persisted state: failed writing root state to storage`, {
|
logger.error(`persisted state: failed writing root state to storage`, {
|
||||||
error: e,
|
error: e,
|
||||||
|
|
|
@ -66,43 +66,45 @@ type LegacySchema = {
|
||||||
|
|
||||||
const DEPRECATED_ROOT_STATE_STORAGE_KEY = 'root'
|
const DEPRECATED_ROOT_STATE_STORAGE_KEY = 'root'
|
||||||
|
|
||||||
export function transform(legacy: LegacySchema): Schema {
|
// TODO remove, assume that partial data may be here during our refactor
|
||||||
|
export function transform(legacy: Partial<LegacySchema>): Schema {
|
||||||
return {
|
return {
|
||||||
colorMode: legacy.shell?.colorMode || defaults.colorMode,
|
colorMode: legacy.shell?.colorMode || defaults.colorMode,
|
||||||
session: {
|
session: {
|
||||||
accounts: legacy.session.accounts || defaults.session.accounts,
|
accounts: legacy.session?.accounts || defaults.session.accounts,
|
||||||
currentAccount:
|
currentAccount:
|
||||||
legacy.session.accounts.find(a => a.did === legacy.session.data.did) ||
|
legacy.session?.accounts?.find(
|
||||||
defaults.session.currentAccount,
|
a => a.did === legacy.session?.data?.did,
|
||||||
|
) || defaults.session.currentAccount,
|
||||||
},
|
},
|
||||||
reminders: {
|
reminders: {
|
||||||
lastEmailConfirm:
|
lastEmailConfirm:
|
||||||
legacy.reminders.lastEmailConfirm ||
|
legacy.reminders?.lastEmailConfirm ||
|
||||||
defaults.reminders.lastEmailConfirm,
|
defaults.reminders.lastEmailConfirm,
|
||||||
},
|
},
|
||||||
languagePrefs: {
|
languagePrefs: {
|
||||||
primaryLanguage:
|
primaryLanguage:
|
||||||
legacy.preferences.primaryLanguage ||
|
legacy.preferences?.primaryLanguage ||
|
||||||
defaults.languagePrefs.primaryLanguage,
|
defaults.languagePrefs.primaryLanguage,
|
||||||
contentLanguages:
|
contentLanguages:
|
||||||
legacy.preferences.contentLanguages ||
|
legacy.preferences?.contentLanguages ||
|
||||||
defaults.languagePrefs.contentLanguages,
|
defaults.languagePrefs.contentLanguages,
|
||||||
postLanguage:
|
postLanguage:
|
||||||
legacy.preferences.postLanguage || defaults.languagePrefs.postLanguage,
|
legacy.preferences?.postLanguage || defaults.languagePrefs.postLanguage,
|
||||||
postLanguageHistory:
|
postLanguageHistory:
|
||||||
legacy.preferences.postLanguageHistory ||
|
legacy.preferences?.postLanguageHistory ||
|
||||||
defaults.languagePrefs.postLanguageHistory,
|
defaults.languagePrefs.postLanguageHistory,
|
||||||
},
|
},
|
||||||
requireAltTextEnabled:
|
requireAltTextEnabled:
|
||||||
legacy.preferences.requireAltTextEnabled ||
|
legacy.preferences?.requireAltTextEnabled ||
|
||||||
defaults.requireAltTextEnabled,
|
defaults.requireAltTextEnabled,
|
||||||
mutedThreads: legacy.mutedThreads.uris || defaults.mutedThreads,
|
mutedThreads: legacy.mutedThreads?.uris || defaults.mutedThreads,
|
||||||
invites: {
|
invites: {
|
||||||
copiedInvites:
|
copiedInvites:
|
||||||
legacy.invitedUsers.copiedInvites || defaults.invites.copiedInvites,
|
legacy.invitedUsers?.copiedInvites || defaults.invites.copiedInvites,
|
||||||
},
|
},
|
||||||
onboarding: {
|
onboarding: {
|
||||||
step: legacy.onboarding.step || defaults.onboarding.step,
|
step: legacy.onboarding?.step || defaults.onboarding.step,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,17 @@ import {z} from 'zod'
|
||||||
import {deviceLocales} from '#/platform/detection'
|
import {deviceLocales} from '#/platform/detection'
|
||||||
|
|
||||||
// only data needed for rendering account page
|
// only data needed for rendering account page
|
||||||
|
// TODO agent.resumeSession requires the following fields
|
||||||
const accountSchema = z.object({
|
const accountSchema = z.object({
|
||||||
service: z.string(),
|
service: z.string(),
|
||||||
did: z.string(),
|
did: z.string(),
|
||||||
refreshJwt: z.string().optional(),
|
handle: z.string(),
|
||||||
accessJwt: z.string().optional(),
|
refreshJwt: z.string().optional(), // optional because it can expire
|
||||||
handle: z.string().optional(),
|
accessJwt: z.string().optional(), // optional because it can expire
|
||||||
displayName: z.string().optional(),
|
// displayName: z.string().optional(),
|
||||||
aviUrl: z.string().optional(),
|
// aviUrl: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
export type PersistedAccount = z.infer<typeof accountSchema>
|
||||||
|
|
||||||
export const schema = z.object({
|
export const schema = z.object({
|
||||||
colorMode: z.enum(['system', 'light', 'dark']),
|
colorMode: z.enum(['system', 'light', 'dark']),
|
||||||
|
|
|
@ -0,0 +1,384 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {BskyAgent, AtpPersistSessionHandler} from '@atproto/api'
|
||||||
|
|
||||||
|
import {networkRetry} from '#/lib/async/retry'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import * as persisted from '#/state/persisted'
|
||||||
|
|
||||||
|
export type SessionAccount = persisted.PersistedAccount
|
||||||
|
|
||||||
|
export type StateContext = {
|
||||||
|
isInitialLoad: boolean
|
||||||
|
agent: BskyAgent
|
||||||
|
accounts: persisted.PersistedAccount[]
|
||||||
|
currentAccount: persisted.PersistedAccount | undefined
|
||||||
|
hasSession: boolean
|
||||||
|
}
|
||||||
|
export type ApiContext = {
|
||||||
|
createAccount: (props: {
|
||||||
|
service: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
handle: string
|
||||||
|
inviteCode?: string
|
||||||
|
}) => Promise<void>
|
||||||
|
login: (props: {
|
||||||
|
service: string
|
||||||
|
identifier: string
|
||||||
|
password: string
|
||||||
|
}) => Promise<void>
|
||||||
|
logout: () => Promise<void>
|
||||||
|
initSession: (account: persisted.PersistedAccount) => Promise<void>
|
||||||
|
resumeSession: (account?: persisted.PersistedAccount) => Promise<void>
|
||||||
|
removeAccount: (
|
||||||
|
account: Partial<Pick<persisted.PersistedAccount, 'handle' | 'did'>>,
|
||||||
|
) => void
|
||||||
|
updateCurrentAccount: (
|
||||||
|
account: Pick<persisted.PersistedAccount, 'handle'>,
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PUBLIC_BSKY_AGENT = new BskyAgent({
|
||||||
|
service: 'https://api.bsky.app',
|
||||||
|
})
|
||||||
|
|
||||||
|
const StateContext = React.createContext<StateContext>({
|
||||||
|
hasSession: false,
|
||||||
|
isInitialLoad: true,
|
||||||
|
accounts: [],
|
||||||
|
currentAccount: undefined,
|
||||||
|
agent: PUBLIC_BSKY_AGENT,
|
||||||
|
})
|
||||||
|
|
||||||
|
const ApiContext = React.createContext<ApiContext>({
|
||||||
|
createAccount: async () => {},
|
||||||
|
login: async () => {},
|
||||||
|
logout: async () => {},
|
||||||
|
initSession: async () => {},
|
||||||
|
resumeSession: async () => {},
|
||||||
|
removeAccount: () => {},
|
||||||
|
updateCurrentAccount: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
function createPersistSessionHandler(
|
||||||
|
account: persisted.PersistedAccount,
|
||||||
|
persistSessionCallback: (props: {
|
||||||
|
expired: boolean
|
||||||
|
refreshedAccount: persisted.PersistedAccount
|
||||||
|
}) => void,
|
||||||
|
): AtpPersistSessionHandler {
|
||||||
|
return function persistSession(event, session) {
|
||||||
|
const expired = !(event === 'create' || event === 'update')
|
||||||
|
const refreshedAccount = {
|
||||||
|
service: account.service,
|
||||||
|
did: session?.did || account.did,
|
||||||
|
handle: session?.handle || account.handle,
|
||||||
|
refreshJwt: session?.refreshJwt, // undefined when expired or creation fails
|
||||||
|
accessJwt: session?.accessJwt, // undefined when expired or creation fails
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`session: BskyAgent.persistSession`, {
|
||||||
|
expired,
|
||||||
|
did: refreshedAccount.did,
|
||||||
|
handle: refreshedAccount.handle,
|
||||||
|
})
|
||||||
|
|
||||||
|
persistSessionCallback({
|
||||||
|
expired,
|
||||||
|
refreshedAccount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
|
const [state, setState] = React.useState<StateContext>({
|
||||||
|
hasSession: false,
|
||||||
|
isInitialLoad: true, // try to resume the session first
|
||||||
|
accounts: persisted.get('session').accounts,
|
||||||
|
currentAccount: undefined, // assume logged out to start
|
||||||
|
agent: PUBLIC_BSKY_AGENT,
|
||||||
|
})
|
||||||
|
|
||||||
|
const upsertAccount = React.useCallback(
|
||||||
|
(account: persisted.PersistedAccount, expired = false) => {
|
||||||
|
setState(s => {
|
||||||
|
return {
|
||||||
|
...s,
|
||||||
|
currentAccount: expired ? undefined : account,
|
||||||
|
accounts: [account, ...s.accounts.filter(a => a.did !== account.did)],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[setState],
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO have not connected this yet
|
||||||
|
const createAccount = React.useCallback<ApiContext['createAccount']>(
|
||||||
|
async ({service, email, password, handle, inviteCode}: any) => {
|
||||||
|
logger.debug(`session: creating account`, {
|
||||||
|
service,
|
||||||
|
handle,
|
||||||
|
})
|
||||||
|
|
||||||
|
const agent = new BskyAgent({service})
|
||||||
|
|
||||||
|
await agent.createAccount({
|
||||||
|
handle,
|
||||||
|
password,
|
||||||
|
email,
|
||||||
|
inviteCode,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!agent.session) {
|
||||||
|
throw new Error(`session: createAccount failed to establish a session`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const account: persisted.PersistedAccount = {
|
||||||
|
service,
|
||||||
|
did: agent.session.did,
|
||||||
|
refreshJwt: agent.session.refreshJwt,
|
||||||
|
accessJwt: agent.session.accessJwt,
|
||||||
|
handle: agent.session.handle,
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.setPersistSessionHandler(
|
||||||
|
createPersistSessionHandler(account, ({expired, refreshedAccount}) => {
|
||||||
|
upsertAccount(refreshedAccount, expired)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
upsertAccount(account)
|
||||||
|
|
||||||
|
logger.debug(`session: created account`, {
|
||||||
|
service,
|
||||||
|
handle,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[upsertAccount],
|
||||||
|
)
|
||||||
|
|
||||||
|
const login = React.useCallback<ApiContext['login']>(
|
||||||
|
async ({service, identifier, password}) => {
|
||||||
|
logger.debug(`session: login`, {
|
||||||
|
service,
|
||||||
|
identifier,
|
||||||
|
})
|
||||||
|
|
||||||
|
const agent = new BskyAgent({service})
|
||||||
|
|
||||||
|
await agent.login({identifier, password})
|
||||||
|
|
||||||
|
if (!agent.session) {
|
||||||
|
throw new Error(`session: login failed to establish a session`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const account: persisted.PersistedAccount = {
|
||||||
|
service,
|
||||||
|
did: agent.session.did,
|
||||||
|
refreshJwt: agent.session.refreshJwt,
|
||||||
|
accessJwt: agent.session.accessJwt,
|
||||||
|
handle: agent.session.handle,
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.setPersistSessionHandler(
|
||||||
|
createPersistSessionHandler(account, ({expired, refreshedAccount}) => {
|
||||||
|
upsertAccount(refreshedAccount, expired)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
upsertAccount(account)
|
||||||
|
|
||||||
|
logger.debug(`session: logged in`, {
|
||||||
|
service,
|
||||||
|
identifier,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[upsertAccount],
|
||||||
|
)
|
||||||
|
|
||||||
|
const logout = React.useCallback<ApiContext['logout']>(async () => {
|
||||||
|
logger.debug(`session: logout`)
|
||||||
|
setState(s => {
|
||||||
|
return {
|
||||||
|
...s,
|
||||||
|
agent: PUBLIC_BSKY_AGENT,
|
||||||
|
currentAccount: undefined,
|
||||||
|
accounts: s.accounts.map(a => ({
|
||||||
|
...a,
|
||||||
|
refreshJwt: undefined,
|
||||||
|
accessJwt: undefined,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [setState])
|
||||||
|
|
||||||
|
const initSession = React.useCallback<ApiContext['initSession']>(
|
||||||
|
async account => {
|
||||||
|
logger.debug(`session: initSession`, {
|
||||||
|
did: account.did,
|
||||||
|
handle: account.handle,
|
||||||
|
})
|
||||||
|
|
||||||
|
const agent = new BskyAgent({
|
||||||
|
service: account.service,
|
||||||
|
persistSession: createPersistSessionHandler(
|
||||||
|
account,
|
||||||
|
({expired, refreshedAccount}) => {
|
||||||
|
upsertAccount(refreshedAccount, expired)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
await networkRetry(3, () =>
|
||||||
|
agent.resumeSession({
|
||||||
|
accessJwt: account.accessJwt || '',
|
||||||
|
refreshJwt: account.refreshJwt || '',
|
||||||
|
did: account.did,
|
||||||
|
handle: account.handle,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
upsertAccount(account)
|
||||||
|
},
|
||||||
|
[upsertAccount],
|
||||||
|
)
|
||||||
|
|
||||||
|
const resumeSession = React.useCallback<ApiContext['resumeSession']>(
|
||||||
|
async account => {
|
||||||
|
try {
|
||||||
|
if (account) {
|
||||||
|
await initSession(account)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`session: resumeSession failed`, {error: e})
|
||||||
|
} finally {
|
||||||
|
setState(s => ({
|
||||||
|
...s,
|
||||||
|
isInitialLoad: false,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[initSession],
|
||||||
|
)
|
||||||
|
|
||||||
|
const removeAccount = React.useCallback<ApiContext['removeAccount']>(
|
||||||
|
account => {
|
||||||
|
setState(s => {
|
||||||
|
return {
|
||||||
|
...s,
|
||||||
|
accounts: s.accounts.filter(
|
||||||
|
a => !(a.did === account.did || a.handle === account.handle),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[setState],
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateCurrentAccount = React.useCallback<
|
||||||
|
ApiContext['updateCurrentAccount']
|
||||||
|
>(
|
||||||
|
account => {
|
||||||
|
setState(s => {
|
||||||
|
const currentAccount = s.currentAccount
|
||||||
|
|
||||||
|
// ignore, should never happen
|
||||||
|
if (!currentAccount) return s
|
||||||
|
|
||||||
|
const updatedAccount = {
|
||||||
|
...currentAccount,
|
||||||
|
handle: account.handle, // only update handle rn
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...s,
|
||||||
|
currentAccount: updatedAccount,
|
||||||
|
accounts: s.accounts.filter(a => a.did !== currentAccount.did),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[setState],
|
||||||
|
)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
persisted.write('session', {
|
||||||
|
accounts: state.accounts,
|
||||||
|
currentAccount: state.currentAccount,
|
||||||
|
})
|
||||||
|
}, [state])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
return persisted.onUpdate(() => {
|
||||||
|
const session = persisted.get('session')
|
||||||
|
|
||||||
|
logger.debug(`session: onUpdate`)
|
||||||
|
|
||||||
|
if (session.currentAccount) {
|
||||||
|
if (session.currentAccount?.did !== state.currentAccount?.did) {
|
||||||
|
logger.debug(`session: switching account`, {
|
||||||
|
from: {
|
||||||
|
did: state.currentAccount?.did,
|
||||||
|
handle: state.currentAccount?.handle,
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
did: session.currentAccount.did,
|
||||||
|
handle: session.currentAccount.handle,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
initSession(session.currentAccount)
|
||||||
|
}
|
||||||
|
} else if (!session.currentAccount && state.currentAccount) {
|
||||||
|
logger.debug(`session: logging out`, {
|
||||||
|
did: state.currentAccount?.did,
|
||||||
|
handle: state.currentAccount?.handle,
|
||||||
|
})
|
||||||
|
|
||||||
|
logout()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [state, logout, initSession])
|
||||||
|
|
||||||
|
const stateContext = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
...state,
|
||||||
|
hasSession: !!state.currentAccount,
|
||||||
|
}),
|
||||||
|
[state],
|
||||||
|
)
|
||||||
|
|
||||||
|
const api = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
createAccount,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
initSession,
|
||||||
|
resumeSession,
|
||||||
|
removeAccount,
|
||||||
|
updateCurrentAccount,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
createAccount,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
initSession,
|
||||||
|
resumeSession,
|
||||||
|
removeAccount,
|
||||||
|
updateCurrentAccount,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StateContext.Provider value={stateContext}>
|
||||||
|
<ApiContext.Provider value={api}>{children}</ApiContext.Provider>
|
||||||
|
</StateContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSession() {
|
||||||
|
return React.useContext(StateContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSessionApi() {
|
||||||
|
return React.useContext(ApiContext)
|
||||||
|
}
|
|
@ -82,12 +82,16 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
return persisted.onUpdate(() => {
|
return persisted.onUpdate(() => {
|
||||||
|
const next = persisted.get('onboarding').step
|
||||||
|
// TODO we've introduced a footgun
|
||||||
|
if (state.step !== next) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'set',
|
type: 'set',
|
||||||
step: persisted.get('onboarding').step as OnboardingStep,
|
step: persisted.get('onboarding').step as OnboardingStep,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}, [dispatch])
|
}, [state, dispatch])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<stateContext.Provider value={state}>
|
<stateContext.Provider value={state}>
|
||||||
|
|
|
@ -5,23 +5,23 @@ import {
|
||||||
FontAwesomeIconStyle,
|
FontAwesomeIconStyle,
|
||||||
} from '@fortawesome/react-native-fontawesome'
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {useStores} from 'state/index'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {DropdownItem, NativeDropdown} from './forms/NativeDropdown'
|
import {DropdownItem, NativeDropdown} from './forms/NativeDropdown'
|
||||||
import * as Toast from '../../com/util/Toast'
|
import * as Toast from '../../com/util/Toast'
|
||||||
|
import {useSessionApi} from '#/state/session'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
|
|
||||||
export function AccountDropdownBtn({handle}: {handle: string}) {
|
export function AccountDropdownBtn({handle}: {handle: string}) {
|
||||||
const store = useStores()
|
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
const {removeAccount} = useSessionApi()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
|
||||||
const items: DropdownItem[] = [
|
const items: DropdownItem[] = [
|
||||||
{
|
{
|
||||||
label: 'Remove account',
|
label: 'Remove account',
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
store.session.removeAccount(handle)
|
removeAccount({handle})
|
||||||
Toast.show('Account removed from quick access')
|
Toast.show('Account removed from quick access')
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
|
|
|
@ -57,6 +57,7 @@ import {
|
||||||
useRequireAltTextEnabled,
|
useRequireAltTextEnabled,
|
||||||
useSetRequireAltTextEnabled,
|
useSetRequireAltTextEnabled,
|
||||||
} from '#/state/preferences'
|
} from '#/state/preferences'
|
||||||
|
import {useSession, useSessionApi} from '#/state/session'
|
||||||
|
|
||||||
// TEMPORARY (APP-700)
|
// TEMPORARY (APP-700)
|
||||||
// remove after backend testing finishes
|
// remove after backend testing finishes
|
||||||
|
@ -87,6 +88,8 @@ export const SettingsScreen = withAuthRequired(
|
||||||
store.agent,
|
store.agent,
|
||||||
)
|
)
|
||||||
const {openModal} = useModalControls()
|
const {openModal} = useModalControls()
|
||||||
|
const {logout} = useSessionApi()
|
||||||
|
const {accounts} = useSession()
|
||||||
|
|
||||||
const primaryBg = useCustomPalette<ViewStyle>({
|
const primaryBg = useCustomPalette<ViewStyle>({
|
||||||
light: {backgroundColor: colors.blue0},
|
light: {backgroundColor: colors.blue0},
|
||||||
|
@ -153,8 +156,9 @@ export const SettingsScreen = withAuthRequired(
|
||||||
|
|
||||||
const onPressSignout = React.useCallback(() => {
|
const onPressSignout = React.useCallback(() => {
|
||||||
track('Settings:SignOutButtonClicked')
|
track('Settings:SignOutButtonClicked')
|
||||||
|
logout()
|
||||||
store.session.logout()
|
store.session.logout()
|
||||||
}, [track, store])
|
}, [track, store, logout])
|
||||||
|
|
||||||
const onPressDeleteAccount = React.useCallback(() => {
|
const onPressDeleteAccount = React.useCallback(() => {
|
||||||
openModal({name: 'delete-account'})
|
openModal({name: 'delete-account'})
|
||||||
|
@ -294,7 +298,7 @@ export const SettingsScreen = withAuthRequired(
|
||||||
</View>
|
</View>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{store.session.switchableAccounts.map(account => (
|
{accounts.map(account => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID={`switchToAccountBtn-${account.handle}`}
|
testID={`switchToAccountBtn-${account.handle}`}
|
||||||
key={account.did}
|
key={account.did}
|
||||||
|
@ -306,10 +310,11 @@ export const SettingsScreen = withAuthRequired(
|
||||||
accessibilityLabel={`Switch to ${account.handle}`}
|
accessibilityLabel={`Switch to ${account.handle}`}
|
||||||
accessibilityHint="Switches the account you are logged in to">
|
accessibilityHint="Switches the account you are logged in to">
|
||||||
<View style={styles.avi}>
|
<View style={styles.avi}>
|
||||||
<UserAvatar size={40} avatar={account.aviUrl} />
|
{/*<UserAvatar size={40} avatar={account.aviUrl} />*/}
|
||||||
</View>
|
</View>
|
||||||
<View style={[s.flex1]}>
|
<View style={[s.flex1]}>
|
||||||
<Text type="md-bold" style={pal.text}>
|
<Text type="md-bold" style={pal.text}>
|
||||||
|
{/* @ts-ignore */}
|
||||||
{account.displayName || account.handle}
|
{account.displayName || account.handle}
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="sm" style={pal.textLight}>
|
<Text type="sm" style={pal.textLight}>
|
||||||
|
|
Loading…
Reference in New Issue