feat(lstmobile): intial scanner setup kinda working
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m7s

This commit is contained in:
2026-04-17 16:47:09 -05:00
parent a1eeadeec4
commit 3734d9daac
42 changed files with 14627 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
import { Tabs } from 'expo-router'
import React from 'react'
import { colors } from '../../stlyes/global'
import { Home,Settings } from 'lucide-react-native'
export default function TabLayout() {
return (
<Tabs
screenOptions={{
headerShown: false,
tabBarStyle:{
},
tabBarActiveTintColor: 'black',
tabBarInactiveTintColor: colors.textSecondary,
}}
>
<Tabs.Screen
name='index'
options={{
title:'Home',
tabBarIcon: ({color, size})=>(
<Home color={color} size={size}/>
)
}}
/>
<Tabs.Screen
name='config'
options={{
title: 'Config',
tabBarIcon: ({color, size})=> (
<Settings size={size} color={color}/>
)
}}
/>
</Tabs>
)
}

View File

@@ -0,0 +1,92 @@
// app/config.tsx
import { useEffect, useState } from "react";
import { View, Text, TextInput, Button, Alert } from "react-native";
import { useRouter } from "expo-router";
import { AppConfig, getConfig, saveConfig } from "../../lib/storage";
import Constants from "expo-constants";
export default function Config() {
const [serverUrl, setServerUrl] = useState("");
const [scannerId, setScannerId] = useState("");
const [config, setConfig] = useState<AppConfig | null>(null)
const [loading, setLoading] = useState(true);
const router = useRouter()
const version = Constants.expoConfig?.version;
const build = Constants.expoConfig?.android?.versionCode ?? 1;
useEffect(() => {
const loadConfig = async () => {
const existing = await getConfig();
if (existing) {
setServerUrl(existing.serverUrl);
setScannerId(existing.scannerId);
setConfig(existing)
}
setLoading(false);
};
loadConfig();
}, []);
const handleSave = async () => {
if (!serverUrl.trim() || !scannerId.trim()) {
Alert.alert("Missing info", "Please fill in both fields.");
return;
}
await saveConfig({
serverUrl: serverUrl.trim(),
scannerId: scannerId.trim(),
});
Alert.alert("Saved", "Config saved to device.");
//router.replace("/");
};
if (loading) {
return <Text>Loading config...</Text>;
}
return (
<View style={{ flex: 1, padding: 16, gap: 12 }}>
<View style={{alignItems: "center", margin: 10}}>
<Text style={{ fontSize: 20, fontWeight: "600"}}>LST Scanner Config</Text>
</View>
<Text>Server IP</Text>
<TextInput
value={serverUrl}
onChangeText={setServerUrl}
placeholder="192.168.1.1"
autoCapitalize="none"
keyboardType="numeric"
style={{ borderWidth: 1, padding: 10, borderRadius: 8 }}
/>
<Text>Server port</Text>
<TextInput
value={scannerId}
onChangeText={setScannerId}
placeholder="3000"
autoCapitalize="characters"
keyboardType="numeric"
style={{ borderWidth: 1, padding: 10, borderRadius: 8, }}
/>
<View style={{flexDirection: 'row',justifyContent: 'center', padding: 3}}>
<Button title="Save Config" onPress={handleSave} />
</View>
<View style={{ marginTop: "auto", alignItems: "center", padding: 10 }}>
<Text style={{ fontSize: 12, color: "#666" }}>
LST Scanner v{version}-{build}
</Text>
</View>
</View>
);
}

View File

