5 Commits

Author SHA1 Message Date
4ca74de279 refactor(mobile): valildation of server after each scan
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 3m40s
2026-05-06 12:10:14 -05:00
12412536d1 refactor(scanner): finished login stuff for current routes 2026-05-06 12:09:47 -05:00
a38e2e0339 refactor(scanner): added in running number 2026-05-06 12:09:09 -05:00
8c253a90b6 chore(release): 0.0.2-alpha.8
Some checks failed
Build and Push LST Docker Image / docker (push) Failing after 4m12s
Release and Build Image / release (push) Failing after 2m19s
2026-05-06 05:08:27 -05:00
ba30281e59 feat(mobile): auth added in
Some checks failed
Build and Push LST Docker Image / docker (push) Has been cancelled
2026-05-06 05:07:16 -05:00
31 changed files with 7170 additions and 218 deletions

View File

@@ -1,5 +1,12 @@
# All Changes to LST can be found below. # All Changes to LST can be found below.
## [0.0.2-alpha.8](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.7...v0.0.2-alpha.8) (2026-05-06)
### 🌟 Enhancements
* **mobile:** auth added in ([ba30281](https://git.tuffraid.net/cowch/lst_v3/commits/ba30281e59040513a036fb7413e372457d04a7c8))
## [0.0.2-alpha.7](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.6...v0.0.2-alpha.7) (2026-05-06) ## [0.0.2-alpha.7](https://git.tuffraid.net/cowch/lst_v3/compare/v0.0.2-alpha.6...v0.0.2-alpha.7) (2026-05-06)

View File

@@ -1,5 +1,6 @@
import { import {
boolean, boolean,
jsonb,
pgEnum, pgEnum,
pgTable, pgTable,
text, text,
@@ -25,7 +26,7 @@ export const scanUser = pgTable(
scannerId: text("scanner_id").unique().notNull(), scannerId: text("scanner_id").unique().notNull(),
pinNumber: text("pin_number").unique().notNull(), pinNumber: text("pin_number").unique().notNull(),
pinHash: text("pin_hash").notNull(), pinHash: text("pin_hash").notNull(),
excludedCommand: text("excluded_commands").default(""), excludedCommand: jsonb("excluded_commands").default([]),
role: mobileRoleEnum("role").notNull().default("user"), role: mobileRoleEnum("role").notNull().default("user"),
active: boolean("active").default(true), active: boolean("active").default(true),
lastScan: timestamp("last_scan").defaultNow(), lastScan: timestamp("last_scan").defaultNow(),

View File

@@ -4,10 +4,12 @@ import type z from "zod";
export const scanLog = pgTable("scan_log", { export const scanLog = pgTable("scan_log", {
id: uuid("id").defaultRandom().primaryKey(), id: uuid("id").defaultRandom().primaryKey(),
user: text("user"),
scannerId: text("scanner_id"), scannerId: text("scanner_id"),
message: text("message").notNull(), message: text("message").notNull(),
prompt: text("prompt"), prompt: text("prompt"),
commandDescription: text("command_description"), commandDescription: text("command_description"),
runningNumber: text("running_number").default("0"),
status: text("status"), status: text("status"),
lines: jsonb("lines").default([]), lines: jsonb("lines").default([]),
add_Date: timestamp("add_Date").defaultNow(), add_Date: timestamp("add_Date").defaultNow(),

View File

@@ -263,6 +263,10 @@ r.patch("/user/:id", requireAuth, async (req, res) => {
updates.active = req.body.active; updates.active = req.body.active;
} }
if (req.body?.excludedCommand !== undefined) {
updates.excludedCommand = req.body.excludedCommand;
}
if (req.body?.role !== undefined) { if (req.body?.role !== undefined) {
updates.role = req.body.role; updates.role = req.body.role;
} }

View File

@@ -18,6 +18,8 @@ router.post("/", async (req, res) => {
commandDescription: body.commandDescription, commandDescription: body.commandDescription,
status: body.status, status: body.status,
lines: body.lines, lines: body.lines,
user: body.user,
runningNumber: body.runningNumber,
}) })
.returning(); .returning();

View File

@@ -15,8 +15,8 @@
"foregroundImage": "./assets/adaptive-icon-white.png", "foregroundImage": "./assets/adaptive-icon-white.png",
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
}, },
"versionCode": 23, "versionCode": 30,
"minSupportedVersionCode": 21, "minSupportedVersionCode": 26,
"predictiveBackGestureEnabled": false, "predictiveBackGestureEnabled": false,
"package": "net.alpla.lst.mobile" "package": "net.alpla.lst.mobile"
}, },

Binary file not shown.

View File

@@ -1,9 +1,34 @@
import { Tabs } from "expo-router"; import { Redirect, Tabs } from "expo-router";
import { Home, Settings } from "lucide-react-native"; import { Container, Home, Logs, Rows4, Settings } from "lucide-react-native";
import { useAppStore } from "../../hooks/useAppStore"; import { useAppStore } from "../../hooks/useAppStore";
import { useMobileAuthStore } from "../../hooks/useMobileAuth";
// const roles = {
// adminOnly: ["admin"],
// management: ["admin", "manager"],
// allStaff: ["admin", "manager", "driver", "lead", "user"],
// };
export default function TabsLayout() { export default function TabsLayout() {
const serverPort = useAppStore((s) => s.serverPort); const serverPort = useAppStore((s) => s.serverPort);
const user = useMobileAuthStore((s) => s.user);
const isUnlocked = useMobileAuthStore((s) => s.isUnlocked);
const port = parseInt(serverPort || "0", 10) >= 50000;
console.log(port);
if (!port) {
if (!user || !isUnlocked) {
return <Redirect href="/login" />;
}
}
const isNormalScanner = parseInt(serverPort || "0", 10) >= 50000;
const hasRole = (allowed: string[] = []) => {
const role = user?.role?.toLowerCase();
return role ? allowed.includes(role) : false;
};
return ( return (
<Tabs <Tabs
screenOptions={{ screenOptions={{
@@ -27,11 +52,24 @@ export default function TabsLayout() {
}} }}
/> />
<Tabs.Screen <Tabs.Screen
name="config" name="laneCheck"
options={{ options={{
title: "settings", title: "Lane Check",
href: isNormalScanner ? null : "/(tabs)/laneCheck",
tabBarIcon: ({ color, size }) => <Rows4 size={size} color={color} />,
}}
/>
<Tabs.Screen
name="dockScan"
options={{
title: "Dock scan",
href:
isNormalScanner || !hasRole(["admin", "manager"])
? null
: "/(tabs)/dockScan",
tabBarIcon: ({ color, size }) => ( tabBarIcon: ({ color, size }) => (
<Settings size={size} color={color} /> <Container size={size} color={color} />
), ),
}} }}
/> />
@@ -40,7 +78,10 @@ export default function TabsLayout() {
options={{ options={{
title: "Logs", title: "Logs",
href: href:
parseInt(serverPort || "0", 10) >= 50000 ? null : "/(tabs)/logs", isNormalScanner || !hasRole(["admin", "manager"])
? null
: "/(tabs)/logs",
tabBarIcon: ({ color, size }) => <Logs size={size} color={color} />,
}} }}
/> />
{/* <Tabs.Screen {/* <Tabs.Screen
@@ -51,6 +92,15 @@ export default function TabsLayout() {
parseInt(serverPort || "0", 10) >= 50000 ? null : "/(tabs)/logs", parseInt(serverPort || "0", 10) >= 50000 ? null : "/(tabs)/logs",
}} }}
/> */} /> */}
<Tabs.Screen
name="config"
options={{
title: "settings",
tabBarIcon: ({ color, size }) => (
<Settings size={size} color={color} />
),
}}
/>
</Tabs> </Tabs>
); );
} }

View File

@@ -1,7 +1,5 @@
import { Link } from "expo-router";
import { Text, View } from "react-native";
import Setup from "../setup"; import Setup from "../setup";
export default function SettingsTab() { export default function SettingsTab() {
return <Setup /> return <Setup />;
} }

View File

@@ -0,0 +1,26 @@
import React from "react";
import { Text, View } from "react-native";
import { Button } from "../../components/ui/button";
export default function LaneCheck() {
const getInfo = async () => {
const info = "ho";
console.log(info);
};
return (
<View
style={{
flex: 1,
//justifyContent: "center",
alignItems: "center",
marginTop: 50,
}}
>
<Text>Dock Scanning</Text>
<Button onPress={getInfo}>
<Text>Check info</Text>
</Button>
</View>
);
}

View File

@@ -0,0 +1,37 @@
import React, { useCallback, useEffect } from "react";
import { Text, View } from "react-native";
import { Button } from "../../components/ui/button";
import { type ZebraScanResult, zebraScanner } from "../../lib/ZebraScanner";
export default function LaneCheck() {
const handleScan = useCallback(async (scan: ZebraScanResult) => {
console.log(scan);
}, []);
useEffect(() => {
zebraScanner.ensureProfile();
zebraScanner.startListening();
const sub = zebraScanner.addScanListener((scan) => {
//console.log("SCAN:", scan);
handleScan(scan);
});
return () => {
sub.remove();
zebraScanner.stopListening();
};
}, [handleScan]);
return (
<View
style={{
flex: 1,
//justifyContent: "center",
alignItems: "center",
marginTop: 50,
}}
>
<Text>LaneChecks</Text>
</View>
);
}

View File

@@ -1,26 +1,21 @@
import { PortalHost } from "@rn-primitives/portal";
import { Stack } from "expo-router"; import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar"; import { StatusBar } from "expo-status-bar";
import "../../global.css"; import "../../global.css";
import { PortalHost } from "@rn-primitives/portal"; import useDeviceLock from "../hooks/useDeviceCheck";
import { View } from "react-native";
export default function RootLayout() { export default function RootLayout() {
useDeviceLock();
return ( return (
<> <>
<StatusBar style="dark" /> <StatusBar style="dark" />
<Stack screenOptions={{ headerShown: false }}> <Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" /> <Stack.Screen name="index" />
<View className="items-center"> <Stack.Screen name="login" />
<Stack.Screen <Stack.Screen name="setup" />
name="(tabs)" <Stack.Screen name="updateScreen" />
options={{ <Stack.Screen name="(tabs)" />
title: "Pending update",
headerStyle: {
backgroundColor: "lightblue",
},
}}
/>
</View>
</Stack> </Stack>
<PortalHost /> <PortalHost />
</> </>

View File

@@ -1,127 +1,31 @@
import axios from "axios"; import { Redirect } from "expo-router";
import Constants from "expo-constants";
import { Redirect, useRouter } from "expo-router";
import { useEffect, useState } from "react";
import { ActivityIndicator, Text, View } from "react-native"; import { ActivityIndicator, Text, View } from "react-native";
import { useAppStore } from "../hooks/useAppStore"; import { useAppStartup } from "../hooks/useAppStartup";
import { useMobileAuthStore } from "../hooks/useMobileAuth";
import { useServerStore } from "../hooks/useServerCheck";
import { devDelay } from "../lib/devMode";
export default function Index() { const startupMessages = {
const router = useRouter(); loading: "Loading app...",
const [message, setMessage] = useState(<Text>Starting app...</Text>); validating: "Validating data...",
const [ready, setReady] = useState(false); scannerMode: "Checking scanner mode...",
const setServerVersion = useServerStore((s) => s.setServerVersion); normalScanner: "Starting normal ALPLAprod scanner that has no LST rules",
//const { isUnlocked } = useMobileAuthStore(); checkingUpdates: "Checking for updates...",
opening: "Opening LST scan app...",
const hasHydrated = useAppStore((s) => s.hasHydrated); error: "Something went wrong during startup.",
const serverPort = useAppStore((s) => s.serverPort);
const serverIp = useAppStore((s) => s.serverIp);
const build = Constants.expoConfig?.android?.versionCode ?? 1;
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;
}
// checking for lst.
console.log(
`http://${serverIp}:${parseInt(serverPort || "0", 10) >= 50000 ? "3000" : serverPort}/lst/api/mobile/version`,
);
try {
const res = await axios.get(
`http://${serverIp}:${parseInt(serverPort || "0", 10) >= 50000 ? "3000" : serverPort}/lst/api/mobile/version`,
{
timeout: 5000,
},
);
console.log(res.data);
// if the build version dose not match the latest server version force update
if (res.status === 200) {
setServerVersion(res.data);
}
// TODO: change the header to show orange and theres a new version
// console.log(build < res.data.minSupportedVersionCode);
// if (build < res.data.minSupportedVersionCode) {
// router.replace("/updateScreen");
// return;
// }
} catch (error) {
console.log("Error: ", error);
}
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("/scanner");
setReady(true);
return;
}
setMessage(<Text>Checking for updates</Text>);
await devDelay(1500);
// TODO if theres an update go to update screen message :D
setMessage(<Text>Opening LST scan app</Text>);
await devDelay(3250);
setReady(true);
} catch (error) {
console.log("Startup error", error);
setMessage(<Text>Something went wrong during startup.</Text>);
}
}; };
startup(); export default function Index() {
}, [ const { ready, startupRoute, status } = useAppStartup();
hasHydrated,
hasValidSetup,
serverPort,
serverIp,
router,
setServerVersion,
]);
// if (ready && !isUnlocked) { if (ready && startupRoute) {
// return <Redirect href={"/login"} />; return <Redirect href={startupRoute as any} />;
// }
if (ready) {
return <Redirect href="/(tabs)/scanner" />;
} }
if (ready) {
return <Redirect href="/login" />;
}
return ( return (
<View <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
style={{ <Text>{startupMessages[status]}</Text>
flex: 1,
justifyContent: "center",
alignItems: "center",
marginTop: 12,
}}
>
{message}
<ActivityIndicator size="large" /> <ActivityIndicator size="large" />
</View> </View>
); );

View File

@@ -1,5 +1,5 @@
import axios from "axios"; import axios from "axios";
import Constants from "expo-constants";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { useState } from "react"; import { useState } from "react";
import { Alert, Button, Text, View } from "react-native"; import { Alert, Button, Text, View } from "react-native";
@@ -8,20 +8,43 @@ import { useAppStore } from "../hooks/useAppStore";
import { useMobileAuthStore } from "../hooks/useMobileAuth"; import { useMobileAuthStore } from "../hooks/useMobileAuth";
export default function Login() { export default function Login() {
const { setUser } = useMobileAuthStore(); // doing this causes rerender and sub
//const { setUser } = useMobileAuthStore();
const [pin, setPin] = useState("");
const serverPort = useAppStore((s) => s.serverPort); const serverPort = useAppStore((s) => s.serverPort);
const serverIp = useAppStore((s) => s.serverIp); const serverIp = useAppStore((s) => s.serverIp);
const router = useRouter();
const onLogin = async () => { const onLogin = async () => {
if (pin.length < 6) {
console.log("pin must be min 6 ");
}
console.log(pin);
try { try {
const res = await axios.get( const res = await axios.post(
`http://${serverIp}:${parseInt(serverPort || "0", 10) >= 50000 ? "3000" : serverPort}/lst/api/mobile/version`, `http://${serverIp}:${parseInt(serverPort || "0", 10) >= 50000 ? "3000" : serverPort}/lst/api/mobile/auth/pin`,
{ pin },
{ {
timeout: 5000, timeout: 5000,
}, },
); );
console.log(res.data); if (res.status === 200) {
} catch (error) {} // this way to set the user is direct and basically a 1 shot
useMobileAuthStore.getState().setUser(res.data.data);
return router.replace("/(tabs)/scanner");
}
} catch (error) {
console.log(error);
Alert.alert("Login Error", `Invalid pin please try again`);
}
};
const config = () => {
console.log("config");
return router.replace("/setup");
}; };
return ( return (
@@ -43,10 +66,21 @@ export default function Login() {
keyboardType="number-pad" keyboardType="number-pad"
textContentType="oneTimeCode" textContentType="oneTimeCode"
placeholder="Pin number" placeholder="Pin number"
onChangeText={setPin}
/> />
</View> </View>
</View> </View>
<View>
<Text className="p-3">
Warning: If you are logged into another scanner you will encounter
scan errors, please do not try to log into more than 1 scanner at a
time.
</Text>
</View>
<View className="flex gap-2 flex-row">
<Button title="Login" onPress={onLogin} /> <Button title="Login" onPress={onLogin} />
<Button title="Config" onPress={config} />
</View>
</View> </View>
); );
} }

View File

@@ -25,6 +25,8 @@ export default function Setup() {
const server = useServerStore((s) => s.serverVersion); const server = useServerStore((s) => s.serverVersion);
// TODO: if on lst version and the user is manager or admin just login
const authCheck = () => { const authCheck = () => {
if (pin === "6971") { if (pin === "6971") {
setAuth(true); setAuth(true);
@@ -151,7 +153,7 @@ export default function Setup() {
marginTop: "auto", marginTop: "auto",
alignItems: "center", alignItems: "center",
padding: 10, padding: 10,
marginBottom: 12, marginBottom: 50,
}} }}
> >
<Text className="text-sm color-[#312f2f]"> <Text className="text-sm color-[#312f2f]">

View File

@@ -1,15 +1,154 @@
import { useCallback, useEffect } from "react"; import axios from "axios";
import { Text, View } from "react-native"; import { format } from "date-fns-tz";
import { useCallback, useEffect, useState } from "react";
import { Alert, Button, Text, View } from "react-native";
import { useAppStore } from "../hooks/useAppStore";
import { useMobileAuthStore } from "../hooks/useMobileAuth";
import { useScannerStore } from "../hooks/useScannerStore";
import { scannerFeedback } from "../lib/feedbackScan";
import { sendTcpMessage } from "../lib/tcpScan";
import { versionCheck } from "../lib/versionValidation";
import { type ZebraScanResult, zebraScanner } from "../lib/ZebraScanner";
import { ScannedLabelBox } from "./ScannedLabels";
import { GlobalFooter } from "./UpdateFooter";
import { Separator } from "./ui/separator";
import { zebraScanner } from "../lib/ZebraScanner"; const STX = "\x02";
const ETX = "\x03";
const formatName = (name?: string) =>
name ? name.charAt(0).toUpperCase() + name.slice(1).toLowerCase() : "";
export default function LSTScanner() { export default function LSTScanner() {
const handleScan = useCallback(async (scan: any) => { const user = useMobileAuthStore((s) => s.user);
console.log(scan); const logout = useMobileAuthStore((s) => s.logout);
}, []);
// TODO : move to off tcp stuff after od
const lastScan = useScannerStore((s) => s.lastScan);
const setLastScan = useScannerStore((s) => s.setLastScan);
const [tagScans, setTagScans] = useState<any>([]);
const scannerIdFromStore = useAppStore((s) => s.scannerId);
const serverIp = useAppStore((s) => s.serverIp);
const serverPort = useAppStore((s) => s.serverPort);
const [bgColor, setBGColor] = useState<string | null>(null);
const handleScan = useCallback(
async (scan: ZebraScanResult) => {
await scannerFeedback({
type: "scan",
sound: true,
vibrate: true,
led: true,
});
const isAlphaStart = /^[a-zA-Z]/.test(scan.data);
const isExcluded = (user?.excludedCommand ?? []).some((cmd) =>
scan.data.toLowerCase().includes(cmd.toLowerCase()),
);
console.log(user?.excludedCommand);
if (isAlphaStart && isExcluded) {
Alert.alert(
"Command not allowed",
`Command: ${scan.data}\n\nPlease contact logistics if this is an error`,
);
return;
}
let commandToSend = `${STX}${user?.scannerId}@${scan.data}${ETX}`;
// if we are sscc we need to scan like this .... <STX>98@]C100090087710038712256<ETX>
if (scan.data.startsWith("000")) {
commandToSend = `${STX}${user?.scannerId}@]C1${scan.data}${ETX}`;
setTagScans((prev: any) => [
{
label: parseInt(scan.data.slice(10, -1) || "000", 10).toString(),
date: format(new Date(Date.now()), "HH:mm"),
},
...prev,
]);
}
const scanned = (await sendTcpMessage(
commandToSend,
serverIp,
50004,
)) as any;
// send the logs to lst but allow it to time out if it dose not exist just bc.
try {
await axios.post(`http://${serverIp.trim()}:3000/lst/api/mobile/logs`, {
scannerId: user?.scannerId ?? "0",
message: scanned.data.message,
prompt: scanned.data.prompt,
commandDescription: scanned.data.commandDescription,
status: scanned.data.status,
lines: scanned.data.lines,
user: user?.name ?? "prodScan",
runningNumber: scan.data.startsWith("000")
? parseInt(scan.data.slice(10, -1) || "000", 10).toString()
: "0",
});
} catch (error) {
console.log(error);
}
// const response = await sendTcpMessage(tcpMessage);
console.log(scanned.data);
if (scanned.data.status !== "error") {
await scannerFeedback({
type: "good",
sound: true,
vibrate: true,
led: true,
});
setBGColor("bg-green-500");
// version check
versionCheck();
// auth update
useMobileAuthStore.getState().updateLastScan();
setTimeout(() => {
setBGColor(null);
}, 1 * 1000);
}
if (scanned.data.status === "error") {
await scannerFeedback({
type: scanned.data.status === "error" ? "bad" : "good",
sound: true,
vibrate: true,
led: true,
});
setBGColor("bg-red-500");
setTimeout(() => {
setBGColor(null);
}, 1 * 1000);
}
setLastScan(scanned.data);
// if we change commands we want to zero out the last scanned labels
if (isAlphaStart) {
setTagScans([]);
}
},
[
serverIp,
setLastScan,
user?.scannerId,
user?.name,
user?.excludedCommand?.some,
user?.excludedCommand,
],
);
const clearScans = () => { const clearScans = () => {
// add in setTagScans([]);
}; };
//console.log(lastScan); //console.log(lastScan);
@@ -29,23 +168,69 @@ export default function LSTScanner() {
}; };
}, [handleScan]); }, [handleScan]);
return ( return (
<View> <View className={`${bgColor ?? ""} flex-1 w-screen`}>
<View style={{ alignItems: "center", margin: 10 }}> <View style={{ alignItems: "center", margin: 5 }}>
<Text style={{ fontSize: 20, fontWeight: "600" }}>LST Scanner</Text> <Text style={{ fontSize: 14, fontWeight: "600" }}>
</View> User: {formatName(user?.name ?? "")}
</Text>
<Text style={{ fontSize: 18, fontWeight: "600" }}>
LST Scanner id: {user?.scannerId}
</Text>
<View <View
style={{ style={{
marginTop: 50, marginTop: 5,
alignItems: "center", alignItems: "center",
}} }}
> >
<Text>Relocate</Text> {!lastScan ? (
<Text>0 / 4</Text> <View style={{ marginTop: 10, alignItems: "center" }}>
<Text className="text-xl font-bold">Ready to scan</Text>
<Text>Waiting for first scan...</Text>
</View>
) : (
<View
style={{
marginTop: 10,
alignItems: "center",
}}
>
{lastScan.lines
?.filter((line) => !/^\d+@$/.test(line))
.map((i) => {
return (
<View
style={{ marginTop: 10, alignItems: "center" }}
key={i}
>
<Text style={{ fontSize: 18, fontWeight: "600" }}>
{i}
</Text>
</View>
);
})}
</View>
)}
</View>
</View>
<Separator className="m-2" />
<View className="flex-1 w-full px-4">
<ScannedLabelBox
labels={tagScans}
color={bgColor}
clearScan={clearScans}
/>
</View> </View>
{/* <View> <View className="m-2">
<Text>List of recent scanned pallets TBA</Text> {user && (
</View> */} <View className="items-center">
<Button title="Logout" onPress={logout} />
</View>
)}
</View>
<View>
<GlobalFooter />
</View>
</View> </View>
); );
} }

