diff --git a/apps/mobile/src/components/organization/organization-boundary.mounted.test.tsx b/apps/mobile/src/components/organization/organization-boundary.mounted.test.tsx index d62026d323a..04c7246be84 100644 --- a/apps/mobile/src/components/organization/organization-boundary.mounted.test.tsx +++ b/apps/mobile/src/components/organization/organization-boundary.mounted.test.tsx @@ -2,10 +2,11 @@ // to four distinct copies. An account with no memberships gets the // no-organization state whose action opens the web organizations page; // memberships with nothing selected get the picker state whose action opens -// the context picker sheet; a deep-link override absent from the member list -// is access-denied; a fetch failure is a retryable QueryError; a still -// resolving context is a spinner. A stale persisted selection (no override) -// keeps the older "unavailable" copy. +// the context picker sheet; a deep-link override absent from a NON-empty +// member list is access-denied, while the same override against zero +// memberships is the no-organization state; a fetch failure is a retryable +// QueryError; a still resolving context is a spinner. A stale persisted +// selection (no override) keeps the older "unavailable" copy. import { type ComponentType, createElement, type ReactNode } from 'react'; import { act, TestRenderer } from '@/test/renderer'; @@ -189,9 +190,13 @@ describe('OrganizationBoundary settled states', () => { }); }); - it('renders access-denied when an override org is absent from the member list', () => { + it('renders access-denied when an override org is absent from a non-empty member list', () => { useOrgBoundaryMock.mockReturnValue( - boundaryState({ organizationId: 'org-missing', org: undefined }) + boundaryState({ + organizationId: 'org-missing', + org: undefined, + orgs: [{ organizationId: 'org-1', organizationName: 'Organization' }], + }) ); const renderer = mountBoundary('org-missing'); @@ -241,6 +246,38 @@ describe('OrganizationBoundary settled states', () => { }); }); + it('offers the web organizations page for a deep-link override with no memberships', () => { + useOrgBoundaryMock.mockReturnValue( + boundaryState({ organizationId: 'org-linked', org: undefined, orgs: [] }) + ); + + const renderer = mountBoundary('org-linked'); + const texts = collectText(renderer.toJSON()); + + expect(findByType(renderer.root, 'EmptyStateMock')).toHaveLength(1); + expect(texts).toContain('No organization'); + expect(texts).toContain('Open organizations on web'); + expect(texts).toContain('Back to profile'); + expect(texts).not.toContain('Access denied'); + expect(texts).not.toContain('Select an organization'); + + const webButton = renderer.root.find( + node => + String(node.type) === 'ButtonMock' && + collectText(node).includes('Open organizations on web') + ); + act(() => { + (webButton.props.onPress as (() => void) | undefined)?.(); + }); + expect(openExternalUrlMock).toHaveBeenCalledWith('https://cloud.example.test/organizations', { + label: 'Open organizations on web', + }); + + act(() => { + renderer.unmount(); + }); + }); + it('opens the context picker when memberships exist but nothing is selected', () => { useOrgBoundaryMock.mockReturnValue( boundaryState({ diff --git a/apps/mobile/src/components/organization/organization-boundary.tsx b/apps/mobile/src/components/organization/organization-boundary.tsx index 32ea9fde568..1bece5bc650 100644 --- a/apps/mobile/src/components/organization/organization-boundary.tsx +++ b/apps/mobile/src/components/organization/organization-boundary.tsx @@ -42,8 +42,11 @@ type OrganizationBoundaryProps = Readonly<{ * to profile stays as the plain exit. * - memberships exist but nothing is selected → the action opens the * context picker sheet, the same account switcher the composer uses. - * - a deep-link override absent from the list is access-denied; a stale - * persisted selection keeps the older "unavailable" copy. + * - a deep-link override absent from a non-empty member list is + * access-denied; a stale persisted selection keeps the older + * "unavailable" copy. An absent override with zero memberships is the + * no-organization state above, not access-denied: the account has no + * organization to be denied access to. * Calls `useOrgBoundary()` itself — cheap, context/query-cache backed — so * callers only need to pass a `title` for full screens (sheets omit it). */ @@ -100,8 +103,7 @@ export function OrganizationBoundary({ ); const hasOrganizations = (orgs?.length ?? 0) > 0; - const isAbsentOverride = organizationIdOverride != null && org == null; - if (!hasOrganizations && !isAbsentOverride) { + if (!hasOrganizations) { const openWebOrganizations = () => { void openExternalUrl(`${WEB_BASE_URL}/organizations`, { label: t('organization.boundary.openWebOrganizations'),