diff --git a/lstMobile/app.json b/lstMobile/app.json index 2225ee0..d5c071e 100644 --- a/lstMobile/app.json +++ b/lstMobile/app.json @@ -2,7 +2,7 @@ "expo": { "name": "LST mobile", "slug": "lst-mobile", - "version": "0.0.1-alpha", + "version": "0.11.1-alpha", "orientation": "portrait", "icon": "./assets/images/icon.png", "scheme": "lstmobile", @@ -16,9 +16,10 @@ "foregroundImage": "./assets/images/android-icon-foreground.png", "backgroundImage": "./assets/images/android-icon-background.png", "monochromeImage": "./assets/images/android-icon-monochrome.png", - "package": "net.alpla.lst.mobile", - "versionCode": 1 + "package": "net.alpla.lst.mobile" + }, + "versionCode": 5, "predictiveBackGestureEnabled": false, "package": "com.anonymous.lstMobile" }, @@ -27,6 +28,7 @@ "favicon": "./assets/images/favicon.png" }, "plugins": [ + "./plugins/withZebraScanner", "expo-router", [ "expo-splash-screen", diff --git a/lstMobile/package.json b/lstMobile/package.json index 8428b02..95b61fb 100644 --- a/lstMobile/package.json +++ b/lstMobile/package.json @@ -1,7 +1,7 @@ { "name": "lstmobile", "main": "expo-router/entry", - "version": "0.0.1-alpha", + "version": "0.0.2-alpha", "scripts": { "start": "expo start", "reset-project": "node ./scripts/reset-project.js", @@ -9,7 +9,8 @@ "ios": "expo run:ios", "web": "expo start --web", "lint": "expo lint", - "build:apk": "expo prebuild --clean && cd android && gradlew.bat assembleRelease ", + "build:apk:clean": "expo prebuild --clean && cd android && gradlew.bat assembleRelease ", + "build:apk": "expo prebuild && cd android && gradlew.bat assembleRelease ", "update": "adb install android/app/build/outputs/apk/release/app-release.apk" }, "dependencies": { diff --git a/lstMobile/plugins/withZebraScanner.js b/lstMobile/plugins/withZebraScanner.js new file mode 100644 index 0000000..49140d8 --- /dev/null +++ b/lstMobile/plugins/withZebraScanner.js @@ -0,0 +1,293 @@ +const { withDangerousMod } = require("@expo/config-plugins"); +const fs = require("fs"); +const path = require("path"); + +const packageName = "com.anonymous.lstMobile"; +const packagePath = "com/anonymous/lstMobile"; + +const moduleCode = `package ${packageName}.scanner + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.Bundle +import com.facebook.react.bridge.* +import com.facebook.react.modules.core.DeviceEventManagerModule + +class ZebraScannerModule( + private val reactContext: ReactApplicationContext +) : ReactContextBaseJavaModule(reactContext) { + + override fun getName(): String = "ZebraScanner" + + private val scanAction = "com.lst.mobile.SCAN" + private var receiverRegistered = false + + private val scanReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + println("LST SCANNER: received intent -> \${intent?.action}") + + if (intent?.action != scanAction) { + println("LST SCANNER: wrong action") + return + } + + val barcodeData: String? = + intent.getStringExtra("com.symbol.datawedge.data_string") + + val labelType: String? = + intent.getStringExtra("com.symbol.datawedge.label_type") + + val source: String? = + intent.getStringExtra("com.symbol.datawedge.source") + + println("LST SCANNER: data=\$barcodeData label=\$labelType source=\$source") + + if (barcodeData.isNullOrBlank()) { + println("LST SCANNER: empty barcode") + return + } + + val payload = Arguments.createMap().apply { + putString("data", barcodeData) + putString("labelType", labelType) + putString("source", source) + putDouble("timestamp", System.currentTimeMillis().toDouble()) + } + + sendEvent("barcodeScanned", payload) + } + } + + @ReactMethod + fun startListening() { + if (receiverRegistered) return + + reactContext.registerReceiver( + scanReceiver, + IntentFilter(scanAction), + Context.RECEIVER_EXPORTED + ) + + receiverRegistered = true + } + + @ReactMethod + fun stopListening() { + if (!receiverRegistered) return + + try { + reactContext.unregisterReceiver(scanReceiver) + } catch (_: Exception) {} + + receiverRegistered = false + } + + /** + * Required for React Native NativeEventEmitter + */ + @ReactMethod + fun addListener(eventName: String) { + // No-op + } + + /** + * Required for React Native NativeEventEmitter + */ + @ReactMethod + fun removeListeners(count: Int) { + // No-op + } + + @ReactMethod + fun triggerScan() { + val intent = Intent().apply { + action = "com.symbol.datawedge.api.ACTION" + putExtra("com.symbol.datawedge.api.SOFT_SCAN_TRIGGER", "TOGGLE_SCANNING") + } + + reactContext.sendBroadcast(intent) + } + + private fun sendCommand(command: String, value: Any) { + val intent = Intent().apply { + action = "com.symbol.datawedge.api.ACTION" + + when (value) { + is String -> putExtra(command, value) + is Bundle -> putExtra(command, value) + } + } + + reactContext.sendBroadcast(intent) + } + + private fun sendEvent(eventName: String, payload: WritableMap) { + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit(eventName, payload) + } + + // + @ReactMethod + fun ensureProfile() { + val profileName = "LST_MOBILE" + + // Create profile (safe to call even if it exists) + sendCommand( + "com.symbol.datawedge.api.CREATE_PROFILE", + profileName + ) + + Thread.sleep(500) + // Configure profile + val profileConfig = Bundle().apply { + putString("PROFILE_NAME", profileName) + putString("PROFILE_ENABLED", "true") + putString("CONFIG_MODE", "CREATE_IF_NOT_EXIST") + + val barcodeConfig = Bundle().apply { + putString("PLUGIN_NAME", "BARCODE") + putString("RESET_CONFIG", "true") + + val props = Bundle().apply { + putString("scanner_input_enabled", "true") + } + + putBundle("PARAM_LIST", props) + } + + val intentConfig = Bundle().apply { + putString("PLUGIN_NAME", "INTENT") + putString("RESET_CONFIG", "true") + + val props = Bundle().apply { + putString("intent_output_enabled", "true") + putString("intent_action", scanAction) + putString("intent_delivery", "2") // broadcast + putString("intent_use_content_provider", "false") // optional but helps + } + + putBundle("PARAM_LIST", props) + } + + val keystrokeConfig = Bundle().apply { + putString("PLUGIN_NAME", "KEYSTROKE") + putString("RESET_CONFIG", "true") + + val props = Bundle().apply { + putString("keystroke_output_enabled", "false") + } + + putBundle("PARAM_LIST", props) + } + + putParcelableArrayList( + "PLUGIN_CONFIG", + arrayListOf(barcodeConfig, intentConfig, keystrokeConfig) +) + } + + sendCommand("com.symbol.datawedge.api.SET_CONFIG", profileConfig) + + // Associate with your app + val appConfig = Bundle().apply { + putString("PACKAGE_NAME", reactContext.packageName) + putStringArray("ACTIVITY_LIST", arrayOf("*")) + } + + val associateConfig = Bundle().apply { + putString("PROFILE_NAME", profileName) + putString("CONFIG_MODE", "UPDATE") + putParcelableArray("APP_LIST", arrayOf(appConfig)) + } + + sendCommand("com.symbol.datawedge.api.SET_CONFIG", associateConfig) + } +} +`; + +const packageCode = `package ${packageName}.scanner + +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ViewManager + +class ZebraScannerPackage : ReactPackage { + + override fun createNativeModules( + reactContext: ReactApplicationContext + ): List { + return listOf(ZebraScannerModule(reactContext)) + } + + override fun createViewManagers( + reactContext: ReactApplicationContext + ): List> { + return emptyList() + } +} +`; + +function patchMainApplication(mainApplicationPath) { + let contents = fs.readFileSync(mainApplicationPath, "utf8"); + + const importLine = `import ${packageName}.scanner.ZebraScannerPackage`; + + if (!contents.includes(importLine)) { + contents = contents.replace( + /import com\.facebook\.react\.PackageList/, + `import com.facebook.react.PackageList\n${importLine}`, + ); + } + + if (!contents.includes("add(ZebraScannerPackage())")) { + contents = contents.replace( + /PackageList\(this\)\.packages\.apply\s*\{/, + `PackageList(this).packages.apply {\n add(ZebraScannerPackage())`, + ); + } + + fs.writeFileSync(mainApplicationPath, contents); +} + +module.exports = function withZebraScanner(config) { + return withDangerousMod(config, [ + "android", + async (config) => { + const androidRoot = config.modRequest.platformProjectRoot; + + const scannerDir = path.join( + androidRoot, + "app/src/main/java", + packagePath, + "scanner", + ); + + fs.mkdirSync(scannerDir, { recursive: true }); + + fs.writeFileSync( + path.join(scannerDir, "ZebraScannerModule.kt"), + moduleCode, + ); + + fs.writeFileSync( + path.join(scannerDir, "ZebraScannerPackage.kt"), + packageCode, + ); + + const mainApplicationPath = path.join( + androidRoot, + "app/src/main/java", + packagePath, + "MainApplication.kt", + ); + + patchMainApplication(mainApplicationPath); + + return config; + }, + ]); +}; diff --git a/lstMobile/src/app/(tabs)/_layout.tsx b/lstMobile/src/app/(tabs)/_layout.tsx new file mode 100644 index 0000000..6958238 --- /dev/null +++ b/lstMobile/src/app/(tabs)/_layout.tsx @@ -0,0 +1,19 @@ +import { Tabs } from "expo-router"; +import { useAppStore } from "../../hooks/useAppStore"; + +export default function TabsLayout() { + const serverPort = useAppStore((s) => s.serverPort); + return ( + + + + = 50000 ? null : "/(tabs)/logs", + }} + /> + + ); +} \ No newline at end of file diff --git a/lstMobile/src/app/(tabs)/config.tsx b/lstMobile/src/app/(tabs)/config.tsx new file mode 100644 index 0000000..0a505f5 --- /dev/null +++ b/lstMobile/src/app/(tabs)/config.tsx @@ -0,0 +1,7 @@ +import { Link } from "expo-router"; +import { Text, View } from "react-native"; +import Setup from "../setup"; + +export default function SettingsTab() { + return +} \ No newline at end of file diff --git a/lstMobile/src/app/(tabs)/logs.tsx b/lstMobile/src/app/(tabs)/logs.tsx new file mode 100644 index 0000000..b41d477 --- /dev/null +++ b/lstMobile/src/app/(tabs)/logs.tsx @@ -0,0 +1,13 @@ +import React from 'react' +import { Text, View } from 'react-native' + +export default function Logs() { + return ( + Logs + ) +} diff --git a/lstMobile/src/app/(tabs)/scanner.tsx b/lstMobile/src/app/(tabs)/scanner.tsx new file mode 100644 index 0000000..8eb0db4 --- /dev/null +++ b/lstMobile/src/app/(tabs)/scanner.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { View } from "react-native"; + +import { useAppStore } from "../../hooks/useAppStore"; +import ProdScanner from "../../components/ProdScanner"; +import LSTScanner from "../../components/LSTScanner"; + +export default function scanner() { + const serverPort = useAppStore((s) => s.serverPort); + return ( + + {parseInt(serverPort || "0", 10) >= 50000 ? : } + + ); +} diff --git a/lstMobile/src/app/index.tsx b/lstMobile/src/app/index.tsx index a5784f8..7f9ebdc 100644 --- a/lstMobile/src/app/index.tsx +++ b/lstMobile/src/app/index.tsx @@ -1,4 +1,4 @@ -import { useRouter } from "expo-router"; +import { Redirect, useRouter } from "expo-router"; import { useEffect, useState } from "react"; import { ActivityIndicator, Text, View } from "react-native"; import { useAppStore } from "../hooks/useAppStore"; @@ -7,6 +7,7 @@ import { devDelay } from "../lib/devMode"; export default function Index() { const router = useRouter(); const [message, setMessage] = useState(Starting app...); + const [ready, setReady] = useState(false); const hasHydrated = useAppStore((s) => s.hasHydrated); const serverPort = useAppStore((s) => s.serverPort); @@ -40,13 +41,18 @@ export default function Index() { , ); await devDelay(1500); - router.replace("/scanner"); + //router.replace("/scanner"); + setReady(true) return; } + setMessage(Checking for updates) + await devDelay(1500) + // TODO if theres an update go to update screen message :D setMessage(Opening LST scan app); await devDelay(3250); - router.replace("/scanner"); + //router.replace("/scanner"); + setReady(true) } catch (error) { console.log("Startup error", error); setMessage(Something went wrong during startup.); @@ -56,6 +62,9 @@ export default function Index() { startup(); }, [hasHydrated, hasValidSetup, serverPort, router]); + if (ready) { + return ; + } return ( - - LST Scanner - - - Relocate - 0 / 4 - - - {/* - List of recent scanned pallets TBA - */} - - ); -} diff --git a/lstMobile/src/app/setup.tsx b/lstMobile/src/app/setup.tsx index fb4ed8a..951a877 100644 --- a/lstMobile/src/app/setup.tsx +++ b/lstMobile/src/app/setup.tsx @@ -4,7 +4,7 @@ import { useState } from "react"; import { Alert, Button, Text, TextInput, View } from "react-native"; import { useAppStore } from "../hooks/useAppStore"; -export default function setup() { +export default function Setup() { const router = useRouter(); const [auth, setAuth] = useState(false); const [pin, setPin] = useState(""); @@ -40,6 +40,7 @@ export default function setup() { updateAppState({ serverIp: serverIp.trim(), serverPort: serverPort.trim(), + scannerId: scannerId?.trim(), setupCompleted: true, isRegistered: true, }); @@ -80,7 +81,7 @@ export default function setup() { borderRadius: 8, }} > -