refactor(scanner): more basic work to get the scanner just running
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m33s
All checks were successful
Build and Push LST Docker Image / docker (push) Successful in 2m33s
This commit is contained in:
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"editor.defaultFormatter": "biomejs.biome",
|
"editor.defaultFormatter": "biomejs.biome",
|
||||||
"workbench.colorTheme": "Default Dark+",
|
"workbench.colorTheme": "Dark+",
|
||||||
"terminal.integrated.env.windows": {},
|
"terminal.integrated.env.windows": {},
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"typescript.preferences.importModuleSpecifier": "relative",
|
"typescript.preferences.importModuleSpecifier": "relative",
|
||||||
|
|||||||
32
lstMobile/package-lock.json
generated
32
lstMobile/package-lock.json
generated
@@ -39,7 +39,8 @@
|
|||||||
"react-native-web": "~0.21.0",
|
"react-native-web": "~0.21.0",
|
||||||
"react-native-worklets": "0.7.2",
|
"react-native-worklets": "0.7.2",
|
||||||
"socket.io-client": "^4.8.3",
|
"socket.io-client": "^4.8.3",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6",
|
||||||
|
"zustand": "^5.0.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "~19.2.2",
|
"@types/react": "~19.2.2",
|
||||||
@@ -13898,6 +13899,35 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zustand": {
|
||||||
|
"version": "5.0.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz",
|
||||||
|
"integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=18.0.0",
|
||||||
|
"immer": ">=9.0.6",
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"use-sync-external-store": ">=1.2.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"use-sync-external-store": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,8 @@
|
|||||||
"react-native-web": "~0.21.0",
|
"react-native-web": "~0.21.0",
|
||||||
"react-native-worklets": "0.7.2",
|
"react-native-worklets": "0.7.2",
|
||||||
"socket.io-client": "^4.8.3",
|
"socket.io-client": "^4.8.3",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6",
|
||||||
|
"zustand": "^5.0.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "~19.2.2",
|
"@types/react": "~19.2.2",
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
// 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import {StatusBar} from 'expo-status-bar'
|
import { StatusBar } from "expo-status-bar";
|
||||||
import { colors } from "../stlyes/global";
|
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
return <>
|
return (
|
||||||
<StatusBar style="dark" />
|
<>
|
||||||
<Stack screenOptions={{ headerShown: false }}>
|
<StatusBar style="dark" />
|
||||||
<Stack.Screen name='(tabs)' />
|
<Stack screenOptions={{ headerShown: false }}>
|
||||||
</Stack>
|
<Stack.Screen name="index" />
|
||||||
</>;
|
{/* <Stack.Screen name="(tabs)" /> */}
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
9
lstMobile/src/app/blocked.tsx
Normal file
9
lstMobile/src/app/blocked.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Text, View } from "react-native";
|
||||||
|
|
||||||
|
export default function blocked() {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text>Blocked</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
72
lstMobile/src/app/index.tsx
Normal file
72
lstMobile/src/app/index.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { useRouter } from "expo-router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { ActivityIndicator, Text, View } from "react-native";
|
||||||
|
import { useAppStore } from "../hooks/useAppStore";
|
||||||
|
import { devDelay } from "../lib/devMode";
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [message, setMessage] = useState(<Text>Starting app...</Text>);
|
||||||
|
|
||||||
|
const hasHydrated = useAppStore((s) => s.hasHydrated);
|
||||||
|
const serverPort = useAppStore((s) => s.serverPort);
|
||||||
|
const hasValidSetup = useAppStore((s) => s.hasValidSetup);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasHydrated) {
|
||||||
|
setMessage(<Text>Loading app...</Text>);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startup = async () => {
|
||||||
|
try {
|
||||||
|
await devDelay(1500);
|
||||||
|
|
||||||
|
setMessage(<Text>Validating data...</Text>);
|
||||||
|
await devDelay(1500);
|
||||||
|
|
||||||
|
if (!hasValidSetup()) {
|
||||||
|
router.replace("/setup");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessage(<Text>Checking scanner mode...</Text>);
|
||||||
|
await devDelay(1500);
|
||||||
|
|
||||||
|
if (parseInt(serverPort || "0", 10) >= 50000) {
|
||||||
|
setMessage(
|
||||||
|
<Text>
|
||||||
|
Starting normal alplaprod scanner that has no LST rules
|
||||||
|
</Text>,
|
||||||
|
);
|
||||||
|
await devDelay(1500);
|
||||||
|
router.replace("/setup");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessage(<Text>Opening LST scan app</Text>);
|
||||||
|
await devDelay(3250);
|
||||||
|
router.replace("/scanner");
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Startup error", error);
|
||||||
|
setMessage(<Text>Something went wrong during startup.</Text>);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
startup();
|
||||||
|
}, [hasHydrated, hasValidSetup, serverPort, router]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
<ActivityIndicator size="large" />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
lstMobile/src/app/scanner.tsx
Normal file
32
lstMobile/src/app/scanner.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Text, View } from "react-native";
|
||||||
|
|
||||||
|
export default function scanner() {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
//justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: 50,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={{ alignItems: "center", margin: 10 }}>
|
||||||
|
<Text style={{ fontSize: 20, fontWeight: "600" }}>LST Scanner</Text>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
marginTop: 50,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>Relocate</Text>
|
||||||
|
<Text>0 / 4</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* <View>
|
||||||
|
<Text>List of recent scanned pallets TBA</Text>
|
||||||
|
</View> */}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
156
lstMobile/src/app/setup.tsx
Normal file
156
lstMobile/src/app/setup.tsx
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import Constants from "expo-constants";
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Alert, Button, Text, TextInput, View } from "react-native";
|
||||||
|
import { useAppStore } from "../hooks/useAppStore";
|
||||||
|
|
||||||
|
export default function setup() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [auth, setAuth] = useState(false);
|
||||||
|
const [pin, setPin] = useState("");
|
||||||
|
const version = Constants.expoConfig?.version;
|
||||||
|
const build = Constants.expoConfig?.android?.versionCode ?? 1;
|
||||||
|
|
||||||
|
const serverIpFromStore = useAppStore((s) => s.serverIp);
|
||||||
|
const serverPortFromStore = useAppStore((s) => s.serverPort);
|
||||||
|
|
||||||
|
const updateAppState = useAppStore((s) => s.updateAppState);
|
||||||
|
|
||||||
|
// local form state
|
||||||
|
const [serverIp, setLocalServerIp] = useState(serverIpFromStore);
|
||||||
|
const [serverPort, setLocalServerPort] = useState(serverPortFromStore);
|
||||||
|
|
||||||
|
const authCheck = () => {
|
||||||
|
if (pin === "6971") {
|
||||||
|
setAuth(true);
|
||||||
|
} else {
|
||||||
|
Alert.alert("Incorrect pin entered please try again");
|
||||||
|
setPin("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!serverIp.trim() || !serverPort.trim()) {
|
||||||
|
Alert.alert("Missing info", "Please fill in both fields.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAppState({
|
||||||
|
serverIp: serverIp.trim(),
|
||||||
|
serverPort: serverPort.trim(),
|
||||||
|
setupCompleted: true,
|
||||||
|
isRegistered: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
Alert.alert("Saved", "Config saved to device.");
|
||||||
|
//router.replace("/");
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
//justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: 50,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={{ alignItems: "center", margin: 10 }}>
|
||||||
|
<Text style={{ fontSize: 20, fontWeight: "600" }}>
|
||||||
|
LST Scanner Config
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{!auth ? (
|
||||||
|
<View>
|
||||||
|
<Text>Pin Number</Text>
|
||||||
|
<TextInput
|
||||||
|
value={pin}
|
||||||
|
onChangeText={setPin}
|
||||||
|
placeholder=""
|
||||||
|
//autoCapitalize="none"
|
||||||
|
keyboardType="numeric"
|
||||||
|
style={{ borderWidth: 1, padding: 10, borderRadius: 8, width: 128 }}
|
||||||
|
/>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "center",
|
||||||
|
padding: 3,
|
||||||
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button title="Save Config" onPress={authCheck} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View>
|
||||||
|
<Text>Server IP</Text>
|
||||||
|
<TextInput
|
||||||
|
value={serverIp}
|
||||||
|
onChangeText={setLocalServerIp}
|
||||||
|
placeholder="192.168.1.1"
|
||||||
|
//autoCapitalize="none"
|
||||||
|
keyboardType="numeric"
|
||||||
|
style={{ borderWidth: 1, padding: 10, borderRadius: 8 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text>Server port</Text>
|
||||||
|
<TextInput
|
||||||
|
value={serverPort}
|
||||||
|
onChangeText={setLocalServerPort}
|
||||||
|
placeholder="3000"
|
||||||
|
autoCapitalize="characters"
|
||||||
|
keyboardType="numeric"
|
||||||
|
style={{ borderWidth: 1, padding: 10, borderRadius: 8 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{parseInt(serverPort ?? "0", 10) >= 50000 && (
|
||||||
|
<View>
|
||||||
|
<Text>Scanner ID</Text>
|
||||||
|
<Text style={{ width: 250 }}>
|
||||||
|
This is needed as you will be redirected to the standard scanner
|
||||||
|
with no rules except the rules that alplaprod puts in
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
value={scannerId}
|
||||||
|
onChangeText={setScannerId}
|
||||||
|
placeholder="0001"
|
||||||
|
autoCapitalize="characters"
|
||||||
|
keyboardType="numeric"
|
||||||
|
style={{ borderWidth: 1, padding: 10, borderRadius: 8 }}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "center",
|
||||||
|
padding: 3,
|
||||||
|
gap: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button title="Save Config" onPress={handleSave} />
|
||||||
|
<Button
|
||||||
|
title="Home"
|
||||||
|
onPress={() => {
|
||||||
|
router.push("/");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
marginTop: "auto",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: 10,
|
||||||
|
marginBottom: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: 12, color: "#666" }}>
|
||||||
|
LST Scanner v{version}-{build}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
151
lstMobile/src/hooks/useAppStore.ts
Normal file
151
lstMobile/src/hooks/useAppStore.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { createJSONStorage, persist } from "zustand/middleware";
|
||||||
|
|
||||||
|
export type ValidationStatus = "idle" | "pending" | "passed" | "failed";
|
||||||
|
|
||||||
|
export type AppState = {
|
||||||
|
serverIp: string;
|
||||||
|
serverPort: string;
|
||||||
|
scannerId?: string;
|
||||||
|
stageId?: string;
|
||||||
|
deviceName?: string;
|
||||||
|
|
||||||
|
setupCompleted: boolean;
|
||||||
|
isRegistered: boolean;
|
||||||
|
|
||||||
|
lastValidationStatus: ValidationStatus;
|
||||||
|
lastValidationAt?: string;
|
||||||
|
|
||||||
|
appVersion?: string;
|
||||||
|
|
||||||
|
hasHydrated: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AppActions = {
|
||||||
|
setServerIp: (value: string) => void;
|
||||||
|
setServerPort: (value: string) => void;
|
||||||
|
setScannerId: (value?: string) => void;
|
||||||
|
setStageId: (value?: string) => void;
|
||||||
|
setDeviceName: (value?: string) => void;
|
||||||
|
setSetupCompleted: (value: boolean) => void;
|
||||||
|
setIsRegistered: (value: boolean) => void;
|
||||||
|
setValidationStatus: (status: ValidationStatus, validatedAt?: string) => void;
|
||||||
|
setAppVersion: (value?: string) => void;
|
||||||
|
setHasHydrated: (value: boolean) => void;
|
||||||
|
|
||||||
|
updateAppState: (updates: Partial<AppState>) => void;
|
||||||
|
resetApp: () => void;
|
||||||
|
|
||||||
|
hasValidSetup: () => boolean;
|
||||||
|
canEnterApp: () => boolean;
|
||||||
|
getServerUrl: () => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AppStore = AppState & AppActions;
|
||||||
|
|
||||||
|
const defaultAppState: AppState = {
|
||||||
|
serverIp: "",
|
||||||
|
serverPort: "",
|
||||||
|
scannerId: "0001",
|
||||||
|
stageId: undefined,
|
||||||
|
deviceName: undefined,
|
||||||
|
|
||||||
|
setupCompleted: false,
|
||||||
|
isRegistered: false,
|
||||||
|
|
||||||
|
lastValidationStatus: "idle",
|
||||||
|
lastValidationAt: undefined,
|
||||||
|
|
||||||
|
appVersion: undefined,
|
||||||
|
|
||||||
|
hasHydrated: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAppStore = create<AppStore>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
...defaultAppState,
|
||||||
|
|
||||||
|
setServerIp: (value) => set({ serverIp: value }),
|
||||||
|
setServerPort: (value) => set({ serverPort: value }),
|
||||||
|
setScannerId: (value) => set({ scannerId: value }),
|
||||||
|
setStageId: (value) => set({ stageId: value }),
|
||||||
|
setDeviceName: (value) => set({ deviceName: value }),
|
||||||
|
setSetupCompleted: (value) => set({ setupCompleted: value }),
|
||||||
|
setIsRegistered: (value) => set({ isRegistered: value }),
|
||||||
|
|
||||||
|
setValidationStatus: (status, validatedAt) =>
|
||||||
|
set({
|
||||||
|
lastValidationStatus: status,
|
||||||
|
lastValidationAt: validatedAt,
|
||||||
|
}),
|
||||||
|
|
||||||
|
setAppVersion: (value) => set({ appVersion: value }),
|
||||||
|
setHasHydrated: (value) => set({ hasHydrated: value }),
|
||||||
|
|
||||||
|
updateAppState: (updates) =>
|
||||||
|
set((state) => ({
|
||||||
|
...state,
|
||||||
|
...updates,
|
||||||
|
})),
|
||||||
|
|
||||||
|
resetApp: () =>
|
||||||
|
set({
|
||||||
|
...defaultAppState,
|
||||||
|
hasHydrated: true,
|
||||||
|
}),
|
||||||
|
|
||||||
|
hasValidSetup: () => {
|
||||||
|
const state = get();
|
||||||
|
return Boolean(
|
||||||
|
state.serverIp?.trim() &&
|
||||||
|
state.serverPort?.trim() &&
|
||||||
|
state.setupCompleted,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
canEnterApp: () => {
|
||||||
|
const state = get();
|
||||||
|
return Boolean(
|
||||||
|
state.serverIp?.trim() &&
|
||||||
|
state.serverPort?.trim() &&
|
||||||
|
state.setupCompleted &&
|
||||||
|
state.isRegistered,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
getServerUrl: () => {
|
||||||
|
const { serverIp, serverPort } = get();
|
||||||
|
|
||||||
|
if (!serverIp?.trim() || !serverPort?.trim()) return "";
|
||||||
|
return `http://${serverIp.trim()}:${serverPort.trim()}`;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "lst_mobile_app_store",
|
||||||
|
storage: createJSONStorage(() => AsyncStorage),
|
||||||
|
|
||||||
|
onRehydrateStorage: () => (state, error) => {
|
||||||
|
if (error) {
|
||||||
|
console.log("Failed to hydrate app state", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
state?.setHasHydrated(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
partialize: (state) => ({
|
||||||
|
serverIp: state.serverIp,
|
||||||
|
serverPort: state.serverPort,
|
||||||
|
scannerId: state.scannerId,
|
||||||
|
stageId: state.stageId,
|
||||||
|
deviceName: state.deviceName,
|
||||||
|
setupCompleted: state.setupCompleted,
|
||||||
|
isRegistered: state.isRegistered,
|
||||||
|
lastValidationStatus: state.lastValidationStatus,
|
||||||
|
lastValidationAt: state.lastValidationAt,
|
||||||
|
appVersion: state.appVersion,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
1
lstMobile/src/lib/delay.ts
Normal file
1
lstMobile/src/lib/delay.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
||||||
7
lstMobile/src/lib/devMode.ts
Normal file
7
lstMobile/src/lib/devMode.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { delay } from "./delay";
|
||||||
|
|
||||||
|
export const devDelay = async (ms: number) => {
|
||||||
|
if (__DEV__) {
|
||||||
|
await delay(ms);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
|
|
||||||
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()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
33
lstMobile/temps/(tabs)/_layout.tsx
Normal file
33
lstMobile/temps/(tabs)/_layout.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Tabs } from "expo-router";
|
||||||
|
import { Home, Settings } from "lucide-react-native";
|
||||||
|
import { colors } from "../../stlyes/global";
|
||||||
|
|
||||||
|
export default function TabLayout() {
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
screenOptions={{
|
||||||
|
headerShown: false,
|
||||||
|
tabBarStyle: {},
|
||||||
|
tabBarActiveTintColor: "black",
|
||||||
|
tabBarInactiveTintColor: colors.textSecondary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tabs.Screen
|
||||||
|
name="home"
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
94
lstMobile/temps/(tabs)/config.tsx
Normal file
94
lstMobile/temps/(tabs)/config.tsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
// app/config.tsx
|
||||||
|
|
||||||
|
import Constants from "expo-constants";
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Alert, Button, Text, TextInput, View } from "react-native";
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
lstMobile/temps/(tabs)/home.tsx
Normal file
43
lstMobile/temps/(tabs)/home.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
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 { hasValidSetup, type PersistedAppState } from "../../lib/storage";
|
||||||
|
import {
|
||||||
|
evaluateVersion,
|
||||||
|
type ServerVersionInfo,
|
||||||
|
type StartupStatus,
|
||||||
|
} from "../../lib/versionValidation";
|
||||||
|
import { globalStyles } from "../../stlyes/global";
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
return (
|
||||||
|
<ScrollView>
|
||||||
|
<View style={globalStyles.container}>
|
||||||
|
<HomeHeader />
|
||||||
|
|
||||||
|
<Text>Welcome. Blake</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -536,9 +536,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -556,9 +553,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -576,9 +570,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -596,9 +587,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|||||||
Reference in New Issue
Block a user