diff --git a/bun.lock b/bun.lock
index 563e8f3..96a5ff6 100644
--- a/bun.lock
+++ b/bun.lock
@@ -558,7 +558,7 @@
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
- "framer-motion": ["framer-motion@12.28.2", "", { "dependencies": { "motion-dom": "^12.28.2", "motion-utils": "^12.27.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-LNzKJo1VWz3RqNSeBzMbDIdLLpUw7hp/PpsvOpDlLqT1KlAAGsoSZ3OxNAKmS89AUeRv0rBbYgFLAWzO33xzDw=="],
+ "framer-motion": ["framer-motion@12.29.0", "", { "dependencies": { "motion-dom": "^12.29.0", "motion-utils": "^12.27.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-1gEFGXHYV2BD42ZPTFmSU9buehppU+bCuOnHU0AD18DKh9j4DuTx47MvqY5ax+NNWRtK32qIcJf1UxKo1WwjWg=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
@@ -746,7 +746,7 @@
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
- "motion-dom": ["motion-dom@12.28.2", "", { "dependencies": { "motion-utils": "^12.27.2" } }, "sha512-Lu+N4CSyc0JZlssX1OvdAgHanP8SuXCa4ZU/dEjGbtwZOjC2BH5e7MfDDi/Kgnzsu+Oyw4QYKhgFici4LlLiyg=="],
+ "motion-dom": ["motion-dom@12.29.0", "", { "dependencies": { "motion-utils": "^12.27.2" } }, "sha512-3eiz9bb32yvY8Q6XNM4AwkSOBPgU//EIKTZwsSWgA9uzbPBhZJeScCVcBuwwYVqhfamewpv7ZNmVKTGp5qnzkA=="],
"motion-utils": ["motion-utils@12.27.2", "", {}, "sha512-B55gcoL85Mcdt2IEStY5EEAsrMSVE2sI14xQ/uAdPL+mfQxhKKFaEag9JmfxedJOR4vZpBGoPeC/Gm13I/4g5Q=="],
diff --git a/package-lock.json b/package-lock.json
index bcfd352..8c863a5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,6 +19,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.5.0",
+ "jspdf": "^4.0.0",
"lucide-react": "^0.482.0",
"next": "15.3.8",
"next-themes": "^0.4.6",
@@ -53,6 +54,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@babel/runtime": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
+ "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@emnapi/core": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz",
@@ -3369,6 +3379,19 @@
"undici-types": "~6.19.2"
}
},
+ "node_modules/@types/pako": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz",
+ "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/raf": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
+ "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@types/react": {
"version": "19.0.11",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.11.tgz",
@@ -3389,6 +3412,13 @@
"@types/react": "^19.0.0"
}
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.26.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz",
@@ -4185,6 +4215,16 @@
"node": ">=0.12.0"
}
},
+ "node_modules/base64-arraybuffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+ "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -4352,6 +4392,26 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/canvg": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
+ "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "@types/raf": "^3.4.0",
+ "core-js": "^3.8.3",
+ "raf": "^3.4.1",
+ "regenerator-runtime": "^0.13.7",
+ "rgbcolor": "^1.0.1",
+ "stackblur-canvas": "^2.0.0",
+ "svg-pathdata": "^6.0.3"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -4435,6 +4495,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/core-js": {
+ "version": "3.48.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz",
+ "integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -4450,6 +4522,16 @@
"node": ">= 8"
}
},
+ "node_modules/css-line-break": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
+ "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "utrie": "^1.0.2"
+ }
+ },
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -4617,6 +4699,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/dompurify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
+ "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optional": true,
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -5321,6 +5413,17 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-png": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz",
+ "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/pako": "^2.0.3",
+ "iobuffer": "^5.3.2",
+ "pako": "^2.1.0"
+ }
+ },
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@@ -5340,6 +5443,12 @@
"is-retry-allowed": "^3.0.0"
}
},
+ "node_modules/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+ "license": "MIT"
+ },
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -5759,6 +5868,20 @@
"node": ">= 0.4"
}
},
+ "node_modules/html2canvas": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
+ "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "css-line-break": "^2.1.0",
+ "text-segmentation": "^1.0.3"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -5837,6 +5960,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/iobuffer": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
+ "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==",
+ "license": "MIT"
+ },
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -6333,6 +6462,23 @@
"json5": "lib/cli.js"
}
},
+ "node_modules/jspdf": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.0.0.tgz",
+ "integrity": "sha512-w12U97Z6edKd2tXDn3LzTLg7C7QLJlx0BPfM3ecjK2BckUl9/81vZ+r5gK4/3KQdhAcEZhENUxRhtgYBj75MqQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "fast-png": "^6.2.0",
+ "fflate": "^0.8.1"
+ },
+ "optionalDependencies": {
+ "canvg": "^3.0.11",
+ "core-js": "^3.6.0",
+ "dompurify": "^3.2.4",
+ "html2canvas": "^1.0.0-rc.5"
+ }
+ },
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -7083,6 +7229,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/pako": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
+ "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
+ "license": "(MIT AND Zlib)"
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -7123,6 +7275,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/performance-now": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -7240,6 +7399,16 @@
],
"license": "MIT"
},
+ "node_modules/raf": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
+ "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "performance-now": "^2.1.0"
+ }
+ },
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -7369,6 +7538,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/regenerator-runtime": {
+ "version": "0.13.11",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
+ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@@ -7456,6 +7632,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/rgbcolor": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
+ "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
+ "license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.8.15"
+ }
+ },
"node_modules/rspack-resolver": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/rspack-resolver/-/rspack-resolver-1.1.2.tgz",
@@ -7838,6 +8024,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/stackblur-canvas": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
+ "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.1.14"
+ }
+ },
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
@@ -8031,6 +8227,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/svg-pathdata": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
+ "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/tailwind-merge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.2.tgz",
@@ -8066,6 +8272,16 @@
"node": ">=6"
}
},
+ "node_modules/text-segmentation": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
+ "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "utrie": "^1.0.2"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz",
@@ -8358,6 +8574,16 @@
}
}
},
+ "node_modules/utrie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
+ "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "base64-arraybuffer": "^1.0.2"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/package.json b/package.json
index 2b32795..b029286 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.5.0",
+ "jspdf": "^4.0.0",
"lucide-react": "^0.482.0",
"next": "15.3.8",
"next-themes": "^0.4.6",
diff --git a/src/components/escrow/EscrowDetails.tsx b/src/components/escrow/EscrowDetails.tsx
index fb583bb..8c4986a 100644
--- a/src/components/escrow/EscrowDetails.tsx
+++ b/src/components/escrow/EscrowDetails.tsx
@@ -24,6 +24,7 @@ import {
} from "@/utils/transactionFetcher";
import { LedgerBalancePanel } from "@/components/escrow/LedgerBalancePanel";
import { useIsMobile } from "@/hooks/useIsMobile";
+import { ExportPdfButton } from "./ExportPdfButton";
// ⬇️ New hooks
import { useEscrowData } from "@/hooks/useEscrowData";
diff --git a/src/components/escrow/ExportPdfButton.tsx b/src/components/escrow/ExportPdfButton.tsx
new file mode 100644
index 0000000..f2342b3
--- /dev/null
+++ b/src/components/escrow/ExportPdfButton.tsx
@@ -0,0 +1,54 @@
+import { Button } from "@/components/ui/button";
+import { FileDown } from "lucide-react";
+import { generateEscrowPdf } from "@/utils/pdf-export";
+import type { OrganizedEscrowData } from "@/mappers/escrow-mapper";
+import type { NetworkType } from "@/lib/network-config";
+
+interface ExportPdfButtonProps {
+ organized: OrganizedEscrowData;
+ network: NetworkType;
+ contractId?: string;
+ initialEscrowId?: string;
+}
+
+export function ExportPdfButton({
+ organized,
+ network,
+ contractId,
+ initialEscrowId,
+}: ExportPdfButtonProps) {
+ const handleExport = async () => {
+ const contractIdToUse =
+ organized.properties.escrow_id ||
+ contractId ||
+ initialEscrowId;
+
+ if (!contractIdToUse) {
+ alert("Error: Contract ID is required to export PDF.");
+ return;
+ }
+
+ try {
+ await generateEscrowPdf({
+ organized,
+ network,
+ contractId: contractIdToUse,
+ });
+ } catch (error) {
+ console.error("Error generating PDF:", error);
+ alert("Error generating PDF. Please try again.");
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/utils/pdf-export.ts b/src/utils/pdf-export.ts
new file mode 100644
index 0000000..f306d8b
--- /dev/null
+++ b/src/utils/pdf-export.ts
@@ -0,0 +1,282 @@
+import jsPDF from 'jspdf';
+import type { OrganizedEscrowData } from '@/mappers/escrow-mapper';
+import type { NetworkType } from '@/lib/network-config';
+import { ROLE_MAPPING } from '@/lib/escrow-constants';
+
+interface PdfExportOptions {
+ organized: OrganizedEscrowData;
+ network: NetworkType;
+ contractId: string;
+}
+
+async function imageToBase64(url: string): Promise {
+ try {
+ const response = await fetch(url);
+ const blob = await response.blob();
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onloadend = () => resolve(reader.result as string);
+ reader.onerror = reject;
+ reader.readAsDataURL(blob);
+ });
+ } catch (error) {
+ console.warn('Failed to load logo image:', error);
+ return '';
+ }
+}
+
+export async function generateEscrowPdf({ organized, network, contractId }: PdfExportOptions): Promise {
+ const doc = new jsPDF({
+ orientation: 'portrait',
+ unit: 'mm',
+ format: 'a4',
+ });
+
+ const pageWidth = doc.internal.pageSize.getWidth();
+ const pageHeight = doc.internal.pageSize.getHeight();
+ const margin = 20;
+ const contentWidth = pageWidth - 2 * margin;
+ let yPosition = margin;
+
+ const checkPageBreak = (requiredSpace: number) => {
+ if (yPosition + requiredSpace > pageHeight - margin) {
+ doc.addPage();
+ yPosition = margin;
+ return true;
+ }
+ return false;
+ };
+
+ const addText = (text: string, fontSize: number, isBold = false, color: [number, number, number] = [0, 0, 0]) => {
+ doc.setFontSize(fontSize);
+ doc.setFont('helvetica', isBold ? 'bold' : 'normal');
+ doc.setTextColor(color[0], color[1], color[2]);
+
+ const lines = doc.splitTextToSize(text, contentWidth);
+ lines.forEach((line: string) => {
+ checkPageBreak(fontSize * 0.5);
+ doc.text(line, margin, yPosition);
+ yPosition += fontSize * 0.5;
+ });
+ yPosition += 3;
+ };
+
+ const addSectionHeader = (title: string) => {
+ checkPageBreak(15);
+ yPosition += 5;
+ addText(title, 14, true, [0, 51, 102]);
+ doc.setDrawColor(0, 51, 102);
+ doc.setLineWidth(0.5);
+ doc.line(margin, yPosition - 2, pageWidth - margin, yPosition - 2);
+ yPosition += 3;
+ };
+
+ const addKeyValue = (key: string, value: string, indent = 0) => {
+ checkPageBreak(8);
+ const xPos = margin + indent;
+ doc.setFontSize(10);
+ doc.setFont('helvetica', 'bold');
+ doc.setTextColor(60, 60, 60);
+ doc.text(`${key}:`, xPos, yPosition);
+
+ const valueX = xPos + 50;
+ doc.setFont('helvetica', 'normal');
+ doc.setTextColor(0, 0, 0);
+ const valueLines = doc.splitTextToSize(value, contentWidth - 50 - indent);
+ valueLines.forEach((line: string) => {
+ doc.text(line, valueX, yPosition);
+ yPosition += 5;
+ });
+ yPosition += 2;
+ };
+
+ const headerHeight = 40;
+ doc.setFillColor(0, 51, 102);
+ doc.rect(0, 0, pageWidth, headerHeight, 'F');
+
+ try {
+ const logoBase64 = await imageToBase64('/logo.png');
+ if (logoBase64) {
+ doc.addImage(logoBase64, 'PNG', 15, 11, 18, 18);
+ }
+ } catch {
+ // Logo failed to load, continue without it
+ }
+
+ const logoWidth = 18;
+ const logoSpacing = 5;
+ const textStartX = 15 + logoWidth + logoSpacing;
+
+ doc.setTextColor(255, 255, 255);
+ doc.setFontSize(20);
+ doc.setFont('helvetica', 'bold');
+ doc.text('Trustless Work', textStartX, 18);
+
+ doc.setFontSize(16);
+ doc.setFont('helvetica', 'normal');
+ doc.text('Escrow Report', textStartX, 26);
+
+ const rightMargin = 15;
+ const labelWidth = 25;
+ const valueWidth = 45;
+ const rightLabelX = pageWidth - rightMargin - labelWidth - valueWidth;
+ const rightValueX = pageWidth - rightMargin - valueWidth;
+
+ doc.setFontSize(9);
+ doc.setFont('helvetica', 'bold');
+ doc.text('Network:', rightLabelX, 16);
+ doc.setFont('helvetica', 'normal');
+ const networkText = network === 'mainnet' ? 'Mainnet' : 'Testnet';
+ doc.text(networkText, rightValueX, 16);
+
+ const exportDate = new Date().toLocaleString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+ doc.setFont('helvetica', 'bold');
+ doc.text('Exported:', rightLabelX, 24);
+ doc.setFont('helvetica', 'normal');
+ const dateLines = doc.splitTextToSize(exportDate, valueWidth);
+ let dateY = 24;
+ dateLines.forEach((line: string) => {
+ doc.text(line, rightValueX, dateY);
+ dateY += 4;
+ });
+
+ yPosition = 50;
+
+ addSectionHeader('Escrow Summary');
+
+ addKeyValue('Escrow ID', contractId);
+ addKeyValue('Title', organized.title || 'N/A');
+ addKeyValue('Description', organized.description || 'N/A');
+ addKeyValue('Escrow Type', organized.escrowType === 'multi-release' ? 'Multi-Release' : 'Single-Release');
+
+ if (organized.properties.trustline) {
+ addKeyValue('Asset / Trustline', organized.properties.trustline);
+ }
+
+ if (organized.properties.amount) {
+ addKeyValue('Total Amount', `${organized.properties.amount} ${organized.properties.trustline ? organized.properties.trustline.split(':')[0] : ''}`);
+ }
+
+ if (organized.properties.balance) {
+ addKeyValue('Current Balance', `${organized.properties.balance} ${organized.properties.trustline ? organized.properties.trustline.split(':')[0] : ''}`);
+ }
+
+ if (organized.properties.platform_fee) {
+ addKeyValue('Platform Fee', organized.properties.platform_fee);
+ }
+
+ if (organized.properties.amount) {
+ const amount = parseFloat(organized.properties.amount);
+ if (!isNaN(amount)) {
+ const trustlessWorkFee = (amount * 0.003).toFixed(2);
+ addKeyValue('Trustless Work Fee (0.3%)', trustlessWorkFee);
+ }
+ }
+
+ if (organized.properties.engagement_id) {
+ addKeyValue('Engagement ID', organized.properties.engagement_id);
+ }
+
+ addSectionHeader('Escrow Status');
+
+ const getStatusText = () => {
+ if (organized.flags.resolved_flag === 'true') return 'Resolved';
+ if (organized.flags.dispute_flag === 'true') return 'Disputed';
+ if (organized.flags.release_flag === 'true') return 'Released';
+ return 'Active';
+ };
+
+ addKeyValue('Current Status', getStatusText());
+ addKeyValue('Dispute Flag', organized.flags.dispute_flag === 'true' ? 'Yes' : 'No');
+ addKeyValue('Release Flag', organized.flags.release_flag === 'true' ? 'Yes' : 'No');
+ addKeyValue('Resolved Flag', organized.flags.resolved_flag === 'true' ? 'Yes' : 'No');
+ addKeyValue('Progress', `${organized.progress.toFixed(1)}%`);
+
+ addSectionHeader('Assigned Roles');
+
+ if (Object.keys(organized.roles).length === 0) {
+ addText('No roles assigned', 10, false, [100, 100, 100]);
+ } else {
+ Object.entries(organized.roles).forEach(([roleKey, address]) => {
+ const roleName = ROLE_MAPPING[roleKey] || roleKey;
+ addKeyValue(roleName, address, 5);
+ });
+ }
+
+ addSectionHeader('Milestones');
+
+ if (organized.milestones.length === 0) {
+ addText('No milestones defined', 10, false, [100, 100, 100]);
+ } else {
+ organized.milestones.forEach((milestone, index) => {
+ checkPageBreak(30);
+ yPosition += 3;
+
+ doc.setFontSize(11);
+ doc.setFont('helvetica', 'bold');
+ doc.setTextColor(0, 51, 102);
+ doc.text(`Milestone ${milestone.id + 1}: ${milestone.title}`, margin + 5, yPosition);
+ yPosition += 6;
+
+ addKeyValue('Description', milestone.description || 'N/A', 10);
+
+ if (milestone.amount) {
+ addKeyValue('Amount', milestone.amount, 10);
+ }
+
+ const statusText = milestone.approved
+ ? 'Approved'
+ : milestone.release_flag
+ ? 'Released'
+ : milestone.dispute_flag
+ ? 'Disputed'
+ : milestone.resolved_flag
+ ? 'Resolved'
+ : 'Pending';
+ addKeyValue('Status', statusText, 10);
+
+ if (milestone.signer) {
+ addKeyValue('Signer', milestone.signer, 10);
+ }
+
+ if (milestone.approver) {
+ addKeyValue('Approver', milestone.approver, 10);
+ }
+
+ yPosition += 3;
+
+ if (index < organized.milestones.length - 1) {
+ doc.setDrawColor(200, 200, 200);
+ doc.setLineWidth(0.2);
+ doc.line(margin + 5, yPosition, pageWidth - margin - 5, yPosition);
+ yPosition += 3;
+ }
+ });
+ }
+
+ const totalPages = doc.getNumberOfPages();
+ for (let i = 1; i <= totalPages; i++) {
+ doc.setPage(i);
+
+ doc.setDrawColor(200, 200, 200);
+ doc.setLineWidth(0.2);
+ doc.line(margin, pageHeight - 15, pageWidth - margin, pageHeight - 15);
+
+ doc.setFontSize(8);
+ doc.setFont('helvetica', 'normal');
+ doc.setTextColor(100, 100, 100);
+ doc.text('Generated by Trustless Work Escrow Viewer', margin, pageHeight - 10);
+ doc.text('https://trustless.work', pageWidth - margin - 40, pageHeight - 10);
+ doc.text(`Page ${i} of ${totalPages}`, pageWidth - margin - 20, pageHeight - 5, { align: 'right' });
+ }
+
+ const dateStr = new Date().toISOString().split('T')[0];
+ const filename = `escrow-report-${contractId.substring(0, 8)}-${dateStr}.pdf`;
+ doc.save(filename);
+}