From 6599e8fbdd74d3e89ba715f4357959f549888b3a Mon Sep 17 00:00:00 2001 From: Yoav Shai Date: Fri, 19 Dec 2025 21:10:48 +0200 Subject: [PATCH] Fixes for Android & other people's setups - Added an android package name to allow installation - Removed DynamicColorIOS which prevented from running - Fixed the bottom icons and top bar margin - Save last connected server & auto-connect to it --- app.json | 1 + app/(tabs)/_layout.tsx | 38 +++++++++++++-------- app/_layout.tsx | 18 +++------- app/index.tsx | 7 +--- package-lock.json | 34 +++++++++++++++++++ package.json | 1 + src/providers/OpenCodeProvider.tsx | 53 ++++++++++++++++++++++++++++-- src/screens/SessionsScreen.tsx | 5 +-- src/screens/SettingsScreen.tsx | 5 +-- 9 files changed, 122 insertions(+), 40 deletions(-) diff --git a/app.json b/app.json index 2b489cd..951ef0a 100644 --- a/app.json +++ b/app.json @@ -18,6 +18,7 @@ "bundleIdentifier": "com.openpad.app" }, "android": { + "package": "com.openpad.app", "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 62de6f8..37238d3 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -1,7 +1,19 @@ -import { DynamicColorIOS } from 'react-native'; -import { NativeTabs, Icon, Label } from 'expo-router/unstable-native-tabs'; +import { Platform } from 'react-native'; +import { NativeTabs, Icon, Label, VectorIcon } from 'expo-router/unstable-native-tabs'; +import { useTheme } from '../../src/hooks/useTheme'; +import { Ionicons } from '@expo/vector-icons'; export default function TabLayout() { + const { isDark } = useTheme(); + + const labelColor = Platform.OS === 'ios' + ? { color: { dark: 'white', light: 'black' } as any } + : isDark ? 'white' : 'black'; + + const tintColor = Platform.OS === 'ios' + ? { color: { dark: '#22d3ee', light: '#0891b2' } as any } + : isDark ? '#22d3ee' : '#0891b2'; + return ( - + } + /> - + - + } + /> diff --git a/app/_layout.tsx b/app/_layout.tsx index 95fdec4..3b44ada 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { Stack, useRouter, useSegments } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; import { useColorScheme } from 'react-native'; @@ -9,29 +9,19 @@ function AuthRedirect({ children }: { children: React.ReactNode }) { const { connected, connecting, connect } = useOpenCode(); const segments = useSegments(); const router = useRouter(); - const [autoConnectAttempted, setAutoConnectAttempted] = useState(false); - - // Auto-connect on mount - useEffect(() => { - if (!autoConnectAttempted) { - setAutoConnectAttempted(true); - connect(); - } - }, [autoConnectAttempted, connect]); useEffect(() => { // Wait for auto-connect to finish - if (connecting) return; + if (connecting) { + return; + } const inAuthGroup = segments[0] === '(tabs)'; const inChatScreen = segments[0] === 'chat'; - const onConnectScreen = segments[0] === 'connect'; if (!connected && (inAuthGroup || inChatScreen)) { - // Redirect to connect if not authenticated router.replace('/connect'); } else if (connected && !inAuthGroup && !inChatScreen && segments.length > 0) { - // Redirect to tabs if authenticated and not already in tabs or chat router.replace('/(tabs)/sessions'); } }, [connected, connecting, segments, router]); diff --git a/app/index.tsx b/app/index.tsx index bcaeb5e..aedd532 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -2,12 +2,7 @@ import { Redirect } from 'expo-router'; import { useOpenCode } from '../src/providers/OpenCodeProvider'; export default function Index() { - const { connected, connecting } = useOpenCode(); - - // While connecting, show nothing (the layout handles the redirect) - if (connecting) { - return null; - } + const { connected } = useOpenCode(); if (connected) { return ; diff --git a/package-lock.json b/package-lock.json index 5442014..6b995fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@expo/vector-icons": "^15.0.3", "@opencode-ai/sdk": "^1.0.167", + "@react-native-async-storage/async-storage": "2.2.0", "expo": "~54.0.30", "expo-blur": "^15.0.8", "expo-glass-effect": "^0.1.8", @@ -2931,6 +2932,18 @@ } } }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz", + "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==", + "license": "MIT", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.65 <1.0" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.81.5", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz", @@ -6263,6 +6276,15 @@ "node": ">=0.12.0" } }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -7209,6 +7231,18 @@ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", "license": "MIT" }, + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "license": "MIT", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", diff --git a/package.json b/package.json index 0d1d3c0..c65a8b3 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@expo/vector-icons": "^15.0.3", "@opencode-ai/sdk": "^1.0.167", + "@react-native-async-storage/async-storage": "2.2.0", "expo": "~54.0.30", "expo-blur": "^15.0.8", "expo-glass-effect": "^0.1.8", diff --git a/src/providers/OpenCodeProvider.tsx b/src/providers/OpenCodeProvider.tsx index 165a54f..ac0a978 100644 --- a/src/providers/OpenCodeProvider.tsx +++ b/src/providers/OpenCodeProvider.tsx @@ -1,6 +1,9 @@ import React, { createContext, useContext, useState, useCallback, useRef, useEffect, ReactNode } from 'react'; +import AsyncStorage from '@react-native-async-storage/async-storage'; import { createOpencodeClient } from '@opencode-ai/sdk/client'; +const SERVER_URL_KEY = 'opencode_server_url'; + export type OpenCodeClient = ReturnType; export interface Session { @@ -124,7 +127,14 @@ export function OpenCodeProvider({ children, defaultServerUrl = 'http://10.0.10. const [connected, setConnected] = useState(false); const [connecting, setConnecting] = useState(false); const [error, setError] = useState(null); - const [serverUrl, setServerUrl] = useState(defaultServerUrl); + const [serverUrl, setServerUrlState] = useState(defaultServerUrl); + + const setServerUrl = useCallback((url: string) => { + setServerUrlState(url); + AsyncStorage.setItem(SERVER_URL_KEY, url).catch(err => { + console.error('Failed to save server URL to storage:', err); + }); + }, []); const clientRef = useRef(null); @@ -176,9 +186,13 @@ export function OpenCodeProvider({ children, defaultServerUrl = 'http://10.0.10. return ''; }; + const connectionIdRef = useRef(0); + // Connect to server const connect = useCallback(async (url?: string) => { const targetUrl = url || serverUrl; + const connectionId = ++connectionIdRef.current; + setConnecting(true); setError(null); @@ -187,15 +201,28 @@ export function OpenCodeProvider({ children, defaultServerUrl = 'http://10.0.10. baseUrl: targetUrl, }); - // Test connection - await client.session.list(); + // Test connection with a timeout + const listPromise = client.session.list(); + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Connection timed out')), 5000) + ); + + await Promise.race([listPromise, timeoutPromise]); + if (connectionId !== connectionIdRef.current) { + return false; + } + clientRef.current = client; setServerUrl(targetUrl); setConnected(true); setConnecting(false); return true; } catch (err) { + if (connectionId !== connectionIdRef.current) { + return false; + } + setError((err as Error).message); setConnected(false); setConnecting(false); @@ -203,6 +230,26 @@ export function OpenCodeProvider({ children, defaultServerUrl = 'http://10.0.10. } }, [serverUrl]); + // Load saved URL and auto-connect on mount + useEffect(() => { + const init = async () => { + try { + const savedUrl = await AsyncStorage.getItem(SERVER_URL_KEY); + if (savedUrl) { + setServerUrlState(savedUrl); + // Silently try to connect + connect(savedUrl); + } else { + connect(serverUrl); + } + } catch (err) { + // Fallback to default + connect(serverUrl); + } + }; + init(); + }, []); // Run once on mount + // Disconnect const disconnect = useCallback(() => { // Stop any SSE subscription diff --git a/src/screens/SessionsScreen.tsx b/src/screens/SessionsScreen.tsx index 44d05cf..e677fe9 100644 --- a/src/screens/SessionsScreen.tsx +++ b/src/screens/SessionsScreen.tsx @@ -6,6 +6,7 @@ import { TouchableOpacity, RefreshControl, StyleSheet, + Platform, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../hooks/useTheme'; @@ -184,8 +185,8 @@ export function SessionsScreen({ ); }; - // Extra padding for the floating liquid glass tab bar on iPad - const topPadding = insets.top + 60; + // Extra padding for the floating liquid glass tab bar on iOS/iPad + const topPadding = Platform.OS === 'ios' ? insets.top + 60 : insets.top + spacing.lg; return ( diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 9cfa41d..740d0d5 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -5,6 +5,7 @@ import { TouchableOpacity, StyleSheet, ScrollView, + Platform, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTheme } from '../hooks/useTheme'; @@ -23,8 +24,8 @@ export function SettingsScreen({ const { theme, colors: c } = useTheme(); const insets = useSafeAreaInsets(); - // Extra padding for the floating liquid glass tab bar on iPad - const topPadding = insets.top + 60; + // Extra padding for the floating liquid glass tab bar on iOS/iPad + const topPadding = Platform.OS === 'ios' ? insets.top + 60 : insets.top + spacing.lg; return (