Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@
"deny": [
"Read(./apps/cli/**)",
"Edit(./apps/cli/**)",
"Write(./apps/cli/**)",
"Read(./apps/mobile/**)",
"Edit(./apps/mobile/**)",
"Write(./apps/mobile/**)"
"Write(./apps/cli/**)"
]
}
}
30 changes: 24 additions & 6 deletions apps/mobile/app.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"expo": {
"name": "PostHog AI",
"name": "PostHog Code",
"slug": "posthog",
"version": "1.0.0",
"orientation": "portrait",
Expand All @@ -16,10 +16,14 @@
"ios": {
"icon": "./assets/posthog.icon",
"supportsTablet": true,
"bundleIdentifier": "com.posthog.mobile",
"bundleIdentifier": "com.posthog.code.mobile",
"infoPlist": {
"NSMicrophoneUsageDescription": "Allow PostHog to use your microphone for voice-to-text input",
"ITSAppUsesNonExemptEncryption": false
"NSSpeechRecognitionUsageDescription": "Allow PostHog to transcribe your voice input on-device",
"NSPhotoLibraryUsageDescription": "Allow PostHog to access your photos for attaching images to tasks",
"NSCameraUsageDescription": "Allow PostHog to use your camera to scan QR codes for sign-in",
"ITSAppUsesNonExemptEncryption": false,
"LSApplicationQueriesSchemes": ["posthog-code"]
}
},
"android": {
Expand All @@ -29,10 +33,11 @@
},
"edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false,
"package": "com.posthog.mobile",
"package": "com.posthog.code.mobile",
"permissions": [
"android.permission.RECORD_AUDIO",
"android.permission.MODIFY_AUDIO_SETTINGS"
"android.permission.MODIFY_AUDIO_SETTINGS",
"android.permission.CAMERA"
]
},
"web": {
Expand All @@ -46,6 +51,12 @@
"microphonePermission": "Allow PostHog to use your microphone for voice-to-text input"
}
],
[
"expo-camera",
{
"cameraPermission": "Allow PostHog to use your camera to scan QR codes for sign-in"
}
],
[
"expo-font",
{
Expand Down Expand Up @@ -152,7 +163,14 @@
}
}
],
"expo-localization"
"expo-localization",
[
"expo-speech-recognition",
{
"microphonePermission": "Allow PostHog to use your microphone for voice-to-text input",
"speechRecognitionPermission": "Allow PostHog to transcribe your voice input on-device"
}
]
],
"extra": {
"router": {},
Expand Down
Binary file added apps/mobile/assets/sounds/meep.mp3
Binary file not shown.
1 change: 1 addition & 0 deletions apps/mobile/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "expo-router/entry";
8 changes: 7 additions & 1 deletion apps/mobile/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@posthog/mobile",
"version": "1.0.0",
"main": "expo-router/entry",
"main": "./index.js",
"scripts": {
"start": "expo start",
"start:clear": "expo start --clear",
Expand All @@ -24,28 +24,34 @@
"dependencies": {
"@expo/ui": "0.2.0-beta.9",
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-native-community/netinfo": "^12.0.1",
"@tanstack/react-query": "^5.90.12",
"date-fns": "^4.1.0",
"expo": "~54.0.27",
"expo-application": "~7.0.8",
"expo-auth-session": "^7.0.10",
"expo-av": "~16.0.8",
"expo-camera": "^55.0.15",
"expo-clipboard": "^55.0.13",
"expo-constants": "~18.0.11",
"expo-crypto": "^15.0.8",
"expo-dev-client": "~6.0.20",
"expo-device": "~8.0.10",
"expo-file-system": "~19.0.21",
"expo-font": "^14.0.10",
"expo-glass-effect": "~0.1.8",
"expo-haptics": "^55.0.14",
"expo-linear-gradient": "^15.0.8",
"expo-linking": "~8.0.10",
"expo-localization": "~17.0.8",
"expo-router": "~6.0.17",
"expo-secure-store": "^15.0.8",
"expo-speech-recognition": "^3.1.2",
"expo-splash-screen": "~31.0.12",
"expo-status-bar": "~3.0.9",
"expo-system-ui": "~6.0.9",
"expo-web-browser": "^15.0.10",
"highlight.js": "^11.11.1",
"nativewind": "^4.2.1",
"phosphor-react-native": "^3.0.2",
"posthog-react-native": "^4.18.0",
Expand Down
30 changes: 17 additions & 13 deletions apps/mobile/src/app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Icon, Label, NativeTabs } from "expo-router/unstable-native-tabs";
import { DynamicColorIOS, Platform } from "react-native";
import { usePreferencesStore } from "@/features/preferences/stores/preferencesStore";
import { useThemeColors } from "@/lib/theme";

export default function TabsLayout() {
const themeColors = useThemeColors();
const aiChatEnabled = usePreferencesStore((s) => s.aiChatEnabled);

// Dynamic colors for liquid glass effect on iOS
const dynamicTextColor =
Expand All @@ -30,21 +32,23 @@ export default function TabsLayout() {
tintColor={dynamicTintColor}
minimizeBehavior="onScrollDown"
>
{/* Conversations - First Tab (default landing) */}
<NativeTabs.Trigger name="index">
<Label>Chats</Label>
<Icon
sf={{
default: "bubble.left.and.bubble.right",
selected: "bubble.left.and.bubble.right.fill",
}}
drawable="ic_menu_send"
/>
</NativeTabs.Trigger>
{/* Conversations - Chats tab, hidden by default to focus on Code */}
{aiChatEnabled && (
<NativeTabs.Trigger name="index">
<Label>Chats</Label>
<Icon
sf={{
default: "bubble.left.and.bubble.right",
selected: "bubble.left.and.bubble.right.fill",
}}
drawable="ic_menu_send"
/>
</NativeTabs.Trigger>
)}

{/* Tasks Tab */}
{/* Code tab (task list for PostHog Code) */}
<NativeTabs.Trigger name="tasks">
<Label>Tasks</Label>
<Label>Code</Label>
<Icon
sf={{ default: "checklist", selected: "checklist" }}
drawable="ic_menu_agenda"
Expand Down
8 changes: 7 additions & 1 deletion apps/mobile/src/app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { Text } from "@components/text";
import { useRouter } from "expo-router";
import { Redirect, useRouter } from "expo-router";
import { Pressable, View } from "react-native";
import {
type ConversationDetail,
ConversationList,
} from "@/features/conversations";
import { usePreferencesStore } from "@/features/preferences/stores/preferencesStore";

export default function ConversationsScreen() {
const router = useRouter();
const aiChatEnabled = usePreferencesStore((s) => s.aiChatEnabled);

if (!aiChatEnabled) {
return <Redirect href="/(tabs)/tasks" />;
}

const handleConversationPress = (conversation: ConversationDetail) => {
router.push(`/chat/${conversation.id}`);
Expand Down
36 changes: 36 additions & 0 deletions apps/mobile/src/app/(tabs)/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@ import { router } from "expo-router";
import {
Linking,
ScrollView,
Switch,
Text,
TouchableOpacity,
View,
} from "react-native";
import { useAuthStore, useUserQuery } from "@/features/auth";
import { usePreferencesStore } from "@/features/preferences/stores/preferencesStore";

export default function SettingsScreen() {
const { logout, cloudRegion, getCloudUrlFromRegion } = useAuthStore();
const { data: userData } = useUserQuery();
const aiChatEnabled = usePreferencesStore((s) => s.aiChatEnabled);
const setAiChatEnabled = usePreferencesStore((s) => s.setAiChatEnabled);
const pingsEnabled = usePreferencesStore((s) => s.pingsEnabled);
const setPingsEnabled = usePreferencesStore((s) => s.setPingsEnabled);

const handleLogout = async () => {
await logout();
Expand Down Expand Up @@ -88,6 +94,36 @@ export default function SettingsScreen() {
</View>
</View>

{/* Labs */}
<View className="mb-6 rounded-xl bg-gray-2 p-4">
<Text className="mb-1 font-semibold text-gray-12 text-lg">Labs</Text>
<Text className="mb-4 text-gray-11 text-xs">
Experimental features
</Text>
<View className="flex-row items-center justify-between py-2">
<View className="flex-1 pr-4">
<Text className="font-medium text-gray-12 text-sm">
PostHog AI chat
</Text>
<Text className="text-gray-11 text-xs">
Show the Chats tab for PostHog AI conversations
</Text>
</View>
<Switch value={aiChatEnabled} onValueChange={setAiChatEnabled} />
</View>
<View className="flex-row items-center justify-between py-2">
<View className="flex-1 pr-4">
<Text className="font-medium text-gray-12 text-sm">
Enable pings
</Text>
<Text className="text-gray-11 text-xs">
Play a sound when a task completes
</Text>
</View>
<Switch value={pingsEnabled} onValueChange={setPingsEnabled} />
</View>
</View>

{/* All Settings Button */}
<TouchableOpacity
className="mb-6 items-center rounded-lg border border-gray-6 bg-gray-3 py-4"
Expand Down
45 changes: 36 additions & 9 deletions apps/mobile/src/app/(tabs)/tasks.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,54 @@
import { Text } from "@components/text";
import { useRouter } from "expo-router";
import { Pressable, View } from "react-native";
import { useFocusEffect, useRouter } from "expo-router";
import { useCallback, useRef } from "react";
import { InteractionManager, Pressable, View } from "react-native";
import { TaskList } from "@/features/tasks";

export default function TasksScreen() {
const router = useRouter();
const readyRef = useRef(true);

const handleCreateTask = () => {
// Block navigation while a modal dismiss animation is in progress.
// When the screen loses focus (modal opens), readyRef is false.
// When focus returns (modal dismissed), we wait for all native
// animations to finish before allowing the next push.
useFocusEffect(
useCallback(() => {
const handle = InteractionManager.runAfterInteractions(() => {
readyRef.current = true;
});
return () => {
readyRef.current = false;
handle.cancel();
};
}, []),
);

const handleCreateTask = useCallback(() => {
if (!readyRef.current) return;
readyRef.current = false;
router.push("/task");
};
}, [router]);

const handleTaskPress = (taskId: string) => {
router.push(`/task/${taskId}`);
};
const handleTaskPress = useCallback(
(taskId: string) => {
if (!readyRef.current) return;
readyRef.current = false;
router.push(`/task/${taskId}`);
},
[router],
);

return (
<View className="flex-1 bg-background">
{/* Header */}
<View className="border-gray-6 border-b px-4 pt-16 pb-4">
<View className="flex-row items-center justify-between">
<View>
<Text className="font-bold text-2xl text-gray-12">Tasks</Text>
<Text className="text-gray-11 text-sm">Your PostHog tasks</Text>
<Text className="font-bold text-2xl text-gray-12">Code</Text>
<Text className="text-gray-11 text-sm">
Your PostHog Code sessions
</Text>
</View>
<Pressable
onPress={handleCreateTask}
Expand Down
46 changes: 27 additions & 19 deletions apps/mobile/src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import { useEffect } from "react";
import { ActivityIndicator, View } from "react-native";
import { KeyboardProvider } from "react-native-keyboard-controller";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { OfflineBanner } from "@/components/OfflineBanner";
import { useAuthStore } from "@/features/auth";
import { usePreferencesStore } from "@/features/preferences/stores/preferencesStore";
import {
POSTHOG_API_KEY,
POSTHOG_OPTIONS,
Expand All @@ -20,6 +22,7 @@ import { darkTheme, lightTheme, useThemeColors } from "@/lib/theme";

function RootLayoutNav() {
const { isLoading, initializeAuth } = useAuthStore();
const aiChatEnabled = usePreferencesStore((s) => s.aiChatEnabled);
const themeColors = useThemeColors();

useScreenTracking();
Expand Down Expand Up @@ -47,25 +50,29 @@ function RootLayoutNav() {
<Stack.Screen name="auth" options={{ headerShown: false }} />
<Stack.Screen name="index" options={{ headerShown: false }} />

{/* Chat routes - regular stack navigation */}
<Stack.Screen
name="chat/index"
options={{
headerShown: true,
headerBackTitle: "",
headerStyle: { backgroundColor: themeColors.background },
headerTintColor: themeColors.gray[12],
}}
/>
<Stack.Screen
name="chat/[id]"
options={{
headerShown: true,
headerBackTitle: "Back",
headerStyle: { backgroundColor: themeColors.background },
headerTintColor: themeColors.gray[12],
}}
/>
{/* Chat routes - only registered when AI chat feature is enabled */}
{aiChatEnabled && (
<>
<Stack.Screen
name="chat/index"
options={{
headerShown: true,
headerBackTitle: "",
headerStyle: { backgroundColor: themeColors.background },
headerTintColor: themeColors.gray[12],
}}
/>
<Stack.Screen
name="chat/[id]"
options={{
headerShown: true,
headerBackTitle: "Back",
headerStyle: { backgroundColor: themeColors.background },
headerTintColor: themeColors.gray[12],
}}
/>
</>
)}

{/* Task routes - modal presentation */}
<Stack.Screen
Expand Down Expand Up @@ -109,6 +116,7 @@ export default function RootLayout() {
<QueryClientProvider client={queryClient}>
<View style={themeVars} className="flex-1">
<RootLayoutNav />
<OfflineBanner />
</View>
<StatusBar style={colorScheme === "dark" ? "light" : "dark"} />
</QueryClientProvider>
Expand Down
Loading
Loading