commit 4e39abcb6107a4a52816f199fa21ade3684143ec
parent 59a07069b193cbc143d8d6fc16a9a965b03aae6b
Author: maydayv7 <maydayv7@gmail.com>
Date: Fri, 3 Oct 2025 23:56:23 +0530
feat(frontend): Add QR Modal and reset screens properly
Diffstat:
9 files changed, 691 insertions(+), 66 deletions(-)
diff --git a/backend/server.js b/backend/server.js
@@ -76,6 +76,16 @@ app.post("/api/auth/login", async (req, res) => {
// Middleware
function authenticateMiddleware(req, res, next) {
if (req.path === "/auth/login") return next();
+
+ // Public Queries
+ if (
+ req.method === "GET" &&
+ (req.path.startsWith("/getProduce") ||
+ req.path.startsWith("/getProduceByOwner"))
+ ) {
+ return next();
+ }
+
const header = req.headers["authorization"];
if (!header)
return res.status(401).json({ error: "missing authorization header" });
@@ -83,7 +93,7 @@ function authenticateMiddleware(req, res, next) {
if (!token) return res.status(401).json({ error: "missing token" });
jwt.verify(token, JWT_SECRET, (err, payload) => {
if (err) return res.status(403).json({ error: "invalid token" });
- req.user = payload; // { id, role, username }
+ req.user = payload;
next();
});
}
@@ -246,6 +256,24 @@ router.post("/recordPayment", async (req, res) => {
}
});
+router.post("/registerUser", async (req, res) => {
+ try {
+ const { role, details } = req.body;
+ const { contract, gateway } = await getContract();
+ const result = await contract.submitTransaction(
+ "registerUser",
+ role,
+ JSON.stringify(details)
+ );
+ await gateway.disconnect();
+ return res.json({ success: true, user: JSON.parse(result.toString()) });
+ } catch (err) {
+ console.error("registerUser error", err);
+ return res.status(500).json({ error: err.message });
+ }
+});
+
+// Queries
router.get("/getProduce/:id", async (req, res) => {
try {
const { contract, gateway } = await getContract();
@@ -276,23 +304,6 @@ router.get("/getProduceByOwner/:ownerId", async (req, res) => {
}
});
-router.post("/registerUser", async (req, res) => {
- try {
- const { role, details } = req.body;
- const { contract, gateway } = await getContract();
- const result = await contract.submitTransaction(
- "registerUser",
- role,
- JSON.stringify(details)
- );
- await gateway.disconnect();
- return res.json({ success: true, user: JSON.parse(result.toString()) });
- } catch (err) {
- console.error("registerUser error", err);
- return res.status(500).json({ error: err.message });
- }
-});
-
router.get("/getUser/:userKey", async (req, res) => {
try {
const { contract, gateway } = await getContract();
diff --git a/frontend/components/QRModal.js b/frontend/components/QRModal.js
@@ -0,0 +1,82 @@
+import {
+ Alert,
+ Modal,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+} from "react-native";
+import QRCode from "react-native-qrcode-svg";
+import * as Clipboard from "expo-clipboard";
+import { colors } from "../styles";
+
+export default function QRModal({ visible, onClose, value }) {
+ const copyToClipboard = async () => {
+ try {
+ await Clipboard.setStringAsync(value);
+ Alert.alert("Copied", "Produce ID copied to clipboard");
+ } catch (e) {
+ Alert.alert("Error", "Failed to copy");
+ }
+ };
+
+ if (!visible) return null;
+
+ return (
+ <Modal animationType="slide" transparent visible={visible}>
+ <View style={styles.backdrop}>
+ <View style={styles.card}>
+ <Text style={styles.title}>Produce Registered</Text>
+ <Text style={styles.subtitle}>Scan to view provenance</Text>
+ <View style={styles.qrWrap}>
+ <QRCode value={value} size={180} />
+ </View>
+
+ <Text style={styles.idText}>{value}</Text>
+
+ <View style={styles.row}>
+ <TouchableOpacity style={styles.button} onPress={copyToClipboard}>
+ <Text style={styles.buttonText}>Copy ID</Text>
+ </TouchableOpacity>
+ <TouchableOpacity
+ style={[styles.button, styles.close]}
+ onPress={onClose}
+ >
+ <Text style={styles.buttonText}>Close</Text>
+ </TouchableOpacity>
+ </View>
+ </View>
+ </View>
+ </Modal>
+ );
+}
+
+const styles = StyleSheet.create({
+ backdrop: {
+ flex: 1,
+ backgroundColor: "rgba(0,0,0,0.45)",
+ justifyContent: "center",
+ alignItems: "center",
+ },
+ card: {
+ width: "86%",
+ backgroundColor: "white",
+ borderRadius: 12,
+ padding: 20,
+ alignItems: "center",
+ },
+ title: { fontSize: 20, fontWeight: "700", color: colors.darkGreen },
+ subtitle: { marginTop: 6, color: colors.midGreen, marginBottom: 12 },
+ qrWrap: { padding: 12, backgroundColor: colors.cream, borderRadius: 8 },
+ idText: { marginTop: 12, color: colors.darkGreen, fontWeight: "600" },
+ row: { flexDirection: "row", marginTop: 14 },
+ button: {
+ paddingVertical: 10,
+ paddingHorizontal: 14,
+ backgroundColor: colors.darkGreen,
+ borderRadius: 8,
+ marginHorizontal: 6,
+ },
+ close: { backgroundColor: colors.midGreen },
+ buttonText: { color: "white", fontWeight: "600" },
+});
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
@@ -17,6 +17,7 @@
"@react-navigation/stack": "^7.4.8",
"expo": "~54.0.11",
"expo-camera": "~17.0.8",
+ "expo-clipboard": "~8.0.7",
"expo-constants": "~18.0.9",
"expo-dev-client": "^6.0.13",
"expo-font": "~14.0.8",
@@ -34,9 +35,11 @@
"react-dom": "19.1.0",
"react-native": "0.81.4",
"react-native-gesture-handler": "~2.28.0",
+ "react-native-qrcode-svg": "^6.3.15",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
+ "react-native-svg": "15.12.1",
"react-native-vector-icons": "^10.3.0",
"react-native-web": "~0.21.0",
"react-native-worklets": "0.5.1"
@@ -4094,6 +4097,12 @@
"node": ">=0.6"
}
},
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+ "license": "ISC"
+ },
"node_modules/bplist-creator": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
@@ -4701,6 +4710,56 @@
"hyphenate-style-name": "^1.0.3"
}
},
+ "node_modules/css-select": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
+ "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.1.0",
+ "domhandler": "^5.0.2",
+ "domutils": "^3.0.1",
+ "nth-check": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-tree": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
+ "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.0.14",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/css-tree/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
+ "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -4718,6 +4777,15 @@
}
}
},
+ "node_modules/decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/decode-uri-component": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
@@ -4836,6 +4904,67 @@
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
+ "node_modules/dijkstrajs": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
+ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
+ "license": "MIT"
+ },
+ "node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
"node_modules/dotenv": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
@@ -4905,6 +5034,18 @@
"once": "^1.4.0"
}
},
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/env-editor": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
@@ -5083,6 +5224,17 @@
}
}
},
+ "node_modules/expo-clipboard": {
+ "version": "8.0.7",
+ "resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-8.0.7.tgz",
+ "integrity": "sha512-zvlfFV+wB2QQrQnHWlo0EKHAkdi2tycLtE+EXFUWTPZYkgu1XcH+aiKfd4ul7Z0SDF+1IuwoiW9AA9eO35aj3Q==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/expo-constants": {
"version": "18.0.9",
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.9.tgz",
@@ -7044,6 +7196,12 @@
"integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==",
"license": "Apache-2.0"
},
+ "node_modules/mdn-data": {
+ "version": "2.0.14",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
+ "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
+ "license": "CC0-1.0"
+ },
"node_modules/memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
@@ -7597,6 +7755,18 @@
"node": ">=10"
}
},
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
"node_modules/nullthrows": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
@@ -8150,6 +8320,23 @@
"node": ">=6"
}
},
+ "node_modules/qrcode": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
+ "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "dijkstrajs": "^1.0.1",
+ "pngjs": "^5.0.0",
+ "yargs": "^15.3.1"
+ },
+ "bin": {
+ "qrcode": "bin/qrcode"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/qrcode-terminal": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz",
@@ -8158,6 +8345,122 @@
"qrcode-terminal": "bin/qrcode-terminal.js"
}
},
+ "node_modules/qrcode/node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qrcode/node_modules/cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
+ "node_modules/qrcode/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/qrcode/node_modules/pngjs": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
+ "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/qrcode/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/y18n": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+ "license": "ISC"
+ },
+ "node_modules/qrcode/node_modules/yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/qrcode/node_modules/yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "license": "ISC",
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/query-string": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
@@ -8358,6 +8661,22 @@
"react-native": "*"
}
},
+ "node_modules/react-native-qrcode-svg": {
+ "version": "6.3.15",
+ "resolved": "https://registry.npmjs.org/react-native-qrcode-svg/-/react-native-qrcode-svg-6.3.15.tgz",
+ "integrity": "sha512-vLuNImGfstE8u+rlF4JfFpq65nPhmByuDG6XUPWh8yp8MgLQX11rN5eQ8nb/bf4OB+V8XoLTJB/AZF2g7jQSSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "prop-types": "^15.8.0",
+ "qrcode": "^1.5.4",
+ "text-encoding": "^0.7.0"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": ">=0.63.4",
+ "react-native-svg": ">=14.0.0"
+ }
+ },
"node_modules/react-native-reanimated": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.2.tgz",
@@ -8411,6 +8730,21 @@
"react-native": "*"
}
},
+ "node_modules/react-native-svg": {
+ "version": "15.12.1",
+ "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.12.1.tgz",
+ "integrity": "sha512-vCuZJDf8a5aNC2dlMovEv4Z0jjEUET53lm/iILFnFewa15b4atjVxU6Wirm6O9y6dEsdjDZVD7Q3QM4T1wlI8g==",
+ "license": "MIT",
+ "dependencies": {
+ "css-select": "^5.1.0",
+ "css-tree": "^1.1.3",
+ "warn-once": "0.1.1"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/react-native-vector-icons": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.3.0.tgz",
@@ -8811,6 +9145,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "license": "ISC"
+ },
"node_modules/requireg": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/requireg/-/requireg-0.2.2.tgz",
@@ -9125,6 +9465,12 @@
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
"license": "MIT"
},
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+ "license": "ISC"
+ },
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
@@ -9675,6 +10021,13 @@
"node": "*"
}
},
+ "node_modules/text-encoding": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz",
+ "integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==",
+ "deprecated": "no longer maintained",
+ "license": "(Unlicense OR Apache-2.0)"
+ },
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@@ -10097,6 +10450,12 @@
"node": ">= 8"
}
},
+ "node_modules/which-module": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
+ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
+ "license": "ISC"
+ },
"node_modules/wonka": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.5.tgz",
diff --git a/frontend/package.json b/frontend/package.json
@@ -15,6 +15,7 @@
"@react-navigation/stack": "^7.4.8",
"expo": "~54.0.11",
"expo-camera": "~17.0.8",
+ "expo-clipboard": "~8.0.7",
"expo-constants": "~18.0.9",
"expo-dev-client": "^6.0.13",
"expo-font": "~14.0.8",
@@ -32,9 +33,11 @@
"react-dom": "19.1.0",
"react-native": "0.81.4",
"react-native-gesture-handler": "~2.28.0",
+ "react-native-qrcode-svg": "^6.3.15",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
+ "react-native-svg": "15.12.1",
"react-native-vector-icons": "^10.3.0",
"react-native-web": "~0.21.0",
"react-native-worklets": "0.5.1"
diff --git a/frontend/screens/Distributor.js b/frontend/screens/Distributor.js
@@ -38,6 +38,10 @@ export default function DistributorScreen({ navigation, route }) {
// updateStorageConditions
const [storageConditions, setStorageConditions] = useState("");
+ const resetCommon = () => {
+ setProduceId("");
+ };
+
const updateLocation = async () => {
try {
const res = await fetch(`${API_BASE}/updateLocation`, {
@@ -54,7 +58,9 @@ export default function DistributorScreen({ navigation, route }) {
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Error");
- Alert.alert("Location Updated", JSON.stringify(data.produce, null, 2));
+ Alert.alert("Location Updated", "Location updated successfully");
+ resetCommon();
+ setLocation("");
} catch (err) {
Alert.alert("Error", err.message);
}
@@ -77,7 +83,12 @@ export default function DistributorScreen({ navigation, route }) {
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Error");
- Alert.alert("Transferred", JSON.stringify(data.result, null, 2));
+ Alert.alert("Transferred", "Ownership transferred successfully");
+ // reset fields
+ resetCommon();
+ setNewOwnerId("");
+ setQty("");
+ setSalePrice("");
} catch (err) {
Alert.alert("Error", err.message);
}
@@ -95,7 +106,10 @@ export default function DistributorScreen({ navigation, route }) {
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Error");
- Alert.alert("Marked Unavailable", JSON.stringify(data.produce, null, 2));
+ Alert.alert("Marked Unavailable", "Produce marked unavailable");
+ resetCommon();
+ setReason("");
+ setNewStatus("");
} catch (err) {
Alert.alert("Error", err.message);
}
@@ -117,10 +131,9 @@ export default function DistributorScreen({ navigation, route }) {
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Error");
- Alert.alert(
- "Updated Storage Conditions",
- JSON.stringify(data.produce, null, 2)
- );
+ Alert.alert("Updated Storage Conditions", "Storage conditions updated");
+ resetCommon();
+ setStorageConditions("");
} catch (err) {
Alert.alert("Error", err.message);
}
diff --git a/frontend/screens/Farmer.js b/frontend/screens/Farmer.js
@@ -12,6 +12,7 @@ import ScreenHeader from "../components/ScreenHeader";
import ActionButton from "../components/ActionButton";
import Scanner from "../components/Scanner";
import LocationPicker from "../components/LocationPicker";
+import QRModal from "../components/QRModal";
import { API_BASE } from "../config";
import styles from "../styles";
import { AuthContext } from "../AuthContext";
@@ -40,6 +41,26 @@ export default function FarmerScreen({ navigation, route }) {
// splitProduce
const [splitQty, setSplitQty] = useState("");
+ // QR Modal
+ const [qrVisible, setQrVisible] = useState(false);
+ const [lastProduceId, setLastProduceId] = useState(null);
+
+ const resetRegisterForm = () => {
+ setCropType("");
+ setQty("");
+ setQtyUnit("KG");
+ setPricePerUnit("");
+ setHarvestDate("");
+ setQuality("");
+ setExpiryDate("");
+ setStorageConditions("");
+ setLocation("");
+ };
+
+ const resetCommon = () => {
+ setProduceId("");
+ };
+
const registerProduce = async () => {
try {
const res = await fetch(`${API_BASE}/registerProduce`, {
@@ -52,9 +73,9 @@ export default function FarmerScreen({ navigation, route }) {
farmerId: userId,
details: {
cropType,
- qty: parseFloat(qty),
+ qty: parseFloat(qty) || 0,
qtyUnit,
- pricePerUnit: parseFloat(pricePerUnit),
+ pricePerUnit: parseFloat(pricePerUnit) || 0,
harvestDate,
quality,
expiryDate,
@@ -68,7 +89,14 @@ export default function FarmerScreen({ navigation, route }) {
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Error");
- Alert.alert("Registered", JSON.stringify(data.produce, null, 2));
+
+ const produced = data.produce;
+ setLastProduceId(produced.id);
+ setQrVisible(true);
+
+ Alert.alert("Registered", "Produce registered successfully");
+ resetRegisterForm();
+ resetCommon();
} catch (err) {
Alert.alert("Error", err.message);
}
@@ -95,7 +123,12 @@ export default function FarmerScreen({ navigation, route }) {
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Error");
- Alert.alert("Updated", JSON.stringify(data.produce, null, 2));
+
+ Alert.alert("Updated", "Produce details updated");
+ // Reset inputs for update flow only
+ resetCommon();
+ setPricePerUnit("");
+ setStorageConditions("");
} catch (err) {
Alert.alert("Error", err.message);
}
@@ -117,7 +150,10 @@ export default function FarmerScreen({ navigation, route }) {
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Error");
- Alert.alert("Split", JSON.stringify(data.split, null, 2));
+
+ Alert.alert("Split", "Produce split successfully");
+ setSplitQty("");
+ resetCommon();
} catch (err) {
Alert.alert("Error", err.message);
}
@@ -139,7 +175,9 @@ export default function FarmerScreen({ navigation, route }) {
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Error");
- Alert.alert("Moved", JSON.stringify(data.produce, null, 2));
+ Alert.alert("Moved", "Location updated successfully");
+ resetCommon();
+ setLocation("");
} catch (err) {
Alert.alert("Error", err.message);
}
@@ -295,6 +333,12 @@ export default function FarmerScreen({ navigation, route }) {
</View>
)}
</ScrollView>
+
+ <QRModal
+ visible={qrVisible}
+ onClose={() => setQrVisible(false)}
+ value={lastProduceId || ""}
+ />
</SafeAreaView>
);
}
diff --git a/frontend/screens/Inspector.js b/frontend/screens/Inspector.js
@@ -25,6 +25,10 @@ export default function InspectorScreen({ navigation, route }) {
const [quality, setQuality] = useState("");
const [expiryDate, setExpiryDate] = useState("");
+ const resetCommon = () => {
+ setProduceId("");
+ };
+
const inspectProduce = async () => {
try {
const res = await fetch(`${API_BASE}/inspectProduce`, {
@@ -41,7 +45,10 @@ export default function InspectorScreen({ navigation, route }) {
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Error");
- Alert.alert("Inspected", JSON.stringify(data.produce, null, 2));
+ Alert.alert("Inspected", "Inspection recorded successfully");
+ resetCommon();
+ setQuality("");
+ setExpiryDate("");
} catch (err) {
Alert.alert("Error", err.message);
}
diff --git a/frontend/screens/Retailer.js b/frontend/screens/Retailer.js
@@ -31,6 +31,10 @@ export default function RetailerScreen({ navigation, route }) {
const [qty, setQty] = useState("");
const [salePrice, setSalePrice] = useState("");
+ const resetCommon = () => {
+ setProduceId("");
+ };
+
const updateLocation = async () => {
try {
const res = await fetch(`${API_BASE}/updateLocation`, {
@@ -47,7 +51,9 @@ export default function RetailerScreen({ navigation, route }) {
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Error");
- Alert.alert("Location Updated", JSON.stringify(data.produce, null, 2));
+ Alert.alert("Location Updated", "Location updated successfully");
+ resetCommon();
+ setLocation("");
} catch (err) {
Alert.alert("Error", err.message);
}
@@ -70,7 +76,11 @@ export default function RetailerScreen({ navigation, route }) {
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Error");
- Alert.alert("Transferred", JSON.stringify(data.result, null, 2));
+ Alert.alert("Transferred", "Sale / Transfer successful");
+ resetCommon();
+ setNewOwnerId("");
+ setQty("");
+ setSalePrice("");
} catch (err) {
Alert.alert("Error", err.message);
}
@@ -95,7 +105,10 @@ export default function RetailerScreen({ navigation, route }) {
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Error");
- Alert.alert("Updated Details", JSON.stringify(data.produce, null, 2));
+ Alert.alert("Updated Details", "Produce details updated");
+ resetCommon();
+ setPricePerUnit("");
+ setStorageConditions("");
} catch (err) {
Alert.alert("Error", err.message);
}
diff --git a/frontend/screens/Search.js b/frontend/screens/Search.js
@@ -1,9 +1,3 @@
-/*
- Produce → search by produceId → shows produce asset
- Owner → search by role id (e.g. "farmer1" or "retailer1") → lists all assets owned by entity
- User → search by user key (e.g. "FARMER-farmer1") → shows the user’s profile
-*/
-
import { useState } from "react";
import {
Alert,
@@ -12,10 +6,12 @@ import {
TextInput,
TouchableOpacity,
View,
+ StyleSheet,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import ScreenHeader from "../components/ScreenHeader";
+import Scanner from "../components/Scanner";
import { API_BASE } from "../config";
import styles, { colors } from "../styles";
@@ -23,23 +19,108 @@ export default function SearchScreen({ navigation }) {
const [tab, setTab] = useState("Produce");
const [input, setInput] = useState("");
const [result, setResult] = useState(null);
+ const [loading, setLoading] = useState(false);
const fetchResult = async () => {
+ if (!input.trim()) {
+ return Alert.alert("Enter a valid ID");
+ }
try {
+ setLoading(true);
+ setResult(null);
+
let url = "";
- if (tab === "Produce") url = `${API_BASE}/getProduce/${input}`;
- if (tab === "Owner") url = `${API_BASE}/getProduceByOwner/${input}`;
- if (tab === "User") url = `${API_BASE}/getUser/${input}`;
+ if (tab === "Produce") url = `${API_BASE}/getProduce/${input.trim()}`;
+ if (tab === "User") url = `${API_BASE}/getUser/${input.trim()}`;
const res = await fetch(url);
- if (!res.ok) throw new Error(`Server ${res.status}`);
const data = await res.json();
+ if (!res.ok) throw new Error(data.error || `Server ${res.status}`);
setResult(data);
} catch (err) {
Alert.alert("Error", err.message);
+ } finally {
+ setLoading(false);
}
};
+ const renderProduce = (produce) => (
+ <View style={local.card}>
+ <Text style={local.title}>Produce ID: {produce.id}</Text>
+ <Text>Crop: {produce.cropType}</Text>
+ <Text>
+ Quantity: {produce.qty} {produce.qtyUnit}
+ </Text>
+ <Text>Price Per Unit: {produce.pricePerUnit}</Text>
+ <Text>Quality: {produce.quality}</Text>
+ <Text>Status: {produce.status}</Text>
+ <Text>Owner: {produce.currentOwner}</Text>
+ <Text>Location: {produce.currentLocation}</Text>
+ <Text>Harvest Date: {produce.harvestDate}</Text>
+ <Text>Expiry: {produce.expiryDate}</Text>
+ <Text>Available: {produce.isAvailable ? "Yes" : "No"}</Text>
+
+ {/* Provenance Timeline */}
+ {produce.actionHistory && (
+ <View style={{ marginTop: 14 }}>
+ <Text style={local.subtitle}>Action History:</Text>
+ {produce.actionHistory.map((a, idx) => (
+ <View key={idx} style={local.timelineItem}>
+ <Text>
+ • {a.timestamp} → {a.action}
+ </Text>
+ <Text style={local.small}>
+ Location: {a.currentLocation || "N/A"} | Owner:{" "}
+ {a.currentOwner || "N/A"}
+ </Text>
+ </View>
+ ))}
+ </View>
+ )}
+
+ {produce.saleHistory && produce.saleHistory.length > 0 && (
+ <View style={{ marginTop: 14 }}>
+ <Text style={local.subtitle}>Sale History:</Text>
+ {produce.saleHistory.map((s, idx) => (
+ <View key={idx} style={local.timelineItem}>
+ <Text>
+ • {s.timestamp} → {s.prevOwner} → {s.newOwner}
+ </Text>
+ <Text style={local.small}>
+ Qty: {s.qtyBought}, Price: {s.salePrice}
+ </Text>
+ </View>
+ ))}
+ </View>
+ )}
+ </View>
+ );
+
+ const renderResults = () => {
+ if (!result) return null;
+
+ if (tab === "Produce" && result.produce) {
+ return renderProduce(result.produce);
+ }
+ if (tab === "User" && result.user) {
+ const u = result.user;
+ return (
+ <View style={local.card}>
+ <Text style={local.title}>{u.role} Profile</Text>
+ <Text>ID: {u.id}</Text>
+ <Text>Name: {u.name}</Text>
+ {u.location && <Text>Location: {u.location}</Text>}
+ {u.walletId && <Text>Wallet: {u.walletId}</Text>}
+ {u.certification && (
+ <Text>Certifications: {u.certification.join(", ")}</Text>
+ )}
+ </View>
+ );
+ }
+
+ return <Text style={{ color: "red" }}>No results found.</Text>;
+ };
+
return (
<SafeAreaView style={styles.container}>
<ScreenHeader
@@ -49,11 +130,11 @@ export default function SearchScreen({ navigation }) {
/>
<Text style={{ marginBottom: 12, color: colors.darkGreen }}>
- Search by Produce ID, Owner ID, or User Key
+ Search by Produce ID or User Key
</Text>
<View style={{ flexDirection: "row", justifyContent: "space-around" }}>
- {["Produce", "Owner", "User"].map((t) => (
+ {["Produce", "User"].map((t) => (
<TouchableOpacity
key={t}
style={{
@@ -71,13 +152,7 @@ export default function SearchScreen({ navigation }) {
}}
>
<MaterialCommunityIcons
- name={
- t === "Produce"
- ? "leaf"
- : t === "Owner"
- ? "account"
- : "account-badge"
- }
+ name={t === "Produce" ? "leaf" : "account-badge"}
size={20}
color={tab === t ? "white" : colors.darkGreen}
/>
@@ -94,23 +169,41 @@ export default function SearchScreen({ navigation }) {
))}
</View>
- <TextInput
- style={[styles.input, { marginTop: 20 }]}
- placeholder={`Enter ${tab} ID`}
- value={input}
- onChangeText={setInput}
- />
+ {/* ✅ For Produce tab → show scanner + input */}
+ {tab === "Produce" ? (
+ <Scanner value={input} onChange={setInput} />
+ ) : (
+ <TextInput
+ style={[styles.input, { marginTop: 20 }]}
+ placeholder={`Enter ${tab} ID`}
+ value={input}
+ onChangeText={setInput}
+ />
+ )}
+
<TouchableOpacity style={styles.primaryButton} onPress={fetchResult}>
<Text style={styles.buttonText}>Search</Text>
</TouchableOpacity>
- <ScrollView style={{ marginTop: 20 }}>
- {result && (
- <Text style={{ color: colors.darkGreen }}>
- {JSON.stringify(result, null, 2)}
- </Text>
- )}
+ <ScrollView style={{ marginTop: 20, paddingHorizontal: 12 }}>
+ {loading && <Text>Loading...</Text>}
+ {!loading && renderResults()}
</ScrollView>
</SafeAreaView>
);
}
+
+const local = StyleSheet.create({
+ card: {
+ borderWidth: 1,
+ borderColor: "#ddd",
+ borderRadius: 10,
+ padding: 14,
+ marginBottom: 16,
+ backgroundColor: "#fafafa",
+ },
+ title: { fontWeight: "700", marginBottom: 6, fontSize: 16 },
+ subtitle: { fontWeight: "600", marginTop: 6, marginBottom: 4 },
+ timelineItem: { marginLeft: 8, marginBottom: 4 },
+ small: { fontSize: 12, color: "#555" },
+});