From 0c19ad848792aee0c5ac182badd1185cb6a8690b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= <4062236+iscekic@users.noreply.github.com> Date: Thu, 22 Oct 2026 10:13:38 +0000 Subject: [PATCH 1/2] fix(mobile): show the no-organization state for deep links without memberships An account with no memberships that opened a shared organization link carrying an explicit org id landed in access-denied with only a Back to profile action. The settled-branch order gave an absent deep-link override precedence over the empty membership list, so the no-organization state, which offers the web organizations page where create and join live, was skipped. The boundary now decides the no-organization state first, from the membership list alone. Access denied keeps its own case: an override absent from a non-empty member list, where the account does have organizations but not that one. Regression tests cover the deep-link zero-membership case and pin the access-denied case to a non-empty member list. Verified on the Android emulator with a test account that has no memberships: the deep link renders the no-organization state and its action opens the web organizations page. --- .../organization-boundary.mounted.test.tsx | 49 ++++++++++++++++--- .../organization/organization-boundary.tsx | 10 ++-- 2 files changed, 49 insertions(+), 10 deletions(-) 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'), From ca54f04d7a2e97c7e28cc9c8649c4a44ac1001fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= <4062236+iscekic@users.noreply.github.com> Date: Thu, 22 Oct 2026 10:30:36 +0000 Subject: [PATCH 2/2] chore(mobile): request review after a failed kilo review run The previous Kilo Code Review check ended with an environment preparation timeout before any review ran, so no verdict was recorded for this head. This empty commit re-requests the review without changing the tree.