Skip to content

feat(mobile): random mobile progress#1349

Merged
saddlepaddle merged 9 commits into
mainfrom
implement-mobie
Feb 9, 2026
Merged

feat(mobile): random mobile progress#1349
saddlepaddle merged 9 commits into
mainfrom
implement-mobie

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Feb 9, 2026

Summary

  • Adds a custom floating tab bar as a local Expo Module (@superset/tab-bar) built with SwiftUI
  • Replaces the standard NativeTabs with headless Tabs from expo-router/ui + a native SwiftUI view bridge
  • Linear-style glass capsule tab bar with spring-animated expand/collapse menu
  • Includes org switcher pill, settings gear, search circle, and placeholder menu actions (Views, Customize)

Architecture

Native layer (modules/tab-bar/ios/):

  • TabBarView.swift — Root SwiftUI view with expand/collapse state machine
  • CollapsedBarView.swift — Glass capsule with tab icons (Home, Tasks, More)
  • ExpandedMenuView.swift — Menu list + org switcher + settings gear
  • GlassCircleButton.swift — Reusable glass circle button (search, settings)
  • TabBarProps.swift — Props + EventDispatchers for JS bridge

JS layer:

  • TabBarView.tsxrequireNativeView("TabBar") wrapped in <Host> from @expo/ui/swift-ui
  • AuthenticatedTabBar.tsx — Bridges native view to useTabTrigger from expo-router/ui
  • Dynamic container sizing: 96px when collapsed, full-screen when expanded (communicated via onExpandedChange event)

Test plan

  • npx expo prebuild --clean && npx expo run:ios from apps/mobile/
  • App loads, sign in works
  • Collapsed: glass capsule at bottom with Home/Tasks/More icons + search circle
  • Active tab icon highlighted
  • Tap ellipsis → menu expands with spring animation
  • Expanded: org switcher pill + settings gear above, menu list below, search stays
  • Tap tab in menu → switches screen, menu closes
  • Tap outside expanded menu → dismisses
  • Settings gear → pushes settings screen
  • Tab stacks preserve navigation state when switching

Summary by CodeRabbit

  • New Features

    • Tab-based mobile navigation with native tab bar and expanded menu
    • Social sign-in (GitHub/Google) and streamlined sign-in screen
    • Organization switcher, workspaces, tasks, workspace & task detail screens
    • Settings and More menu, sign-out flow, themed dark mode
  • Refactor

    • Mobile layout reworked to Tabs; many authenticated screens reorganized
  • Chores

    • Bumped React ecosystem (React/React Native/Expo-related) and @types/react versions

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 9, 2026

📝 Walkthrough

Walkthrough

Updates React/react-dom to 19.2.0 and @types/react to ~19.2.2 across packages; upgrades the mobile app to Expo 55 and restructures mobile navigation to tabs; adds a native iOS TabBar Expo module with a React Native bridge; introduces many mobile screens, hooks, and organization-switching UI; removes legacy demo/home screens.

Changes

