test(android app): this is the start to the android app

This commit is contained in:
2025-11-15 16:22:19 -06:00
parent eb6b9ce388
commit 7b630d5c0b
25 changed files with 14215 additions and 1 deletions

1
.gitignore vendored
View File

@@ -13,7 +13,6 @@ controllerBuilds
# ignoring the old app that will be built into this one to make deploying faster and more easy as we do the migration
lstV2/frontend/.tanstack
mobileLst
keys
# Logs
logs

43
mobileLst/.gitignore vendored Normal file
View File

@@ -0,0 +1,43 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
expo-env.d.ts
# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo
app-example
# generated native folders
/ios
/android

50
mobileLst/README.md Normal file
View File

@@ -0,0 +1,50 @@
# Welcome to your Expo app 👋
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
## Get started
1. Install dependencies
```bash
npm install
```
2. Start the app
```bash
npx expo start
```
In the output, you'll find options to open the app in a
- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
## Get a fresh project
When you're ready, run:
```bash
npm run reset-project
```
This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
## Learn more
To learn more about developing your project with Expo, look at the following resources:
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
## Join the community
Join our community of developers creating universal apps.
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.

71
mobileLst/app.json Normal file
View File

@@ -0,0 +1,71 @@
{
"expo": {
"name": "LSTScanner",
"slug": "lst-scanner-app",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"scheme": "lstscanner",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"ios": {
"supportsTablet": true
},
"updates": {
"enabled": true,
"url": "http://10.193.0.56:4000/api/mobile/updates",
"fallbackToCacheTimeout": 30000,
"codeSigningCertificate": "./certs/certificate.pem",
"codeSigningMetadata": {
"keyid": "self-hosted",
"alg": "rsa-v1_5-sha256"
}
},
"runtimeVersion": "1.0.0",
"android": {
"adaptiveIcon": {
"backgroundColor": "#E6F4FE",
"foregroundImage": "./assets/images/android-icon-foreground.png",
"backgroundImage": "./assets/images/android-icon-background.png",
"monochromeImage": "./assets/images/android-icon-monochrome.png"
},
"package": "com.company.lstscanner",
"versionCode": 1,
"edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false
},
"web": {
"output": "static",
"favicon": "./assets/images/favicon.png"
},
"plugins": [
"expo-router",
"./plugins/androidCustomizations",
[
"expo-splash-screen",
{
"image": "./assets/icon.png",
"imageWidth": 200,
"resizeMode": "contain",
"backgroundColor": "#ffffff",
"dark": {
"backgroundColor": "#000000"
}
}
],
[
"expo-build-properties",
{
"android": {
"usesCleartextTraffic": true
},
"ios": {}
}
]
],
"experiments": {
"typedRoutes": true,
"reactCompiler": true
}
}
}

View File

@@ -0,0 +1,5 @@
import { Stack } from "expo-router";
export default function RootLayout() {
return <Stack />;
}

122
mobileLst/app/index.tsx Normal file
View File

