From b8ba8a99063e1800c2836b30b18a6e1f60928a79 Mon Sep 17 00:00:00 2001 From: Noel Chou Date: Fri, 17 Jul 2026 17:18:00 -0400 Subject: [PATCH 1/2] TT-7549 fix: show Switch Teams button for first-time mobile users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Switch Teams button was hidden for any user whose only team is their personal team (teams.length === 0), which incorrectly caught normal first-time mobile users who have not yet joined a team — leaving them unable to reach Add Team or access Team projects. Detect true Work-Alone-Offline / PAP-like users via the existing `offlineOnly` global instead of empty team list alone. Extract the shared check into a `useIsPapLike` hook so ProjectsScreen and the SwitchTeamsGuard in SwitchTeams stay in sync. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/renderer/src/routes/ProjectsScreen.tsx | 17 ++++----------- src/renderer/src/routes/SwitchTeams.tsx | 13 +++-------- src/renderer/src/utils/useIsPapLike.ts | 25 ++++++++++++++++++++++ 3 files changed, 32 insertions(+), 23 deletions(-) create mode 100644 src/renderer/src/utils/useIsPapLike.ts diff --git a/src/renderer/src/routes/ProjectsScreen.tsx b/src/renderer/src/routes/ProjectsScreen.tsx index 2e9e5978..b83e4950 100644 --- a/src/renderer/src/routes/ProjectsScreen.tsx +++ b/src/renderer/src/routes/ProjectsScreen.tsx @@ -16,6 +16,7 @@ import { remoteId, defaultWorkflow, useTeamWorkflowProcess } from '../crud'; import { UnsavedContext } from '../context/UnsavedContext'; import BigDialog from '../hoc/BigDialog'; import { StepEditor } from '../components/StepEditor'; +import { useIsPapLike } from '../utils/useIsPapLike'; interface ProjectBoxProps extends BoxProps { isMobile?: boolean; @@ -32,14 +33,8 @@ export const ProjectsScreenInner: React.FC = () => { const navigate = useMyNavigate(); const teamId = localStorage.getItem(localUserKey(LocalKey.team)); const ctx = React.useContext(TeamContext); - const { - teamProjects, - personalProjects, - personalTeam, - teams, - teamDirectoryReady, - isAdmin, - } = ctx.state; + const { teamProjects, personalProjects, personalTeam, teams, isAdmin } = + ctx.state; const t: ICardsStrings = useSelector(cardsSelector, shallowEqual); const { pathname } = useLocation(); const [plan] = useGlobal('plan'); @@ -74,11 +69,7 @@ export const ProjectsScreenInner: React.FC = () => { }, []); const isPersonal = teamId === personalTeam; - const isPapLike = - Boolean(personalTeam) && - isPersonal && - teams.length === 0 && - teamDirectoryReady; + const isPapLike = useIsPapLike() && isPersonal; const projects = React.useMemo( () => (isPersonal ? personalProjects : teamId ? teamProjects(teamId) : []), [isPersonal, personalProjects, teamId, teamProjects] diff --git a/src/renderer/src/routes/SwitchTeams.tsx b/src/renderer/src/routes/SwitchTeams.tsx index ab335524..a8f5b691 100644 --- a/src/renderer/src/routes/SwitchTeams.tsx +++ b/src/renderer/src/routes/SwitchTeams.tsx @@ -26,6 +26,7 @@ import { useGlobal } from '../context/useGlobal'; import { useTeamActions } from '../components/Team/useTeamActions'; import { SharedContentCreatorDialog } from '../components/Team/SharedContentCreatorDialog'; import StickyRedirect from '../components/StickyRedirect'; +import { useIsPapLike } from '../utils/useIsPapLike'; interface ISettingsButtonProps { label: string; @@ -404,16 +405,8 @@ export const SwitchTeamsGuard: React.FC<{ children: React.ReactNode }> = ({ children, }) => { const ctx = React.useContext(TeamContext); - const { teams, personalTeam, teamDirectoryReady } = ctx.state; - const [isOffline] = useGlobal('offline'); - const [offlineOnly] = useGlobal('offlineOnly'); - // When offline && !offlineOnly, getTeams() may be empty because shared teams - // without local projects are filtered out — not the same as true PAP-only. - const isPapLike = - Boolean(personalTeam) && - teams.length === 0 && - teamDirectoryReady && - (!isOffline || offlineOnly); + const { personalTeam } = ctx.state; + const isPapLike = useIsPapLike(); React.useEffect(() => { if (!isPapLike || !personalTeam) return; diff --git a/src/renderer/src/utils/useIsPapLike.ts b/src/renderer/src/utils/useIsPapLike.ts new file mode 100644 index 00000000..13bb8032 --- /dev/null +++ b/src/renderer/src/utils/useIsPapLike.ts @@ -0,0 +1,25 @@ +import React from 'react'; +import { TeamContext } from '../context/TeamContext'; +import { useGlobal } from '../context/useGlobal'; + +/** + * True when the signed-in user is Work-Alone-Offline / PAP-like: only a personal + * team exists and we can trust that emptiness. Detect "Work Alone Offline" via the + * `offlineOnly` global — NOT `teams.length === 0` alone, which is also true for a + * normal user who simply hasn't joined a team yet. + * + * When offline && !offlineOnly, getTeams() may be empty because shared teams + * without local projects are filtered out — not the same as true PAP-only. + */ +export const useIsPapLike = (): boolean => { + const ctx = React.useContext(TeamContext); + const { teams, personalTeam, teamDirectoryReady } = ctx.state; + const [isOffline] = useGlobal('offline'); + const [offlineOnly] = useGlobal('offlineOnly'); + return ( + Boolean(personalTeam) && + teams.length === 0 && + teamDirectoryReady && + (!isOffline || offlineOnly) + ); +}; From 880d1b3261913c030a74a3373429cdcf49ac1297 Mon Sep 17 00:00:00 2001 From: Noel Chou Date: Fri, 17 Jul 2026 19:04:17 -0400 Subject: [PATCH 2/2] TT-7549 fix: detect Work-Alone-Offline via offlineOnly, not connectivity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shared useIsPapLike hook gated on (!isOffline || offlineOnly), which is true for any online user — so a first-time online mobile user (no teams yet) was still classified PAP-like and lost the Switch Teams button. Use the offlineOnly global alone, the reliable local-only signal set at login, and drop the isOffline dependency (which was also undefined during transient default-context renders). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/renderer/src/utils/useIsPapLike.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/renderer/src/utils/useIsPapLike.ts b/src/renderer/src/utils/useIsPapLike.ts index 13bb8032..ae1e614a 100644 --- a/src/renderer/src/utils/useIsPapLike.ts +++ b/src/renderer/src/utils/useIsPapLike.ts @@ -3,23 +3,19 @@ import { TeamContext } from '../context/TeamContext'; import { useGlobal } from '../context/useGlobal'; /** - * True when the signed-in user is Work-Alone-Offline / PAP-like: only a personal - * team exists and we can trust that emptiness. Detect "Work Alone Offline" via the - * `offlineOnly` global — NOT `teams.length === 0` alone, which is also true for a - * normal user who simply hasn't joined a team yet. + * True when the signed-in user is Work-Alone-Offline / PAP-like * - * When offline && !offlineOnly, getTeams() may be empty because shared teams - * without local projects are filtered out — not the same as true PAP-only. + * Detect "Work Alone Offline" via the `offlineOnly` global — `teams.length + * === 0` alone is not enough as it is also true for a normal user who simply hasn't joined a team yet. + * And not `!offline` (an online first-time user is online but still needs Switch + * Teams / Add Team). `offlineOnly` is set at login for users with no remoteId + * (see Access.tsx) and is the single reliable signal. */ export const useIsPapLike = (): boolean => { const ctx = React.useContext(TeamContext); const { teams, personalTeam, teamDirectoryReady } = ctx.state; - const [isOffline] = useGlobal('offline'); const [offlineOnly] = useGlobal('offlineOnly'); - return ( - Boolean(personalTeam) && - teams.length === 0 && - teamDirectoryReady && - (!isOffline || offlineOnly) + return Boolean( + personalTeam && teams.length === 0 && teamDirectoryReady && offlineOnly ); };