Cohort / File(s) Summary
React & Type Definitions
apps/admin/package.json, apps/api/package.json, apps/cli/package.json, apps/desktop/package.json, apps/docs/package.json, apps/marketing/package.json, apps/web/package.json, packages/durable-session/package.json, packages/email/package.json, packages/ui/package.json
Bumped react and react-dom to 19.2.0 and @types/react to ~19.2.2 across the repo.
Mobile: Expo & deps
apps/mobile/package.json
Major Expo SDK upgrade to 55-era packages; RN -> 0.83.1; added/updated many Expo/RN libraries; changed start script to dev.
Mobile: App config & toolchain
apps/mobile/app.config.ts, apps/mobile/metro.config.js, apps/mobile/tsconfig.json, apps/mobile/global.d.ts
Switched UI style to dark, removed newArch/edgeToEdge flags, added metro alias for @superset/tab-bar, added TS path alias and SVG declarations.
Mobile: Root & auth routing
apps/mobile/app/index.tsx, apps/mobile/app/(auth)/sign-in.tsx, apps/mobile/screens/RootLayout/RootLayout.tsx
Removed root redirect file; simplified sign-in export; introduced ThemeProvider + session-protected routing and session-loading handling.
Mobile: Navigation layout
apps/mobile/app/(authenticated)/_layout.tsx, apps/mobile/app/(authenticated)/(home)/_layout.tsx, apps/mobile/app/(authenticated)/(more)/_layout.tsx, apps/mobile/app/(authenticated)/(tasks)/_layout.tsx
Replaced stack-based authenticated layout with tabs and added nested layouts for home, tasks, and more.
Mobile: Route re-exports & removals
apps/mobile/app/(authenticated)/index.tsx, apps/mobile/app/(authenticated)/demo.tsx, apps/mobile/app/index.tsx
Removed legacy HomeScreen/DemoScreen re-exports and deleted root index redirect component.
Mobile: Screens (home, tasks, more, workspaces, workspace detail, task detail)
apps/mobile/screens/(authenticated)/..., apps/mobile/screens/(auth)/sign-in/... (many new files)
Added WorkspacesScreen, TasksScreen, TaskDetailScreen, WorkspaceDetailScreen, MoreMenuScreen, SettingsScreen, redesigned SignInScreen, and related component re-exports.
Mobile: Auth & org hooks
apps/mobile/hooks/useSignOut/*, apps/mobile/hooks/useTheme/*, apps/mobile/screens/(authenticated)/hooks/useOrganizations/*
Added useSignOut (clears query cache, navigates to sign-in), useTheme, and useOrganizations (fetch + switch org).
Mobile: Organization UI components
apps/mobile/components/..., apps/mobile/screens/(authenticated)/.../components/... (OrganizationHeaderButton, OrganizationSwitcherSheet, OrganizationAvatar, OrgDropdown, OrganizationSwitcher removed)
Added header button, bottom-sheet switcher, avatar component, org dropdown; removed legacy OrganizationSwitcher component and its re-export.
Mobile: Tab bar components & accessory
apps/mobile/screens/(authenticated)/components/AuthenticatedTabBar/*, apps/mobile/screens/(authenticated)/components/TabBarAccessory/*
Added AuthenticatedTabBar and TabBarAccessory to integrate tab bar, menu actions, expanded/collapsed states, and org switching flows.
Native Tab Bar Expo module (iOS)
apps/mobile/modules/tab-bar/package.json, .../expo-module.config.json, ios/TabBar.podspec, ios/TabBarModule.swift, ios/TabBarProps.swift, ios/TabBarView.swift, ios/CollapsedBarView.swift, ios/ExpandedMenuView.swift, ios/GlassCircleButton.swift
Introduced @superset/tab-bar native module with SwiftUI TabBarView, props and models, collapsed/expanded views, podspec and expo module config.
React Native bridge for TabBar
apps/mobile/modules/tab-bar/src/TabBarView.tsx, apps/mobile/modules/tab-bar/src/TabBarView.types.ts, apps/mobile/modules/tab-bar/src/index.tsx
Added RN wrapper Host for the native TabBar, event mapping, and TypeScript types (TabItem, MenuAction, TabBarViewProps).
Other
apps/streams/src/index.ts
Changed internal default port from 8081 to 8082.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through files both new and old,

Tabs unfurled where stacks were told,
React bumped up, native views aligned,
Org switches, sheets, and themes combined,
A little hop — mobile reshaped, bold.

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is vague and generic, using non-descriptive phrasing ('random mobile progress') that doesn't convey the actual significant changes in the changeset. Update the title to be more specific and descriptive, e.g., 'feat(mobile): add custom SwiftUI tab bar with org switcher and menu' or 'feat(mobile): replace native tabs with custom Expo Module tab bar'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description is comprehensive and well-structured, covering summary, architecture, and test plan. However, it only partially follows the repository template, missing the 'Related Issues' section and 'Type of Change' checkboxes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch implement-mobie

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 9, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ✅ Electric Fly.io app

Thank you for your contribution! 🎉

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/mobile/app.config.ts (1)

52-52: ⚠️ Potential issue | 🟡 Minor

Typo in owner: "supserset-sh""superset-sh".

This appears to be a pre-existing typo ("supserset" instead of "superset"), but it could affect EAS build ownership resolution.

Proposed fix
-	owner: "supserset-sh",
+	owner: "superset-sh",
🤖 Fix all issues with AI agents
In `@apps/mobile/modules/tab-bar/ios/ExpandedMenuView.swift`:
- Around line 73-74: The Divider's .background modifier won't tint the divider
line; replace the current Divider().background(Color.white.opacity(0.2)) with a
divider-styling that actually colors the line—either use an overlay (e.g.,
Divider().overlay(Rectangle().foregroundColor(Color.white.opacity(0.2))).frame(height:
1)) or swap the Divider for a Rectangle directly
(Rectangle().foregroundColor(Color.white.opacity(0.2)).frame(height: 1)); update
the code in ExpandedMenuView where Divider is used to one of these options so
the visible line color matches the intended semi-transparent white.

In `@apps/mobile/modules/tab-bar/ios/TabBar.podspec`:
- Line 15: The Podspec's s.source is pointing to the personal fork
(nicksupersetsh/superset.git); update the s.source value in TabBar.podspec to
point to the canonical repository (superset-sh/superset.git) so the pod resolves
from the organization repo (change the git URL assigned to s.source
accordingly).

In `@apps/mobile/screens/`(auth)/sign-in/SignInScreen.tsx:
- Around line 9-10: The onPress handlers that call Linking.openURL with
TERMS_URL and PRIVACY_URL can reject and currently fire-and-forget; add a small
helper (e.g., openURL) that calls Linking.openURL(url).catch(err =>
console.error("[sign-in] Failed to open URL:", url, err)) and replace direct
Linking.openURL calls in the SignInScreen onPress callbacks with this helper to
ensure promise rejections are logged instead of swallowed.

In `@apps/mobile/screens/`(authenticated)/(home)/workspaces/WorkspacesScreen.tsx:
- Around line 29-32: onRefresh is currently a no-op because it sets
setRefreshing(true) then immediately setRefreshing(false); update useCallback
onRefresh to perform the actual refresh by invoking the workspace data
loader/refetch function (e.g., call loadWorkspaces, fetchWorkspaces, or
refetchWorkspaces) and await its result, and ensure setRefreshing(false) runs in
a finally block so the spinner stops after the fetch completes; if this is
intentional scaffolding, replace the body with a clear TODO comment referencing
onRefresh and setRefreshing to indicate the refresh implementation is pending.

In `@apps/mobile/screens/`(authenticated)/(more)/MoreMenuScreen.tsx:
- Around line 25-50: MoreMenuScreen duplicates organization logic (session, orgs
query, activeOrg resolution, handleSwitchOrg and switching state) that the
shared useOrganizations hook already encapsulates; refactor MoreMenuScreen to
import and use useOrganizations instead of re-implementing that logic, move the
local switching state into the shared hook (or expose a switching setter and
switchOrg method) so MoreMenuScreen can call the hook's switch function (replace
handleSwitchOrg), consume activeOrg, otherOrgs, and switching from the hook, and
remove the duplicate useLiveQuery/session code—ensure the hook API matches what
OrgDropdown expects so both components reuse the same functions and state.

In
`@apps/mobile/screens/`(authenticated)/components/AuthenticatedTabBar/AuthenticatedTabBar.tsx:
- Around line 34-49: The collapse timeout (collapseTimer used inside
handleExpandedChange) is not cleared on unmount, risking a leaked timer and a
stale setIsExpanded call; add a useEffect cleanup that on unmount checks
collapseTimer.current, calls clearTimeout if present, and sets
collapseTimer.current = null so any pending timeout started by
handleExpandedChange (and using COLLAPSE_ANIMATION_MS) is cancelled when the
component unmounts.

In `@apps/mobile/screens/`(authenticated)/components/OrgDropdown/OrgDropdown.tsx:
- Around line 30-54: OrgDropdown duplicates session/orgs/finding/switching
logic; refactor to reuse the existing useOrganizations hook by removing local
session/orgs/find/switch logic and consuming the hook's data and actions
instead. Update the useOrganizations hook (in useOrganizations.ts) to expose the
switching state (isSwitching) and an action like switchOrganization or
setActiveOrganization that encapsulates the current handleSwitchOrg behavior
(including router.replace). In OrgDropdown.tsx, replace local state "switching"
and the handleSwitchOrg function with the hook's isSwitching and
switchOrganization API and use the hook's org list and activeOrg lookup (keep
orgInitial calculation if needed). Ensure callers such as WorkspacesScreen and
TabBarAccessory continue to work with the expanded hook surface.

In
`@apps/mobile/screens/`(authenticated)/hooks/useOrganizations/useOrganizations.ts:
- Around line 10-11: The code currently assigns authClient.useSession() to a
session variable and accesses nested fields
(session.data?.session?.activeOrganizationId); change this to use the same
destructuring pattern used elsewhere by calling authClient.useSession() and
destructuring its returned shape (e.g., { data, ... } or { data: { session } }
as needed) so you can directly access activeOrganizationId without extra
nesting; update references in this file (the useSession() call and any use of
session.data?.session?.activeOrganizationId) to the destructured names so it
matches CollectionsProvider/RootLayout/PostHogUserIdentifier patterns.
🧹 Nitpick comments (27)
apps/mobile/screens/RootLayout/RootLayout.tsx (1)

9-9: Module-level side effect for theme initialization.

Uniwind.setTheme("dark") executes on import, which means it runs every time this module is loaded. This is likely intentional to guarantee the dark theme is set before any component renders, but be aware this makes light-mode support harder to add later and couples theme selection to module loading order.

apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/components/OrganizationAvatar/OrganizationAvatar.tsx (2)

16-22: Consider adding accessibilityLabel to the avatar image.

When rendering the logo, adding an accessibilityLabel (e.g., the organization name) would improve screen reader support.

Suggested improvement
 		return (
 			<Image
 				source={{ uri: logo }}
 				style={{ width: size, height: size, borderRadius: size / 2 }}
+				accessibilityLabel={name ?? "Organization logo"}
 			/>
 		);

25-25: Nit: extract the default initial as a named constant.

The fallback "O" is a magic string. A named constant like DEFAULT_ORG_INITIAL would make intent clearer. As per coding guidelines: "Extract hardcoded magic numbers, strings, and enums to named constants at module top instead of leaving them inline in logic."

apps/mobile/screens/(auth)/sign-in/components/SocialButton/SocialButton.tsx (1)

58-59: useColorScheme() can return null.

On some platforms/initial renders, useColorScheme() returns null. The current code handles this safely (falls through to "black"), but an explicit fallback makes the intent clearer and guards against future refactors.

Optional: explicit fallback
 const colorScheme = useColorScheme();
-const iconColor = colorScheme === "dark" ? "white" : "black";
+const iconColor = colorScheme === "dark" ? "white" : "black"; // null → "black" (light default)

A comment is the lightest option. Alternatively: const iconColor = colorScheme === "dark" ? "white" : "black"; is already correct — just flagging for awareness.

packages/ui/package.json (1)

89-91: Consider using a version range for the react peer dependency.

The react peer dependency is pinned to an exact version (19.2.0). For a shared UI package consumed across the monorepo, this is fragile — any consumer on a patch like 19.2.1 would trigger peer dependency warnings. A semver range like ^19.0.0 (similar to how packages/durable-session declares "^18.0.0 || ^19.0.0") would be more resilient to minor/patch bumps.

Suggested change
 	"peerDependencies": {
-		"react": "19.2.0"
+		"react": "^19.0.0"
 	}
apps/mobile/screens/(authenticated)/(tasks)/tasks/TasksScreen.tsx (1)

8-12: Refresh handler completes synchronously — spinner won't be visible.

Since there's no await between setRefreshing(true) and setRefreshing(false), React will batch both state updates and the refresh indicator will never render. When the actual data fetch is added, ensure the await precedes setRefreshing(false).

 const onRefresh = useCallback(async () => {
   setRefreshing(true);
-  // TODO: refresh task data
+  // TODO: refresh task data — await the async work here
+  // await fetchTasks();
   setRefreshing(false);
 }, []);

Would you like me to open an issue to track implementing the task data refresh?

apps/mobile/screens/(authenticated)/tasks/[id]/TaskDetailScreen.tsx (2)

19-24: Consider extracting the shared back-button header into a reusable component.

This exact header pattern (back chevron + title in a flex-row) is duplicated in SettingsScreen.tsx and WorkspaceDetailScreen.tsx. As more screens are added, this will proliferate. A small ScreenHeader component under @/components/ui/ would DRY this up.

// Example: `@/components/ui/screen-header.tsx`
export function ScreenHeader({ title, onBack }: { title: string; onBack: () => void }) {
  return (
    <View className="flex-row items-center gap-2">
      <Pressable onPress={onBack} className="p-1">
        <Icon as={ChevronLeft} className="text-foreground size-6" />
      </Pressable>
      <Text className="text-2xl font-bold">{title}</Text>
    </View>
  );
}

13-17: ScrollView with safe-area pattern is also repeated across screens.

The <ScrollView className="flex-1 bg-background" contentContainerStyle={{ paddingTop: insets.top }}> wrapper is identical in all three new screens. Consider combining this with the header into a ScreenLayout wrapper to reduce boilerplate.

apps/mobile/modules/tab-bar/ios/TabBar.podspec (1)

24-24: Broad source_files glob may pick up unintended files.

"**/*.{h,m,mm,swift,hpp,cpp}" recursively matches all subdirectories. If any test or example files land under this path, they'll be compiled into the pod. Consider scoping to a specific directory or excluding test paths.

