Session fixes, pt. 1 (#3762)
* Update persisted schema for new source of truth, implement in existing session (cherry picked from commit b1e5f12baee932721d66c60dd51c981b46b0c274) * Improve toasts, log caught error, during switch account (cherry picked from commit fe0d1507063d2e532b7b1a447670b689292d1dc3) * Handle thrown errors from initSession during login (cherry picked from commit 2c85c045917e923901284b9ba310a82e28f37b5c) --------- Co-authored-by: Eric Bailey <git@esb.lol>zio/stable
parent
4de78fb69e
commit
2b7d796ca9
|
@ -1,6 +1,9 @@
|
||||||
import {useCallback} from 'react'
|
import {useCallback} from 'react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useAnalytics} from '#/lib/analytics/analytics'
|
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||||
|
import {logger} from '#/logger'
|
||||||
import {isWeb} from '#/platform/detection'
|
import {isWeb} from '#/platform/detection'
|
||||||
import {SessionAccount, useSessionApi} from '#/state/session'
|
import {SessionAccount, useSessionApi} from '#/state/session'
|
||||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||||
|
@ -8,6 +11,7 @@ import * as Toast from '#/view/com/util/Toast'
|
||||||
import {LogEvents} from '../statsig/statsig'
|
import {LogEvents} from '../statsig/statsig'
|
||||||
|
|
||||||
export function useAccountSwitcher() {
|
export function useAccountSwitcher() {
|
||||||
|
const {_} = useLingui()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const {selectAccount, clearCurrentAccount} = useSessionApi()
|
const {selectAccount, clearCurrentAccount} = useSessionApi()
|
||||||
const {requestSwitchToAccount} = useLoggedOutViewControls()
|
const {requestSwitchToAccount} = useLoggedOutViewControls()
|
||||||
|
@ -31,21 +35,26 @@ export function useAccountSwitcher() {
|
||||||
}
|
}
|
||||||
await selectAccount(account, logContext)
|
await selectAccount(account, logContext)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Toast.show(`Signed in as @${account.handle}`)
|
Toast.show(_(msg`Signed in as @${account.handle}`))
|
||||||
}, 100)
|
}, 100)
|
||||||
} else {
|
} else {
|
||||||
requestSwitchToAccount({requestedAccount: account.did})
|
requestSwitchToAccount({requestedAccount: account.did})
|
||||||
Toast.show(
|
Toast.show(
|
||||||
`Please sign in as @${account.handle}`,
|
_(msg`Please sign in as @${account.handle}`),
|
||||||
'circle-exclamation',
|
'circle-exclamation',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
Toast.show('Sorry! We need you to enter your password.')
|
logger.error(`switch account: selectAccount failed`, {
|
||||||
|
message: e.message,
|
||||||
|
})
|
||||||
clearCurrentAccount() // back user out to login
|
clearCurrentAccount() // back user out to login
|
||||||
|
setTimeout(() => {
|
||||||
|
Toast.show(_(msg`Sorry! We need you to enter your password.`))
|
||||||
|
}, 100)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[track, clearCurrentAccount, selectAccount, requestSwitchToAccount],
|
[_, track, clearCurrentAccount, selectAccount, requestSwitchToAccount],
|
||||||
)
|
)
|
||||||
|
|
||||||
return {onPressSwitchAccount}
|
return {onPressSwitchAccount}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useAnalytics} from '#/lib/analytics/analytics'
|
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
|
import {logger} from '#/logger'
|
||||||
import {SessionAccount, useSession, useSessionApi} from '#/state/session'
|
import {SessionAccount, useSession, useSessionApi} from '#/state/session'
|
||||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
|
@ -38,6 +39,7 @@ export const ChooseAccountForm = ({
|
||||||
setShowLoggedOut(false)
|
setShowLoggedOut(false)
|
||||||
Toast.show(_(msg`Already signed in as @${account.handle}`))
|
Toast.show(_(msg`Already signed in as @${account.handle}`))
|
||||||
} else {
|
} else {
|
||||||
|
try {
|
||||||
await initSession(account)
|
await initSession(account)
|
||||||
logEvent('account:loggedIn', {
|
logEvent('account:loggedIn', {
|
||||||
logContext: 'ChooseAccountForm',
|
logContext: 'ChooseAccountForm',
|
||||||
|
@ -47,6 +49,12 @@ export const ChooseAccountForm = ({
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
Toast.show(_(msg`Signed in as @${account.handle}`))
|
Toast.show(_(msg`Signed in as @${account.handle}`))
|
||||||
}, 100)
|
}, 100)
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.error('choose account: initSession failed', {
|
||||||
|
message: e.message,
|
||||||
|
})
|
||||||
|
onSelectAccount(account)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
onSelectAccount(account)
|
onSelectAccount(account)
|
||||||
|
|
|
@ -4,7 +4,10 @@ import {deviceLocales, prefersReducedMotion} from '#/platform/detection'
|
||||||
|
|
||||||
const externalEmbedOptions = ['show', 'hide'] as const
|
const externalEmbedOptions = ['show', 'hide'] as const
|
||||||
|
|
||||||
// only data needed for rendering account page
|
/**
|
||||||
|
* A account persisted to storage. Stored in the `accounts[]` array. Contains
|
||||||
|
* base account info and access tokens.
|
||||||
|
*/
|
||||||
const accountSchema = z.object({
|
const accountSchema = z.object({
|
||||||
service: z.string(),
|
service: z.string(),
|
||||||
did: z.string(),
|
did: z.string(),
|
||||||
|
@ -19,12 +22,26 @@ const accountSchema = z.object({
|
||||||
})
|
})
|
||||||
export type PersistedAccount = z.infer<typeof accountSchema>
|
export type PersistedAccount = z.infer<typeof accountSchema>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current account. Stored in the `currentAccount` field.
|
||||||
|
*
|
||||||
|
* In previous versions, this included tokens and other info. Now, it's used
|
||||||
|
* only to reference the `did` field, and all other fields are marked as
|
||||||
|
* optional. They should be considered deprecated and not used, but are kept
|
||||||
|
* here for backwards compat.
|
||||||
|
*/
|
||||||
|
const currentAccountSchema = accountSchema.extend({
|
||||||
|
service: z.string().optional(),
|
||||||
|
handle: z.string().optional(),
|
||||||
|
})
|
||||||
|
export type PersistedCurrentAccount = z.infer<typeof currentAccountSchema>
|
||||||
|
|
||||||
export const schema = z.object({
|
export const schema = z.object({
|
||||||
colorMode: z.enum(['system', 'light', 'dark']),
|
colorMode: z.enum(['system', 'light', 'dark']),
|
||||||
darkTheme: z.enum(['dim', 'dark']).optional(),
|
darkTheme: z.enum(['dim', 'dark']).optional(),
|
||||||
session: z.object({
|
session: z.object({
|
||||||
accounts: z.array(accountSchema),
|
accounts: z.array(accountSchema),
|
||||||
currentAccount: accountSchema.optional(),
|
currentAccount: currentAccountSchema.optional(),
|
||||||
}),
|
}),
|
||||||
reminders: z.object({
|
reminders: z.object({
|
||||||
lastEmailConfirm: z.string().optional(),
|
lastEmailConfirm: z.string().optional(),
|
||||||
|
|
|
@ -618,20 +618,24 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
|
|
||||||
logger.debug(`session: persisted onUpdate`, {})
|
logger.debug(`session: persisted onUpdate`, {})
|
||||||
|
|
||||||
if (session.currentAccount && session.currentAccount.refreshJwt) {
|
const selectedAccount = session.accounts.find(
|
||||||
if (session.currentAccount?.did !== state.currentAccount?.did) {
|
a => a.did === session.currentAccount?.did,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (selectedAccount && selectedAccount.refreshJwt) {
|
||||||
|
if (selectedAccount.did !== state.currentAccount?.did) {
|
||||||
logger.debug(`session: persisted onUpdate, switching accounts`, {
|
logger.debug(`session: persisted onUpdate, switching accounts`, {
|
||||||
from: {
|
from: {
|
||||||
did: state.currentAccount?.did,
|
did: state.currentAccount?.did,
|
||||||
handle: state.currentAccount?.handle,
|
handle: state.currentAccount?.handle,
|
||||||
},
|
},
|
||||||
to: {
|
to: {
|
||||||
did: session.currentAccount.did,
|
did: selectedAccount.did,
|
||||||
handle: session.currentAccount.handle,
|
handle: selectedAccount.handle,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
initSession(session.currentAccount)
|
initSession(selectedAccount)
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`session: persisted onUpdate, updating session`, {})
|
logger.debug(`session: persisted onUpdate, updating session`, {})
|
||||||
|
|
||||||
|
@ -641,9 +645,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
* already persisted, and we'll get a loop between tabs.
|
* already persisted, and we'll get a loop between tabs.
|
||||||
*/
|
*/
|
||||||
// @ts-ignore we checked for `refreshJwt` above
|
// @ts-ignore we checked for `refreshJwt` above
|
||||||
__globalAgent.session = session.currentAccount
|
__globalAgent.session = selectedAccount
|
||||||
}
|
}
|
||||||
} else if (!session.currentAccount && state.currentAccount) {
|
} else if (!selectedAccount && state.currentAccount) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`session: persisted onUpdate, logging out`,
|
`session: persisted onUpdate, logging out`,
|
||||||
{},
|
{},
|
||||||
|
@ -662,7 +666,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
setState(s => ({
|
setState(s => ({
|
||||||
...s,
|
...s,
|
||||||
accounts: session.accounts,
|
accounts: session.accounts,
|
||||||
currentAccount: session.currentAccount,
|
currentAccount: selectedAccount,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
}, [state, setState, clearCurrentAccount, initSession])
|
}, [state, setState, clearCurrentAccount, initSession])
|
||||||
|
|
Loading…
Reference in New Issue