Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions shared/login/flow.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/// <reference types="jest" />

import {
getLoggedOutBannerMessage,
getReloginNeedPassword,
getRootLoginMode,
isNeedPasswordError,
needPasswordError,
} from './flow'

test('getRootLoginMode preserves the logged-out routing priority', () => {
expect(
getRootLoginMode({
configuredAccountsLength: 2,
handshakeState: 'done',
isLoggedIn: true,
userSwitching: false,
})
).toBe('hidden')

expect(
getRootLoginMode({
configuredAccountsLength: 2,
handshakeState: 'starting',
isLoggedIn: false,
userSwitching: false,
})
).toBe('loading')

expect(
getRootLoginMode({
configuredAccountsLength: 2,
handshakeState: 'done',
isLoggedIn: false,
userSwitching: true,
})
).toBe('loading')

expect(
getRootLoginMode({
configuredAccountsLength: 2,
handshakeState: 'done',
isLoggedIn: false,
userSwitching: false,
})
).toBe('relogin')

expect(
getRootLoginMode({
configuredAccountsLength: 0,
handshakeState: 'done',
isLoggedIn: false,
userSwitching: false,
})
).toBe('intro')
})

test('getLoggedOutBannerMessage prefers deletion, then revocation, then nothing', () => {
expect(
getLoggedOutBannerMessage({justDeletedSelf: 'alice', justRevokedSelf: 'bob'})
).toBe('Your Keybase account alice has been deleted. Au revoir!')
expect(getLoggedOutBannerMessage({justDeletedSelf: '', justRevokedSelf: 'bob'})).toBe(
'bob was revoked successfully'
)
expect(getLoggedOutBannerMessage({justDeletedSelf: '', justRevokedSelf: ''})).toBe('')
})

test('relogin password helpers preserve the stored-secret and empty-passphrase branches', () => {
expect(getReloginNeedPassword(true, false)).toBe(false)
expect(getReloginNeedPassword(false, false)).toBe(true)
expect(getReloginNeedPassword(true, true)).toBe(true)
expect(isNeedPasswordError(needPasswordError)).toBe(true)
expect(isNeedPasswordError('Incorrect password.')).toBe(false)
})
55 changes: 55 additions & 0 deletions shared/login/flow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type * as T from '@/constants/types'

export type RootLoginMode = 'hidden' | 'intro' | 'loading' | 'relogin'

type RootLoginState = {
configuredAccountsLength: number
handshakeState: T.Config.DaemonHandshakeState
isLoggedIn: boolean
userSwitching: boolean
}

type LoggedOutBannerState = {
justDeletedSelf: string
justRevokedSelf: string
}

export const needPasswordError = 'passphrase cannot be empty'

export const getRootLoginMode = ({
configuredAccountsLength,
handshakeState,
isLoggedIn,
userSwitching,
}: RootLoginState): RootLoginMode => {
if (isLoggedIn) {
return 'hidden'
}
if (handshakeState !== 'done' || userSwitching) {
return 'loading'
}
if (configuredAccountsLength > 0) {
return 'relogin'
}
return 'intro'
}

export const getLoggedOutBannerMessage = ({
justDeletedSelf,
justRevokedSelf,
}: LoggedOutBannerState): string => {
if (justDeletedSelf) {
return `Your Keybase account ${justDeletedSelf} has been deleted. Au revoir!`
}
if (justRevokedSelf) {
return `${justRevokedSelf} was revoked successfully`
}
return ''
}

export const getReloginNeedPassword = (
hasStoredSecret: boolean,
promptedForPassword: boolean
): boolean => !hasStoredSecret || promptedForPassword

export const isNeedPasswordError = (error: string): boolean => error === needPasswordError
57 changes: 32 additions & 25 deletions shared/login/index.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,46 @@
import * as React from 'react'
import * as C from '@/constants'
import {getRootLoginMode} from './flow'
import {useConfigState} from '@/stores/config'
import {useDaemonState} from '@/stores/daemon'

const Loading = React.lazy(async () => import('./loading'))
const Relogin = React.lazy(async () => import('./relogin/container'))
const JoinOrLogin = React.lazy(async () => import('./join-or-login'))