@@ -0,0 +1,134 @@
import * as Application from "expo-application";
import * as Device from "expo-device";
import { useRouter } from "expo-router";
import { useEffect, useState } from "react";
import {
Alert,
Platform,
ScrollView,
Text,
View,
} from "react-native";
import HomeHeader from "../../components/HomeHeader";
import { type AppConfig, getConfig, hasValidConfig } from "../../lib/storage";
import {
evaluateVersion,
type ServerVersionInfo,
type StartupStatus,
} from "../../lib/versionValidation";
import { globalStyles } from "../../stlyes/global";
import axios from 'axios'
export default function Index() {
const [config, setConfig] = useState<AppConfig | null>(null);
const [loading, setLoading] = useState(true);
const [startupStatus, setStartupStatus] = useState<StartupStatus>({state: "checking"});
const [serverInfo, setServerInfo] = useState<ServerVersionInfo>()
const router = useRouter();
const versionName = Application.nativeApplicationVersion ?? "unknown";
const versionCode = Number(Application.nativeBuildVersion ?? "0");
useEffect(() => {
let isMounted = true;
const startUp = async () => {
try {
const savedConfig = await getConfig();
if (!hasValidConfig(savedConfig)) {
router.replace("/config");
return;
}
if (!isMounted) return;
setConfig(savedConfig);
// temp while testing
const appBuildCode = 1;
try {
const res = await axios.get(`http://${savedConfig?.serverUrl}:${savedConfig?.scannerId}/lst/api/mobile/version`);
console.log(res)
const server = (await res.data) as ServerVersionInfo;
if (!isMounted) return;
const result = evaluateVersion(appBuildCode, server);
setStartupStatus(result);
setServerInfo(server)
if (result.state === "warning") {
Alert.alert("Update available", result.message);
}
} catch {
if (!isMounted) return;
setStartupStatus({ state: "offline" });
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
startUp();
return () => {
isMounted = false;
};
}, [router]);
if (loading) {
return <Text>Validating Configs.</Text>;
}
if (startupStatus.state === "checking") {
return <Text>Checking device and server status...</Text>;
}
if (startupStatus.state === "blocked") {
return (
<View>
<Text>Update Required</Text>
<Text>This scanner must be updated before it can be used.</Text>
<Text>Scan the update code to continue.</Text>
</View>
);
}
if (startupStatus.state === "offline") {
// app still renders, but show disconnected state
}
return (
<ScrollView >
<View style={globalStyles.container}>
<HomeHeader />
<Text>
Welcome.{versionName} - {versionCode}
</Text>
<Text>Running on: {Platform.OS}</Text>
<Text>Device model: {Device.modelName}</Text>
<Text>Device Brand: {Device.brand}</Text>
<Text> OS Version: {Device.osVersion}</Text>
<View style={{ flex: 1, padding: 16, gap: 12 }}>
<Text style={{ fontSize: 22, fontWeight: "600" }}>Welcome</Text>
{config ? (
<>
<Text>Server: {config.serverUrl}</Text>
<Text>Scanner: {config.scannerId}</Text>
<Text>Server: v{serverInfo?.versionName}</Text>
</>
) : (
<Text>No config found yet.</Text>
)}
</View></View>
</ScrollView>
);
}

View File

@@ -0,0 +1,12 @@
import { Stack } from "expo-router";
import {StatusBar} from 'expo-status-bar'
import { colors } from "../stlyes/global";
export default function RootLayout() {
return <>
<StatusBar style="dark" />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name='(tabs)' />
</Stack>
</>;
}

View File

@@ -0,0 +1,24 @@
import React from "react";
import { StyleSheet, Text, View } from "react-native";
import { colors, globalStyles } from "../stlyes/global";
export default function HomeHeader() {
const currentDate = new Date().toLocaleDateString("en-US", {
weekday: "long",
month: "long",
day: "numeric",
});
return (
<View >
<Text style={styles.date}>{currentDate}</Text>
</View>
);
}
const styles = StyleSheet.create({
date: {
fontSize: 14,
color: colors.textSecondary,
marginTop: 4,
marginBottom: 30,
},
});

View File

@@ -0,0 +1,36 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
export type AppConfig = {
serverUrl: string;
scannerId: string;
};
const CONFIG_KEY = "scanner_app_config";
export async function saveConfig(config: AppConfig) {
await AsyncStorage.setItem(CONFIG_KEY, JSON.stringify(config));
}
export async function getConfig(): Promise<AppConfig | null> {
const raw = await AsyncStorage.getItem(CONFIG_KEY);
if (!raw) return null;
try {
return JSON.parse(raw) as AppConfig;
} catch (error) {
console.log("Error", error)
return null;
}
}
export function hasValidConfig(config: AppConfig | null) {
if (!config) return false;
return Boolean(
config.serverUrl?.trim() &&
config.scannerId?.trim()
);
}

View File

@@ -0,0 +1,43 @@
export type ServerVersionInfo = {
packageName: string;
versionName: string;
versionCode: number;
minSupportedVersionCode: number;
fileName: string;
};
export type StartupStatus =
| { state: "checking" }
| { state: "needs-config" }
| { state: "offline" }
| { state: "blocked"; reason: string; server: ServerVersionInfo }
| { state: "warning"; message: string; server: ServerVersionInfo }
| { state: "ready"; server: ServerVersionInfo | null };
export function evaluateVersion(
appBuildCode: number,
server: ServerVersionInfo
): StartupStatus {
if (appBuildCode < server.minSupportedVersionCode) {
return {
state: "blocked",
reason: "This scanner app is too old and must be updated before use.",
server,
};
}
if (appBuildCode !== server.versionCode) {
return {
state: "warning",
message: `A newer version is available. Installed build: ${appBuildCode}, latest build: ${server.versionCode}.`,
server,
};
}
return {
state: "ready",
server,
};
}

View File

@@ -0,0 +1,21 @@
import { StyleSheet } from "react-native";
export const colors = {
background: "white",
header: "white",
primary: 'blue',
textSecondary: "blue",
};
export const globalStyles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
justifyContent: 'center',
alignItems: 'center',
paddingTop: 60,
},
header: {
padding: 4,
},
});