Skip to content

Commit 2d4bfee

Browse files
authored
Merge pull request #10 from H1D/t3code/qr-payment-note
Add payment URL QR codes to invoice PDFs
2 parents 1f3dea0 + e580d22 commit 2d4bfee

11 files changed

Lines changed: 202 additions & 24 deletions

File tree

bun.lock

Lines changed: 63 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

e2e/form-defaults.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ test.describe('Form loads with correct defaults', () => {
109109
await expect(paymentMethod).toHaveValue('Bank Transfer');
110110
});
111111

112+
test('payment URL defaults to empty', async ({ page }) => {
113+
const paymentUrl = page.locator('[data-testid="payment-url-input"]');
114+
await expect(paymentUrl).toBeVisible();
115+
await expect(paymentUrl).toHaveValue('');
116+
});
117+
112118
test('one default invoice item exists', async ({ page }) => {
113119
const items = page.locator('[data-testid="invoice-item"]');
114120
await expect(items).toHaveCount(1);

e2e/share-download.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,34 @@ test.describe('Share link and download', () => {
8080
await expect(page.locator('[data-testid="item-net-price-input"]').first()).toHaveValue('150');
8181
});
8282

83+
test('payment URL persists in shared URL', async ({ page, context }) => {
84+
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
85+
86+
await page.locator('[data-testid="payment-url-input"]').fill('https://example.com/pay/invoice-123');
87+
await page.locator('[data-testid="share-invoice-link-button"]').click();
88+
89+
const toast = page.locator('[data-testid="toast"]');
90+
await expect(toast).toContainText('Invoice link copied to clipboard!');
91+
await expect(toast).toHaveClass(/success/);
92+
await expect(toast).not.toContainText('QR code not included');
93+
94+
const sharedUrl = page.url();
95+
await page.evaluate(() => localStorage.clear());
96+
await page.goto(sharedUrl);
97+
await page.waitForSelector('[data-testid="items-container"]');
98+
99+
await expect(page.locator('[data-testid="payment-url-input"]')).toHaveValue('https://example.com/pay/invoice-123');
100+
});
101+
102+
test('payment URL persists after reload', async ({ page }) => {
103+
await page.locator('[data-testid="payment-url-input"]').fill('https://example.com/pay/reload-test');
104+
await page.waitForTimeout(700);
105+
await page.reload();
106+
await page.waitForSelector('[data-testid="items-container"]');
107+
108+
await expect(page.locator('[data-testid="payment-url-input"]')).toHaveValue('https://example.com/pay/reload-test');
109+
});
110+
83111
test('share shows success toast', async ({ page, context }) => {
84112
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
85113

@@ -112,6 +140,22 @@ test.describe('Share link and download', () => {
112140
expect(filename).toMatch(/\.pdf$/);
113141
});
114142

143+
test('download with payment URL triggers PDF generation', async ({ page }) => {
144+
await page.locator('[data-testid="seller-name-input"]').fill('Test Seller');
145+
await page.locator('[data-testid="seller-email-input"]').fill('seller@test.com');
146+
await page.locator('[data-testid="seller-address-input"]').fill('123 Test St');
147+
await page.locator('[data-testid="buyer-name-input"]').fill('Test Buyer');
148+
await page.locator('[data-testid="buyer-email-input"]').fill('buyer@test.com');
149+
await page.locator('[data-testid="buyer-address-input"]').fill('456 Test Ave');
150+
await page.locator('[data-testid="payment-url-input"]').fill('https://example.com/pay/invoice-123');
151+
152+
const downloadPromise = page.waitForEvent('download');
153+
await page.locator('[data-testid="download-invoice-button"]').click();
154+
155+
const download = await downloadPromise;
156+
expect(download.suggestedFilename()).toMatch(/\.pdf$/);
157+
});
158+
115159
test('share with logo shows error toast', async ({ page, context }) => {
116160
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
117161

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
"dependencies": {
1515
"jspdf": "^2.5.2",
1616
"jspdf-autotable": "^3.8.4",
17-
"lz-string": "^1.5.0"
17+
"lz-string": "^1.5.0",
18+
"qrcode": "1.5.4"
1819
},
1920
"devDependencies": {
2021
"@playwright/test": "^1.49.0",
2122
"@types/lz-string": "^1.5.0",
23+
"@types/qrcode": "1.5.6",
2224
"bun-types": "^1.3.9",
2325
"typescript": "^5.9.3"
2426
}

public/index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,13 @@ <h1 class="logo">EasyPDF <span class="logo-lite">Lite</span></h1>
368368
</div>
369369
</div>
370370

371+
<div class="form-row">
372+
<div class="form-group full-width">
373+
<label for="paymentUrl">Payment URL</label>
374+
<input type="url" id="paymentUrl" name="paymentUrl" maxlength="2000" placeholder="https://..." data-testid="payment-url-input">
375+
</div>
376+
</div>
377+
371378
<div class="form-row">
372379
<div class="form-group full-width">
373380
<div class="field-toggle">

public/js/app.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ function showToast(message: string, type: "success" | "error" = "success") {
3232
function refreshPreview() {
3333
try {
3434
const data = extractFormData();
35-
updatePdfPreview(data);
35+
void updatePdfPreview(data).catch((err) => {
36+
console.error("PDF generation error:", err);
37+
});
3638
} catch (err) {
3739
console.error("PDF generation error:", err);
3840
}
@@ -233,10 +235,10 @@ function setupShareAndDownload() {
233235
}
234236

235237
if (downloadBtn) {
236-
downloadBtn.addEventListener("click", () => {
238+
downloadBtn.addEventListener("click", async () => {
237239
try {
238240
const data = extractFormData();
239-
downloadPdf(data);
241+
await downloadPdf(data);
240242
showToast("PDF downloaded!", "success");
241243
} catch (err) {
242244
console.error("Download error:", err);
@@ -407,6 +409,7 @@ function getDefaultData(): InvoiceData {
407409
vatTableSummaryIsVisible: true,
408410
paymentMethod: "Bank Transfer",
409411
paymentMethodFieldIsVisible: true,
412+
paymentUrl: "",
410413
notes: "",
411414
notesFieldIsVisible: true,
412415
personAuthorizedToReceiveFieldIsVisible: true,

public/js/form.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export function populateForm(data: InvoiceData): void {
104104

105105
// Payment & notes
106106
setInputValue("paymentMethod", data.paymentMethod ?? "");
107+
setInputValue("paymentUrl", data.paymentUrl ?? "");
107108
setTextareaValue("invoiceNotes", data.notes ?? "");
108109

109110
// Visibility checkboxes
@@ -226,6 +227,7 @@ export function extractFormData(): InvoiceData {
226227
vatTableSummaryIsVisible: getCheckbox("vatTableSummaryIsVisible"),
227228
paymentMethod: getInputValue("paymentMethod"),
228229
paymentMethodFieldIsVisible: getCheckbox("paymentMethodFieldIsVisible"),
230+
paymentUrl: getInputValue("paymentUrl") || undefined,
229231
notes: getTextareaValue("invoiceNotes"),
230232
notesFieldIsVisible: getCheckbox("notesFieldIsVisible"),
231233
personAuthorizedToReceiveFieldIsVisible: getCheckbox("personAuthorizedToReceiveFieldIsVisible"),

0 commit comments

Comments
 (0)