diff --git a/src/renderer/src/components/Landing.tsx b/src/renderer/src/components/Landing.tsx index 277d7bcd3d9..87b6a076a32 100644 --- a/src/renderer/src/components/Landing.tsx +++ b/src/renderer/src/components/Landing.tsx @@ -1,4 +1,5 @@ import { LandingCloudCard } from './landing/LandingCloudCard' +import { LandingCloudSignInCard, useSignedOutOfOrcaCloud } from './landing/LandingCloudSignInCard' import { CloudWorkspacesHome, useCloudWorkspacesPresent @@ -225,6 +226,10 @@ export default function Landing(): React.JSX.Element { const cloudListPresent = useCloudWorkspacesPresent() const repos = useAppStore((s) => s.repos) const openModal = useAppStore((s) => s.openModal) + const signedOutOfCloud = useSignedOutOfOrcaCloud() + const reconnectRequired = useAppStore( + (s) => s.orcaProfileAuthStatus?.state === 'reconnect-required' + ) const createActionLabel = repos.length > 0 && repos.every((repo) => isGitRepoKind(repo)) @@ -298,7 +303,12 @@ export default function Landing(): React.JSX.Element { onCreate={() => openModal('new-workspace-composer', { telemetrySource: 'unknown' })} /> - {hasProjects ? : null} + {/* V3-08: signed out, the team's cloud workspaces can't be listed; say so where they'd be. */} + {hasProjects && signedOutOfCloud && !cloudListPresent ? ( + + ) : hasProjects ? ( + + ) : null} {!cloudListPresent && } diff --git a/src/renderer/src/components/cloud-workspaces/use-cloud-workspaces.ts b/src/renderer/src/components/cloud-workspaces/use-cloud-workspaces.ts index 0ffa89ecbf5..072497ba928 100644 --- a/src/renderer/src/components/cloud-workspaces/use-cloud-workspaces.ts +++ b/src/renderer/src/components/cloud-workspaces/use-cloud-workspaces.ts @@ -65,6 +65,13 @@ export function cloudWorkspacesAvailable(): Promise { return availability } +/** Fixtures only: forget the cached answer so the next probe reads the stubbed bridge. */ +export function resetCloudWorkspacesAvailabilityForFixtures(): void { + availability = null + availableValue = null + availabilityListeners.forEach((listener) => listener()) +} + /** null until the first availability answer arrives. */ export function useCloudWorkspacesAvailable(): boolean | null { return useSyncExternalStore( diff --git a/src/renderer/src/components/landing/LandingCloudSignIn.fixture.tsx b/src/renderer/src/components/landing/LandingCloudSignIn.fixture.tsx new file mode 100644 index 00000000000..80c3a444514 --- /dev/null +++ b/src/renderer/src/components/landing/LandingCloudSignIn.fixture.tsx @@ -0,0 +1,77 @@ +// V3-08: Home signed out of Orca. Cloud builds with projects show the "Cloud workspaces · Sign in" +// card where the list would be; without projects, and in local builds, Home is unchanged. +import type { Repo } from '../../../../shared/repo-types' +import type { OrcaProfileAuthStatus } from '../../../../shared/orca-profiles' +import { defineFixture } from '../../component-fixtures/component-fixture' +import { resetCloudWorkspacesAvailabilityForFixtures } from '../cloud-workspaces/use-cloud-workspaces' +import { makeRepo } from '../worktree-jump/worktree-jump-palette-test-fixtures' +import Landing from '../Landing' + +const repo: Repo = { ...makeRepo(), displayName: 'orca' } +const auth = ( + configured: boolean, + state: OrcaProfileAuthStatus['state'] +): OrcaProfileAuthStatus => ({ activeProfileId: 'default', configured, state, persistence: 'none' }) +const signedOut = auth(true, 'local') +const sessionEnded = auth(true, 'reconnect-required') +const localBuild = auth(false, 'unconfigured') + +const build = (cloud: boolean) => (): void => { + window.api.cloudWorkspaces = { ...window.api.cloudWorkspaces, available: async () => cloud } + resetCloudWorkspacesAvailabilityForFixtures() +} +// A healthy machine, so the shots show only Home's cloud area. +const healthy = { + preflightStatus: { + git: { installed: true }, + gh: { installed: true, authenticated: true }, + glab: { installed: true, authenticated: true } + }, + refreshPreflightStatus: async () => {} +} + +const home = (): React.JSX.Element => ( +
+ +
+) + +/** Cloud build, signed out, has projects: the sign-in card. */ +export const cloudSignedOutWithProjects = defineFixture({ + width: 760, + setup: build(true), + store: { ...healthy, repos: [repo], orcaProfileAuthStatus: signedOut }, + render: home +}) + +/** Cloud build, the session ended: same card, "Sign in again". */ +export const cloudSessionEndedWithProjects = defineFixture({ + width: 760, + setup: build(true), + store: { ...healthy, repos: [repo], orcaProfileAuthStatus: sessionEnded }, + render: home +}) + +/** Cloud build, signed out, no projects: unchanged (the welcome screen is V3-05, not built). */ +export const cloudSignedOutNoProjects = defineFixture({ + width: 760, + setup: build(true), + store: { ...healthy, repos: [], orcaProfileAuthStatus: signedOut }, + render: home +}) + +/** Local build with projects: unchanged, no cloud card. */ +export const localBuildWithProjects = defineFixture({ + width: 760, + setup: build(false), + store: { ...healthy, repos: [repo], orcaProfileAuthStatus: localBuild }, + render: home +}) + +/** Local build, no projects: unchanged. */ +export const localBuildNoProjects = defineFixture({ + width: 760, + setup: build(false), + store: { ...healthy, repos: [], orcaProfileAuthStatus: localBuild }, + render: home +}) diff --git a/src/renderer/src/components/landing/LandingCloudSignInCard.test.ts b/src/renderer/src/components/landing/LandingCloudSignInCard.test.ts new file mode 100644 index 00000000000..e79c9ca7796 --- /dev/null +++ b/src/renderer/src/components/landing/LandingCloudSignInCard.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest' +import type { OrcaProfileAuthStatus } from '../../../../shared/orca-profiles' +import { isSignedOutOfOrcaCloud } from './LandingCloudSignInCard' + +const auth = ( + configured: boolean, + state: OrcaProfileAuthStatus['state'] +): OrcaProfileAuthStatus => ({ activeProfileId: 'default', configured, state, persistence: 'none' }) + +describe('isSignedOutOfOrcaCloud', () => { + it('holds in a cloud build when never signed in or the session ended', () => { + expect(isSignedOutOfOrcaCloud(true, auth(true, 'local'))).toBe(true) + expect(isSignedOutOfOrcaCloud(true, auth(true, 'reconnect-required'))).toBe(true) + }) + + it('does not hold when signed in', () => { + expect(isSignedOutOfOrcaCloud(true, auth(true, 'connected'))).toBe(false) + }) + + it('does not hold in a local build or before either answer arrives', () => { + expect(isSignedOutOfOrcaCloud(false, auth(true, 'local'))).toBe(false) + expect(isSignedOutOfOrcaCloud(null, auth(true, 'local'))).toBe(false) + expect(isSignedOutOfOrcaCloud(true, auth(false, 'unconfigured'))).toBe(false) + expect(isSignedOutOfOrcaCloud(true, null)).toBe(false) + }) +}) diff --git a/src/renderer/src/components/landing/LandingCloudSignInCard.tsx b/src/renderer/src/components/landing/LandingCloudSignInCard.tsx new file mode 100644 index 00000000000..df6d8ddaf8d --- /dev/null +++ b/src/renderer/src/components/landing/LandingCloudSignInCard.tsx @@ -0,0 +1,68 @@ +// V3-08: signed out of Orca in a build that offers Orca Cloud, Home says where the team's cloud +// workspaces went and how to see them, in the place the cloud list would be. Local builds and +// signed-in people never see it. While it shows, it stands in for the trial card (one cloud card). +import React from 'react' +import { Cloud } from 'lucide-react' +import { useAppStore } from '@/store' +import { useTranslate } from '@/i18n/use-translate' +import { useCloudWorkspacesAvailable } from '@/components/cloud-workspaces/use-cloud-workspaces' +import type { OrcaProfileAuthStatus } from '../../../../shared/orca-profiles' +import { OrcaSignInButton, OrcaSignInWaitNotice } from '../OrcaSignInProgress' + +/** Signed out of an Orca account this build can sign in to: never signed in, or the session ended. */ +export function isSignedOutOfOrcaCloud( + cloudAvailable: boolean | null, + status: OrcaProfileAuthStatus | null | undefined +): boolean { + return ( + cloudAvailable === true && + status?.configured === true && + (status.state === 'local' || status.state === 'reconnect-required') + ) +} + +export function useSignedOutOfOrcaCloud(): boolean { + const available = useCloudWorkspacesAvailable() + const status = useAppStore((s) => s.orcaProfileAuthStatus) + return isSignedOutOfOrcaCloud(available, status) +} + +/** Render only when `useSignedOutOfOrcaCloud()` holds (Landing decides; fixtures render it directly). */ +export function LandingCloudSignInCard({ + reconnect +}: { + /** The session ended (`reconnect-required`), as opposed to never signed in. */ + reconnect: boolean +}): React.JSX.Element { + const t = useTranslate() + return ( +
+
+ +
+

+ {t('components.landing.cloudSignIn.title', 'Cloud workspaces')} +

+

+ {t( + 'components.landing.cloudSignIn.body', + 'Sign in to see your team’s cloud workspaces.' + )} +

+
+ + {reconnect + ? t('components.landing.cloudSignIn.again', 'Sign in again') + : t('components.landing.cloudSignIn.action', 'Sign in')} + + +
+
+
+
+ ) +} diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 25f74ebb1b1..5497446e552 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -24311,6 +24311,12 @@ "planBody": "Agents keep running while your laptop sleeps.", "trialBody": "Agents keep running while your laptop sleeps. No card needed.", "try": "Try Orca Cloud" + }, + "cloudSignIn": { + "title": "Cloud workspaces", + "body": "Sign in to see your team’s cloud workspaces.", + "again": "Sign in again", + "action": "Sign in" } }, "cloudWorkspaces": {