@@ -0,0 +1,122 @@
import React, { useState } from "react";
import {
Button,
FlatList,
Pressable,
StyleSheet,
Text,
TextInput,
View,
} from "react-native";
import * as Updates from 'expo-updates';
export default function App() {
const [buffer, setBuffer] = useState("");
const [items, setItems] = useState<string[]>([]);
const [status, setStatus] = useState("");
const [updates, setUpdates] = useState("")
const handleChange = (text: string) => {
// Scanner "types" characters then appends Enter (\n)
if (text.endsWith("\n")) {
const code = text.trim(); // Remove newline
handleScan(code);
setBuffer(""); // Reset for next scan
} else {
setBuffer(text);
}
};
async function checkServerUpdate() {
setUpdates('Checking for updates...');
try {
const res = await Updates.checkForUpdateAsync();
if (res.isAvailable) {
setUpdates('Update available! Fetching and reloading...');
await Updates.fetchUpdateAsync();
await Updates.reloadAsync();
} else {
setUpdates('No new update available.');
}
} catch (e:any) {
setUpdates(`Update check failed: ${e.message}`);
}
}
const handleScan = (code: string) => {
//console.log("Scanned:", code);
if (code.toUpperCase().startsWith("LOC")) {
console.log("Triggered relocate to lane");
performRelocate(code);
} else {
setItems((prev) => [...prev, code]);
setStatus(`Added: ${code}`);
}
};
const performRelocate = (locationCode: string) => {
if (items.length === 0) {
setStatus(`Relocate to ${locationCode} requested, but no items`);
return;
}
// Do your API call or whatever the "relocate" means
console.log("Relocating", items, "to", locationCode);
setStatus(`Moved ${items.length} items to ${locationCode}`);
setItems([]);
};
return (
<View style={styles.container}>
<Button
title="Clear Items"
onPress={() => {
setItems([]);
}}
/>
<Text style={styles.title}>Scan Barcodes</Text>
{/* The working input you already have */}
<TextInput
autoFocus
value={buffer}
onChangeText={handleChange}
showSoftInputOnFocus={false}
keyboardType="visible-password"
style={{
position: "absolute",
opacity: 0, // invisible
height: 0, // takes no space
width: 0,
}}
/>
<Pressable onPress={checkServerUpdate}style={{
marginVertical: 12,
padding: 12,
backgroundColor: '#007AFF',
borderRadius: 6,
}}>
<Text>Check Update</Text>
</Pressable>
<Text>Update Data</Text>
<Text>{updates}</Text>
<FlatList
data={items}
keyExtractor={(_, i) => i.toString()}
renderItem={({ item }) => <Text style={styles.item}>{item}</Text>}
/>
<Text style={styles.status}>{status}</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16 },
title: { fontWeight: "bold", fontSize: 20 },
item: { paddingVertical: 4, fontSize: 16 },
status: { marginTop: 20, fontStyle: "italic" },
});

