mirror of
https://github.com/stablyai/orca.git
synced 2026-09-24 08:02:33 +00:00
fix(cloud-auth): keep Sign in clickable during a pending browser wait
Closing the cloud sign-in tab used to leave every Sign in button disabled as "Signing in…" until the 5-minute loopback timeout. A second click now starts another wait, the first tab can still complete, and the first successful callback wins. STA-7610
This commit is contained in:
@@ -148,4 +148,37 @@ describe('Orca cloud PKCE flow', () => {
|
||||
await readHttp(callbackUrl(redirectUri, { code: 'real-code', state }))
|
||||
await expect(flow).resolves.toMatchObject({ code: 'real-code', nonce })
|
||||
})
|
||||
|
||||
it('keeps the first loopback alive when a second sign-in starts', async () => {
|
||||
const first = beginOrcaCloudPkceFlow(config, 'local-default')
|
||||
await vi.waitFor(() => expect(openExternalMock).toHaveBeenCalledTimes(1))
|
||||
const firstUrl = new URL(String(openExternalMock.mock.calls[0]?.[0]))
|
||||
const firstRedirectUri = firstUrl.searchParams.get('redirect_uri')
|
||||
const firstState = firstUrl.searchParams.get('state')
|
||||
if (!firstRedirectUri || !firstState) {
|
||||
throw new Error('Expected the first PKCE flow to create redirect_uri and state')
|
||||
}
|
||||
|
||||
const second = beginOrcaCloudPkceFlow(config, 'local-default')
|
||||
await vi.waitFor(() => expect(openExternalMock).toHaveBeenCalledTimes(2))
|
||||
const secondUrl = new URL(String(openExternalMock.mock.calls[1]?.[0]))
|
||||
const secondRedirectUri = secondUrl.searchParams.get('redirect_uri')
|
||||
const secondState = secondUrl.searchParams.get('state')
|
||||
if (!secondRedirectUri || !secondState) {
|
||||
throw new Error('Expected the second PKCE flow to create redirect_uri and state')
|
||||
}
|
||||
expect(secondRedirectUri).not.toBe(firstRedirectUri)
|
||||
|
||||
const firstResponse = await readHttp(
|
||||
callbackUrl(firstRedirectUri, { code: 'first-code', state: firstState })
|
||||
)
|
||||
expect(firstResponse.statusCode).toBe(200)
|
||||
await expect(first).resolves.toMatchObject({ code: 'first-code', state: firstState })
|
||||
|
||||
const secondResponse = await readHttp(
|
||||
callbackUrl(secondRedirectUri, { code: 'second-code', state: secondState })
|
||||
)
|
||||
expect(secondResponse.statusCode).toBe(200)
|
||||
await expect(second).resolves.toMatchObject({ code: 'second-code', state: secondState })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -48,7 +48,6 @@ export function UnexpectedSignoutCard(): React.JSX.Element | null {
|
||||
const persistedDismissedVersion = useAppStore((s) => s.dismissedUnexpectedSignoutVersion)
|
||||
const dismissedVersions = useAppStore((s) => s.unexpectedSignoutDismissedVersions)
|
||||
const dismissForVersion = useAppStore((s) => s.dismissUnexpectedSignoutCard)
|
||||
const connecting = useAppStore((s) => s.orcaProfileConnecting)
|
||||
const connect = useAppStore((s) => s.connectCurrentOrcaProfile)
|
||||
const [appVersion, setAppVersion] = useState<string | null>(null)
|
||||
const [authRefreshReady, setAuthRefreshReady] = useState(false)
|
||||
@@ -236,12 +235,10 @@ export function UnexpectedSignoutCard(): React.JSX.Element | null {
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
disabled={!canConnect || connecting}
|
||||
disabled={!canConnect}
|
||||
onClick={() => void connect()}
|
||||
>
|
||||
{connecting
|
||||
? translate('auto.components.UnexpectedSignoutCard.7e1a9c4d2f', 'Signing in…')
|
||||
: translate('auto.components.UnexpectedSignoutCard.c5b3e8a17d', 'Sign in to Orca')}
|
||||
{translate('auto.components.UnexpectedSignoutCard.c5b3e8a17d', 'Sign in to Orca')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,6 @@ const mocks = vi.hoisted(() => ({
|
||||
openPopover: null as ((open: boolean) => void) | null,
|
||||
state: {
|
||||
orcaProfileAuthStatus: { configured: true, state: 'connected' } as Record<string, unknown>,
|
||||
orcaProfileConnecting: false,
|
||||
settings: { artifactSharingEnabled: true }
|
||||
}
|
||||
}))
|
||||
@@ -83,7 +82,6 @@ describe('ArtifactPublishButton', () => {
|
||||
mocks.copyLink.mockResolvedValue(true)
|
||||
mocks.openPopover = null
|
||||
mocks.state.orcaProfileAuthStatus = { configured: true, state: 'connected' }
|
||||
mocks.state.orcaProfileConnecting = false
|
||||
mocks.state.settings = { artifactSharingEnabled: true }
|
||||
})
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ export function ArtifactPublishButton({
|
||||
const lookupSequence = useRef(0)
|
||||
const popoverContentRef = useRef<HTMLDivElement>(null)
|
||||
const authStatus = useAppStore((state) => state.orcaProfileAuthStatus)
|
||||
const connecting = useAppStore((state) => state.orcaProfileConnecting)
|
||||
const connect = useAppStore((state) => state.connectCurrentOrcaProfile)
|
||||
const openSettingsPage = useAppStore((state) => state.openSettingsPage)
|
||||
const openSettingsTarget = useAppStore((state) => state.openSettingsTarget)
|
||||
@@ -56,7 +55,7 @@ export function ArtifactPublishButton({
|
||||
const checkingLink =
|
||||
signedIn && currentLookup?.status !== 'loaded' && currentLookup?.status !== 'error'
|
||||
const publishedLink = currentLookup?.status === 'loaded' ? currentLookup.shareUrl : null
|
||||
const busy = publishing || connecting
|
||||
const busy = publishing
|
||||
const blocked = disabled || busy
|
||||
|
||||
useEffect(() => {
|
||||
@@ -184,23 +183,15 @@ export function ArtifactPublishButton({
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
disabled={connecting || authStatus?.configured !== true}
|
||||
disabled={authStatus?.configured !== true}
|
||||
onClick={() => void connect()}
|
||||
>
|
||||
{connecting
|
||||
{authStatus?.state === 'reconnect-required'
|
||||
? translate(
|
||||
'auto.components.artifacts.ArtifactPublishButton.signingIn',
|
||||
'Signing in…'
|
||||
'auto.components.artifacts.ArtifactPublishButton.signInAgain',
|
||||
'Sign in again'
|
||||
)
|
||||
: authStatus?.state === 'reconnect-required'
|
||||
? translate(
|
||||
'auto.components.artifacts.ArtifactPublishButton.signInAgain',
|
||||
'Sign in again'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.artifacts.ArtifactPublishButton.signIn',
|
||||
'Sign in'
|
||||
)}
|
||||
: translate('auto.components.artifacts.ArtifactPublishButton.signIn', 'Sign in')}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -63,7 +63,7 @@ function storeState(): Record<string, unknown> {
|
||||
closeArtifactsPage: mocks.closePage,
|
||||
connectCurrentOrcaProfile: mocks.connect,
|
||||
orcaProfileAuthStatus: mocks.authStatus,
|
||||
orcaProfileConnecting: false,
|
||||
|
||||
refreshCurrentOrcaProfileAuth: mocks.refreshAuth,
|
||||
settings: mocks.settings,
|
||||
updateSettings: mocks.updateSettings,
|
||||
|
||||
@@ -20,7 +20,6 @@ const LOCAL_RUNTIME = { kind: 'local' } as const
|
||||
export default function ArtifactsPage(): React.JSX.Element {
|
||||
const closePage = useAppStore((state) => state.closeArtifactsPage)
|
||||
const authStatus = useAppStore((state) => state.orcaProfileAuthStatus)
|
||||
const connecting = useAppStore((state) => state.orcaProfileConnecting)
|
||||
const connect = useAppStore((state) => state.connectCurrentOrcaProfile)
|
||||
const refreshAuth = useAppStore((state) => state.refreshCurrentOrcaProfileAuth)
|
||||
const openSettingsPage = useAppStore((state) => state.openSettingsPage)
|
||||
@@ -192,7 +191,6 @@ export default function ArtifactsPage(): React.JSX.Element {
|
||||
) : null}
|
||||
{!signedIn ? (
|
||||
<ArtifactsPageAuthState
|
||||
connecting={connecting}
|
||||
needsReconnect={needsReconnect}
|
||||
configured={authStatus?.configured === true}
|
||||
onConnect={() => void connect()}
|
||||
|
||||
@@ -22,13 +22,11 @@ export function ArtifactsPageErrorBanner({
|
||||
}
|
||||
|
||||
export function ArtifactsPageAuthState({
|
||||
connecting,
|
||||
needsReconnect,
|
||||
configured,
|
||||
onConnect,
|
||||
onOpenAccountSettings
|
||||
}: {
|
||||
connecting: boolean
|
||||
needsReconnect: boolean
|
||||
configured: boolean
|
||||
onConnect: () => void
|
||||
@@ -62,15 +60,13 @@ export function ArtifactsPageAuthState({
|
||||
</p>
|
||||
</div>
|
||||
{configured ? (
|
||||
<Button size="sm" disabled={connecting} onClick={onConnect}>
|
||||
{connecting
|
||||
? translate('auto.components.artifacts.ArtifactsPage.signingIn', 'Signing in…')
|
||||
: needsReconnect
|
||||
? translate(
|
||||
'auto.components.artifacts.ArtifactsPage.signInAgainAction',
|
||||
'Sign in again'
|
||||
)
|
||||
: translate('auto.components.artifacts.ArtifactsPage.signIn', 'Sign in to Orca')}
|
||||
<Button size="sm" onClick={onConnect}>
|
||||
{needsReconnect
|
||||
? translate(
|
||||
'auto.components.artifacts.ArtifactsPage.signInAgainAction',
|
||||
'Sign in again'
|
||||
)
|
||||
: translate('auto.components.artifacts.ArtifactsPage.signIn', 'Sign in to Orca')}
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
|
||||
@@ -15,7 +15,6 @@ const mocks = vi.hoisted(() => ({
|
||||
configured: true,
|
||||
state: 'connected'
|
||||
} as Record<string, unknown> | null,
|
||||
orcaProfileConnecting: false,
|
||||
isWebClient: false
|
||||
}
|
||||
}))
|
||||
@@ -46,7 +45,6 @@ describe('ArtifactsSettingsPane', () => {
|
||||
mocks.fetchAuthStatus.mockReset()
|
||||
mocks.openArtifactsPage.mockReset()
|
||||
mocks.state.orcaProfileAuthStatus = { configured: true, state: 'connected' }
|
||||
mocks.state.orcaProfileConnecting = false
|
||||
mocks.state.isWebClient = false
|
||||
})
|
||||
|
||||
@@ -92,19 +90,11 @@ describe('ArtifactsSettingsPane', () => {
|
||||
expect(mocks.connect).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('shows reconnect and connecting states', () => {
|
||||
it('keeps sign in clickable while reconnect is required', () => {
|
||||
mocks.state.orcaProfileAuthStatus = { configured: true, state: 'reconnect-required' }
|
||||
const { rerender } = render(
|
||||
<ArtifactsSettingsPane settings={getDefaultSettings('/tmp')} updateSettings={vi.fn()} />
|
||||
)
|
||||
render(<ArtifactsSettingsPane settings={getDefaultSettings('/tmp')} updateSettings={vi.fn()} />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Sign in again' })).toBeEnabled()
|
||||
|
||||
mocks.state.orcaProfileConnecting = true
|
||||
rerender(
|
||||
<ArtifactsSettingsPane settings={getDefaultSettings('/tmp')} updateSettings={vi.fn()} />
|
||||
)
|
||||
expect(screen.getByRole('button', { name: 'Signing in…' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('loads missing account status and disables sign in until configured', () => {
|
||||
|
||||
@@ -18,7 +18,6 @@ export function ArtifactsSettingsPane({
|
||||
}): React.JSX.Element {
|
||||
const openArtifactsPage = useAppStore((state) => state.openArtifactsPage)
|
||||
const authStatus = useAppStore((state) => state.orcaProfileAuthStatus)
|
||||
const connecting = useAppStore((state) => state.orcaProfileConnecting)
|
||||
const connect = useAppStore((state) => state.connectCurrentOrcaProfile)
|
||||
const signedIn = authStatus?.state === 'connected'
|
||||
// Why: the capability lives in the desktop host's store and is deliberately absent from the
|
||||
@@ -128,14 +127,12 @@ export function ArtifactsSettingsPane({
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={connecting || authStatus?.configured !== true}
|
||||
disabled={authStatus?.configured !== true}
|
||||
onClick={() => void connect()}
|
||||
>
|
||||
{connecting
|
||||
? translate('auto.components.settings.artifacts.signingIn', 'Signing in…')
|
||||
: authStatus?.state === 'reconnect-required'
|
||||
? translate('auto.components.settings.artifacts.signInAgain', 'Sign in again')
|
||||
: translate('auto.components.settings.artifacts.signIn', 'Sign in to Orca')}
|
||||
{authStatus?.state === 'reconnect-required'
|
||||
? translate('auto.components.settings.artifacts.signInAgain', 'Sign in again')
|
||||
: translate('auto.components.settings.artifacts.signIn', 'Sign in to Orca')}
|
||||
</Button>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
@@ -139,7 +139,6 @@ function showDeleteFailureToast(): void {
|
||||
// progress; this surfaces it (and its status) in dev when the env vars are set.
|
||||
function OrcaCloudDevSubsection(): React.JSX.Element {
|
||||
const authStatus = useAppStore((s) => s.orcaProfileAuthStatus)
|
||||
const connecting = useAppStore((s) => s.orcaProfileConnecting)
|
||||
const connect = useAppStore((s) => s.connectCurrentOrcaProfile)
|
||||
const signOut = useAppStore((s) => s.signOutCurrentOrcaProfile)
|
||||
const refresh = useAppStore((s) => s.fetchOrcaProfileAuthStatus)
|
||||
@@ -170,23 +169,11 @@ function OrcaCloudDevSubsection(): React.JSX.Element {
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{connected ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={connecting}
|
||||
onClick={() => void signOut()}
|
||||
>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => void signOut()}>
|
||||
{translate('auto.components.settings.DevToolsPane.orcaCloudSignOut', 'Sign out')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={connecting}
|
||||
onClick={() => void connect()}
|
||||
>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => void connect()}>
|
||||
{translate(
|
||||
'auto.components.settings.DevToolsPane.orcaCloudConnect',
|
||||
'Connect profile'
|
||||
|
||||
@@ -12,7 +12,6 @@ import { MobilePairingConnectionOptions } from './MobilePairingConnectionOptions
|
||||
|
||||
type MobileRelayStoreState = {
|
||||
orcaProfileAuthStatus: OrcaProfileAuthStatus | null
|
||||
orcaProfileConnecting: boolean
|
||||
connectCurrentOrcaProfile: () => Promise<null>
|
||||
fetchOrcaProfileAuthStatus: () => Promise<OrcaProfileAuthStatus | null>
|
||||
}
|
||||
@@ -73,7 +72,6 @@ describe('MobilePairingConnectionOptions', () => {
|
||||
state: 'local',
|
||||
persistence: 'none'
|
||||
},
|
||||
orcaProfileConnecting: false,
|
||||
connectCurrentOrcaProfile: connect,
|
||||
fetchOrcaProfileAuthStatus: fetchAuthStatus
|
||||
}
|
||||
@@ -222,7 +220,6 @@ describe('MobilePairingConnectionOptions', () => {
|
||||
state: 'connected',
|
||||
persistence: 'encrypted'
|
||||
},
|
||||
orcaProfileConnecting: false,
|
||||
connectCurrentOrcaProfile: connect,
|
||||
fetchOrcaProfileAuthStatus: fetchAuthStatus
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { Badge } from '../ui/badge'
|
||||
import { Button } from '../ui/button'
|
||||
import { translate } from '../../i18n/i18n'
|
||||
@@ -66,7 +65,6 @@ export function MobilePairingConnectionOptions({
|
||||
relayMintRetrying?: boolean
|
||||
}): React.JSX.Element {
|
||||
const authStatus = useAppStore((state) => state.orcaProfileAuthStatus)
|
||||
const connecting = useAppStore((state) => state.orcaProfileConnecting)
|
||||
const connect = useAppStore((state) => state.connectCurrentOrcaProfile)
|
||||
const [relayStatus, setRelayStatus] = useState<MobileRelayStatus>('offline')
|
||||
const [relayCellUrl, setRelayCellUrl] = useState<string | undefined>(undefined)
|
||||
@@ -227,13 +225,11 @@ export function MobilePairingConnectionOptions({
|
||||
type="button"
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
disabled={connecting}
|
||||
onClick={() => {
|
||||
onChange('automatic')
|
||||
void connect()
|
||||
}}
|
||||
>
|
||||
{connecting ? <Loader2 className="animate-spin" /> : null}
|
||||
{reconnectRequired
|
||||
? translate(
|
||||
'auto.components.settings.MobilePairingConnectionOptions.signInAgain',
|
||||
|
||||
@@ -6,19 +6,27 @@ import { cleanup, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
connect: vi.fn(),
|
||||
fetchAuthStatus: vi.fn(),
|
||||
signOut: vi.fn(),
|
||||
state: {
|
||||
orcaProfileAuthStatus: {
|
||||
configured: true,
|
||||
state: 'connected',
|
||||
cloud: { displayName: 'Ada Lovelace', email: 'ada@example.com' }
|
||||
} as Record<string, unknown> | null,
|
||||
orcaProfileConnecting: false
|
||||
type MockAuthStatus = {
|
||||
configured: boolean
|
||||
state: string
|
||||
cloud?: { displayName: string; email: string }
|
||||
} | null
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const connectedAuth: MockAuthStatus = {
|
||||
configured: true,
|
||||
state: 'connected',
|
||||
cloud: { displayName: 'Ada Lovelace', email: 'ada@example.com' }
|
||||
}
|
||||
}))
|
||||
return {
|
||||
connect: vi.fn(),
|
||||
fetchAuthStatus: vi.fn(),
|
||||
signOut: vi.fn(),
|
||||
state: {
|
||||
orcaProfileAuthStatus: connectedAuth
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/i18n/i18n', () => ({
|
||||
translate: (_key: string, fallback: string) => fallback
|
||||
@@ -58,7 +66,6 @@ describe('OrcaAccountSettingsPane', () => {
|
||||
state: 'connected',
|
||||
cloud: { displayName: 'Ada Lovelace', email: 'ada@example.com' }
|
||||
}
|
||||
mocks.state.orcaProfileConnecting = false
|
||||
})
|
||||
|
||||
afterEach(cleanup)
|
||||
@@ -89,6 +96,9 @@ describe('OrcaAccountSettingsPane', () => {
|
||||
).toBeInTheDocument()
|
||||
await user.click(screen.getByRole('button', { name: 'Sign in to Orca' }))
|
||||
expect(mocks.connect).toHaveBeenCalledOnce()
|
||||
expect(screen.getByRole('button', { name: 'Sign in to Orca' })).toBeEnabled()
|
||||
await user.click(screen.getByRole('button', { name: 'Sign in to Orca' }))
|
||||
expect(mocks.connect).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('loads account status when it is not hydrated yet', () => {
|
||||
|
||||
@@ -60,7 +60,6 @@ function AccountBenefit({
|
||||
|
||||
export function OrcaAccountSettingsPane(): React.JSX.Element {
|
||||
const authStatus = useAppStore((state) => state.orcaProfileAuthStatus)
|
||||
const connecting = useAppStore((state) => state.orcaProfileConnecting)
|
||||
const connect = useAppStore((state) => state.connectCurrentOrcaProfile)
|
||||
const signOut = useAppStore((state) => state.signOutCurrentOrcaProfile)
|
||||
const [signOutOpen, setSignOutOpen] = useState(false)
|
||||
@@ -117,17 +116,10 @@ export function OrcaAccountSettingsPane(): React.JSX.Element {
|
||||
{translate('auto.components.settings.orcaAccount.signOut', 'Sign out')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={!canConnect || connecting}
|
||||
onClick={() => void connect()}
|
||||
>
|
||||
{connecting
|
||||
? translate('auto.components.settings.orcaAccount.signingIn', 'Signing in…')
|
||||
: authStatus?.state === 'reconnect-required'
|
||||
? translate('auto.components.settings.orcaAccount.signInAgain', 'Sign in again')
|
||||
: translate('auto.components.settings.orcaAccount.signIn', 'Sign in to Orca')}
|
||||
<Button type="button" size="sm" disabled={!canConnect} onClick={() => void connect()}>
|
||||
{authStatus?.state === 'reconnect-required'
|
||||
? translate('auto.components.settings.orcaAccount.signInAgain', 'Sign in again')
|
||||
: translate('auto.components.settings.orcaAccount.signIn', 'Sign in to Orca')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,6 @@ const mocks = vi.hoisted(() => ({
|
||||
string,
|
||||
unknown
|
||||
> | null,
|
||||
orcaProfileConnecting: false,
|
||||
isWebClient: false,
|
||||
settings: { showSkillsButton: false, agentSkillSharingEnabled: false }
|
||||
}
|
||||
@@ -50,7 +49,6 @@ describe('ShareSkillsSettingsPane', () => {
|
||||
mocks.openSkillsPage.mockReset()
|
||||
mocks.updateSettings.mockReset()
|
||||
mocks.state.orcaProfileAuthStatus = { configured: true, state: 'connected' }
|
||||
mocks.state.orcaProfileConnecting = false
|
||||
mocks.state.isWebClient = false
|
||||
Object.defineProperty(window, 'api', {
|
||||
configurable: true,
|
||||
|
||||
@@ -14,7 +14,6 @@ export function ShareSkillsSettingsPane(): React.JSX.Element {
|
||||
const settings = useAppStore((state) => state.settings)
|
||||
const updateSettings = useAppStore((state) => state.updateSettings)
|
||||
const authStatus = useAppStore((state) => state.orcaProfileAuthStatus)
|
||||
const connecting = useAppStore((state) => state.orcaProfileConnecting)
|
||||
const connect = useAppStore((state) => state.connectCurrentOrcaProfile)
|
||||
const signedIn = authStatus?.state === 'connected'
|
||||
const isWebClient = isWebClientLocation()
|
||||
@@ -135,14 +134,12 @@ export function ShareSkillsSettingsPane(): React.JSX.Element {
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
disabled={connecting || authStatus?.configured !== true}
|
||||
disabled={authStatus?.configured !== true}
|
||||
onClick={() => void connect()}
|
||||
>
|
||||
{connecting
|
||||
? translate('auto.components.settings.shareSkills.signingIn', 'Signing in…')
|
||||
: authStatus?.state === 'reconnect-required'
|
||||
? translate('auto.components.settings.shareSkills.signInAgain', 'Sign in again')
|
||||
: translate('auto.components.settings.shareSkills.signIn', 'Sign in to Orca')}
|
||||
{authStatus?.state === 'reconnect-required'
|
||||
? translate('auto.components.settings.shareSkills.signInAgain', 'Sign in again')
|
||||
: translate('auto.components.settings.shareSkills.signIn', 'Sign in to Orca')}
|
||||
</Button>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createTestStore } from './store-test-helpers'
|
||||
import type {
|
||||
ConnectCurrentOrcaProfileResult,
|
||||
CreateCloudLinkedOrcaProfileResult,
|
||||
@@ -9,6 +8,21 @@ import type {
|
||||
SelectOrcaProfileOrgResult,
|
||||
SignOutCurrentOrcaProfileResult
|
||||
} from '../../../../shared/orca-profiles'
|
||||
import { createTestStore } from './store-test-helpers'
|
||||
|
||||
const { toastErrorMock, toastSuccessMock } = vi.hoisted(() => ({
|
||||
toastErrorMock: vi.fn(),
|
||||
toastSuccessMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: {
|
||||
error: toastErrorMock,
|
||||
info: vi.fn(),
|
||||
success: toastSuccessMock,
|
||||
warning: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
const listState: OrcaProfileListState = {
|
||||
activeProfileId: 'local-default',
|
||||
@@ -73,6 +87,8 @@ const orcaProfilesApi = {
|
||||
describe('orca profile auth actions slice', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
toastErrorMock.mockReset()
|
||||
toastSuccessMock.mockReset()
|
||||
orcaProfilesApi.authStatus.mockResolvedValue(localAuthStatus)
|
||||
vi.stubGlobal('window', {
|
||||
api: {
|
||||
@@ -98,13 +114,51 @@ describe('orca profile auth actions slice', () => {
|
||||
orcaProfilesApi.connectCurrent.mockResolvedValue(result)
|
||||
const store = createTestStore()
|
||||
|
||||
const pending = store.getState().connectCurrentOrcaProfile()
|
||||
|
||||
expect(store.getState().orcaProfileConnecting).toBe(true)
|
||||
await expect(pending).resolves.toEqual(result)
|
||||
expect(store.getState().orcaProfileConnecting).toBe(false)
|
||||
await expect(store.getState().connectCurrentOrcaProfile()).resolves.toEqual(result)
|
||||
expect(store.getState().orcaProfileAuthStatus).toEqual(connectedAuthStatus)
|
||||
expect(store.getState().orcaProfiles).toEqual(connectedProfiles)
|
||||
expect(toastSuccessMock).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('starts a second sign-in while the first browser wait is still open', async () => {
|
||||
const connectedProfiles = [
|
||||
{
|
||||
...listState.profiles[0],
|
||||
kind: 'cloud-linked' as const,
|
||||
cloud: connectedAuthStatus.cloud
|
||||
}
|
||||
]
|
||||
const connected: ConnectCurrentOrcaProfileResult = {
|
||||
status: 'connected',
|
||||
auth: connectedAuthStatus,
|
||||
activeProfileId: 'local-default',
|
||||
profiles: connectedProfiles
|
||||
}
|
||||
const cancelled: ConnectCurrentOrcaProfileResult = {
|
||||
status: 'cancelled',
|
||||
auth: connectedAuthStatus
|
||||
}
|
||||
let finishFirst!: (value: ConnectCurrentOrcaProfileResult) => void
|
||||
orcaProfilesApi.connectCurrent
|
||||
.mockReturnValueOnce(
|
||||
new Promise<ConnectCurrentOrcaProfileResult>((resolve) => {
|
||||
finishFirst = resolve
|
||||
})
|
||||
)
|
||||
.mockResolvedValueOnce(connected)
|
||||
const store = createTestStore()
|
||||
|
||||
const first = store.getState().connectCurrentOrcaProfile()
|
||||
const second = store.getState().connectCurrentOrcaProfile()
|
||||
|
||||
expect(orcaProfilesApi.connectCurrent).toHaveBeenCalledTimes(2)
|
||||
await expect(second).resolves.toEqual(connected)
|
||||
expect(toastSuccessMock).toHaveBeenCalledOnce()
|
||||
finishFirst(cancelled)
|
||||
await expect(first).resolves.toEqual(cancelled)
|
||||
expect(toastErrorMock).not.toHaveBeenCalled()
|
||||
expect(toastSuccessMock).toHaveBeenCalledOnce()
|
||||
expect(store.getState().orcaProfileAuthStatus).toEqual(connectedAuthStatus)
|
||||
})
|
||||
|
||||
it('refreshes current profile auth and stores fresh capability flags', async () => {
|
||||
|
||||
@@ -70,14 +70,12 @@ export const createOrcaProfilesAuthActions: StateCreator<
|
||||
},
|
||||
|
||||
connectCurrentOrcaProfile: async () => {
|
||||
if (get().orcaProfileConnecting) {
|
||||
return null
|
||||
}
|
||||
set({ orcaProfileConnecting: true })
|
||||
try {
|
||||
// Why: a pending browser callback must not block retry. Another click
|
||||
// starts a second PKCE wait; the earlier tab can still complete.
|
||||
const alreadyConnected = get().orcaProfileAuthStatus?.state === 'connected'
|
||||
const result = await window.api.orcaProfiles.connectCurrent()
|
||||
set({
|
||||
orcaProfileConnecting: false,
|
||||
orcaProfileAuthStatus: result.auth,
|
||||
...(result.status === 'connected'
|
||||
? {
|
||||
@@ -96,24 +94,29 @@ export const createOrcaProfilesAuthActions: StateCreator<
|
||||
description: result.auth.setupMessage
|
||||
}
|
||||
)
|
||||
} else if (result.status === 'failed') {
|
||||
} else if (
|
||||
result.status === 'failed' &&
|
||||
!alreadyConnected &&
|
||||
result.auth.state !== 'connected'
|
||||
) {
|
||||
toast.error(
|
||||
translate('auto.store.slices.orca.profiles.33290e88ed', 'Failed to connect profile'),
|
||||
{ description: result.error }
|
||||
)
|
||||
} else if (result.status === 'connected') {
|
||||
} else if (result.status === 'connected' && !alreadyConnected) {
|
||||
toast.success(translate('auto.store.slices.orca.profiles.9fcb07a796', 'Profile connected'))
|
||||
}
|
||||
return result
|
||||
} catch (err) {
|
||||
console.error('Failed to connect Orca profile:', err)
|
||||
set({ orcaProfileConnecting: false })
|
||||
toast.error(
|
||||
translate('auto.store.slices.orca.profiles.33290e88ed', 'Failed to connect profile'),
|
||||
{
|
||||
description: err instanceof Error ? err.message : String(err)
|
||||
}
|
||||
)
|
||||
if (get().orcaProfileAuthStatus?.state !== 'connected') {
|
||||
toast.error(
|
||||
translate('auto.store.slices.orca.profiles.33290e88ed', 'Failed to connect profile'),
|
||||
{
|
||||
description: err instanceof Error ? err.message : String(err)
|
||||
}
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
@@ -21,7 +21,6 @@ export type OrcaProfilesSlice = OrcaProfilesAuthActions & {
|
||||
orcaProfilesMultiProfileUi: boolean
|
||||
orcaProfilesLoading: boolean
|
||||
orcaProfileSwitching: boolean
|
||||
orcaProfileConnecting: boolean
|
||||
fetchOrcaProfiles: () => Promise<void>
|
||||
fetchOrcaProfileAuthStatus: () => Promise<OrcaProfileAuthStatus | null>
|
||||
createLocalOrcaProfile: (name?: string) => Promise<OrcaProfileSummary | null>
|
||||
@@ -42,7 +41,6 @@ export const createOrcaProfilesSlice: StateCreator<AppState, [], [], OrcaProfile
|
||||
orcaProfilesMultiProfileUi: false,
|
||||
orcaProfilesLoading: false,
|
||||
orcaProfileSwitching: false,
|
||||
orcaProfileConnecting: false,
|
||||
|
||||
fetchOrcaProfiles: async () => {
|
||||
set({ orcaProfilesLoading: true })
|
||||
|
||||
Reference in New Issue
Block a user