Move global "Sign out" out of the current account row (#4941)

* Rename logout to logoutEveryAccount

* Add logoutCurrentAccount()

* Make all "Log out" buttons refer to current account

Each of these usages is completely contextual and refers to a specific account.

* Add Sign out of all accounts to Settings

* Move single account Sign Out below as well

* Prompt on account removal

* Add Other Accounts header to reduce ambiguity

* Spacing fix

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
This commit is contained in:
dan 2024-08-15 20:58:13 +01:00 committed by GitHub
parent f3b57dd456
commit b6e515c664
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 247 additions and 77 deletions

View file

@ -76,7 +76,7 @@ describe('session', () => {
state = run(state, [
{
type: 'logged-out',
type: 'logged-out-every-account',
},
])
// Should keep the account but clear out the tokens.
@ -372,7 +372,7 @@ describe('session', () => {
state = run(state, [
{
// Log everyone out.
type: 'logged-out',
type: 'logged-out-every-account',
},
])
expect(state.accounts.length).toBe(3)
@ -466,7 +466,7 @@ describe('session', () => {
state = run(state, [
{
type: 'logged-out',
type: 'logged-out-every-account',
},
])
expect(state.accounts.length).toBe(1)
@ -674,6 +674,103 @@ describe('session', () => {
expect(state.currentAgentState.did).toBe(undefined)
})
it('can log out of the current account', () => {
let state = getInitialState([])
const agent1 = new BskyAgent({service: 'https://alice.com'})
agent1.sessionManager.session = {
active: true,
did: 'alice-did',
handle: 'alice.test',
accessJwt: 'alice-access-jwt-1',
refreshJwt: 'alice-refresh-jwt-1',
}
state = run(state, [
{
type: 'switched-to-account',
newAgent: agent1,
newAccount: agentToSessionAccountOrThrow(agent1),
},
])
expect(state.accounts.length).toBe(1)
expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1')
expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-1')
expect(state.currentAgentState.did).toBe('alice-did')
const agent2 = new BskyAgent({service: 'https://bob.com'})
agent2.sessionManager.session = {
active: true,
did: 'bob-did',
handle: 'bob.test',
accessJwt: 'bob-access-jwt-1',
refreshJwt: 'bob-refresh-jwt-1',
}
state = run(state, [
{
type: 'switched-to-account',
newAgent: agent2,
newAccount: agentToSessionAccountOrThrow(agent2),
},
])
expect(state.accounts.length).toBe(2)
expect(state.accounts[0].accessJwt).toBe('bob-access-jwt-1')
expect(state.accounts[0].refreshJwt).toBe('bob-refresh-jwt-1')
expect(state.currentAgentState.did).toBe('bob-did')
state = run(state, [
{
type: 'logged-out-current-account',
},
])
expect(state.accounts.length).toBe(2)
expect(state.accounts[0].accessJwt).toBe(undefined)
expect(state.accounts[0].refreshJwt).toBe(undefined)
expect(state.accounts[1].accessJwt).toBe('alice-access-jwt-1')
expect(state.accounts[1].refreshJwt).toBe('alice-refresh-jwt-1')
expect(state.currentAgentState.did).toBe(undefined)
expect(printState(state)).toMatchInlineSnapshot(`
{
"accounts": [
{
"accessJwt": undefined,
"active": true,
"did": "bob-did",
"email": undefined,
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "bob.test",
"pdsUrl": undefined,
"refreshJwt": undefined,
"service": "https://bob.com/",
"signupQueued": false,
"status": undefined,
},
{
"accessJwt": "alice-access-jwt-1",
"active": true,
"did": "alice-did",
"email": undefined,
"emailAuthFactor": false,
"emailConfirmed": false,
"handle": "alice.test",
"pdsUrl": undefined,
"refreshJwt": "alice-refresh-jwt-1",
"service": "https://alice.com/",
"signupQueued": false,
"status": undefined,
},
],
"currentAgentState": {
"agent": {
"service": "https://public.api.bsky.app/",
},
"did": undefined,
},
"needsPersist": true,
}
`)
})
it('updates stored account with refreshed tokens', () => {
let state = getInitialState([])

View file

@ -35,7 +35,8 @@ const AgentContext = React.createContext<BskyAgent | null>(null)
const ApiContext = React.createContext<SessionApiContext>({
createAccount: async () => {},
login: async () => {},
logout: async () => {},
logoutCurrentAccount: async () => {},
logoutEveryAccount: async () => {},
resumeSession: async () => {},
removeAccount: () => {},
})
@ -115,14 +116,31 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
[onAgentSessionChange, cancelPendingTask],
)
const logout = React.useCallback<SessionApiContext['logout']>(
const logoutCurrentAccount = React.useCallback<
SessionApiContext['logoutEveryAccount']
>(
logContext => {
addSessionDebugLog({type: 'method:start', method: 'logout'})
cancelPendingTask()
dispatch({
type: 'logged-out',
type: 'logged-out-current-account',
})
logEvent('account:loggedOut', {logContext})
logEvent('account:loggedOut', {logContext, scope: 'current'})
addSessionDebugLog({type: 'method:end', method: 'logout'})
},
[cancelPendingTask],
)
const logoutEveryAccount = React.useCallback<
SessionApiContext['logoutEveryAccount']
>(
logContext => {
addSessionDebugLog({type: 'method:start', method: 'logout'})
cancelPendingTask()
dispatch({
type: 'logged-out-every-account',
})
logEvent('account:loggedOut', {logContext, scope: 'every'})
addSessionDebugLog({type: 'method:end', method: 'logout'})
},
[cancelPendingTask],
@ -230,11 +248,19 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
() => ({
createAccount,
login,
logout,
logoutCurrentAccount,
logoutEveryAccount,
resumeSession,
removeAccount,
}),
[createAccount, login, logout, resumeSession, removeAccount],
[
createAccount,
login,
logoutCurrentAccount,
logoutEveryAccount,
resumeSession,
removeAccount,
],
)
// @ts-ignore

View file

@ -42,7 +42,10 @@ export type Action =
accountDid: string
}
| {
type: 'logged-out'
type: 'logged-out-current-account'
}
| {
type: 'logged-out-every-account'
}
| {
type: 'synced-accounts'
@ -138,7 +141,23 @@ let reducer = (state: State, action: Action): State => {
needsPersist: true,
}
}
case 'logged-out': {
case 'logged-out-current-account': {
const {currentAgentState} = state
return {
accounts: state.accounts.map(a =>
a.did === currentAgentState.did
? {
...a,
refreshJwt: undefined,
accessJwt: undefined,
}
: a,
),
currentAgentState: createPublicAgentState(),
needsPersist: true,
}
}
case 'logged-out-every-account': {
return {
accounts: state.accounts.map(a => ({
...a,

View file

@ -29,12 +29,12 @@ export type SessionApiContext = {
},
logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void>
/**
* A full logout. Clears the `currentAccount` from session, AND removes
* access tokens from all accounts, so that returning as any user will
* require a full login.
*/
logout: (logContext: LogEvents['account:loggedOut']['logContext']) => void
logoutCurrentAccount: (
logContext: LogEvents['account:loggedOut']['logContext'],
) => void
logoutEveryAccount: (
logContext: LogEvents['account:loggedOut']['logContext'],
) => void
resumeSession: (account: SessionAccount) => Promise<void>
removeAccount: (account: SessionAccount) => void
}