BIN
mobileLst/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFRzCCAy+gAwIBAgIQF/BpAuRICIFJboP3h6vDRzANBgkqhkiG9w0BAQ0FADA1
MQswCQYDVQQGEwJBVDEOMAwGA1UEChMFQUxQTEExFjAUBgNVBAMTDUFMUExBIFJv
b3QgQ0EwIBcNMjAxMDI3MTIzNTUxWhgPMjA1MDEwMjcxMjQ1NTBaMDUxCzAJBgNV
BAYTAkFUMQ4wDAYDVQQKEwVBTFBMQTEWMBQGA1UEAxMNQUxQTEEgUm9vdCBDQTCC
AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKt7ZejwqZz7AvCN7zCSAxVZ
Htw/WKX3UezouDBdWY6UQh54POCTEwv5BZ8wiAp1GvkJSbUVbB/9VBNZtopAGFkp
pB5MAjoPY/PWir01vxymgDXDOEn8Big/ahUt7MQI02UwpQ/j0fwxcVtWOxyoKN1O
E1yR3vJfXOwGZ4y8zIN2ErBg+z1CUXUS/UxfpmAdQ6vJgYL6pjcCxKOHPdYo7a2H
RD3Ck6qEcD0K89/Cx3R5Q3poc3YKM3A3xnIb43wUhzIXDdPQtIxMA4dA/Tz8LAWE
ZQ/6bkOpDFPZMxy4x5mcIMo6Ak0CIOFLoWfnukcpUbzgfcDInot3X5YX5NboDdPV
yidWzyT3pM5AO4Kk6by3Y9KiiRmj83k3F7TaiwqU9o9Q5M3U14ZCV2D4coCrMYzw
VHBFSV2LiH8qEaV4DXBO36Ty6TQBGbFUJUtxbD9pDOoOab3KEda7JtEbFolZyQAk
21hyoPs7DmhbVmV5ih6QEKNHOt+7U9UeGy39kbEShVVoF0VaC+7J8SyRjO7/QawW
JjFOkF8QSPGAH7l8iGKwtvcZ39VmbJhas16KlxmrlHxRL5Gms4IXYuaqSPsLIAcr
SfOdf2Xq2Up4SYnPHW5Kg2FjHTMRFF0XlMRoXcqD4tTXa9w2IGpQ+EoHeEfNga49
Xw8AaWx6EDzLdyzwXWr1AgMBAAGjUTBPMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8E
BTADAQH/MB0GA1UdDgQWBBTVTBh/edUJ6IgeZ29ZguyqoYipdjAQBgkrBgEEAYI3
FQEEAwIBADANBgkqhkiG9w0BAQ0FAAOCAgEAm/HO1AIJSX4ic8Soph2pak6ztxNo
crd8jVkGsRI2l1vmVBhvsSZkDUGiCnTVm3bYrHEEgmZlp2uTdMd8t5ufjM55VWZ2
gdOuuqha8cXDuwqsOzhX/IGdARrRJFABHO4amdQpMBDxPGswIjXQfKc0UeFQ+/lr
Fe0lmqegIKlpogEydz6qMQ6BnsUI8WQiakggVkl9flpsCR7lk8gWoBHQebGs2TWt
7IA/RRWm5S2OSToTh2E38MuRaiAwwaJNch2WflixJyWUVi5FbZ0l8ZGzxTvCcDgn
UC4KpyeId3fONANQrSmCz6X+ORyPbzLiIojG+y65h/cKw7cMI99+sU3s2akVAGpo
rYH1MU4KAqyChp71Xr+V0bvGMLQKuFFn/6xQ/MF24Btxjcp3ODVEyQ+lNMFfg5pB
RuhXiYAMZJSUyJGRLOI5RFBB2l8rBRRe7epsjPKrfwt30YXX7hO2Uo1JoL1ov1Um
Vt2SpB3b2Kk4xHwp2PCAfA+VtB8nJVC49JdXi7rlYh5IUAUexrsvMPLeHlDrldYb
1TcSeglLTIGFQBxElG4I7yDaWnIzTmlY/zDiK4h4l/rzyWo0NVu2KLnT0iAZ2/w5
AjAeIyKnFP7PXYQwAdiW/B/cwfZTrsDaxq8PkZjiMznVzG+EQSgmcqT0z6aNJmoU
3z/B7cuoI5Eu0OU=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>

View File

@@ -0,0 +1,41 @@
import React, { useEffect, useRef, useState } from "react";
import { Text, TextInput, View } from "react-native";
export default function HiddenScannerListener({ onScan }: { onScan: any }) {
const [buffer, setBuffer] = useState("");
const inputRef = useRef<TextInput>(null);
// Keep focusing the invisible input
useEffect(() => {
const timer = setInterval(() => {
if (inputRef.current) inputRef.current.focus();
}, 500);
return () => clearInterval(timer);
}, []);
const handleChange = (text: string) => {
// Most scanners append '\n' (Enter) or '\t'; trim and send
if (text.endsWith("\n") || text.endsWith("\t")) {
const code = text.trim();
if (code) onScan?.(code);
setBuffer("");
} else {
setBuffer(text);
}
};
return (
<TextInput
ref={inputRef}
value={buffer}
onChangeText={handleChange}
autoFocus
style={{
position: "absolute",
opacity: 0, // invisible
height: 0, // takes no space
width: 0,
}}
/>
);
}

View File

@@ -0,0 +1,10 @@
// https://docs.expo.dev/guides/using-eslint/
const { defineConfig } = require('eslint/config');
const expoConfig = require('eslint-config-expo/flat');
module.exports = defineConfig([
expoConfig,
{
ignores: ['dist/*'],
},
]);

13666
mobileLst/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

58
mobileLst/package.json Normal file
View File

