diff --git a/Govt-Billing-React/.env.example b/Govt-Billing-React/.env.example index 05373f2..d395c6e 100644 --- a/Govt-Billing-React/.env.example +++ b/Govt-Billing-React/.env.example @@ -4,4 +4,8 @@ VITE_FIREBASE_AUTH_DOMAIN="domain_name.firebaseapp.com" VITE_FIREBASE_PROJECT_ID="project_id" VITE_FIREBASE_STORAGE_BUCKET="storage_bucket" VITE_FIREBASE_MESSAGING_SENDER_ID="sender_id" -VITE_FIREBASE_APP_ID="firebase_app_id" \ No newline at end of file +VITE_FIREBASE_APP_ID="firebase_app_id" + +# Required for Save to IPFS/Filecoin feature. +# Get a Lighthouse API key at https://lighthouse.storage +VITE_LIGHTHOUSE_API_KEY="your_lighthouse_api_key_here" diff --git a/Govt-Billing-React/src/components/Menu/Menu.tsx b/Govt-Billing-React/src/components/Menu/Menu.tsx index 392becf..7b0aac1 100644 --- a/Govt-Billing-React/src/components/Menu/Menu.tsx +++ b/Govt-Billing-React/src/components/Menu/Menu.tsx @@ -5,8 +5,9 @@ import { isPlatform, IonToast } from "@ionic/react"; import { EmailComposer } from "capacitor-email-composer"; import { Printer } from "@ionic-native/printer"; import { IonActionSheet, IonAlert } from "@ionic/react"; -import { saveOutline, save, mail, print } from "ionicons/icons"; +import { saveOutline, save, mail, print, cloudUpload } from "ionicons/icons"; import { APP_NAME } from "../../app-data.js"; +import { uploadInvoiceToIPFS } from "../../services/lighthouseService"; const Menu: React.FC<{ showM: boolean; @@ -20,8 +21,12 @@ const Menu: React.FC<{ const [showAlert2, setShowAlert2] = useState(false); const [showAlert3, setShowAlert3] = useState(false); const [showAlert4, setShowAlert4] = useState(false); + const [showAlert5, setShowAlert5] = useState(false); const [showToast1, setShowToast1] = useState(false); const [toastMessage, setToastMessage] = useState(""); + const [showSaveAsAfterToast, setShowSaveAsAfterToast] = useState(false); + const [ipfsCid, setIpfsCid] = useState(""); + const [ipfsUrl, setIpfsUrl] = useState(""); /* Utility functions */ const _validateName = async (filename) => { filename = filename.trim(); @@ -108,11 +113,40 @@ const Menu: React.FC<{ props.updateSelectedFile(filename); setShowAlert4(true); } else { + setShowSaveAsAfterToast(true); setShowToast1(true); } } }; + const doSaveToIPFS = async () => { + const apiKey = import.meta.env.VITE_LIGHTHOUSE_API_KEY; + + if (!apiKey) { + setToastMessage( + "Lighthouse API key not set. Add VITE_LIGHTHOUSE_API_KEY to .env" + ); + setShowSaveAsAfterToast(false); + setShowToast1(true); + return; + } + + const content = AppGeneral.getSpreadsheetContent(); + const filename = + props.file === "default" ? "invoice" : _formatString(props.file); + + try { + const result = await uploadInvoiceToIPFS(content, filename, apiKey); + setIpfsCid(result.cid); + setIpfsUrl(result.url); + setShowAlert5(true); + } catch (err: any) { + setToastMessage(err?.message ?? "IPFS upload failed"); + setShowSaveAsAfterToast(false); + setShowToast1(true); + } + }; + const sendEmail = () => { if (isPlatform("hybrid")) { const content = AppGeneral.getCurrentHTMLContent(); @@ -156,6 +190,14 @@ const Menu: React.FC<{ console.log("Save As clicked"); }, }, + { + text: "Save to IPFS", + icon: cloudUpload, + handler: () => { + doSaveToIPFS(); + console.log("Save to IPFS clicked"); + }, + }, { text: "Print", icon: print, @@ -225,12 +267,27 @@ const Menu: React.FC<{ } buttons={["Ok"]} /> + setShowAlert5(false)} + header="Saved to Filecoin/IPFS" + message={ + "Invoice pinned successfully.

" + + `CID: ${ipfsCid}

` + + `View on IPFS Gateway` + } + buttons={["Ok"]} + /> { setShowToast1(false); - setShowAlert3(true); + if (showSaveAsAfterToast) { + setShowSaveAsAfterToast(false); + setShowAlert3(true); + } }} position="bottom" message={toastMessage} diff --git a/Govt-Billing-React/src/services/lighthouseService.ts b/Govt-Billing-React/src/services/lighthouseService.ts new file mode 100644 index 0000000..101e019 --- /dev/null +++ b/Govt-Billing-React/src/services/lighthouseService.ts @@ -0,0 +1,39 @@ +export interface IPFSUploadResult { + cid: string; + url: string; + timestamp: string; +} + +export async function uploadInvoiceToIPFS( + invoiceJson: string, + filename: string, + apiKey: string +): Promise { + const file = new File([invoiceJson], `${filename}.json`, { + type: "application/json", + }); + const formData = new FormData(); + formData.append("file", file); + + const response = await fetch("https://node.lighthouse.storage/api/v0/add", { + method: "POST", + headers: { + Authorization: `Bearer ${apiKey}`, + }, + body: formData, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Lighthouse upload failed: ${response.status} ${errorText}`); + } + + const data = await response.json(); + const cid = data.Hash; + + return { + cid, + url: `https://gateway.lighthouse.storage/ipfs/${cid}`, + timestamp: new Date().toISOString(), + }; +} diff --git a/Govt-Billing-React/src/vite-env.d.ts b/Govt-Billing-React/src/vite-env.d.ts index eecb811..849a1e1 100644 --- a/Govt-Billing-React/src/vite-env.d.ts +++ b/Govt-Billing-React/src/vite-env.d.ts @@ -8,6 +8,7 @@ interface ImportMetaEnv { readonly VITE_FIREBASE_STORAGE_BUCKET: string; readonly VITE_FIREBASE_MESSAGING_SENDER_ID: string; readonly VITE_FIREBASE_APP_ID: string; + readonly VITE_LIGHTHOUSE_API_KEY: string; } interface ImportMeta {