matiru

Blockchain-Based Supply Chain Transparency for Agricultural Produce
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:
Mbackend/server.js | 47+++++++++++++++++++++++++++++------------------
Afrontend/components/QRModal.js | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mfrontend/package-lock.json | 359+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mfrontend/package.json | 3+++
Mfrontend/screens/Distributor.js | 27++++++++++++++++++++-------
Mfrontend/screens/Farmer.js | 56++++++++++++++++++++++++++++++++++++++++++++++++++------
Mfrontend/screens/Inspector.js | 9++++++++-
Mfrontend/screens/Retailer.js | 19++++++++++++++++---
Mfrontend/screens/Search.js | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
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" }, +});