From 3b7452c72ce2806377eb03b63057a1b19cc539ff Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:55:17 +0000 Subject: [PATCH 1/7] fix: make jose SignJWT test mock constructable under vitest 4 Vitest 4 requires vi.fn() mock implementations to use 'function' or 'class' (not arrow functions) to be usable with 'new'. The SignJWT mock used an arrow function, so 'new SignJWT()' in the agent login path threw 'is not a constructor', causing 4 agent.login test failures. Converted all 14 jose mocks to function form. Co-Authored-By: Patrick Munis --- server/floatTopUp.approve.test.ts | 14 ++++++++------ server/fraud.alerts.test.ts | 14 ++++++++------ server/mdm.test.ts | 14 ++++++++------ server/pos.test.ts | 14 ++++++++------ server/security.owasp.test.ts | 14 ++++++++------ server/settlement.test.ts | 14 ++++++++------ server/transactions.create.test.ts | 14 ++++++++------ tests/integration/admin-dashboard.test.ts | 14 ++++++++------ tests/integration/agent-auth.test.ts | 14 ++++++++------ tests/integration/compliance-reporting.test.ts | 14 ++++++++------ tests/integration/disputes.test.ts | 14 ++++++++------ tests/integration/infrastructure-services.test.ts | 14 ++++++++------ tests/integration/pos-features.test.ts | 14 ++++++++------ tests/integration/transactions.test.ts | 14 ++++++++------ 14 files changed, 112 insertions(+), 84 deletions(-) diff --git a/server/floatTopUp.approve.test.ts b/server/floatTopUp.approve.test.ts index 28293b9ba..19104ad9c 100644 --- a/server/floatTopUp.approve.test.ts +++ b/server/floatTopUp.approve.test.ts @@ -73,12 +73,14 @@ vi.mock("bcryptjs", () => ({ })); vi.mock("jose", () => ({ - SignJWT: vi.fn().mockImplementation(() => ({ - setProtectedHeader: vi.fn().mockReturnThis(), - setIssuedAt: vi.fn().mockReturnThis(), - setExpirationTime: vi.fn().mockReturnThis(), - sign: vi.fn().mockResolvedValue("mock.jwt.token"), - })), + SignJWT: vi.fn().mockImplementation(function () { + return { + setProtectedHeader: vi.fn().mockReturnThis(), + setIssuedAt: vi.fn().mockReturnThis(), + setExpirationTime: vi.fn().mockReturnThis(), + sign: vi.fn().mockResolvedValue("mock.jwt.token"), + }; + }), jwtVerify: vi.fn().mockResolvedValue({ payload: { sub: "1", diff --git a/server/fraud.alerts.test.ts b/server/fraud.alerts.test.ts index 2c289eb09..b3b965044 100644 --- a/server/fraud.alerts.test.ts +++ b/server/fraud.alerts.test.ts @@ -84,12 +84,14 @@ vi.mock("bcryptjs", () => ({ })); vi.mock("jose", () => ({ - SignJWT: vi.fn().mockImplementation(() => ({ - setProtectedHeader: vi.fn().mockReturnThis(), - setIssuedAt: vi.fn().mockReturnThis(), - setExpirationTime: vi.fn().mockReturnThis(), - sign: vi.fn().mockResolvedValue("mock.jwt.token"), - })), + SignJWT: vi.fn().mockImplementation(function () { + return { + setProtectedHeader: vi.fn().mockReturnThis(), + setIssuedAt: vi.fn().mockReturnThis(), + setExpirationTime: vi.fn().mockReturnThis(), + sign: vi.fn().mockResolvedValue("mock.jwt.token"), + }; + }), jwtVerify: vi.fn().mockResolvedValue({ payload: { sub: "1", diff --git a/server/mdm.test.ts b/server/mdm.test.ts index 964d7733b..1e01e2949 100644 --- a/server/mdm.test.ts +++ b/server/mdm.test.ts @@ -91,12 +91,14 @@ vi.mock("bcryptjs", () => ({ })); vi.mock("jose", () => ({ - SignJWT: vi.fn().mockImplementation(() => ({ - setProtectedHeader: vi.fn().mockReturnThis(), - setIssuedAt: vi.fn().mockReturnThis(), - setExpirationTime: vi.fn().mockReturnThis(), - sign: vi.fn().mockResolvedValue("mock.jwt.token"), - })), + SignJWT: vi.fn().mockImplementation(function () { + return { + setProtectedHeader: vi.fn().mockReturnThis(), + setIssuedAt: vi.fn().mockReturnThis(), + setExpirationTime: vi.fn().mockReturnThis(), + sign: vi.fn().mockResolvedValue("mock.jwt.token"), + }; + }), jwtVerify: vi.fn().mockResolvedValue({ payload: { sub: "1", diff --git a/server/pos.test.ts b/server/pos.test.ts index 2464171c4..39739774e 100644 --- a/server/pos.test.ts +++ b/server/pos.test.ts @@ -50,12 +50,14 @@ vi.mock("bcryptjs", () => ({ // ─── Mock jose ──────────────────────────────────────────────────────────────── vi.mock("jose", () => ({ - SignJWT: vi.fn().mockImplementation(() => ({ - setProtectedHeader: vi.fn().mockReturnThis(), - setIssuedAt: vi.fn().mockReturnThis(), - setExpirationTime: vi.fn().mockReturnThis(), - sign: vi.fn().mockResolvedValue("mock.jwt.token"), - })), + SignJWT: vi.fn().mockImplementation(function () { + return { + setProtectedHeader: vi.fn().mockReturnThis(), + setIssuedAt: vi.fn().mockReturnThis(), + setExpirationTime: vi.fn().mockReturnThis(), + sign: vi.fn().mockResolvedValue("mock.jwt.token"), + }; + }), jwtVerify: vi.fn().mockResolvedValue({ payload: { sub: "1", diff --git a/server/security.owasp.test.ts b/server/security.owasp.test.ts index 4f0240dfc..e21a1f65e 100644 --- a/server/security.owasp.test.ts +++ b/server/security.owasp.test.ts @@ -82,12 +82,14 @@ vi.mock("bcryptjs", () => ({ })); vi.mock("jose", () => ({ - SignJWT: vi.fn().mockImplementation(() => ({ - setProtectedHeader: vi.fn().mockReturnThis(), - setIssuedAt: vi.fn().mockReturnThis(), - setExpirationTime: vi.fn().mockReturnThis(), - sign: vi.fn().mockResolvedValue("mock.jwt.token"), - })), + SignJWT: vi.fn().mockImplementation(function () { + return { + setProtectedHeader: vi.fn().mockReturnThis(), + setIssuedAt: vi.fn().mockReturnThis(), + setExpirationTime: vi.fn().mockReturnThis(), + sign: vi.fn().mockResolvedValue("mock.jwt.token"), + }; + }), jwtVerify: vi.fn().mockRejectedValue(new Error("Invalid token")), createRemoteJWKSet: vi.fn(), })); diff --git a/server/settlement.test.ts b/server/settlement.test.ts index 57550a535..f7260f87d 100644 --- a/server/settlement.test.ts +++ b/server/settlement.test.ts @@ -82,12 +82,14 @@ vi.mock("bcryptjs", () => ({ })); vi.mock("jose", () => ({ - SignJWT: vi.fn().mockImplementation(() => ({ - setProtectedHeader: vi.fn().mockReturnThis(), - setIssuedAt: vi.fn().mockReturnThis(), - setExpirationTime: vi.fn().mockReturnThis(), - sign: vi.fn().mockResolvedValue("mock.jwt.token"), - })), + SignJWT: vi.fn().mockImplementation(function () { + return { + setProtectedHeader: vi.fn().mockReturnThis(), + setIssuedAt: vi.fn().mockReturnThis(), + setExpirationTime: vi.fn().mockReturnThis(), + sign: vi.fn().mockResolvedValue("mock.jwt.token"), + }; + }), jwtVerify: vi.fn().mockResolvedValue({ payload: { sub: "1", diff --git a/server/transactions.create.test.ts b/server/transactions.create.test.ts index 28ae10c3c..e4da2ee94 100644 --- a/server/transactions.create.test.ts +++ b/server/transactions.create.test.ts @@ -88,12 +88,14 @@ vi.mock("bcryptjs", () => ({ })); vi.mock("jose", () => ({ - SignJWT: vi.fn().mockImplementation(() => ({ - setProtectedHeader: vi.fn().mockReturnThis(), - setIssuedAt: vi.fn().mockReturnThis(), - setExpirationTime: vi.fn().mockReturnThis(), - sign: vi.fn().mockResolvedValue("mock.jwt.token"), - })), + SignJWT: vi.fn().mockImplementation(function () { + return { + setProtectedHeader: vi.fn().mockReturnThis(), + setIssuedAt: vi.fn().mockReturnThis(), + setExpirationTime: vi.fn().mockReturnThis(), + sign: vi.fn().mockResolvedValue("mock.jwt.token"), + }; + }), jwtVerify: vi.fn().mockResolvedValue({ payload: { sub: "1", diff --git a/tests/integration/admin-dashboard.test.ts b/tests/integration/admin-dashboard.test.ts index c05e2c80c..e1d86f2e9 100644 --- a/tests/integration/admin-dashboard.test.ts +++ b/tests/integration/admin-dashboard.test.ts @@ -90,12 +90,14 @@ vi.mock("bcryptjs", () => ({ })); vi.mock("jose", () => ({ - SignJWT: vi.fn().mockImplementation(() => ({ - setProtectedHeader: vi.fn().mockReturnThis(), - setIssuedAt: vi.fn().mockReturnThis(), - setExpirationTime: vi.fn().mockReturnThis(), - sign: vi.fn().mockResolvedValue("mock.jwt.token"), - })), + SignJWT: vi.fn().mockImplementation(function () { + return { + setProtectedHeader: vi.fn().mockReturnThis(), + setIssuedAt: vi.fn().mockReturnThis(), + setExpirationTime: vi.fn().mockReturnThis(), + sign: vi.fn().mockResolvedValue("mock.jwt.token"), + }; + }), jwtVerify: vi.fn().mockResolvedValue({ payload: { sub: "1", role: "admin" }, }), diff --git a/tests/integration/agent-auth.test.ts b/tests/integration/agent-auth.test.ts index 86bd0f37b..d69f0b50d 100644 --- a/tests/integration/agent-auth.test.ts +++ b/tests/integration/agent-auth.test.ts @@ -46,12 +46,14 @@ vi.mock("bcryptjs", () => ({ })); vi.mock("jose", () => ({ - SignJWT: vi.fn().mockImplementation(() => ({ - setProtectedHeader: vi.fn().mockReturnThis(), - setIssuedAt: vi.fn().mockReturnThis(), - setExpirationTime: vi.fn().mockReturnThis(), - sign: vi.fn().mockResolvedValue("mock.jwt.token"), - })), + SignJWT: vi.fn().mockImplementation(function () { + return { + setProtectedHeader: vi.fn().mockReturnThis(), + setIssuedAt: vi.fn().mockReturnThis(), + setExpirationTime: vi.fn().mockReturnThis(), + sign: vi.fn().mockResolvedValue("mock.jwt.token"), + }; + }), jwtVerify: vi.fn().mockResolvedValue({ payload: { sub: "1", diff --git a/tests/integration/compliance-reporting.test.ts b/tests/integration/compliance-reporting.test.ts index c74278d75..245fbbd7b 100644 --- a/tests/integration/compliance-reporting.test.ts +++ b/tests/integration/compliance-reporting.test.ts @@ -79,12 +79,14 @@ vi.mock("bcryptjs", () => ({ hash: vi.fn().mockResolvedValue("$2b$10$hash"), })); vi.mock("jose", () => ({ - SignJWT: vi.fn().mockImplementation(() => ({ - setProtectedHeader: vi.fn().mockReturnThis(), - setIssuedAt: vi.fn().mockReturnThis(), - setExpirationTime: vi.fn().mockReturnThis(), - sign: vi.fn().mockResolvedValue("mock.jwt.token"), - })), + SignJWT: vi.fn().mockImplementation(function () { + return { + setProtectedHeader: vi.fn().mockReturnThis(), + setIssuedAt: vi.fn().mockReturnThis(), + setExpirationTime: vi.fn().mockReturnThis(), + sign: vi.fn().mockResolvedValue("mock.jwt.token"), + }; + }), jwtVerify: vi .fn() .mockResolvedValue({ payload: { sub: "1", role: "admin" } }), diff --git a/tests/integration/disputes.test.ts b/tests/integration/disputes.test.ts index bb8c42085..7f311de63 100644 --- a/tests/integration/disputes.test.ts +++ b/tests/integration/disputes.test.ts @@ -63,12 +63,14 @@ vi.mock("../../server/_core/platformClient", () => ({ })); vi.mock("jose", () => ({ - SignJWT: vi.fn().mockImplementation(() => ({ - setProtectedHeader: vi.fn().mockReturnThis(), - setIssuedAt: vi.fn().mockReturnThis(), - setExpirationTime: vi.fn().mockReturnThis(), - sign: vi.fn().mockResolvedValue("mock.jwt.token"), - })), + SignJWT: vi.fn().mockImplementation(function () { + return { + setProtectedHeader: vi.fn().mockReturnThis(), + setIssuedAt: vi.fn().mockReturnThis(), + setExpirationTime: vi.fn().mockReturnThis(), + sign: vi.fn().mockResolvedValue("mock.jwt.token"), + }; + }), jwtVerify: vi.fn().mockResolvedValue({ payload: { sub: "1", diff --git a/tests/integration/infrastructure-services.test.ts b/tests/integration/infrastructure-services.test.ts index fbb69deb4..da02960cc 100644 --- a/tests/integration/infrastructure-services.test.ts +++ b/tests/integration/infrastructure-services.test.ts @@ -79,12 +79,14 @@ vi.mock("bcryptjs", () => ({ hash: vi.fn().mockResolvedValue("$2b$10$hash"), })); vi.mock("jose", () => ({ - SignJWT: vi.fn().mockImplementation(() => ({ - setProtectedHeader: vi.fn().mockReturnThis(), - setIssuedAt: vi.fn().mockReturnThis(), - setExpirationTime: vi.fn().mockReturnThis(), - sign: vi.fn().mockResolvedValue("mock.jwt.token"), - })), + SignJWT: vi.fn().mockImplementation(function () { + return { + setProtectedHeader: vi.fn().mockReturnThis(), + setIssuedAt: vi.fn().mockReturnThis(), + setExpirationTime: vi.fn().mockReturnThis(), + sign: vi.fn().mockResolvedValue("mock.jwt.token"), + }; + }), jwtVerify: vi .fn() .mockResolvedValue({ payload: { sub: "1", role: "admin" } }), diff --git a/tests/integration/pos-features.test.ts b/tests/integration/pos-features.test.ts index 5a0888e6f..742f1bf14 100644 --- a/tests/integration/pos-features.test.ts +++ b/tests/integration/pos-features.test.ts @@ -88,12 +88,14 @@ vi.mock("bcryptjs", () => ({ })); vi.mock("jose", () => ({ - SignJWT: vi.fn().mockImplementation(() => ({ - setProtectedHeader: vi.fn().mockReturnThis(), - setIssuedAt: vi.fn().mockReturnThis(), - setExpirationTime: vi.fn().mockReturnThis(), - sign: vi.fn().mockResolvedValue("mock.jwt.token"), - })), + SignJWT: vi.fn().mockImplementation(function () { + return { + setProtectedHeader: vi.fn().mockReturnThis(), + setIssuedAt: vi.fn().mockReturnThis(), + setExpirationTime: vi.fn().mockReturnThis(), + sign: vi.fn().mockResolvedValue("mock.jwt.token"), + }; + }), jwtVerify: vi.fn().mockResolvedValue({ payload: { sub: "1", role: "admin" }, }), diff --git a/tests/integration/transactions.test.ts b/tests/integration/transactions.test.ts index 1298ddf5d..d746012a5 100644 --- a/tests/integration/transactions.test.ts +++ b/tests/integration/transactions.test.ts @@ -76,12 +76,14 @@ vi.mock("bcryptjs", () => ({ })); vi.mock("jose", () => ({ - SignJWT: vi.fn().mockImplementation(() => ({ - setProtectedHeader: vi.fn().mockReturnThis(), - setIssuedAt: vi.fn().mockReturnThis(), - setExpirationTime: vi.fn().mockReturnThis(), - sign: vi.fn().mockResolvedValue("mock.jwt.token"), - })), + SignJWT: vi.fn().mockImplementation(function () { + return { + setProtectedHeader: vi.fn().mockReturnThis(), + setIssuedAt: vi.fn().mockReturnThis(), + setExpirationTime: vi.fn().mockReturnThis(), + sign: vi.fn().mockResolvedValue("mock.jwt.token"), + }; + }), jwtVerify: vi.fn().mockResolvedValue({ payload: { sub: "1", From 8c5b9fa2beeb7da859ea0cb4bab7deac4fd3627b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:36:17 +0000 Subject: [PATCH 2/7] fix(ci): unblock Playwright E2E + resolve CodeQL check collision - Add all production-required env vars + SECURITY_FAIL_OPEN=true to the Playwright and k6-smoke jobs so the server starts and the security orchestrator fails open when the Rust/Go/Python sidecars are absent in CI. - Seed agent/admin/supervisor fixtures (scripts/seed-e2e.mjs) before the E2E and smoke runs so AGT001/1234 login works. - AgentLogin: add data-digit hooks to the PIN pad and read the agent code from a ref so a fast Continue click uses the latest keyed value (fixes a controlled-input race under automation). - Remove the duplicate CodeQL job from security-scan.yml; codeql.yml is the single authoritative analysis (fixes the SARIF category collision). Co-Authored-By: Patrick Munis --- .github/workflows/ci.yml | 36 +++++++++++++++++ .github/workflows/security-scan.yml | 24 ++---------- .gitignore | 4 ++ client/src/pages/AgentLogin.tsx | 17 ++++++-- scripts/seed-e2e.mjs | 61 +++++++++++++++++++++++++++++ 5 files changed, 118 insertions(+), 24 deletions(-) create mode 100644 scripts/seed-e2e.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23464ae34..bc3b79eb3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -270,6 +270,20 @@ jobs: KEYCLOAK_CLIENT_ID: pos-shell-app PLATFORM_BASE_URL: https://platform.staging.54link.io PLATFORM_API_KEY: ci-smoke-platform-key + # Required by server env validation in NODE_ENV=production (CI test values). + CRON_SECRET: ci-smoke-cron-secret-32-characters-x + INTERNAL_API_KEY: ci-smoke-internal-api-key-32-chars- + TX_SIGNING_SECRET: ci-smoke-tx-signing-secret-32-chars + KEYCLOAK_CLIENT_SECRET: ci-smoke-keycloak-client-secret-32- + PLATFORM_SERVICE_TOKEN: ci-smoke-platform-service-token-32 + MINIO_SECRET_KEY: ci-smoke-minio-secret-key-32-chars- + MINIO_ACCESS_KEY: ci-smoke-minio-access-key-32-chars- + APISIX_ADMIN_KEY: ci-smoke-apisix-admin-key-32-chars- + TERMII_API_KEY: ci-smoke-termii-api-key-32-chars-xx + FLUVIO_API_KEY: ci-smoke-fluvio-api-key-32-chars-xx + MQTT_PASSWORD: ci-smoke-mqtt-password-32-chars-xxx + # Security sidecars are not deployed in CI — fail open. + SECURITY_FAIL_OPEN: "true" services: postgres: image: postgres:16-alpine @@ -306,6 +320,9 @@ jobs: run: pnpm db:push continue-on-error: true + - name: Seed smoke test data (agents/admin/supervisor fixtures) + run: node scripts/seed-e2e.mjs + - name: Start server in background run: | node dist/index.js & @@ -375,6 +392,22 @@ jobs: BASE_URL: http://localhost:3000 TEST_ADMIN_EMAIL: admin@54link.ng TEST_ADMIN_PASS: TestAdmin123! + # Required by server env validation in NODE_ENV=production (CI test values). + CRON_SECRET: ci-e2e-cron-secret-32-characters-xx + INTERNAL_API_KEY: ci-e2e-internal-api-key-32-chars-xx + TX_SIGNING_SECRET: ci-e2e-tx-signing-secret-32-chars-x + KEYCLOAK_CLIENT_SECRET: ci-e2e-keycloak-client-secret-32-ch + PLATFORM_API_KEY: ci-e2e-platform-api-key-32-chars-xx + PLATFORM_SERVICE_TOKEN: ci-e2e-platform-service-token-32-ch + MINIO_SECRET_KEY: ci-e2e-minio-secret-key-32-chars-xx + MINIO_ACCESS_KEY: ci-e2e-minio-access-key-32-chars-xx + APISIX_ADMIN_KEY: ci-e2e-apisix-admin-key-32-chars-xx + TERMII_API_KEY: ci-e2e-termii-api-key-32-chars-xxxx + FLUVIO_API_KEY: ci-e2e-fluvio-api-key-32-chars-xxxx + MQTT_PASSWORD: ci-e2e-mqtt-password-32-chars-xxxxx + # Security sidecars (Rust DDoS / Go PBAC / Python Fraud ML) are not + # deployed in CI — fail open so requests are not blocked by their absence. + SECURITY_FAIL_OPEN: "true" services: postgres: image: postgres:16-alpine @@ -414,6 +447,9 @@ jobs: run: pnpm db:push continue-on-error: true + - name: Seed E2E test data (agents/admin/supervisor fixtures) + run: node scripts/seed-e2e.mjs + - name: Start server in background run: | node dist/index.js & diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index bdc4a1052..96c5355a9 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -59,26 +59,10 @@ jobs: with: sarif_file: trivy-results.sarif - codeql: - name: CodeQL Analysis - runs-on: ubuntu-latest - timeout-minutes: 20 - strategy: - matrix: - language: [javascript-typescript] - steps: - - uses: actions/checkout@v7 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + # CodeQL analysis is owned by the dedicated codeql.yml workflow (JS/TS, Go, + # Python). A duplicate job here uploaded a second SARIF for the same + # javascript-typescript category, causing a category collision that failed + # the "CodeQL Analysis" check. Removed to keep a single authoritative run. secret-scan: name: Secret Detection diff --git a/.gitignore b/.gitignore index e1dcbe709..45b05a229 100644 --- a/.gitignore +++ b/.gitignore @@ -146,3 +146,7 @@ services/python/ml-pipeline/models/weights/*.json services/python/ml-pipeline/models/lakehouse/ services/python/ml-pipeline/models/registry/ /data/ + +# Playwright E2E artifacts +test-results/ +playwright-report/ diff --git a/client/src/pages/AgentLogin.tsx b/client/src/pages/AgentLogin.tsx index 3b1265962..f84860a95 100644 --- a/client/src/pages/AgentLogin.tsx +++ b/client/src/pages/AgentLogin.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useRef } from "react"; import { trpc } from "../lib/trpc"; import { usePosStore } from "../store/posStore"; import { toast } from "sonner"; @@ -17,6 +17,7 @@ type Screen = "code" | "pin" | "forgot_phone" | "forgot_newpin"; export default function AgentLogin() { const [agentCode, setAgentCode] = useState(""); + const agentCodeRef = useRef(""); const [pin, setPin] = useState(""); const [step, setStep] = useState("code"); const [loading, setLoading] = useState(false); @@ -45,7 +46,10 @@ export default function AgentLogin() { }); const handleCodeSubmit = () => { - if (agentCode.trim().length < 3) { + // Read from the ref so the latest keyed value is used even if the React + // state update from the input's onChange has not committed yet. + const code = (agentCodeRef.current || agentCode).trim(); + if (code.length < 3) { toast.error("Enter a valid agent code"); return; } @@ -63,7 +67,7 @@ export default function AgentLogin() { if (newPin.length === 4) { setLoading(true); loginMutation.mutate({ - agentCode: agentCode.trim().toUpperCase(), + agentCode: (agentCodeRef.current || agentCode).trim().toUpperCase(), pin: newPin, }); } @@ -200,7 +204,11 @@ export default function AgentLogin() { setAgentCode(e.target.value.toUpperCase())} + onChange={e => { + const v = e.target.value.toUpperCase(); + agentCodeRef.current = v; + setAgentCode(v); + }} onKeyDown={e => e.key === "Enter" && handleCodeSubmit()} placeholder="e.g. AGT001" className="w-full rounded-2xl px-4 py-4 text-white text-lg font-bold outline-none mb-4" @@ -316,6 +324,7 @@ export default function AgentLogin() { {PAD_KEYS.map((key, idx) => (