apps/mobile/modules/tab-bar/ios/CollapsedBarView.swift (1)

11-11: Unnecessary enumerated() — index is unused.

Since the tuple index is discarded (_), you can simplify to ForEach(tabs, id: \.name) directly.

Proposed fix
-      ForEach(Array(tabs.enumerated()), id: \.element.name) { _, tab in
+      ForEach(tabs, id: \.name) { tab in
apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/OrganizationSwitcherSheet.tsx (1)

86-86: Extract hardcoded domain string to a named constant.

"superset.sh/" is a magic string. If the domain changes, every occurrence must be found manually.

Proposed fix
+const ORG_SLUG_PREFIX = "superset.sh/";
 // ...
-                        superset.sh/{organization.slug}
+                        {ORG_SLUG_PREFIX}{organization.slug}

As per coding guidelines, "Extract hardcoded magic numbers, strings, and enums to named constants at module top instead of leaving them inline in logic."

apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/index.ts (1)

1-1: Consider re-exporting the Organization type.

OrganizationSwitcherSheet.tsx exports the Organization interface, but this barrel doesn't re-export it. Consumers needing the type would have to bypass the barrel and import directly from the implementation file.

Proposed fix
-export { OrganizationSwitcherSheet } from "./OrganizationSwitcherSheet";
+export { OrganizationSwitcherSheet, type Organization } from "./OrganizationSwitcherSheet";
apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.tsx (2)

25-25: Extract the hardcoded color to a named constant or use a theme value.

"hsl(240 5% 64.9%)" is a magic string. Given that OrganizationAvatar already uses useTheme(), consider using a theme color (e.g., theme.mutedForeground) for consistency, or at minimum extract this to a named constant.

♻️ Proposed fix
+const CHEVRON_COLOR = "hsl(240 5% 64.9%)";
+
 export function OrganizationHeaderButton({
-						<ChevronsUpDown size={14} color="hsl(240 5% 64.9%)" />
+						<ChevronsUpDown size={14} color={CHEVRON_COLOR} />

Or better, use a theme token if one matches this color.

As per coding guidelines: "Extract hardcoded magic numbers, strings, and enums to named constants at module top instead of leaving them inline in logic."


29-31: No-op edit button — is this intentional?

The right toolbar button has an empty onPress={() => {}}. If this is a placeholder for future functionality, consider adding a brief comment or TODO to clarify intent.

apps/mobile/package.json (2)

6-6: EXPO_UNSTABLE_MCP_SERVER=1 in dev script.

This uses an unstable/experimental Expo feature. Worth adding a brief comment in the script or README noting why this is enabled, so future developers understand the dependency on this flag.


57-73: Multiple beta/preview dependencies — ensure the team is tracking stability.

Several core dependencies are on pre-release versions (expo@^55.0.0-beta, expo-router@~55.0.0-preview.6, react-native-gesture-handler@3.0.0-beta.1). As of mid-February 2026, Expo SDK 55 remains in beta with no stable release announced yet, so these versions may change before final release. Consider pinning to exact versions to avoid unexpected breakage from pre-release semver ranges.

apps/mobile/modules/tab-bar/ios/GlassCircleButton.swift (1)

7-14: Consider adding an accessibility label.

The button relies on the SF Symbol name for VoiceOver, which may not produce a user-friendly description for all icons (e.g., "magnifyingglass" reads as "magnifyingglass"). Adding an explicit accessibilityLabel parameter would improve the VoiceOver experience.

♻️ Suggested improvement
 struct GlassCircleButton: View {
   let icon: String
+  var accessibilityLabel: String? = nil
   let action: () -> Void

   var body: some View {
     Button(action: action) {
       Image(systemName: icon)
         .font(.system(size: 16, weight: .medium))
         .foregroundStyle(.white)
         .frame(width: 44, height: 44)
         .background(.ultraThinMaterial, in: Circle())
     }
+    .accessibilityLabel(accessibilityLabel ?? icon)
   }
 }
apps/mobile/modules/tab-bar/src/TabBarView.tsx (1)

29-46: Consider extracting inline event handler type annotations.

The inline NativeSyntheticEvent<{ name: string }> annotations on Lines 33, 38, and 43 duplicate the types already declared in NativeTabBarViewProps. They're harmless but redundant — the compiler already infers the parameter types from the NativeView prop types.

apps/mobile/screens/(authenticated)/components/AuthenticatedTabBar/AuthenticatedTabBar.tsx (1)

62-67: Placeholder handlers are fine for now, but consider logging intent.

onMenuActionPress and onSearchPress silently discard events. A // TODO comment is present for onMenuActionPress but not for onSearchPress. Adding a brief console.debug("[tab-bar] search pressed — not yet implemented") could help during development. Optional nit.

apps/mobile/modules/tab-bar/ios/TabBarView.swift (2)

19-74: Extract the repeated spring animation to a constant.

The same .spring(response: 0.35, dampingFraction: 0.85) appears 6 times. Extracting it reduces duplication and makes tuning easier.

♻️ Suggested improvement

Add near the top of the file or as a private property:

private let menuSpring = Animation.spring(response: 0.35, dampingFraction: 0.85)

Then replace all occurrences:

-            withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) {
+            withAnimation(menuSpring) {

92-94: Refactor onChange closure for iOS 17+ compatibility.

The 1-parameter onChange(of:) { newValue in } form is deprecated in iOS 17. Update to the 2-parameter form:

Suggested change
.onChange(of: isExpanded) { newValue in
-  props.onExpandedChange(["expanded": newValue])
+  props.onExpandedChange(["expanded": $1])
}

Or refactor to:

.onChange(of: isExpanded) { oldValue, newValue in
  props.onExpandedChange(["expanded": newValue])
}

Since Expo SDK 55 targets iOS 15.1+, no warnings appear now, but this will be needed when deployment target advances to iOS 17+.

apps/mobile/screens/(authenticated)/hooks/useOrganizations/useOrganizations.ts (1)

22-33: Wrap switchOrganization in useCallback to stabilize identity.

switchOrganization is recreated every render. Consumers that pass it as a prop (e.g., WorkspacesScreen) will trigger unnecessary child re-renders. Since it captures activeOrganizationId and router, both should be in the dependency array.

♻️ Suggested improvement
-	const switchOrganization = async (organizationId: string) => {
+	const switchOrganization = useCallback(async (organizationId: string) => {
 		if (organizationId === activeOrganizationId) return;
 		try {
 			await authClient.organization.setActive({ organizationId });
 			router.replace("/(authenticated)/(home)");
 		} catch (error) {
 			console.error(
 				"[organization/switch] Failed to switch organization:",
 				error,
 			);
 		}
-	};
+	}, [activeOrganizationId, router]);

Add useCallback to the import:

import { useCallback } from "react";
apps/mobile/screens/(authenticated)/(home)/workspaces/WorkspacesScreen.tsx (1)

24-27: switchOrganization is async but not awaited — the sheet closes before the switch completes.

switchOrganization is an async function (see useOrganizations hook). Not awaiting it here means the sheet closes optimistically regardless of whether the switch succeeds or fails. If the switch fails (network error), the user sees the sheet close with no feedback. Consider awaiting and handling failure, e.g. by re-opening the sheet or showing an error.

Proposed fix
-const handleSwitchOrganization = (organizationId: string) => {
+const handleSwitchOrganization = async (organizationId: string) => {
   setSheetOpen(false);
-  switchOrganization(organizationId);
+  await switchOrganization(organizationId);
 };
apps/mobile/screens/(authenticated)/components/OrgDropdown/OrgDropdown.tsx (1)

85-98: Duplicate filter — extract otherOrgs to avoid computing it twice.

The same orgs?.filter((org) => org.id !== activeOrgId) is evaluated on line 85 for the list and again on line 97 for the separator conditional.

Proposed fix
+  const otherOrgs = orgs?.filter((org) => org.id !== activeOrgId) ?? [];
   ...
-  {orgs
-    ?.filter((org) => org.id !== activeOrgId)
-    .map((org) => (
+  {otherOrgs.map((org) => (
       ...
     ))}
-  {orgs && orgs.filter((org) => org.id !== activeOrgId).length > 0 && (
+  {otherOrgs.length > 0 && (
     <DropdownMenuSeparator />
   )}
apps/mobile/screens/(authenticated)/components/TabBarAccessory/TabBarAccessory.tsx (2)

8-9: Deep import into another component's internals — consider promoting OrganizationAvatar.

OrganizationAvatar is imported from OrganizationSwitcherSheet/components/, which couples this component to the internal structure of OrganizationSwitcherSheet. Since OrganizationAvatar is now used by multiple components (here and inside the sheet), consider promoting it to a shared location (e.g., screens/(authenticated)/components/ or a common UI folder).


24-27: switchOrganization is async but not awaited here either.

Same concern as WorkspacesScreen — the sheet closes optimistically without waiting for the switch result. Consider awaiting for consistency and error feedback.

apps/mobile/modules/tab-bar/ios/ExpandedMenuView.swift (1)

42-42: Simplify ForEach.enumerated() is unnecessary since the index is discarded.

Both ForEach calls wrap arrays in Array(....enumerated()) but discard the index (_). This can be simplified to idiomatic SwiftUI:

Proposed simplification
-ForEach(Array(tabs.filter { !$0.isMenuTrigger }.enumerated()), id: \.element.name) { _, tab in
+ForEach(tabs.filter { !$0.isMenuTrigger }, id: \.name) { tab in
-ForEach(Array(menuActions.enumerated()), id: \.element.name) { _, action in
+ForEach(menuActions, id: \.name) { action in

Also applies to: 77-77

Comment on lines +73 to +74
Divider()
.background(Color.white.opacity(0.2))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

.background on Divider may not produce the intended color effect.

SwiftUI's Divider renders its own line; applying .background(Color.white.opacity(0.2)) sets the background behind the divider, not the divider's line color. To tint the divider itself, use .overlay or replace it with a styled Rectangle.

Proposed fix using overlay or Rectangle
-Divider()
-  .background(Color.white.opacity(0.2))
-  .padding(.vertical, 4)
+Rectangle()
+  .fill(Color.white.opacity(0.2))
+  .frame(height: 1 / UIScreen.main.scale)
+  .padding(.vertical, 4)
🤖 Prompt for AI Agents
In `@apps/mobile/modules/tab-bar/ios/ExpandedMenuView.swift` around lines 73 - 74,
The Divider's .background modifier won't tint the divider line; replace the
current Divider().background(Color.white.opacity(0.2)) with a divider-styling
that actually colors the line—either use an overlay (e.g.,
Divider().overlay(Rectangle().foregroundColor(Color.white.opacity(0.2))).frame(height:
1)) or swap the Divider for a Rectangle directly
(Rectangle().foregroundColor(Color.white.opacity(0.2)).frame(height: 1)); update
the code in ExpandedMenuView where Divider is used to one of these options so
the visible line color matches the intended semi-transparent white.

s.homepage = 'https://superset.sh'
s.platforms = { :ios => '15.1' }
s.swift_version = '5.9'
s.source = { git: 'https://github.com/nicksupersetsh/superset.git' }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Source URL points to a personal fork, not the organization repo.

s.source references nicksupersetsh/superset.git but the project lives at superset-sh/superset. This will cause issues if the pod is ever resolved from source.

Proposed fix
-  s.source         = { git: 'https://github.com/nicksupersetsh/superset.git' }
+  s.source         = { git: 'https://github.com/superset-sh/superset.git' }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
s.source = { git: 'https://github.com/nicksupersetsh/superset.git' }
s.source = { git: 'https://github.com/superset-sh/superset.git' }
🤖 Prompt for AI Agents
In `@apps/mobile/modules/tab-bar/ios/TabBar.podspec` at line 15, The Podspec's
s.source is pointing to the personal fork (nicksupersetsh/superset.git); update
the s.source value in TabBar.podspec to point to the canonical repository
(superset-sh/superset.git) so the pod resolves from the organization repo
(change the git URL assigned to s.source accordingly).

Comment on lines +9 to +10
const TERMS_URL = "https://superset.sh/terms";
const PRIVACY_URL = "https://superset.sh/privacy";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unhandled promise rejection from Linking.openURL.

Linking.openURL returns a Promise that can reject (e.g., if no handler is installed for the URL scheme). The onPress callbacks on lines 67 and 74 fire-and-forget without a .catch.

Proposed fix
-				<Text
-					className="text-xs text-muted-foreground underline"
-					onPress={() => Linking.openURL(TERMS_URL)}
-				>
+				<Text
+					className="text-xs text-muted-foreground underline"
+					onPress={() => Linking.openURL(TERMS_URL).catch(() => {})}
+				>
 					Terms of Service
 				</Text>{" "}
 				and{" "}
 				<Text
-					className="text-xs text-muted-foreground underline"
-					onPress={() => Linking.openURL(PRIVACY_URL)}
+					className="text-xs text-muted-foreground underline"
+					onPress={() => Linking.openURL(PRIVACY_URL).catch(() => {})}
 				>

Or better yet, extract a helper that logs on failure per the coding guideline on never swallowing errors silently:

const openURL = (url: string) =>
  Linking.openURL(url).catch((err) =>
    console.error("[sign-in] Failed to open URL:", url, err),
  );

Also applies to: 63-78

🤖 Prompt for AI Agents
In `@apps/mobile/screens/`(auth)/sign-in/SignInScreen.tsx around lines 9 - 10, The
onPress handlers that call Linking.openURL with TERMS_URL and PRIVACY_URL can
reject and currently fire-and-forget; add a small helper (e.g., openURL) that
calls Linking.openURL(url).catch(err => console.error("[sign-in] Failed to open
URL:", url, err)) and replace direct Linking.openURL calls in the SignInScreen
onPress callbacks with this helper to ensure promise rejections are logged
instead of swallowed.

Comment on lines +29 to +32
const onRefresh = useCallback(async () => {
setRefreshing(true);
setRefreshing(false);
}, []);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

onRefresh is a no-op — the spinner will flash and immediately disappear.

setRefreshing(true) followed immediately by setRefreshing(false) with no awaited work in between means the pull-to-refresh indicator won't persist long enough to be visible, and no data is actually refreshed. If this is intentional scaffolding, consider adding a TODO comment; otherwise wire it to an actual data refetch.

Proposed placeholder fix
 const onRefresh = useCallback(async () => {
   setRefreshing(true);
+  // TODO: trigger actual data refresh here, e.g.:
+  // await refetchWorkspaces();
   setRefreshing(false);
 }, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const onRefresh = useCallback(async () => {
setRefreshing(true);
setRefreshing(false);
}, []);
const onRefresh = useCallback(async () => {
setRefreshing(true);
// TODO: trigger actual data refresh here, e.g.:
// await refetchWorkspaces();
setRefreshing(false);
}, []);
🤖 Prompt for AI Agents
In `@apps/mobile/screens/`(authenticated)/(home)/workspaces/WorkspacesScreen.tsx
around lines 29 - 32, onRefresh is currently a no-op because it sets
setRefreshing(true) then immediately setRefreshing(false); update useCallback
onRefresh to perform the actual refresh by invoking the workspace data
loader/refetch function (e.g., call loadWorkspaces, fetchWorkspaces, or
refetchWorkspaces) and await its result, and ensure setRefreshing(false) runs in
a finally block so the spinner stops after the fetch completes; if this is
intentional scaffolding, replace the body with a clear TODO comment referencing
onRefresh and setRefreshing to indicate the refresh implementation is pending.

Comment on lines +25 to +50
const [switching, setSwitching] = useState(false);

const session = authClient.useSession();
const activeOrgId = session.data?.session?.activeOrganizationId;

const { data: orgs } = useLiveQuery(
(q) => q.from({ organizations: collections.organizations }),
[collections],
);

const activeOrg = orgs?.find((org) => org.id === activeOrgId);
const orgInitial = activeOrg?.name?.charAt(0).toUpperCase() ?? "?";
const otherOrgs = orgs?.filter((org) => org.id !== activeOrgId) ?? [];

const handleSwitchOrg = async (orgId: string) => {
if (orgId === activeOrgId) return;
setSwitching(true);
try {
await authClient.organization.setActive({ organizationId: orgId });
router.replace("/(authenticated)/(home)");
} catch (error) {
console.error("[org/switch] Failed to switch organization:", error);
} finally {
setSwitching(false);
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Same duplicated org-switching logic — reuse useOrganizations hook.

This is the same duplication flagged in OrgDropdown.tsx. Lines 27–50 reimplement session fetching, org querying, active org resolution, and switch handling that useOrganizations already provides. The only addition is the switching loading state — consider adding that to the shared hook so both OrgDropdown and MoreMenuScreen can consume it.

Proposed refactor sketch
-import { useLiveQuery } from "@tanstack/react-db";
 ...
-import { authClient } from "@/lib/auth/client";
-import { useCollections } from "@/screens/(authenticated)/providers/CollectionsProvider";
+import { useOrganizations } from "@/screens/(authenticated)/hooks/useOrganizations";

 export function MoreMenuScreen() {
   const router = useRouter();
   const insets = useSafeAreaInsets();
   const { signOut } = useSignOut();
-  const collections = useCollections();
-  const [switching, setSwitching] = useState(false);
-
-  const session = authClient.useSession();
-  const activeOrgId = session.data?.session?.activeOrganizationId;
-
-  const { data: orgs } = useLiveQuery(
-    (q) => q.from({ organizations: collections.organizations }),
-    [collections],
-  );
-
-  const activeOrg = orgs?.find((org) => org.id === activeOrgId);
-  const orgInitial = activeOrg?.name?.charAt(0).toUpperCase() ?? "?";
-  const otherOrgs = orgs?.filter((org) => org.id !== activeOrgId) ?? [];
-
-  const handleSwitchOrg = async (orgId: string) => {
-    if (orgId === activeOrgId) return;
-    setSwitching(true);
-    try {
-      await authClient.organization.setActive({ organizationId: orgId });
-      router.replace("/(authenticated)/(home)");
-    } catch (error) {
-      console.error("[org/switch] Failed to switch organization:", error);
-    } finally {
-      setSwitching(false);
-    }
-  };
+  const {
+    organizations: orgs,
+    activeOrganization: activeOrg,
+    activeOrganizationId: activeOrgId,
+    switchOrganization,
+  } = useOrganizations();
+
+  const orgInitial = activeOrg?.name?.charAt(0).toUpperCase() ?? "?";
+  const otherOrgs = orgs?.filter((org) => org.id !== activeOrgId) ?? [];
🤖 Prompt for AI Agents
In `@apps/mobile/screens/`(authenticated)/(more)/MoreMenuScreen.tsx around lines
25 - 50, MoreMenuScreen duplicates organization logic (session, orgs query,
activeOrg resolution, handleSwitchOrg and switching state) that the shared
useOrganizations hook already encapsulates; refactor MoreMenuScreen to import
and use useOrganizations instead of re-implementing that logic, move the local
switching state into the shared hook (or expose a switching setter and switchOrg
method) so MoreMenuScreen can call the hook's switch function (replace
handleSwitchOrg), consume activeOrg, otherOrgs, and switching from the hook, and
remove the duplicate useLiveQuery/session code—ensure the hook API matches what
OrgDropdown expects so both components reuse the same functions and state.

Comment on lines +34 to +49
const handleExpandedChange = useCallback((expanded: boolean) => {
if (expanded) {
// Expand container immediately so SwiftUI has room to animate into
if (collapseTimer.current) {
clearTimeout(collapseTimer.current);
collapseTimer.current = null;
}
setIsExpanded(true);
} else {
// Delay container shrink so the SwiftUI close animation isn't clipped
collapseTimer.current = setTimeout(() => {
setIsExpanded(false);
collapseTimer.current = null;
}, COLLAPSE_ANIMATION_MS);
}
}, []);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing cleanup of collapseTimer on unmount.

If the component unmounts while a collapse timeout is pending (e.g., user navigates away mid-animation), the timer fires and calls setIsExpanded on a stale closure. While React 19 won't crash, it's still a leaked timer. Add a cleanup effect.

🛡️ Proposed fix

Add after the handleExpandedChange declaration (e.g., after Line 49):

import { useCallback, useEffect, useRef, useState } from "react";
 	}, []);
+
+	useEffect(() => {
+		return () => {
+			if (collapseTimer.current) {
+				clearTimeout(collapseTimer.current);
+			}
+		};
+	}, []);
 
 	return (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleExpandedChange = useCallback((expanded: boolean) => {
if (expanded) {
// Expand container immediately so SwiftUI has room to animate into
if (collapseTimer.current) {
clearTimeout(collapseTimer.current);
collapseTimer.current = null;
}
setIsExpanded(true);
} else {
// Delay container shrink so the SwiftUI close animation isn't clipped
collapseTimer.current = setTimeout(() => {
setIsExpanded(false);
collapseTimer.current = null;
}, COLLAPSE_ANIMATION_MS);
}
}, []);
const handleExpandedChange = useCallback((expanded: boolean) => {
if (expanded) {
// Expand container immediately so SwiftUI has room to animate into
if (collapseTimer.current) {
clearTimeout(collapseTimer.current);
collapseTimer.current = null;
}
setIsExpanded(true);
} else {
// Delay container shrink so the SwiftUI close animation isn't clipped
collapseTimer.current = setTimeout(() => {
setIsExpanded(false);
collapseTimer.current = null;
}, COLLAPSE_ANIMATION_MS);
}
}, []);
useEffect(() => {
return () => {
if (collapseTimer.current) {
clearTimeout(collapseTimer.current);
}
};
}, []);
🤖 Prompt for AI Agents
In
`@apps/mobile/screens/`(authenticated)/components/AuthenticatedTabBar/AuthenticatedTabBar.tsx
around lines 34 - 49, The collapse timeout (collapseTimer used inside
handleExpandedChange) is not cleared on unmount, risking a leaked timer and a
stale setIsExpanded call; add a useEffect cleanup that on unmount checks
collapseTimer.current, calls clearTimeout if present, and sets
collapseTimer.current = null so any pending timeout started by
handleExpandedChange (and using COLLAPSE_ANIMATION_MS) is cancelled when the
component unmounts.

Comment on lines +30 to +54
const [switching, setSwitching] = useState(false);

const session = authClient.useSession();
const activeOrgId = session.data?.session?.activeOrganizationId;

const { data: orgs } = useLiveQuery(
(q) => q.from({ organizations: collections.organizations }),
[collections],
);

const activeOrg = orgs?.find((org) => org.id === activeOrgId);
const orgInitial = activeOrg?.name?.charAt(0).toUpperCase() ?? "?";

const handleSwitchOrg = async (orgId: string) => {
if (orgId === activeOrgId) return;
setSwitching(true);
try {
await authClient.organization.setActive({ organizationId: orgId });
router.replace("/(authenticated)/(home)");
} catch (error) {
console.error("[org/switch] Failed to switch organization:", error);
} finally {
setSwitching(false);
}
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Duplicated org-fetching and switching logic — reuse useOrganizations hook.

Lines 32–54 reimplement the same session/query/find/switch logic already encapsulated in useOrganizations (see apps/mobile/screens/(authenticated)/hooks/useOrganizations/useOrganizations.ts). The only addition here is the switching loading state. Consider extending useOrganizations to expose an isSwitching flag and reuse it here, as WorkspacesScreen and TabBarAccessory already do.

Proposed refactor sketch
-import { useLiveQuery } from "@tanstack/react-db";
-import { useRouter } from "expo-router";
+import { useRouter } from "expo-router";
 ...
-import { authClient } from "@/lib/auth/client";
-import { useCollections } from "@/screens/(authenticated)/providers/CollectionsProvider";
+import { useOrganizations } from "@/screens/(authenticated)/hooks/useOrganizations";

 export function OrgDropdown() {
   const router = useRouter();
   const { signOut } = useSignOut();
-  const collections = useCollections();
-  const [switching, setSwitching] = useState(false);
-
-  const session = authClient.useSession();
-  const activeOrgId = session.data?.session?.activeOrganizationId;
-
-  const { data: orgs } = useLiveQuery(
-    (q) => q.from({ organizations: collections.organizations }),
-    [collections],
-  );
-
-  const activeOrg = orgs?.find((org) => org.id === activeOrgId);
-  const orgInitial = activeOrg?.name?.charAt(0).toUpperCase() ?? "?";
-
-  const handleSwitchOrg = async (orgId: string) => {
-    if (orgId === activeOrgId) return;
-    setSwitching(true);
-    try {
-      await authClient.organization.setActive({ organizationId: orgId });
-      router.replace("/(authenticated)/(home)");
-    } catch (error) {
-      console.error("[org/switch] Failed to switch organization:", error);
-    } finally {
-      setSwitching(false);
-    }
-  };
+  const {
+    organizations: orgs,
+    activeOrganization: activeOrg,
+    activeOrganizationId: activeOrgId,
+    switchOrganization,
+  } = useOrganizations();
+
+  const orgInitial = activeOrg?.name?.charAt(0).toUpperCase() ?? "?";
🤖 Prompt for AI Agents
In `@apps/mobile/screens/`(authenticated)/components/OrgDropdown/OrgDropdown.tsx
around lines 30 - 54, OrgDropdown duplicates session/orgs/finding/switching
logic; refactor to reuse the existing useOrganizations hook by removing local
session/orgs/find/switch logic and consuming the hook's data and actions
instead. Update the useOrganizations hook (in useOrganizations.ts) to expose the
switching state (isSwitching) and an action like switchOrganization or
setActiveOrganization that encapsulates the current handleSwitchOrg behavior
(including router.replace). In OrgDropdown.tsx, replace local state "switching"
and the handleSwitchOrg function with the hook's isSwitching and
switchOrganization API and use the hook's org list and activeOrg lookup (keep
orgInitial calculation if needed). Ensure callers such as WorkspacesScreen and
TabBarAccessory continue to work with the expanded hook surface.

Comment on lines +10 to +11
const session = authClient.useSession();
const activeOrganizationId = session.data?.session?.activeOrganizationId;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Search for authClient.useSession definition and usage
rg -n "useSession" --type=ts -C5 | head -100

Repository: superset-sh/superset

Length of output: 10456


🏁 Script executed:

# Find authClient definition
rg -n "authClient" --type=ts -C3 | head -150

Repository: superset-sh/superset

Length of output: 13237


🏁 Script executed:

# Look for the useOrganizations file specifically
find . -name "useOrganizations.ts" -type f

Repository: superset-sh/superset

Length of output: 144


🏁 Script executed:

# Find authClient definition
find . -path "*/lib/auth/client*" -type f

Repository: superset-sh/superset

Length of output: 96


🏁 Script executed:

# Search for useSession hook definition in the auth module
rg -n "export.*useSession" --type=ts -C10

Repository: superset-sh/superset

Length of output: 647


🏁 Script executed:

# Read the full authClient file to understand the useSession return type
cat -n apps/mobile/lib/auth/client.ts

Repository: superset-sh/superset

Length of output: 742


🏁 Script executed:

# Also check if there's a shared auth client package
find . -path "*/auth/client*" -type f | grep -v node_modules | head -20

Repository: superset-sh/superset

Length of output: 96


🏁 Script executed:

# Search for better-auth types or documentation references
rg -n "better-auth" --type=ts -C2 | head -50

Repository: superset-sh/superset

Length of output: 3245


🏁 Script executed:

# Check how useSession is used in other files to understand the return type
rg -n "useSession\(\)" --type=ts -A3 | head -100

Repository: superset-sh/superset

Length of output: 10627


🏁 Script executed:

# Count destructuring patterns in mobile app to determine standard
rg "const.*session.*authClient\.useSession\(\)" apps/mobile --type=ts -A1 | grep -E "const.*session|const.*{.*data.*session" | wc -l

Repository: superset-sh/superset

Length of output: 65


🏁 Script executed:

# Show all patterns side-by-side
rg "const.*session.*useSession\(\)" apps/mobile --type=ts -B1 | head -40

Repository: superset-sh/superset

Length of output: 1571


🏁 Script executed:

# Verify the exact imports used - some use useSession from authClient, some import useSession
rg "import.*useSession|from.*useSession" apps/mobile --type=ts

Repository: superset-sh/superset

Length of output: 538


🏁 Script executed:

# Show the full context of how useOrganizations.ts uses the session vs other files
cat -n apps/mobile/screens/\(authenticated\)/hooks/useOrganizations/useOrganizations.ts

Repository: superset-sh/superset

Length of output: 1518


🏁 Script executed:

# Compare with a file that uses destructuring pattern
cat -n apps/mobile/screens/\(authenticated\)/providers/CollectionsProvider/CollectionsProvider.tsx | head -25

Repository: superset-sh/superset

Length of output: 1077


Use destructuring pattern for useSession() to match other mobile files.

useOrganizations.ts should destructure the session data like CollectionsProvider, RootLayout, and PostHogUserIdentifier do:

-	const session = authClient.useSession();
-	const activeOrganizationId = session.data?.session?.activeOrganizationId;
+	const { data: session } = authClient.useSession();
+	const activeOrganizationId = session?.session?.activeOrganizationId;

This reduces nesting and aligns with the established pattern across the mobile app.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const session = authClient.useSession();
const activeOrganizationId = session.data?.session?.activeOrganizationId;
const { data: session } = authClient.useSession();
const activeOrganizationId = session?.session?.activeOrganizationId;
🤖 Prompt for AI Agents
In
`@apps/mobile/screens/`(authenticated)/hooks/useOrganizations/useOrganizations.ts
around lines 10 - 11, The code currently assigns authClient.useSession() to a
session variable and accesses nested fields
(session.data?.session?.activeOrganizationId); change this to use the same
destructuring pattern used elsewhere by calling authClient.useSession() and
destructuring its returned shape (e.g., { data, ... } or { data: { session } }
as needed) so you can directly access activeOrganizationId without extra
nesting; update references in this file (the useSession() call and any use of
session.data?.session?.activeOrganizationId) to the destructured names so it
matches CollectionsProvider/RootLayout/PostHogUserIdentifier patterns.

@saddlepaddle saddlepaddle changed the title feat(mobile): custom SwiftUI tab bar with floating menu feat(mobile): random mobile progress Feb 9, 2026
Replace NativeTabs with a custom floating tab bar built as a local Expo
Module using SwiftUI. The tab bar features a Linear-style glass capsule
with expand/collapse spring animation for the navigation menu.

- New `@superset/tab-bar` local Expo Module (SwiftUI + ExpoModulesCore)
- Collapsed: glass capsule with Home/Tasks/More icons + search circle
- Expanded: org switcher pill, settings gear, menu items with spring animation
- Headless Tabs (expo-router/ui) for routing with hidden TabList
- AuthenticatedTabBar bridges native SwiftUI view to expo-router
- Dynamic container resizing (96px collapsed, full-screen expanded) via
  onExpandedChange event from SwiftUI to JS
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In
`@apps/mobile/screens/`(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/components/OrganizationAvatar/OrganizationAvatar.tsx:
- Around line 16-22: The OrganizationAvatar component currently renders an
<Image> when the logo prop is present but has no fallback if the image fails to
load; add a local boolean state (e.g., imageError via useState) and render the
<Image> only when logo is present and imageError is false, attaching an onError
handler on the <Image> to set imageError true; when imageError is true (or logo
is absent) fall back to the existing initials-based avatar rendering so
broken/stale logo URLs show the initial avatar instead.

In `@apps/mobile/screens/`(authenticated)/(more)/settings/SettingsScreen.tsx:
- Around line 20-22: Add proper accessibility attributes to the back Pressable
in SettingsScreen.tsx: update the Pressable (the wrapper around Icon/ChevronLeft
that calls router.back()) to include accessibilityRole="button" and an
accessibilityLabel describing its action (e.g., "Back" or a localized string
like t('back')) so screen readers announce its purpose; ensure the label is
concise and matches any existing localization function if used in this screen.

In `@apps/mobile/screens/RootLayout/RootLayout.tsx`:
- Around line 17-19: The RootLayout currently returns null while the auth
session resolves (useSession -> isPending), which can flash a blank screen;
instead either keep the splash visible until resolution or render a loading
indicator: update RootLayout so that when isPending is true you call your
splash-screen keep-alive (e.g., ensure SplashScreen.preventAutoHideAsync() is
invoked earlier and only hide after session is settled) or replace the early
return null with a stable loading UI (e.g., an ActivityIndicator or a simple
centered loader) so the UI doesn’t go blank while useSession (data: session,
isPending) resolves.
🧹 Nitpick comments (9)
apps/mobile/screens/(authenticated)/tasks/[id]/TaskDetailScreen.tsx (1)

20-22: Consider adding accessibility props to the back button.

The Pressable lacks accessibilityLabel and accessibilityRole, which hurts VoiceOver/TalkBack usability.

♿ Proposed fix
-				<Pressable onPress={() => router.back()} className="p-1">
+				<Pressable
+					onPress={() => router.back()}
+					className="p-1"
+					accessibilityRole="button"
+					accessibilityLabel="Go back"
+				>
apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/components/OrganizationAvatar/OrganizationAvatar.tsx (1)

25-38: Extract inline magic values to named constants.

0.45 (font-size ratio) and "O" (default initial) are magic values. As per coding guidelines, extract hardcoded magic numbers and strings to named constants at module top.

Suggested extraction
+const FONT_SIZE_RATIO = 0.45;
+const DEFAULT_INITIAL = "O";
+
 export function OrganizationAvatar({
-	const initial = (name ?? "O").charAt(0).toUpperCase();
+	const initial = (name ?? DEFAULT_INITIAL).charAt(0).toUpperCase();
-				style={{ fontSize: size * 0.45, color: theme.mutedForeground }}
+				style={{ fontSize: size * FONT_SIZE_RATIO, color: theme.mutedForeground }}
apps/mobile/screens/(authenticated)/(more)/MoreMenuScreen.tsx (2)

83-98: No visual feedback during org switch.

switching disables the pressables but there's no spinner or opacity change to indicate the operation is in progress. Users may think the tap didn't register.

Suggested approach
 <Pressable
 	key={org.id}
 	onPress={() => handleSwitchOrg(org.id)}
 	disabled={switching}
-	className="flex-row items-center gap-3 px-4 py-3"
+	className={`flex-row items-center gap-3 px-4 py-3 ${switching ? "opacity-50" : ""}`}
 >

Or render an ActivityIndicator when switching is true.


127-133: Consider adding a confirmation before sign-out.

Tapping "Log out" immediately signs the user out with no confirmation step. An accidental tap could be disruptive. A simple Alert.alert confirmation would be a low-effort safeguard.

apps/mobile/screens/(authenticated)/hooks/useOrganizations/useOrganizations.ts (1)

6-41: Add type annotations for the returned organization data.

The hook returns untyped organization data (any[] / any), which removes type safety for all consumers (WorkspacesScreen, TabBarAccessory, OrganizationSwitcherSheet). Define an Organization type (or re-use the one already exported from OrganizationSwitcherSheet.tsx) and annotate the return value.

Suggested approach
+import type { Organization } from "@/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet";
+
 export function useOrganizations() {
   // ...
-  const { data: organizations } = useLiveQuery(
+  const { data: organizations } = useLiveQuery<Organization>(
     (q) => q.from({ organizations: collections.organizations }),
     [collections],
   );

As per coding guidelines, "Maintain type safety by avoiding any types unless absolutely necessary."

apps/mobile/screens/(authenticated)/components/TabBarAccessory/TabBarAccessory.tsx (2)

8-9: Deep import into OrganizationSwitcherSheet's internal components/ breaks encapsulation.

OrganizationAvatar is reached through a sibling component's private components/ directory. If it's needed by multiple components, promote it to a shared location (e.g., (authenticated)/components/OrganizationAvatar/) or re-export it from OrganizationSwitcherSheet's barrel.

As per coding guidelines, "Nest components under their parent's components/ folder if used once; promote to the highest shared parent's components/ if used 2+ times."


12-69: Organization-switching sheet pattern is duplicated with WorkspacesScreen.

Both TabBarAccessory and WorkspacesScreen maintain identical sheetOpen state, handleSwitchOrganization callback, and OrganizationSwitcherSheet render block. Consider extracting a shared hook (e.g., useOrganizationSwitcherSheet) that returns { sheetOpen, openSheet, sheetProps } to DRY this up.

apps/mobile/screens/RootLayout/RootLayout.tsx (1)

5-12: Move the Uniwind.setTheme side effect after all imports.

Placing a side effect call between import statements is fragile — it relies on import ordering and will be flagged by import/first or similar lint rules. Move it below the last import.

♻️ Proposed fix
 import { Uniwind } from "uniwind";
 import { useSession } from "@/lib/auth/client";
 import { NAV_THEME } from "@/lib/theme";
-
-Uniwind.setTheme("dark");
 
 import { PostHogUserIdentifier } from "./components/PostHogUserIdentifier";
 import { PostHogProvider } from "./providers/PostHogProvider";
 
+Uniwind.setTheme("dark");
+
 const queryClient = new QueryClient();
apps/mobile/package.json (1)

6-6: EXPO_UNSTABLE_MCP_SERVER is an unstable/experimental flag.

The EXPO_UNSTABLE_ prefix indicates this is an experimental feature. This is fine for development, but be aware that behavior may change or the flag may be renamed/removed in future Expo releases. Ensure the expo-mcp devDependency on Line 96 stays in sync if the API evolves.

Comment on lines +16 to +22
if (logo) {
return (
<Image
source={{ uri: logo }}
style={{ width: size, height: size, borderRadius: size / 2 }}
/>
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

No fallback when the logo image fails to load.

If logo is a stale or broken URL, the Image will silently render as a blank circle. Consider adding an onError callback to fall through to the initial-based avatar.

Proposed approach
+import { useState } from "react";
+
 export function OrganizationAvatar({
 	name,
 	logo,
 	size,
 }: {
 	name?: string | null;
 	logo?: string | null;
 	size: number;
 }) {
 	const theme = useTheme();
+	const [imageError, setImageError] = useState(false);
 
-	if (logo) {
+	if (logo && !imageError) {
 		return (
 			<Image
 				source={{ uri: logo }}
 				style={{ width: size, height: size, borderRadius: size / 2 }}
+				onError={() => setImageError(true)}
 			/>
 		);
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (logo) {
return (
<Image
source={{ uri: logo }}
style={{ width: size, height: size, borderRadius: size / 2 }}
/>
);
import { useState } from "react";
export function OrganizationAvatar({
name,
logo,
size,
}: {
name?: string | null;
logo?: string | null;
size: number;
}) {
const theme = useTheme();
const [imageError, setImageError] = useState(false);
if (logo && !imageError) {
return (
<Image
source={{ uri: logo }}
style={{ width: size, height: size, borderRadius: size / 2 }}
onError={() => setImageError(true)}
/>
);
}
🤖 Prompt for AI Agents
In
`@apps/mobile/screens/`(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/components/OrganizationAvatar/OrganizationAvatar.tsx
around lines 16 - 22, The OrganizationAvatar component currently renders an
<Image> when the logo prop is present but has no fallback if the image fails to
load; add a local boolean state (e.g., imageError via useState) and render the
<Image> only when logo is present and imageError is false, attaching an onError
handler on the <Image> to set imageError true; when imageError is true (or logo
is absent) fall back to the existing initials-based avatar rendering so
broken/stale logo URLs show the initial avatar instead.

Comment on lines +20 to +22
<Pressable onPress={() => router.back()} className="p-1">
<Icon as={ChevronLeft} className="text-foreground size-6" />
</Pressable>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add an accessibility label to the back button.

The Pressable wrapping the back chevron has no accessibilityLabel or accessibilityRole, which means screen readers won't announce its purpose.

Proposed fix
-				<Pressable onPress={() => router.back()} className="p-1">
+				<Pressable
+					onPress={() => router.back()}
+					className="p-1"
+					accessibilityRole="button"
+					accessibilityLabel="Go back"
+				>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Pressable onPress={() => router.back()} className="p-1">
<Icon as={ChevronLeft} className="text-foreground size-6" />
</Pressable>
<Pressable
onPress={() => router.back()}
className="p-1"
accessibilityRole="button"
accessibilityLabel="Go back"
>
<Icon as={ChevronLeft} className="text-foreground size-6" />
</Pressable>
🤖 Prompt for AI Agents
In `@apps/mobile/screens/`(authenticated)/(more)/settings/SettingsScreen.tsx
around lines 20 - 22, Add proper accessibility attributes to the back Pressable
in SettingsScreen.tsx: update the Pressable (the wrapper around Icon/ChevronLeft
that calls router.back()) to include accessibilityRole="button" and an
accessibilityLabel describing its action (e.g., "Back" or a localized string
like t('back')) so screen readers announce its purpose; ensure the label is
concise and matches any existing localization function if used in this screen.

Comment on lines +17 to +19
const { data: session, isPending } = useSession();

if (isPending) return null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Returning null while session loads may flash a blank screen.

If the Expo splash screen has already been hidden at this point, the user sees an empty view during session resolution. Consider rendering an <ActivityIndicator> or ensuring the splash screen stays visible (e.g., via expo-splash-screen's SplashScreen.preventAutoHideAsync()) until the session state is resolved.

🤖 Prompt for AI Agents
In `@apps/mobile/screens/RootLayout/RootLayout.tsx` around lines 17 - 19, The
RootLayout currently returns null while the auth session resolves (useSession ->
isPending), which can flash a blank screen; instead either keep the splash
visible until resolution or render a loading indicator: update RootLayout so
that when isPending is true you call your splash-screen keep-alive (e.g., ensure
SplashScreen.preventAutoHideAsync() is invoked earlier and only hide after
session is settled) or replace the early return null with a stable loading UI
(e.g., an ActivityIndicator or a simple centered loader) so the UI doesn’t go
blank while useSession (data: session, isPending) resolves.

@saddlepaddle saddlepaddle merged commit 36309de into main Feb 9, 2026
14 checks passed
@Kitenite Kitenite deleted the implement-mobie branch February 10, 2026 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant