Skip to content

Feature – Escrow PDF Export#62

Open
Benjtalkshow wants to merge 3 commits intoTrustless-Work:mainfrom
Benjtalkshow:feature/escrow-pdf-export
Open

Feature – Escrow PDF Export#62
Benjtalkshow wants to merge 3 commits intoTrustless-Work:mainfrom
Benjtalkshow:feature/escrow-pdf-export

Conversation

@Benjtalkshow
Copy link

@Benjtalkshow Benjtalkshow commented Feb 27, 2026

Closes #61

Objective

Implement a client-side PDF export feature that generates structured, audit-ready reports of the current escrow state without requiring backend changes or wallet interactions.

Technical Changes

Dependencies

  • Integrated jspdf and jspdf-autotable for PDF document generation.

Utility Layer

Created src/utils/escrowExport.ts to handle:

  • Data transformation from OrganizedEscrowData into tabular formats suitable for PDF rendering.
  • Dynamic layout handling for both Single-Release and Multi-Release escrow types.
  • Print-friendly styling (forced light mode and professional typography) to meet compliance and reporting standards.
  • Multi-page overflow management with automatic pagination for long milestone lists.

UI Integration

  • Enhanced TitleCard with an "Export to PDF" action button.
  • Refactored EscrowContent to pass organized data context to child components required for the export functionality.

Enhancement

  • Updated the DEBUG flag in EscrowDetails.tsx to automatically disable logging in production environments while keeping it active for development.
  • Fixed all remaining TypeScript type errors in the mapper and export utility.

Result

Users can now export the current escrow state as a clean, structured PDF report directly from the interface, improving transparency and enabling easy sharing for audits or record keeping.

Proof

image

Summary by CodeRabbit

  • New Features

    • Added an "Export to PDF" option for escrow records, producing a multi-section report (summary, status, roles, milestones) for download.
    • Export button is now available alongside progress indicators where applicable.
  • Chores

    • Added PDF generation support to enable the new export capability.

@vercel
Copy link
Contributor

vercel bot commented Feb 27, 2026

@Benjtalkshow is attempting to deploy a commit to the Trustless Work Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Feb 27, 2026

📝 Walkthrough

Walkthrough

Adds client-side PDF export: new jspdf dependencies, a PDF-generation utility, and an "Export to PDF" button in TitleCard that triggers export using the current network and organized escrow data.

Changes

Cohort / File(s) Summary
Dependencies
package.json
Adds jspdf and jspdf-autotable for client-side PDF generation.
TitleCard UI Integration
src/components/escrow/title-card.tsx, src/components/escrow/escrow-content.tsx
TitleCard gains an optional `organized?: OrganizedEscrowData
PDF Export Utility
src/utils/escrowExport.ts
New exportEscrowToPDF(organized: OrganizedEscrowData, network: NetworkType) implementation that builds a multi-section PDF (header, summary, status, roles, milestones, footer), handles pagination, and triggers download with an escrow-ID/date filename.
Minor module change
src/components/escrow/EscrowDetails.tsx
Moves DEBUG constant to module scope (no behavioral change).

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant TitleCard
  participant NetworkCtx as NetworkContext
  participant ExportUtil as exportEscrowToPDF
  participant jsPDF

  User->>TitleCard: Click "Export to PDF"
  TitleCard->>NetworkCtx: read current network
  TitleCard->>ExportUtil: call exportEscrowToPDF(organized, network)
  ExportUtil->>jsPDF: assemble document (sections, tables, pagination)
  jsPDF-->>ExportUtil: PDF blob
  ExportUtil->>User: trigger download (filename with escrow ID + date)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related issues

  • Trustless-Work/Product#157: Implements a client-side "Export to PDF" feature matching the added Export button and PDF-generation utility.
  • Trustless-Work/escrow-viewer#60: Directly aligns with the "Export Escrow Report" feature (structured PDF export of current escrow view).

Possibly related PRs

  • PR #19: Changes to TitleCard props/milestone handling overlap with this PR's addition of organized and export logic.
  • PR #33: Styling/refactor of title-card.tsx may conflict with or require coordination with the Export button UI changes.
  • PR #18: Introduced or modified useNetwork/NetworkType usage that this export code depends on.

Poem

🐰
A button beside the badge, a paper trail to see,
Click "Export to PDF" — the escrow hops out merrily.
Tables, milestones, roles aligned with care,
I stamp it with a carrot seal and send it in the air!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Feature – Escrow PDF Export' clearly and concisely summarizes the main change: adding PDF export functionality to the escrow viewer.
Linked Issues check ✅ Passed The pull request implements all core requirements from issue #61: PDF export functionality with proper header, escrow summary, status, roles, milestones for multi-release types, footer, professional layout, and filename including escrow ID and date.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the PDF export feature. The DEBUG constant refactor in EscrowDetails.tsx is a minor scope-preserving cleanup that affects only debug logging scope without altering behavior.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
src/components/escrow/title-card.tsx (1)

28-32: Handle export failures explicitly in the click handler.

Line 28-Line 32 calls export directly; if PDF generation/save throws, the user gets no feedback. A small try/catch path would improve reliability.

✅ Suggested patch
   const handleExportPDF = () => {
-    if (organized) {
-      exportEscrowToPDF(organized, currentNetwork);
-    }
+    if (!organized) return;
+    try {
+      exportEscrowToPDF(organized, currentNetwork);
+    } catch (error) {
+      console.error("Failed to export escrow PDF", error);
+    }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/escrow/title-card.tsx` around lines 28 - 32, handleExportPDF
currently calls exportEscrowToPDF(organized, currentNetwork) directly and can
throw without user feedback; wrap the call in a try/catch inside
handleExportPDF, call exportEscrowToPDF only if organized is present, and on
error log the exception and surface a user-facing message (e.g., via existing
toast/notification/error state) so the user knows the export failed; reference
the handleExportPDF function and exportEscrowToPDF call and ensure organized and
currentNetwork are still passed through unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/escrow/title-card.tsx`:
- Around line 71-78: Add an explicit type attribute to the Export button to
prevent implicit form submission: in the JSX for the button that calls
handleExportPDF (the button containing <FileDown /> and "Export to PDF" text in
title-card.tsx), set type="button" on that <button> element so clicks only
trigger the export handler and not a form submit.

In `@src/utils/escrowExport.ts`:
- Line 14: The code creates multiple Date instances (timestamp = new
Date().toLocaleString()) and later calls new Date().toISOString() for the
filename, causing mismatched header vs filename times; replace both with a
single Date instance (e.g., create const now = new Date() once) and derive the
displayed timestamp (now.toLocaleString()) and the filename timestamp
(now.toISOString() or sanitized variant) from that same now object so header
metadata and filename always match; update references to timestamp and any
direct new Date().toISOString() calls in escrowExport.ts to use the shared
now-derived values.
- Around line 111-126: The milestones export omits each milestone's description;
update milestoneHead to include "Description" (for single-release:
["ID","Title","Description","Status","Approved"], for multi-release:
["ID","Title","Description","Amount","Status","Approved"]) and update the
milestoneBody build in the organized.milestones.map callback to include
m.description (use empty string if missing) right after m.title; when
organized.escrowType === "multi-release" insert m.amount (or "0.00") at the
position before status (i.e. adjust the splice index to account for the new
description field). Ensure you modify the milestoneHead and the base array
construction in the same function so headers and rows align.
- Around line 61-66: The statusData array in escrowExport (const statusData)
omits the escrow lifecycle state; update statusData to include a lifecycle entry
(e.g., add ["Lifecycle State", organized.lifecycle] or ["Lifecycle State",
organized.flags.lifecycle_state] depending on the existing property) alongside
the existing ["Progress", ...], ["Dispute State", ...], ["Release State", ...],
["Resolution State", ...] entries so the exported status block contains
lifecycle information and matches the other status labels.

---

Nitpick comments:
In `@src/components/escrow/title-card.tsx`:
- Around line 28-32: handleExportPDF currently calls
exportEscrowToPDF(organized, currentNetwork) directly and can throw without user
feedback; wrap the call in a try/catch inside handleExportPDF, call
exportEscrowToPDF only if organized is present, and on error log the exception
and surface a user-facing message (e.g., via existing toast/notification/error
state) so the user knows the export failed; reference the handleExportPDF
function and exportEscrowToPDF call and ensure organized and currentNetwork are
still passed through unchanged.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3240975 and ba667fa.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • package.json
  • src/components/escrow/escrow-content.tsx
  • src/components/escrow/title-card.tsx
  • src/utils/escrowExport.ts

Comment on lines +71 to +78
<button
onClick={handleExportPDF}
className="flex items-center gap-2 px-3 py-1.5 text-sm font-medium bg-primary cursor-pointer hover:bg-primary/10 border border-primary/20 rounded-lg text-primary-foreground transition-all duration-200 transform hover:scale-105 active:scale-95"
title="Export to PDF"
>
<FileDown className="h-4 w-4" />
<span>Export to PDF</span>
</button>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Set explicit type="button" on the export action.

At Line 71, the button implicitly defaults to submit. If this component is ever rendered inside a form, clicking export can trigger unintended form submission.

✅ Suggested patch
               {organized && (
                 <button
+                  type="button"
                   onClick={handleExportPDF}
                   className="flex items-center gap-2 px-3 py-1.5 text-sm font-medium  bg-primary cursor-pointer hover:bg-primary/10 border border-primary/20 rounded-lg  text-primary-foreground transition-all duration-200 transform hover:scale-105 active:scale-95"
                   title="Export to PDF"
                 >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
onClick={handleExportPDF}
className="flex items-center gap-2 px-3 py-1.5 text-sm font-medium bg-primary cursor-pointer hover:bg-primary/10 border border-primary/20 rounded-lg text-primary-foreground transition-all duration-200 transform hover:scale-105 active:scale-95"
title="Export to PDF"
>
<FileDown className="h-4 w-4" />
<span>Export to PDF</span>
</button>
<button
type="button"
onClick={handleExportPDF}
className="flex items-center gap-2 px-3 py-1.5 text-sm font-medium bg-primary cursor-pointer hover:bg-primary/10 border border-primary/20 rounded-lg text-primary-foreground transition-all duration-200 transform hover:scale-105 active:scale-95"
title="Export to PDF"
>
<FileDown className="h-4 w-4" />
<span>Export to PDF</span>
</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/escrow/title-card.tsx` around lines 71 - 78, Add an explicit
type attribute to the Export button to prevent implicit form submission: in the
JSX for the button that calls handleExportPDF (the button containing <FileDown
/> and "Export to PDF" text in title-card.tsx), set type="button" on that
<button> element so clicks only trigger the export handler and not a form
submit.

network: NetworkType
) => {
const doc = new jsPDF();
const timestamp = new Date().toLocaleString();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use one timestamp source for both header metadata and filename.

Line 14/Line 26 uses locale-local time, while Line 160 uses UTC date from toISOString(). Around midnight this can produce mismatched dates between the visible report timestamp and the downloaded filename.

✅ Suggested patch
-    const timestamp = new Date().toLocaleString();
+    const exportDate = new Date();
+    const timestamp = exportDate.toISOString();
@@
-    doc.text(`Export Timestamp: ${timestamp}`, 14, 40);
+    doc.text(`Export Timestamp (UTC): ${timestamp}`, 14, 40);
@@
-    const filename = `EscrowReport_${organized.properties.escrow_id.substring(0, 8)}_${new Date().toISOString().split('T')[0]}.pdf`;
+    const filename = `EscrowReport_${organized.properties.escrow_id.substring(0, 8)}_${timestamp.slice(0, 10)}.pdf`;

Also applies to: 26-26, 160-160

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/escrowExport.ts` at line 14, The code creates multiple Date
instances (timestamp = new Date().toLocaleString()) and later calls new
Date().toISOString() for the filename, causing mismatched header vs filename
times; replace both with a single Date instance (e.g., create const now = new
Date() once) and derive the displayed timestamp (now.toLocaleString()) and the
filename timestamp (now.toISOString() or sanitized variant) from that same now
object so header metadata and filename always match; update references to
timestamp and any direct new Date().toISOString() calls in escrowExport.ts to
use the shared now-derived values.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
src/utils/escrowExport.ts (3)

67-72: ⚠️ Potential issue | 🟠 Major

Add lifecycle state to the Escrow Status block.

Line 67-Line 72 omits lifecycle state, which is part of the required status content.

🔧 Proposed fix
     const statusData = [
+        ["Lifecycle State", organized.properties.lifecycle_state || "N/A"],
         ["Progress", `${organized.progress}%`],
         ["Dispute State", organized.flags.dispute_flag],
         ["Release State", organized.flags.release_flag],
         ["Resolution State", organized.flags.resolved_flag],
     ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/escrowExport.ts` around lines 67 - 72, The Escrow Status block
(statusData) is missing the lifecycle state entry; update the statusData array
in escrowExport.ts to include a ["Lifecycle State", <value>] element using the
appropriate source (prefer organized.flags.lifecycle_state or fallback to
organized.lifecycle) so the lifecycle is shown alongside Progress, Dispute
State, Release State, and Resolution State.

117-132: ⚠️ Potential issue | 🟠 Major

Include milestone description in exported milestones table.

Line 117-Line 132 omits description, so milestones lose key context in the report.

🔧 Proposed fix
         const milestoneHead = organized.escrowType === "multi-release"
-            ? [["ID", "Title", "Amount", "Status", "Approved"]]
-            : [["ID", "Title", "Status", "Approved"]];
+            ? [["ID", "Title", "Description", "Amount", "Status", "Approved"]]
+            : [["ID", "Title", "Description", "Status", "Approved"]];

         const milestoneBody = organized.milestones.map((m: ParsedMilestone) => {
             const base = [
                 String(m.id + 1),
                 m.title,
+                m.description || "",
                 m.status,
                 m.approved ? "Yes" : "No",
             ];
             if (organized.escrowType === "multi-release") {
-                base.splice(2, 0, m.amount || "0.00");
+                base.splice(3, 0, m.amount || "0.00");
             }
             return base;
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/escrowExport.ts` around lines 117 - 132, The exported milestones
table omits the milestone description; update the milestoneHead and
milestoneBody to include a "Description" column and the
ParsedMilestone.description for each row. Modify milestoneHead so multi-release
becomes ["ID","Title","Description","Amount","Status","Approved"] and
single-release becomes ["ID","Title","Description","Status","Approved"]. In
milestoneBody, insert m.description (or "" if missing) after the title (e.g.,
base.splice(2,0,m.description || "")), and adjust the multi-release amount
insertion to splice at index 3 instead of 2 so the amount lands after the
description (keep using organized.escrowType and ParsedMilestone/m identifiers
to locate the code).

20-20: ⚠️ Potential issue | 🟡 Minor

Use one Date instance for header and filename metadata.

Line 20 and Line 166 derive time from different Date objects, which can produce mismatched report timestamp vs filename date.

🔧 Proposed fix
-    const timestamp = new Date().toLocaleString();
+    const exportDate = new Date();
+    const timestamp = exportDate.toLocaleString();
+    const fileDate = exportDate.toISOString().split("T")[0];
@@
-    const filename = `EscrowReport_${organized.properties.escrow_id.substring(0, 8)}_${new Date().toISOString().split('T')[0]}.pdf`;
+    const filename = `EscrowReport_${organized.properties.escrow_id.substring(0, 8)}_${fileDate}.pdf`;

Also applies to: 166-166

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/escrowExport.ts` at line 20, The code creates two separate Date
objects for the report header and the export filename (the existing const
timestamp and the later new Date() at the filename generation site), which can
produce mismatched times; fix it by creating and using a single Date instance
(e.g., const now = new Date()) at the top of the export flow and derive both the
header timestamp (timestamp = now.toLocaleString()) and the filename date string
(from the same now) so both header and filename use the identical moment; update
usages of timestamp and the filename-building expression to read from that
single now value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/escrow/EscrowDetails.tsx`:
- Around line 39-41: The file sets DEBUG to true unconditionally in
EscrowDetails (const DEBUG = true); change this to honor environment or build
flags (e.g., process.env.NODE_ENV !== 'production' or a specific
REACT_APP_DEBUG_ESCROW) so debug logging is enabled only in non-production
builds; update the DEBUG constant in EscrowDetails.tsx to read from that
env/config flag and ensure any debug logs gated by DEBUG remain unchanged.

In `@src/utils/escrowExport.ts`:
- Around line 107-115: The milestone table startY is computed from finalY
(finalY > 240) instead of whether a new page was actually added, which can place
the table too low; update the logic around the doc.addPage() call (where
milestonesY is checked) to set a boolean (e.g., pageAdded) or immediately
compute startY after adding the page, and then use that page-aware startY when
calling the table renderer (ensure the same fix is applied to the other
occurrence around the second milestones block), referencing the
milestonesY/finalY checks, doc.addPage(), and the startY/ table rendering calls
so startY reflects the real current page position.

---

Duplicate comments:
In `@src/utils/escrowExport.ts`:
- Around line 67-72: The Escrow Status block (statusData) is missing the
lifecycle state entry; update the statusData array in escrowExport.ts to include
a ["Lifecycle State", <value>] element using the appropriate source (prefer
organized.flags.lifecycle_state or fallback to organized.lifecycle) so the
lifecycle is shown alongside Progress, Dispute State, Release State, and
Resolution State.
- Around line 117-132: The exported milestones table omits the milestone
description; update the milestoneHead and milestoneBody to include a
"Description" column and the ParsedMilestone.description for each row. Modify
milestoneHead so multi-release becomes
["ID","Title","Description","Amount","Status","Approved"] and single-release
becomes ["ID","Title","Description","Status","Approved"]. In milestoneBody,
insert m.description (or "" if missing) after the title (e.g.,
base.splice(2,0,m.description || "")), and adjust the multi-release amount
insertion to splice at index 3 instead of 2 so the amount lands after the
description (keep using organized.escrowType and ParsedMilestone/m identifiers
to locate the code).
- Line 20: The code creates two separate Date objects for the report header and
the export filename (the existing const timestamp and the later new Date() at
the filename generation site), which can produce mismatched times; fix it by
creating and using a single Date instance (e.g., const now = new Date()) at the
top of the export flow and derive both the header timestamp (timestamp =
now.toLocaleString()) and the filename date string (from the same now) so both
header and filename use the identical moment; update usages of timestamp and the
filename-building expression to read from that single now value.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba667fa and 5dcd9ab.

📒 Files selected for processing (2)
  • src/components/escrow/EscrowDetails.tsx
  • src/utils/escrowExport.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Print Escrow Viewer to PDF – “Export Escrow Report”

1 participant