matiru

Blockchain-Based Supply Chain Transparency for Agricultural Produce
commit c202ea0d6dd687a746db6072380ae961e7894bf6
parent 20b0f563aab43fc1975f6ec3bbfa3b0077fdcb20
Author: maydayv7 <maydayv7@gmail.com>
Date:   Fri, 10 Oct 2025 02:33:58 +0530

feat: Offline Notifications

Also update `docs`

Diffstat:
MREADME.md | 57++++++++++++++++++++++++++++-----------------------------
Mdocs/INSTALL.md | 8++++++--
Mfrontend/components/DatePicker.js | 1+
Mfrontend/screens/Inventory.js | 10++++++++--
Mfrontend/screens/Notifications.js | 65+++++++++++++++++++++++++++++++++++++++++++++++------------------
5 files changed, 90 insertions(+), 51 deletions(-)

diff --git a/README.md b/README.md @@ -2,31 +2,38 @@ **"Trustable Food, Transparent Journey"** -Matiru is a blockchain-powered agriculture supply chain platform that records every stage of the produce lifecycle - from the farmer to the consumer. Each batch of produce is digitally registered with details like crop type, harvest date, quality, ownership, and movement. +Matiru is a blockchain-powered agriculture supply chain platform that records every stage of the produce lifecycle - from the farmer to the consumer. It ensures transparency in pricing, quality, and origin, empowering all stakeholders and building consumer trust. ## The Problem -The current agricultural supply chain often suffers from: +The traditional agricultural supply chain is often plagued by: -- **Opaque pricing:** Farmers have little visibility into the final selling price -- **Unreliable quality checks:** Certifications can be inconsistent and prone to manipulation -- **Lack of consumer trust:** Labels like "Organic" are hard to verify -- **Inefficiencies and waste:** Poor traceability makes it difficult to manage inventory and reduce spoilage +- **Opaque Pricing:** Farmers lack visibility into final market prices, leading to exploitation by middlemen +- **Unreliable Quality Checks:** Manual and inconsistent certifications are susceptible to fraud and manipulation +- **Lack of Consumer Trust:** Claims like "Organic" or "Pesticide-Free" are difficult for consumers to verify independently +- **Inefficiencies and Waste:** Poor traceability makes it challenging to manage inventory, recall contaminated batches, and reduce spoilage -## The Solution +## Core Features -Matiru provides a decentralized platform to track agricultural produce, ensuring transparency in pricing, quality, and origin. Stakeholders (farmers, distributors, retailers, inspectors) can verify transactions, reducing exploitation and fraud in the supply chain. Consumers can instantly access this information by scanning QR codes. +- **Farm-to-Fork Traceability:** Every event in a produce's journey—from registration and transport to inspection and sale—is recorded on an immutable ledger +- **Role-Based Dashboards:** A tailored mobile app experience for each stakeholder: + - **Farmers:** Register produce, generate QR codes, and transfer ownership + - **Distributors:** Update location, split batches, and manage resale + - **Retailers:** Confirm inventory and log final sales + - **Inspectors:** Certify quality and record inspection results on-chain +- **Immutable & Secure:** Built on Hyperledger Fabric, all transactions are cryptographically secured and cannot be altered, preventing fraud +- **Global Chain Access:** A transparent ledger explorer allows authorized users to view all supply chain actions, fostering accountability +- **QR Code Verification:** Consumers can scan a simple QR code on the product packaging to view its entire history, ensuring authenticity and quality -## Key Features +## Architecture -- **Farm-to-Fork Transparency:** Track produce from the farm to the consumer -- **Role-Based Access:** Different stakeholders have different permissions, ensuring data integrity -- **QR Code Integration:** Consumers can easily access produce information by scanning a QR code -- **Secure & Immutable:** Built on Hyperledger Fabric, all transactions are secure and cannot be altered -- **Cross-Platform App:** A user-friendly mobile app for all stakeholders -- **Global Chain Access:** A transparent ledger explorer for maximum accountability +The system is composed of three main parts: -## Technology Stack +1. **Hyperledger Fabric Network:** A private, permissioned blockchain where organizations (Farmers, Distributors, Retailers, Inspectors) run peers. A smart contract (`chaincode`) governs all business logic, such as registering produce, transferring ownership, and performing inspections. +2. **NodeJS Backend:** An API gateway that securely connects the mobile app to the blockchain network. It authenticates users and translates app requests into chaincode invocations. +3. **React Native Mobile App:** A cross-platform application that provides a user-friendly interface for all stakeholders to interact with the supply chain. It includes offline support to ensure functionality in areas with poor connectivity. + +## Tech Stack - **Blockchain:** Hyperledger Fabric - **Frontend:** React Native (Expo) @@ -35,7 +42,7 @@ Matiru provides a decentralized platform to track agricultural produce, ensuring ## Getting Started -To get a local copy up and running follow these steps +To get a local copy up and running, follow these steps: ### Prerequisites @@ -44,24 +51,16 @@ To get a local copy up and running follow these steps ### Installation -1. Clone the repo: +1. Clone the repository: ```sh git clone https://github.com/maydayv7/matiru cd matiru ``` -2. Follow the detailed instructions in the [INSTALL.md](./docs/INSTALL.md) file to set up the blockchain network, start the server and run the app - -## Usage - -The application provides different dashboards for each role in the supply chain: +2. Follow the detailed setup instructions in the **[INSTALL.md](./docs/INSTALL.md)** file to configure the blockchain network, start the server, and run the app -- **Farmer:** Register produce, generate QR codes, and transfer ownership -- **Distributor:** Update produce location, split batches, and manage resale -- **Inspector:** Record inspection results and approve or reject batches -- **Retailer:** Confirm inventory and log sales to consumers -- **Consumer:** Scan QR codes to view the complete provenance of the produce +## Preview -Here are some screenshots of the application: +Screenshots of the application: ![Preview 1](./docs/preview-1.jpeg) ![Preview 2](./docs/preview-2.jpeg) diff --git a/docs/INSTALL.md b/docs/INSTALL.md @@ -1,3 +1,6 @@ +> [!WARNING] +> The following instructions have been written for and tested exclusively on Linux + ### A. Backend 1. Install Docker & Docker Compose @@ -10,7 +13,8 @@ chmod +x install-fabric.sh ./install-fabric.sh ``` -Ensure the `config` directory is present inside `fabric` +> [!IMPORTANT] +> Ensure the `config` directory is generated inside `fabric` 3. Install Chaincode dependencies: @@ -80,7 +84,7 @@ IDENTITY_ORG4="admin-org4" CCP_PATH_ORG4="./connections/connection-org4.json" ``` -> [!IMPORTANT] +> [!NOTE] > Make sure the port is not blocked by a firewall 7. Start `backend`: diff --git a/frontend/components/DatePicker.js b/frontend/components/DatePicker.js @@ -64,6 +64,7 @@ export default function DatePicker({ label, value, onChange }) { display={Platform.OS === "ios" ? "spinner" : "default"} onChange={handleChange} minimumDate={new Date()} + accentColor={colors.darkGreen} /> )} </View> diff --git a/frontend/screens/Inventory.js b/frontend/screens/Inventory.js @@ -79,11 +79,12 @@ export default function InventoryScreen({ navigation, route }) { await AsyncStorage.setItem(CACHE_KEY, JSON.stringify(fetchedProduces)); } catch (err) { if (!produces.length) Alert.alert("Error", "Failed to fetch inventory"); + console.error("Network fetch failed:", err.message); } finally { if (!isRefreshing) setLoading(false); } }, - [role, routeUserId, produces.length] + [role, routeUserId, CACHE_KEY] ); useEffect(() => { @@ -109,7 +110,12 @@ export default function InventoryScreen({ navigation, route }) { <ScrollView contentContainerStyle={{ padding: 12 }} refreshControl={ - <RefreshControl refreshing={refreshing} onRefresh={onRefresh} /> + <RefreshControl + refreshing={refreshing} + onRefresh={onRefresh} + colors={[colors.darkGreen]} + tintColor={colors.darkGreen} + /> } > {loading && produces.length === 0 ? ( diff --git a/frontend/screens/Notifications.js b/frontend/screens/Notifications.js @@ -9,6 +9,7 @@ import { View, } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; +import AsyncStorage from "@react-native-async-storage/async-storage"; import { MaterialCommunityIcons } from "@expo/vector-icons"; import ScreenHeader from "../components/ScreenHeader"; @@ -22,29 +23,52 @@ export default function NotificationsScreen({ navigation }) { const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); - const fetchNotifications = useCallback(async () => { - if (!user?.token) return; - try { - const data = await api.getNotifications(user.token); - const sorted = (data.notifications || []).sort( - (a, b) => new Date(b.date) - new Date(a.date) - ); - setNotifications(sorted); - } catch (err) { - Alert.alert("Error", "Failed to fetch notifications."); - } - }, [user?.token]); + const CACHE_KEY = `@notifications_${user?.id}`; + + const loadNotifications = useCallback( + async (isRefreshing = false) => { + if (!user?.token) return; + if (!isRefreshing) setLoading(true); + + try { + const cachedData = await AsyncStorage.getItem(CACHE_KEY); + if (cachedData && !isRefreshing) { + const parsedData = JSON.parse(cachedData); + parsedData.sort(sortByLastAction); + setProduces(parsedData); + } + } catch (e) { + console.error("Failed to load cache:", e); + } + + try { + const data = await api.getNotifications(user.token); + const sorted = (data.notifications || []).sort( + (a, b) => new Date(b.date) - new Date(a.date) + ); + setNotifications(sorted); + await AsyncStorage.setItem(CACHE_KEY, JSON.stringify(sorted)); + } catch (err) { + if (notifications.length === 0) + Alert.alert("Error", "Failed to fetch new notifications"); + console.error("Network fetch failed:", err.message); + } finally { + if (!isRefreshing) setLoading(false); + } + }, + [user?.token, notifications.length, CACHE_KEY] + ); useEffect(() => { setLoading(true); - fetchNotifications().finally(() => setLoading(false)); - }, [fetchNotifications]); + loadNotifications().finally(() => setLoading(false)); + }, [loadNotifications]); const onRefresh = useCallback(async () => { setRefreshing(true); - await fetchNotifications(); + await loadNotifications(true); setRefreshing(false); - }, [fetchNotifications]); + }, [loadNotifications]); const getIconForTitle = (title) => { if (title.toLowerCase().includes("registered")) return "sprout"; @@ -84,7 +108,7 @@ export default function NotificationsScreen({ navigation }) { hideSearchButton={true} hideNotificationsButton={true} /> - {loading ? ( + {loading && notifications.length === 0 ? ( <ActivityIndicator size="large" color={colors.darkGreen} @@ -100,7 +124,12 @@ export default function NotificationsScreen({ navigation }) { } contentContainerStyle={{ paddingHorizontal: 16 }} refreshControl={ - <RefreshControl refreshing={refreshing} onRefresh={onRefresh} /> + <RefreshControl + refreshing={refreshing} + onRefresh={onRefresh} + colors={[colors.darkGreen]} + tintColor={colors.darkGreen} + /> } /> )}