const RootLogin = () => {
const isLoggedIn = useConfigState(s => s.loggedIn)
const userSwitching = useConfigState(s => s.userSwitching)
const showLoading = useDaemonState(s => s.handshakeState !== 'done' || userSwitching)
const showRelogin = useConfigState(s => !showLoading && s.configuredAccounts.length > 0)
// routing should switch us away so lets not draw anything to speed things up
if (isLoggedIn) return null

if (showLoading) {
return (
<React.Suspense>
<Loading />
</React.Suspense>
)
}
if (showRelogin) {
return (
<React.Suspense>
<Relogin />
</React.Suspense>
)
const renderMode = (mode: ReturnType<typeof getRootLoginMode>) => {
switch (mode) {
case 'loading':
return <Loading />
case 'relogin':
return <Relogin />
case 'intro':
return <JoinOrLogin />
case 'hidden':
return null
}
}

return (
<React.Suspense>
<JoinOrLogin />
</React.Suspense>
const RootLogin = () => {
const {configuredAccountsLength, isLoggedIn, userSwitching} = useConfigState(
C.useShallow(s => ({
configuredAccountsLength: s.configuredAccounts.length,
isLoggedIn: s.loggedIn,
userSwitching: s.userSwitching,
}))
)
const handshakeState = useDaemonState(s => s.handshakeState)
const mode = getRootLoginMode({
configuredAccountsLength,
handshakeState,
isLoggedIn,
userSwitching,
})

// routing should switch us away so lets not draw anything to speed things up
if (mode === 'hidden') return null

return <React.Suspense>{renderMode(mode)}</React.Suspense>
}

export default RootLogin
48 changes: 24 additions & 24 deletions shared/login/join-or-login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,40 @@ import * as Kb from '@/common-adapters'
import {InfoIcon} from '@/signup/common'
import {useSignupState} from '@/stores/signup'
import {useProvisionState} from '@/stores/provision'
import {getLoggedOutBannerMessage} from './flow'

const Intro = () => {
const justDeletedSelf = useConfigState(s => s.justDeletedSelf)
const justRevokedSelf = useConfigState(s => s.justRevokedSelf)
const bannerMessage = justDeletedSelf
? `Your Keybase account ${justDeletedSelf} has been deleted. Au revoir!`
: justRevokedSelf
? `${justRevokedSelf} was revoked successfully`
: ''

const isOnline = useConfigState(s => s.isOnline)
const loadIsOnline = useConfigState(s => s.dispatch.loadIsOnline)

const useLoggedOutIntroState = () => {
const {isOnline, justDeletedSelf, justRevokedSelf, loadIsOnline} = useConfigState(
C.useShallow(s => ({
isOnline: s.isOnline,
justDeletedSelf: s.justDeletedSelf,
justRevokedSelf: s.justRevokedSelf,
loadIsOnline: s.dispatch.loadIsOnline,
}))
)
const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend)
const checkIsOnline = loadIsOnline
const startProvision = useProvisionState(s => s.dispatch.startProvision)
const onLogin = () => {
startProvision()
}
const requestAutoInvite = useSignupState(s => s.dispatch.requestAutoInvite)
const onSignup = () => {
requestAutoInvite()
}
const showProxySettings = () => {
navigateAppend('proxySettingsModal')
}
const startProvision = useProvisionState(s => s.dispatch.startProvision)
const [showing, setShowing] = React.useState(true)
Kb.useInterval(checkIsOnline, showing ? 5000 : undefined)
Kb.useInterval(loadIsOnline, showing ? 5000 : undefined)

C.Router2.useSafeFocusEffect(() => {
setShowing(true)
return () => setShowing(false)
})

return {
bannerMessage: getLoggedOutBannerMessage({justDeletedSelf, justRevokedSelf}),
isOnline,
onLogin: () => startProvision(),
onSignup: () => requestAutoInvite(),
showProxySettings: () => navigateAppend('proxySettingsModal'),
}
}

const Intro = () => {
const {bannerMessage, isOnline, onLogin, onSignup, showProxySettings} = useLoggedOutIntroState()

return (
<Kb.Box2
direction="vertical"
Expand Down
Loading