misc: upgrade to Expo SDK 58
Move the app from SDK 55 onto React Native 0.88 so later widget and auth work can use the SDK 58 packages. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
82c4187826
commit
6c3a474bb0
11 files changed
+3786
-4862
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { NativeTabs } from 'expo-router/unstable-native-tabs';
|
||||
import { NativeTabs } from 'expo-router/native-tabs';
|
||||
|
||||
export default function TabLayout() {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Button, ContextMenu, Host } from '@expo/ui/swift-ui';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import * as Burnt from 'burnt';
|
||||
import { SymbolView } from 'expo-symbols';
|
||||
@@ -19,6 +18,7 @@ import api from '../../../src/services/api';
|
||||
import { syncDevicesToWidget } from '../../../src/services/widgetSync';
|
||||
import { Device } from '../../../src/types';
|
||||
import { useAuth } from '@/src/context/AuthContext';
|
||||
import { DeviceContextMenu } from '@/src/components/DeviceContextMenu';
|
||||
|
||||
const isAuthError = (error: unknown) =>
|
||||
typeof error === 'object' &&
|
||||
@@ -43,7 +43,6 @@ export default function DeviceListScreen() {
|
||||
|
||||
const fetchDevices = useCallback(async (showLoading = false) => {
|
||||
try {
|
||||
if (showLoading) setIsLoading(true);
|
||||
const data = await api.getDevices();
|
||||
setDevices(data);
|
||||
// Sync devices to iOS widget
|
||||
@@ -66,8 +65,31 @@ export default function DeviceListScreen() {
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) return;
|
||||
|
||||
fetchDevices(true);
|
||||
}, [fetchDevices, isAuthenticated]);
|
||||
let cancelled = false;
|
||||
void api.getDevices().then(
|
||||
data => {
|
||||
if (cancelled) return;
|
||||
setDevices(data);
|
||||
syncDevicesToWidget(data);
|
||||
setIsLoading(false);
|
||||
},
|
||||
(error: unknown) => {
|
||||
if (cancelled) return;
|
||||
if (isAuthError(error)) {
|
||||
setDevices([]);
|
||||
} else {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Failed to load devices';
|
||||
Alert.alert('Error', message);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isAuthenticated]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) return;
|
||||
@@ -301,17 +323,7 @@ export default function DeviceListScreen() {
|
||||
const isOffline = item.status?.toLowerCase() === 'offline';
|
||||
|
||||
return (
|
||||
<Host>
|
||||
<ContextMenu>
|
||||
<ContextMenu.Items>
|
||||
<Button
|
||||
systemImage="trash"
|
||||
role="destructive"
|
||||
label="Delete Device"
|
||||
onPress={() => handleDelete(item)}
|
||||
/>
|
||||
</ContextMenu.Items>
|
||||
<ContextMenu.Trigger>
|
||||
<DeviceContextMenu onDelete={() => handleDelete(item)}>
|
||||
<View
|
||||
style={[
|
||||
styles.deviceCard,
|
||||
@@ -392,9 +404,7 @@ export default function DeviceListScreen() {
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</ContextMenu.Trigger>
|
||||
</ContextMenu>
|
||||
</Host>
|
||||
</DeviceContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,60 @@ import { useAuth } from '../../../src/context/AuthContext';
|
||||
import { useRouter } from 'expo-router';
|
||||
import Constants from 'expo-constants';
|
||||
|
||||
function SettingItem({
|
||||
label,
|
||||
value,
|
||||
onPress,
|
||||
textColor,
|
||||
subTextColor,
|
||||
borderBottomColor,
|
||||
}: {
|
||||
label: string;
|
||||
value?: string;
|
||||
onPress?: () => void;
|
||||
textColor: string;
|
||||
subTextColor: string;
|
||||
borderBottomColor: string;
|
||||
}) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.settingItem, { borderBottomColor }]}
|
||||
onPress={onPress}
|
||||
disabled={!onPress}
|
||||
activeOpacity={onPress ? 0.7 : 1}
|
||||
>
|
||||
<Text style={[styles.settingLabel, { color: textColor }]}>{label}</Text>
|
||||
<View style={styles.settingValueContainer}>
|
||||
<Text
|
||||
style={[styles.settingValue, { color: subTextColor }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
function ActionButton({
|
||||
title,
|
||||
onPress,
|
||||
backgroundColor,
|
||||
}: {
|
||||
title: string;
|
||||
onPress: () => void;
|
||||
backgroundColor: string;
|
||||
}) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.actionButton, { backgroundColor }]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Text style={styles.actionButtonText}>{title}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SettingsScreen() {
|
||||
const { user, serverAddress, logout } = useAuth();
|
||||
const router = useRouter();
|
||||
@@ -22,7 +76,6 @@ export default function SettingsScreen() {
|
||||
const sectionTitleBg = isDark ? '#111111' : '#f9f9f9';
|
||||
const textColor = isDark ? '#fff' : '#333';
|
||||
const subTextColor = isDark ? '#c6c6c8' : '#666';
|
||||
const primary = isDark ? '#0A84FF' : '#007AFF';
|
||||
const destructiveColor = '#f44336';
|
||||
|
||||
const handleLogout = () => {
|
||||
@@ -39,55 +92,7 @@ export default function SettingsScreen() {
|
||||
]);
|
||||
};
|
||||
|
||||
const SettingItem = ({
|
||||
label,
|
||||
value,
|
||||
onPress,
|
||||
}: {
|
||||
label: string;
|
||||
value?: string;
|
||||
onPress?: () => void;
|
||||
}) => (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.settingItem,
|
||||
{ borderBottomColor: isDark ? 'rgba(255,255,255,0.03)' : '#f0f0f0' },
|
||||
]}
|
||||
onPress={onPress}
|
||||
disabled={!onPress}
|
||||
activeOpacity={onPress ? 0.7 : 1}
|
||||
>
|
||||
<Text style={[styles.settingLabel, { color: textColor }]}>{label}</Text>
|
||||
<View style={styles.settingValueContainer}>
|
||||
<Text
|
||||
style={[styles.settingValue, { color: subTextColor }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const ActionButton = ({
|
||||
title,
|
||||
onPress,
|
||||
destructive = false,
|
||||
}: {
|
||||
title: string;
|
||||
onPress: () => void;
|
||||
destructive?: boolean;
|
||||
}) => (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.actionButton,
|
||||
{ backgroundColor: destructive ? destructiveColor : primary },
|
||||
]}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Text style={styles.actionButtonText}>{title}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
const borderBottomColor = isDark ? 'rgba(255,255,255,0.03)' : '#f0f0f0';
|
||||
|
||||
return (
|
||||
<ScrollView style={[styles.container, { backgroundColor: bgColor }]} contentInsetAdjustmentBehavior='automatic'>
|
||||
@@ -109,9 +114,27 @@ export default function SettingsScreen() {
|
||||
>
|
||||
Information
|
||||
</Text>
|
||||
<SettingItem label={user?.email ? 'Email' : 'Username'} value={user?.email || user?.username} />
|
||||
<SettingItem label="Server URL" value={serverAddress || 'N/A'} />
|
||||
<SettingItem label="App Version" value={`${Constants.expoConfig?.version}`} />
|
||||
<SettingItem
|
||||
label={user?.email ? 'Email' : 'Username'}
|
||||
value={user?.email || user?.username}
|
||||
textColor={textColor}
|
||||
subTextColor={subTextColor}
|
||||
borderBottomColor={borderBottomColor}
|
||||
/>
|
||||
<SettingItem
|
||||
label="Server URL"
|
||||
value={serverAddress || 'N/A'}
|
||||
textColor={textColor}
|
||||
subTextColor={subTextColor}
|
||||
borderBottomColor={borderBottomColor}
|
||||
/>
|
||||
<SettingItem
|
||||
label="App Version"
|
||||
value={`${Constants.expoConfig?.version}`}
|
||||
textColor={textColor}
|
||||
subTextColor={subTextColor}
|
||||
borderBottomColor={borderBottomColor}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
@@ -131,7 +154,11 @@ export default function SettingsScreen() {
|
||||
>
|
||||
Actions
|
||||
</Text>
|
||||
<ActionButton title="Logout" onPress={handleLogout} destructive />
|
||||
<ActionButton
|
||||
title="Logout"
|
||||
onPress={handleLogout}
|
||||
backgroundColor={destructiveColor}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.footer}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CommonActions, useNavigation } from '@react-navigation/native';
|
||||
import * as Burnt from 'burnt';
|
||||
import { Redirect, useLocalSearchParams } from 'expo-router';
|
||||
import { Redirect, useLocalSearchParams, useNavigation } from 'expo-router';
|
||||
import { CommonActions } from 'expo-router/react-navigation';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import api from '../../../src/services/api';
|
||||
|
||||
|
||||
@@ -56,8 +56,8 @@ export default function ScanDevicesScreen() {
|
||||
ip: deviceIP,
|
||||
netmask: "255.255.255.0",
|
||||
broadcast: "",
|
||||
secureOnPassword: "",
|
||||
port: 9,
|
||||
password: "",
|
||||
ports: [9],
|
||||
groups: [],
|
||||
status: "offline",
|
||||
});
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Learn more about light and dark modes:
|
||||
* https://docs.expo.dev/guides/color-schemes/
|
||||
*/
|
||||
|
||||
import { Colors } from '@/constants/theme';
|
||||
import { useColorScheme } from '@/hooks/use-color-scheme';
|
||||
|
||||
export function useThemeColor(
|
||||
props: { light?: string; dark?: string },
|
||||
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
|
||||
) {
|
||||
const theme = useColorScheme() ?? 'light';
|
||||
const colorFromProps = props[theme];
|
||||
|
||||
if (colorFromProps) {
|
||||
return colorFromProps;
|
||||
} else {
|
||||
return Colors[theme][colorName];
|
||||
}
|
||||
}
|
||||
Generated
+3572
-4706
File diff suppressed because it is too large.
Load diff
+27
-30
@@ -12,42 +12,39 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@bacons/apple-targets": "^3.0.6",
|
||||
"@expo/ui": "~55.0.15",
|
||||
"@expo/ui": "~58.0.21",
|
||||
"@expo/vector-icons": "^15.0.3",
|
||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||
"@react-navigation/elements": "^2.6.3",
|
||||
"@react-navigation/native": "^7.1.8",
|
||||
"burnt": "^0.13.0",
|
||||
"expo": "^55.0.23",
|
||||
"expo-constants": "~55.0.7",
|
||||
"expo-dev-client": "~55.0.33",
|
||||
"expo-font": "~55.0.4",
|
||||
"expo-glass-effect": "~55.0.11",
|
||||
"expo-haptics": "~55.0.14",
|
||||
"expo-image": "~55.0.10",
|
||||
"expo-linking": "~55.0.15",
|
||||
"expo-router": "~55.0.14",
|
||||
"expo-splash-screen": "~55.0.20",
|
||||
"expo-status-bar": "~55.0.6",
|
||||
"expo-symbols": "~55.0.8",
|
||||
"expo-system-ui": "~55.0.17",
|
||||
"expo-web-browser": "~55.0.15",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-native": "0.83.6",
|
||||
"react-native-gesture-handler": "~2.30.0",
|
||||
"react-native-reanimated": "4.2.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.23.0",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-worklets": "0.7.4"
|
||||
"expo": "^58.0.16",
|
||||
"expo-constants": "~58.0.12",
|
||||
"expo-dev-client": "~58.0.14",
|
||||
"expo-font": "~58.0.9",
|
||||
"expo-glass-effect": "~58.0.4",
|
||||
"expo-haptics": "~58.0.7",
|
||||
"expo-image": "~58.0.16",
|
||||
"expo-linking": "~58.0.13",
|
||||
"expo-router": "~58.0.24",
|
||||
"expo-splash-screen": "~58.0.10",
|
||||
"expo-status-bar": "~58.0.3",
|
||||
"expo-symbols": "~58.0.4",
|
||||
"expo-system-ui": "~58.0.6",
|
||||
"expo-web-browser": "~58.0.6",
|
||||
"react": "19.3.0",
|
||||
"react-dom": "19.3.0",
|
||||
"react-native": "0.88.0",
|
||||
"react-native-gesture-handler": "~3.3.0",
|
||||
"react-native-reanimated": "4.7.1",
|
||||
"react-native-safe-area-context": "~5.10.1",
|
||||
"react-native-screens": "~4.30.0",
|
||||
"react-native-web": "^0.21.4",
|
||||
"react-native-worklets": "0.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "~19.2.10",
|
||||
"@types/react": "~19.3.0",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-expo": "~55.0.0",
|
||||
"typescript": "~5.9.2"
|
||||
"eslint-config-expo": "~58.0.4",
|
||||
"typescript": "~6.0.3"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Button, ContextMenu, Host } from '@expo/ui/swift-ui';
|
||||
import React from 'react';
|
||||
|
||||
export function DeviceContextMenu({
|
||||
children,
|
||||
onDelete,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Host>
|
||||
<ContextMenu>
|
||||
<ContextMenu.Items>
|
||||
<Button
|
||||
systemImage="trash"
|
||||
role="destructive"
|
||||
label="Delete Device"
|
||||
onPress={onDelete}
|
||||
/>
|
||||
</ContextMenu.Items>
|
||||
<ContextMenu.Trigger>{children}</ContextMenu.Trigger>
|
||||
</ContextMenu>
|
||||
</Host>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
export function DeviceContextMenu({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
+37
-28
@@ -23,10 +23,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [canCreate, setCanCreate] = useState<boolean | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadAuth();
|
||||
}, []);
|
||||
|
||||
const clearSession = useCallback(async () => {
|
||||
await AsyncStorage.multiRemove(['auth_token', 'auth_user', 'auth_can_create']);
|
||||
|
||||
@@ -45,33 +41,46 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
};
|
||||
}, [clearSession]);
|
||||
|
||||
const loadAuth = async () => {
|
||||
try {
|
||||
const storedToken = await AsyncStorage.getItem('auth_token');
|
||||
const storedUser = await AsyncStorage.getItem('auth_user');
|
||||
const storedServerAddress = await AsyncStorage.getItem('auth_server_address');
|
||||
const storedCanCreate = await AsyncStorage.getItem('auth_can_create');
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
if (storedCanCreate) {
|
||||
setCanCreate(storedCanCreate === 'true');
|
||||
}
|
||||
|
||||
if (storedToken && storedUser) {
|
||||
setToken(storedToken);
|
||||
api.setToken(storedToken);
|
||||
setUser(JSON.parse(storedUser));
|
||||
}
|
||||
void AsyncStorage.multiGet([
|
||||
'auth_token',
|
||||
'auth_user',
|
||||
'auth_server_address',
|
||||
'auth_can_create',
|
||||
]).then(
|
||||
([[, storedToken], [, storedUser], [, storedServerAddress], [, storedCanCreate]]) => {
|
||||
if (cancelled) return;
|
||||
|
||||
if (storedServerAddress) {
|
||||
setServerAddress(storedServerAddress);
|
||||
api.setAddress(storedServerAddress + '/api');
|
||||
if (storedCanCreate) {
|
||||
setCanCreate(storedCanCreate === 'true');
|
||||
}
|
||||
|
||||
if (storedToken && storedUser) {
|
||||
setToken(storedToken);
|
||||
api.setToken(storedToken);
|
||||
setUser(JSON.parse(storedUser));
|
||||
}
|
||||
|
||||
if (storedServerAddress) {
|
||||
setServerAddress(storedServerAddress);
|
||||
api.setAddress(storedServerAddress + '/api');
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
},
|
||||
error => {
|
||||
if (cancelled) return;
|
||||
console.error('Failed to load auth', error);
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load auth', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const login = async (serverAddress: string, identity: string, password: string) => {
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user