diff --git a/shared/login/flow.test.ts b/shared/login/flow.test.ts
new file mode 100644
index 000000000000..985f89b488a2
--- /dev/null
+++ b/shared/login/flow.test.ts
@@ -0,0 +1,74 @@
+///
+
+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)
+})
diff --git a/shared/login/flow.ts b/shared/login/flow.ts
new file mode 100644
index 000000000000..41777112aed6
--- /dev/null
+++ b/shared/login/flow.ts
@@ -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
diff --git a/shared/login/index.tsx b/shared/login/index.tsx
index 1c052d3559d0..5c9994ae3407 100644
--- a/shared/login/index.tsx
+++ b/shared/login/index.tsx
@@ -1,4 +1,6 @@
import * as React from 'react'
+import * as C from '@/constants'
+import {getRootLoginMode} from './flow'
import {useConfigState} from '@/stores/config'
import {useDaemonState} from '@/stores/daemon'
@@ -6,34 +8,39 @@ 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 (
-
-
-
- )
- }
- if (showRelogin) {
- return (
-
-
-
- )
+const renderMode = (mode: ReturnType) => {
+ switch (mode) {
+ case 'loading':
+ return
+ case 'relogin':
+ return
+ case 'intro':
+ return
+ case 'hidden':
+ return null
}
+}
- return (
-
-
-
+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 {renderMode(mode)}
}
export default RootLogin
diff --git a/shared/login/join-or-login.tsx b/shared/login/join-or-login.tsx
index 6112c21883b5..a6de50f7880f 100644
--- a/shared/login/join-or-login.tsx
+++ b/shared/login/join-or-login.tsx
@@ -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 (
{
- const _users = useConfigState(s => s.configuredAccounts)
- const perror = useConfigState(s => s.loginError)
- const pselectedUser = useConfigState(s => s.defaultUsername)
+const useReloginState = () => {
+ const {configuredAccounts, defaultUsername, login, loginError, setLoginError} = useConfigState(
+ C.useShallow(s => ({
+ configuredAccounts: s.configuredAccounts,
+ defaultUsername: s.defaultUsername,
+ login: s.dispatch.login,
+ loginError: s.loginError,
+ setLoginError: s.dispatch.setLoginError,
+ }))
+ )
const startRecoverPassword = useRecoverState(s => s.dispatch.startRecoverPassword)
- const onForgotPassword = (username: string) => {
- startRecoverPassword({username})
- }
- const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend)
- const onFeedback = () => {
- navigateAppend('signupSendFeedbackLoggedOut')
- }
- const onLogin = useConfigState(s => s.dispatch.login)
const requestAutoInvite = useSignupState(s => s.dispatch.requestAutoInvite)
- const onSignup = () => requestAutoInvite()
- const onSomeoneElse = useProvisionState(s => s.dispatch.startProvision)
- const error = perror?.desc || ''
- const loggedInMap = new Map(_users.map(account => [account.username, account.hasStoredSecret]))
- const users = sortBy(_users, 'username')
+ const startProvision = useProvisionState(s => s.dispatch.startProvision)
+ const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend)
+
+ const error = loginError?.desc || ''
+ const users = sortBy(configuredAccounts, 'username')
+ const loggedInMap = new Map(users.map(account => [account.username, account.hasStoredSecret]))
const [password, setPassword] = React.useState('')
- const [selectedUser, setSelectedUser] = React.useState(pselectedUser)
+ const [selectedUser, setSelectedUser] = React.useState(defaultUsername)
const [showTyping, setShowTyping] = React.useState(false)
-
- const setLoginError = useConfigState(s => s.dispatch.setLoginError)
- const prevPasswordRef = React.useRef(password)
- const prevErrorRef = React.useRef(error)
+ const [gotNeedPasswordError, setGotNeedPasswordError] = React.useState(false)
+ const previousErrorRef = React.useRef(error)
React.useEffect(() => {
- if (password.length && !prevPasswordRef.current.length) {
- setLoginError()
+ if (error.length && !previousErrorRef.current.length) {
+ setPassword('')
}
- prevPasswordRef.current = password
- }, [password, setLoginError])
+ previousErrorRef.current = error
+ }, [error])
React.useEffect(() => {
- if (error.length && !prevErrorRef.current.length) {
- setPassword('')
- }
- }, [error, setPassword])
+ setSelectedUser(defaultUsername)
+ }, [defaultUsername])
- const [gotNeedPasswordError, setGotNeedPasswordError] = React.useState(false)
+ React.useEffect(() => {
+ if (isNeedPasswordError(error)) {
+ setGotNeedPasswordError(true)
+ }
+ }, [error])
- const onSubmit = () => {
- onLogin(selectedUser, password)
+ const passwordChange = (nextPassword: string) => {
+ if (nextPassword.length && !password.length) {
+ setLoginError()
+ }
+ setPassword(nextPassword)
}
- const selectedUserChange = (user: string) => {
+ const selectedUserChange = (username: string) => {
setLoginError()
setPassword('')
- setSelectedUser(user)
- if (loggedInMap.get(user)) {
- onLogin(user, '')
+ setSelectedUser(username)
+ if (loggedInMap.get(username)) {
+ login(username, '')
}
}
- React.useEffect(() => {
- setSelectedUser(pselectedUser)
- }, [pselectedUser, setSelectedUser])
-
- React.useEffect(() => {
- if (error === needPasswordError) {
- setGotNeedPasswordError(true)
- }
- }, [error, setGotNeedPasswordError])
+ return {
+ error,
+ needPassword: getReloginNeedPassword(!!loggedInMap.get(selectedUser), gotNeedPasswordError),
+ onFeedback: () => navigateAppend('signupSendFeedbackLoggedOut'),
+ onForgotPassword: () => startRecoverPassword({username: selectedUser}),
+ onSignup: () => requestAutoInvite(),
+ onSomeoneElse: startProvision,
+ onSubmit: () => login(selectedUser, password),
+ password,
+ passwordChange,
+ selectedUser,
+ selectedUserChange,
+ showTyping,
+ showTypingChange: setShowTyping,
+ users,
+ }
+}
- return (
- onForgotPassword(selectedUser)}
- onLogin={onLogin}
- onSignup={onSignup}
- onSomeoneElse={onSomeoneElse}
- onSubmit={onSubmit}
- password={password}
- passwordChange={setPassword}
- selectedUser={selectedUser}
- selectedUserChange={selectedUserChange}
- showTypingChange={setShowTyping}
- showTyping={showTyping}
- users={users}
- />
- )
+const ReloginContainer = () => {
+ const props = useReloginState()
+ return
}
export default ReloginContainer
diff --git a/shared/login/relogin/index.d.ts b/shared/login/relogin/index.d.ts
index c68879ec48cd..bb3129f5e17d 100644
--- a/shared/login/relogin/index.d.ts
+++ b/shared/login/relogin/index.d.ts
@@ -16,7 +16,6 @@ export type Props = {
showTypingChange: (typingChange: boolean) => void
onSubmit: () => void
onFeedback: () => void
- onLogin: (user: string, password: string) => void
}
declare const Login: (p: Props) => React.ReactNode
export default Login