test(mobile): testing for ota updated on android scanner

This commit is contained in:
2025-11-12 20:20:44 -06:00
parent 5277ddfc51
commit 314ab049bb
3 changed files with 239 additions and 32 deletions

View File

@@ -0,0 +1,211 @@
import type { Express, Request, Response } from "express";
import express, { Router } from "express";
import { readdirSync, readFileSync, statSync } from "fs";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
import crypto from "crypto";
import fs from "fs";
export const setupMobileRoutes = (app: Express, basePath: string) => {
const router = Router();
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const distPath = join(__dirname, "../../../../mobileLst/dist");
function generateAssetManifest(baseUrl: string) {
const assets: any[] = [];
const assetsDir = join(distPath, "assets");
try {
if (!fs.existsSync(assetsDir)) {
return assets;
}
const files = readdirSync(assetsDir);
files.forEach((file) => {
const filePath = join(assetsDir, file);
const stats = statSync(filePath);
if (stats.isFile()) {
const content = readFileSync(filePath);
const hash = crypto
.createHash("sha256")
.update(content)
.digest("hex");
assets.push({
hash: hash,
key: file,
fileExtension: `.${file.split(".").pop()}`,
contentType: getContentType(file),
url: `${baseUrl}/assets/${file}`,
});
}
});
} catch (err) {
console.log("Error reading assets:", err);
}
return assets;
}
function getContentType(filename: string): string {
const ext = filename.split(".").pop()?.toLowerCase();
const contentTypes: { [key: string]: string } = {
hbc: "application/javascript",
bundle: "application/javascript",
js: "application/javascript",
json: "application/json",
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
gif: "image/gif",
ttf: "font/ttf",
otf: "font/otf",
woff: "font/woff",
woff2: "font/woff2",
};
return contentTypes[ext || ""] || "application/octet-stream";
}
app.get(basePath + "/api/mobile/updates", (req, res) => {
console.log("=== OTA Update Request ===");
console.log("Headers:", JSON.stringify(req.headers, null, 2));
const runtimeVersion = req.headers["expo-runtime-version"];
const platform = req.headers["expo-platform"] || "android";
const expectedRuntimeVersion = "1.0.0";
if (runtimeVersion !== expectedRuntimeVersion) {
console.log(
`Runtime mismatch: got ${runtimeVersion}, expected ${expectedRuntimeVersion}`
);
return res.status(404).json({
error: "No update available for this runtime version",
requestedVersion: runtimeVersion,
availableVersion: expectedRuntimeVersion,
});
}
try {
// const host = req.get('host');
// // If it's the production domain, force https
// const protocol = host.includes('alpla.net') ? 'https' : req.protocol;
// const baseUrl = `${protocol}://${host}/lst/api/mobile/updates`
const host = req.get('host'); // Should be "usmcd1vms036:4000"
const protocol = 'http';
const baseUrl = `${protocol}://${host}/api/mobile/updates`;
// Find the .hbc file
const bundleDir = join(distPath, "_expo/static/js/android");
if (!fs.existsSync(bundleDir)) {
console.error("Bundle directory does not exist:", bundleDir);
return res
.status(500)
.json({ error: "Bundle directory not found" });
}
const bundleFiles = readdirSync(bundleDir);
console.log("Available bundle files:", bundleFiles);
const bundleFile = bundleFiles.find((f) => f.endsWith(".hbc"));
if (!bundleFile) {
console.error("No .hbc file found in:", bundleDir);
return res
.status(500)
.json({ error: "Hermes bundle (.hbc) not found" });
}
console.log("Using bundle file:", bundleFile);
const bundlePath = join(bundleDir, bundleFile);
const bundleContent = readFileSync(bundlePath);
const bundleHash = crypto
.createHash("sha256")
.update(bundleContent)
.digest("hex");
const updateId = crypto.randomUUID();
const createdAt = new Date().toISOString();
// This is the NEW manifest format for Expo SDK 50+
const manifest = {
id: updateId,
createdAt: createdAt,
runtimeVersion: expectedRuntimeVersion,
launchAsset: {
hash: bundleHash,
key: bundleFile,
contentType: "application/javascript",
fileExtension: ".hbc",
url: `${baseUrl}/_expo/static/js/android/${bundleFile}`,
},
assets: generateAssetManifest(baseUrl),
metadata: {},
extra: {
expoClient: {
name: "LSTScanner",
slug: "lst-scanner-app",
version: "1.0.0",
runtimeVersion: expectedRuntimeVersion,
},
},
};
console.log(
"Returning manifest:",
JSON.stringify(manifest, null, 2)
);
res.setHeader("Content-Type", "application/json");
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("expo-protocol-version", "1");
res.setHeader("expo-sfv-version", "0");
res.json(manifest);
} catch (error: any) {
console.error("Error generating manifest:", error);
res.status(500).json({
error: "Failed to generate manifest",
details: error.message,
stack: error.stack,
});
}
});
// Serve static files
app.use(
basePath + "/api/mobile/updates",
express.static(distPath, {
setHeaders(res, path) {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Cache-Control", "public, max-age=31536000");
if (path.endsWith(".hbc")) {
res.setHeader("Content-Type", "application/javascript");
}
},
})
);
// app.use(
// basePath + "/api/mobile/updates",
// express.static(join(__dirname, mobileDir), {
// setHeaders(res) {
// // OTA runtime needs to fetch these from the device
// console.log("OTA check called");
// res.setHeader("Access-Control-Allow-Origin", "*");
// },
// })
// );
// app.get(basePath + "/api/mobile/updates", (req, res) => {
// res.redirect(basePath + "/api/mobile/updates/metadata.json");
// });
app.get(basePath + "/api/mobile", (_, res) =>
res.status(200).json({ message: "LST OTA server is up." })
);
};

View File

@@ -4,22 +4,24 @@ import { setupAuthRoutes } from "../auth/routes/routes.js";
import { setupForkliftRoutes } from "../forklifts/routes/routes.js";
import { setupLogisticsRoutes } from "../logistics/routes.js";
import { setupSystemRoutes } from "../system/routes.js";
import { setupMobileRoutes } from "../mobile/route.js";
export const setupRoutes = (app: Express, basePath: string) => {
// all routes
setupAuthRoutes(app, basePath);
setupAdminRoutes(app, basePath);
setupSystemRoutes(app, basePath);
setupLogisticsRoutes(app, basePath);
setupForkliftRoutes(app, basePath);
// all routes
setupAuthRoutes(app, basePath);
setupAdminRoutes(app, basePath);
setupSystemRoutes(app, basePath);
setupLogisticsRoutes(app, basePath);
setupForkliftRoutes(app, basePath);
setupMobileRoutes(app, basePath);
// always try to go to the app weather we are in dev or in production.
app.get(basePath + "/", (req: Request, res: Response) => {
res.redirect(basePath + "/app");
});
// always try to go to the app weather we are in dev or in production.
app.get(basePath + "/", (req: Request, res: Response) => {
res.redirect(basePath + "/app");
});
// Fallback 404 handler
app.use((req: Request, res: Response) => {
res.status(404).json({ error: "Not Found" });
});
// Fallback 404 handler
app.use((req: Request, res: Response) => {
res.status(404).json({ error: "Not Found" });
});
};