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",
|
||||
"workbench.colorTheme": "Default Dark+",
|
||||
"workbench.colorTheme": "Dark+",
|
||||
"terminal.integrated.env.windows": {},
|
||||
"editor.formatOnSave": true,
|
||||
"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-worklets": "0.7.2",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"zod": "^4.3.6"
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "~19.2.2",
|
||||
@@ -13898,6 +13899,35 @@
|
||||
"funding": {
|
||||
"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-worklets": "0.7.2",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"zod": "^4.3.6"
|
||||
"zod": "^4.3.6",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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 {StatusBar} from 'expo-status-bar'
|
||||
import { colors } from "../stlyes/global";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
|
||||
export default function RootLayout() {
|
||||
return <>
|
||||
<StatusBar style="dark" />
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name='(tabs)' />
|
||||
</Stack>
|
||||
</>;
|
||||
return (
|
||||
<>
|
||||
<StatusBar style="dark" />
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<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"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -556,9 +553,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -576,9 +570,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -596,9 +587,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
||||
Reference in New Issue
Block a user