@@ -0,0 +1,58 @@
{
"name": "mobilelst",
"main": "expo-router/entry",
"version": "1.0.0",
"scripts": {
"start": "expo start",
"reset-project": "node ./scripts/reset-project.js",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web",
"lint": "expo lint",
"build:clean": "expo prebuild --clean && cd android && gradlew.bat assembleRelease && npm run ",
"build:android": "cd android && gradlew.bat assembleRelease",
"build:ota": "set EXPO_NO_EAS_UPDATE=1 && npx expo export --platform android --output-dir dist --experimental-bundle",
"update": "adb install android/app/build/outputs/apk/release/app-release.apk",
"debug": "adb logcat | Select-String \"Expo"
},
"dependencies": {
"@expo/vector-icons": "^15.0.3",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
"axios": "^1.13.2",
"expo": "~54.0.23",
"expo-build-properties": "~1.0.9",
"expo-camera": "~17.0.9",
"expo-constants": "~18.0.10",
"expo-font": "~14.0.9",
"expo-haptics": "~15.0.7",
"expo-image": "~3.0.10",
"expo-linking": "~8.0.8",
"expo-router": "~6.0.14",
"expo-splash-screen": "~31.0.10",
"expo-status-bar": "~3.0.8",
"expo-symbols": "~1.0.7",
"expo-system-ui": "~6.0.8",
"expo-updates": "~29.0.12",
"expo-web-browser": "~15.0.9",
"expo-zebra-scanner": "^6.0.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
"react-native-gesture-handler": "~2.28.0",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-web": "~0.21.0",
"react-native-worklets": "0.5.1"
},
"devDependencies": {
"@types/react": "~19.1.0",
"eslint": "^9.25.0",
"eslint-config-expo": "~10.0.0",
"typescript": "~5.9.2"
},
"private": true
}

View File

@@ -0,0 +1,94 @@
const {
withAndroidManifest,
withDangerousMod,
withGradleProperties,
} = require("@expo/config-plugins");
const fs = require("fs");
const path = require("path");
function withCustomAndroidConfig(config) {
// Copy cert files
config = withDangerousMod(config, [
"android",
async (config) => {
const projectRoot = config.modRequest.projectRoot;
const certSourceDir = path.join(projectRoot, "certFiles");
const rawDestDir = path.join(
projectRoot,
"android/app/src/main/res/raw"
);
const xmlDestDir = path.join(
projectRoot,
"android/app/src/main/res/xml"
);
// Create directories if they don't exist
if (!fs.existsSync(rawDestDir)) {
fs.mkdirSync(rawDestDir, { recursive: true });
}
if (!fs.existsSync(xmlDestDir)) {
fs.mkdirSync(xmlDestDir, { recursive: true });
}
// Copy your cert files
if (fs.existsSync(path.join(certSourceDir, "raw"))) {
fs.cpSync(path.join(certSourceDir, "raw"), rawDestDir, {
recursive: true,
});
}
if (fs.existsSync(path.join(certSourceDir, "xml"))) {
fs.cpSync(path.join(certSourceDir, "xml"), xmlDestDir, {
recursive: true,
});
}
console.log("✅ Copied cert files to android/app/src/main/res");
return config;
},
]);
// Modify AndroidManifest
config = withAndroidManifest(config, (config) => {
const mainApplication = config.modResults.manifest.application[0];
// Add network security config if not already present
if (!mainApplication.$["android:networkSecurityConfig"]) {
mainApplication.$["android:networkSecurityConfig"] =
"@xml/network_security_config";
}
console.log("✅ Modified AndroidManifest.xml");
return config;
});
// Update gradle.properties for better performance
config = withGradleProperties(config, (config) => {
// Remove existing jvmargs if present
config.modResults = config.modResults.filter(
(item) =>
!(item.type === "property" && item.key === "org.gradle.jvmargs")
);
// Add your custom jvmargs
config.modResults.push(
{
type: "property",
key: "org.gradle.jvmargs",
value: "-Xmx16384m -XX:MaxMetaspaceSize=2024m",
},
{
type: "property",
key: "org.gradle.daemon",
value: "false",
}
);
console.log("✅ Updated gradle.properties with 16GB heap");
return config;
});
return config;
}
module.exports = withCustomAndroidConfig;

15
mobileLst/tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"paths": {
"@/*": [
"./*"
]
}
},
"include": [
"**/*.ts",
"**/*.tsx"
]
}