View File

@@ -3,9 +3,11 @@ import { format } from "date-fns-tz";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { Text, View } from "react-native"; import { Text, View } from "react-native";
import { useAppStore } from "../hooks/useAppStore"; import { useAppStore } from "../hooks/useAppStore";
import { useMobileAuthStore } from "../hooks/useMobileAuth";
import { useScannerStore } from "../hooks/useScannerStore"; import { useScannerStore } from "../hooks/useScannerStore";
import { scannerFeedback } from "../lib/feedbackScan"; import { scannerFeedback } from "../lib/feedbackScan";
import { sendTcpMessage } from "../lib/tcpScan"; import { sendTcpMessage } from "../lib/tcpScan";
import { versionCheck } from "../lib/versionValidation";
import { type ZebraScanResult, zebraScanner } from "../lib/ZebraScanner"; import { type ZebraScanResult, zebraScanner } from "../lib/ZebraScanner";
import { ScannedLabelBox } from "./ScannedLabels"; import { ScannedLabelBox } from "./ScannedLabels";
import { GlobalFooter } from "./UpdateFooter"; import { GlobalFooter } from "./UpdateFooter";
@@ -25,6 +27,13 @@ export default function ProdScanner() {
const handleScan = useCallback( const handleScan = useCallback(
async (scan: ZebraScanResult) => { async (scan: ZebraScanResult) => {
await scannerFeedback({
type: "scan",
sound: true,
vibrate: true,
led: true,
});
let commandToSend = `${STX}${scannerIdFromStore}@${scan.data}${ETX}`; let commandToSend = `${STX}${scannerIdFromStore}@${scan.data}${ETX}`;
// if we are sscc we need to scan like this .... <STX>98@]C100090087710038712256<ETX> // if we are sscc we need to scan like this .... <STX>98@]C100090087710038712256<ETX>
@@ -45,11 +54,16 @@ export default function ProdScanner() {
parseInt(serverPort || "0", 10), parseInt(serverPort || "0", 10),
)) as any; )) as any;
// send the logs to lst but allow it to time out if it dose not exist just bc. // send the logs to lst but allow it to time out if it dose not exist just bc.
const data = {
...scanned.data,
runningNumber: scan.data.startsWith("000")
? parseInt(scan.data.slice(10, -1) || "000", 10).toString()
: "0",
};
try { try {
await axios.post( await axios.post(
`http://${serverIp.trim()}:3000/lst/api/mobile/logs`, `http://${serverIp.trim()}:3000/lst/api/mobile/logs`,
scanned, data,
); );
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@@ -64,6 +78,13 @@ export default function ProdScanner() {
led: true, led: true,
}); });
setBGColor("bg-green-500"); setBGColor("bg-green-500");
// version check
versionCheck();
// auth update
useMobileAuthStore.getState().updateLastScan();
setTimeout(() => { setTimeout(() => {
setBGColor(null); setBGColor(null);
}, 1 * 1000); }, 1 * 1000);

View File

@@ -0,0 +1,111 @@
import axios from "axios";
import Constants from "expo-constants";
import { useEffect, useRef, useState } from "react";
import { devDelay } from "../lib/devMode";
import { versionCheck } from "../lib/versionValidation";
import { useAppStore } from "./useAppStore";
import { useServerStore } from "./useServerCheck";
type StartupStatus =
| "loading"
| "validating"
| "scannerMode"
| "normalScanner"
| "checkingUpdates"
| "opening"
| "error";
export function useAppStartup() {
const [ready, setReady] = useState(false);
const [status, setStatus] = useState<StartupStatus>("loading");
const [startupRoute, setStartupRoute] = useState<string | null>(null);
const hasRunKey = useRef<string | null>(null);
const hasHydrated = useAppStore((s) => s.hasHydrated);
const serverPort = useAppStore((s) => s.serverPort);
const serverIp = useAppStore((s) => s.serverIp);
useEffect(() => {
if (!hasHydrated) {
setStatus("loading");
return;
}
const runKey = `${serverIp}:${serverPort}`;
if (hasRunKey.current === runKey) {
return;
}
hasRunKey.current = runKey;
let cancelled = false;
const startup = async () => {
try {
setReady(false);
setStartupRoute(null);
await devDelay(1500);
if (cancelled) return;
setStatus("validating");
await devDelay(1500);
if (cancelled) return;
const hasValidSetup = useAppStore.getState().hasValidSetup;
if (!hasValidSetup()) {
setStartupRoute("/setup");
setReady(true);
return;
}
await versionCheck();
setStatus("scannerMode");
await devDelay(1500);
if (cancelled) return;
if (parseInt(serverPort || "0", 10) >= 50000) {
setStatus("normalScanner");
await devDelay(1500);
setStartupRoute("/scanner");
setReady(true);
return;
}
setStatus("checkingUpdates");
console.log("checking updates");
await devDelay(1500);
if (cancelled) return;
setStatus("opening");
console.log("opening");
await devDelay(1500);
if (cancelled) return;
setStartupRoute("/(tabs)/scanner");
console.log("scanner");
setReady(true);
} catch (error) {
console.log("Startup error:", error);
setStatus("error");
}
};
startup();
return () => {
cancelled = true;
};
}, [hasHydrated, serverIp, serverPort]);
return {
ready,
startupRoute,
status,
};
}

View File

@@ -0,0 +1,37 @@
import { useEffect, useRef, useState } from "react";
import { AppState, type AppStateStatus } from "react-native";
import { useMobileAuthStore } from "./useMobileAuth";
export default function useDeviceLock() {
const [appState, setAppState] = useState<AppStateStatus>(
AppState.currentState,
);
const appStateRef = useRef<AppStateStatus>(AppState.currentState);
useEffect(() => {
const subscription = AppState.addEventListener("change", (nextAppState) => {
const previousAppState = appStateRef.current;
const wasActive = previousAppState === "active";
// if the we see aggressive locking then we should remove inactive.
const isNowInactive =
nextAppState === "background" || nextAppState === "inactive";
if (wasActive && isNowInactive) {
const auth = useMobileAuthStore.getState();
if (auth.shouldLockForIdle()) {
auth.lock();
}
}
appStateRef.current = nextAppState;
setAppState(nextAppState);
});
return () => subscription.remove();
}, []);
return appState;
}

View File

@@ -1,28 +1,52 @@
import { create } from "zustand"; import { create } from "zustand";
const ONE_HOUR = 1000 * 60 * 60;
type MobileUser = { type MobileUser = {
id: string; id: string;
name: string; name: string;
role: "user" | "lead" | "manager" | "admin"; role: "user" | "lead" | "manager" | "admin";
excludedCommand: string[]; excludedCommand: string[];
scannerId: string;
}; };
type AuthState = { type AuthState = {
user: MobileUser | null; user: MobileUser | null;
isUnlocked: boolean; isUnlocked: boolean;
lastScanAt: number | null;
setUser: (user: MobileUser) => void; setUser: (user: MobileUser) => void;
updateLastScan: () => void;
lock: () => void; lock: () => void;
logout: () => void; logout: () => void;
shouldLockForIdle: () => boolean;
}; };
export const useMobileAuthStore = create<AuthState>((set) => ({ export const useMobileAuthStore = create<AuthState>((set, get) => ({
user: null, user: null,
isUnlocked: false, isUnlocked: false,
lastScanAt: null,
setUser: (user) => set({ user, isUnlocked: true }), setUser: (user) =>
set({
user,
isUnlocked: true,
lastScanAt: Date.now(),
}),
updateLastScan: () => set({ lastScanAt: Date.now() }),
lock: () => set({ isUnlocked: false }), lock: () => set({ isUnlocked: false }),
logout: () => set({ user: null, isUnlocked: false }), logout: () =>
set({
user: null,
isUnlocked: false,
lastScanAt: null,
}),
shouldLockForIdle: () => {
const lastScanAt = get().lastScanAt;
if (!lastScanAt) return true;
return Date.now() - lastScanAt > ONE_HOUR;
},
})); }));

View File

@@ -2,12 +2,13 @@ import { createAudioPlayer } from "expo-audio";
import * as Haptics from "expo-haptics"; import * as Haptics from "expo-haptics";
export type ScanFeedback = { export type ScanFeedback = {
type: "good" | "bad"; type: "good" | "bad" | "scan";
sound?: boolean; sound?: boolean;
vibrate?: boolean; vibrate?: boolean;
led?: boolean; led?: boolean;
}; };
const scan = createAudioPlayer(require("../../assets/sounds/scan.wav"));
const goodSound = createAudioPlayer(require("../../assets/sounds/good.wav")); const goodSound = createAudioPlayer(require("../../assets/sounds/good.wav"));
const badSound = createAudioPlayer(require("../../assets/sounds/bad.wav")); const badSound = createAudioPlayer(require("../../assets/sounds/bad.wav"));
@@ -18,14 +19,15 @@ export async function scannerFeedback({
led = true, led = true,
}: ScanFeedback) { }: ScanFeedback) {
if (sound) { if (sound) {
const player = type === "good" ? goodSound : badSound; const player =
type === "scan" ? scan : type === "good" ? goodSound : badSound;
player.seekTo(0); player.seekTo(0);
player.play(); player.play();
} }
if (vibrate) { if (vibrate) {
await Haptics.notificationAsync( await Haptics.notificationAsync(
type === "good" type === "good" || type === "scan"
? Haptics.NotificationFeedbackType.Success ? Haptics.NotificationFeedbackType.Success
: Haptics.NotificationFeedbackType.Error, : Haptics.NotificationFeedbackType.Error,
); );

View File

@@ -1,4 +1,6 @@
import axios from "axios";
import { useAppStore } from "../hooks/useAppStore";
import { useServerStore } from "../hooks/useServerCheck";
export type ServerVersionInfo = { export type ServerVersionInfo = {
packageName: string; packageName: string;
@@ -18,7 +20,7 @@ export type StartupStatus =
export function evaluateVersion( export function evaluateVersion(
appBuildCode: number, appBuildCode: number,
server: ServerVersionInfo server: ServerVersionInfo,
): StartupStatus { ): StartupStatus {
if (appBuildCode < server.minSupportedVersionCode) { if (appBuildCode < server.minSupportedVersionCode) {
return { return {
@@ -41,3 +43,31 @@ export function evaluateVersion(
server, server,
}; };
} }
export const versionCheck = async () => {
const { setServerVersion } = useServerStore.getState();
const { serverPort, serverIp } = useAppStore.getState();
const port = parseInt(serverPort || "0", 10) >= 50000 ? "3000" : serverPort;
try {
const res = await axios.get(
`http://${serverIp}:${port}/lst/api/mobile/version`,
{ timeout: 5000 },
);
if (res.status === 200) {
setServerVersion(res.data);
}
// const build = Constants.expoConfig?.android?.versionCode ?? 1;
// if (build < res.data.minSupportedVersionCode) {
// setStartupRoute("/updateScreen");
// setReady(true);
// return;
// }
} catch (error) {
console.log("Version check error:", error);
}
};

View File

@@ -0,0 +1,3 @@
ALTER TABLE "scan_users" ALTER COLUMN "excluded_commands" SET DATA TYPE jsonb;--> statement-breakpoint
ALTER TABLE "scan_users" ALTER COLUMN "excluded_commands" SET DEFAULT '';--> statement-breakpoint
ALTER TABLE "scan_log" ADD COLUMN "user" text;

View File

@@ -0,0 +1 @@
ALTER TABLE "scan_users" ALTER COLUMN "excluded_commands" SET DEFAULT '[]'::jsonb;

View File

@@ -0,0 +1 @@
ALTER TABLE "scan_log" ADD COLUMN "running_number" text DEFAULT '0';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -316,6 +316,27 @@
"when": 1777666145468, "when": 1777666145468,
"tag": "0044_steady_magneto", "tag": "0044_steady_magneto",
"breakpoints": true "breakpoints": true
},
{
"idx": 45,
"version": "7",
"when": 1778059667805,
"tag": "0045_quick_khan",
"breakpoints": true
},
{
"idx": 46,
"version": "7",
"when": 1778059910210,
"tag": "0046_chemical_the_leader",
"breakpoints": true
},
{
"idx": 47,
"version": "7",
"when": 1778068577325,
"tag": "0047_spotty_queen_noir",
"breakpoints": true
} }
] ]
} }

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "lst_v3", "name": "lst_v3",
"version": "0.0.2-alpha.7", "version": "0.0.2-alpha.8",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "lst_v3", "name": "lst_v3",
"version": "0.0.2-alpha.7", "version": "0.0.2-alpha.8",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@dotenvx/dotenvx": "^1.57.0", "@dotenvx/dotenvx": "^1.57.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "lst_v3", "name": "lst_v3",
"version": "0.0.2-alpha.7", "version": "0.0.2-alpha.8",
"description": "The tool that supports us in our everyday alplaprod", "description": "The tool that supports us in our everyday alplaprod",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {