chore: improved upload logo e2e test, minor label fix, bump version to 1.0.3, added changelog.md and blog post with 1.0.3 announcement (#202)

This commit is contained in:
Vlad Sazonau 2026-03-30 00:11:50 +02:00 committed by GitHub
parent 2140905142
commit 89df4480b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 249 additions and 259 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 174 KiB

106
CHANGELOG.md Normal file
View file

@ -0,0 +1,106 @@
# Changelog
## [1.0.3] - 2026-03-29
### Added
- Email visibility toggle for seller and buyer sections — control whether the email address appears in the generated PDF
- `ConfirmDiscardDialog` component to warn users about unsaved changes when closing the buyer/seller dialogs (replaces native `window.confirm`)
- `useConfirmDiscard` reusable hook for managing discard confirmation state across buyer and seller dialogs
- Knip CI workflow for automated dead-code and unused-dependency detection
- `update-github-actions` script in `package.json` to streamline GitHub Actions version updates
- Unit tests for the `generate-invoice` API route and core logic (`generate-invoice.ts`, `route.tsx`)
### Changed
- Refactored `generate-invoice` API route: extracted core business logic into a standalone `generate-invoice.ts` module using a dependency-injection pattern for improved testability
- Reworked seller and buyer information form sections with improved layout, locked-state banners, and cleaner field grouping
- Buyer and seller dialogs now reset form values and pre-fill switch to their defaults when closed
- Buyer and seller names are trimmed of whitespace before saving; whitespace-padded duplicates are rejected
- Invalid localStorage entries for buyers and sellers are now validated and silently dropped instead of causing errors
- Out-of-Date dates helper improved with more accurate state detection
- Error message component layout and copy updated for better readability
- Vitest config updated with JSX support (`esbuild.jsx: "automatic"`) to enable unit-testing JSX components
- Restructured buyer and seller dialog components into dedicated feature directories under `sections/components/buyer` and `sections/components/seller`
- GitHub Actions workflows updated to latest action versions; failure handling added to all CI jobs
- Auto-scroll the invoice form on mobile when switching between tabs
### Fixed
- Pre-fill switch in buyer/seller dialogs no longer retains its state after the dialog is closed and reopened
- Rate limit exceeded log upgraded from `console.log` to `console.error` for correct severity
- Loading placeholder display fixed when switching invoice tabs on mobile
## [1.0.2] - 2026-03-10
### Added
- QR code generation for invoices with customizable descriptions and visibility toggles, supported in both default and Stripe templates
- Logo upload for the default invoice template (previously available only in the Stripe template)
- Searchable currency combobox with grouped categories, replacing the native dropdown for faster selection
- Improved multi-page PDF support with automatic pagination and page breaks
### Changed
- Increased QR code size and improved rendering quality for better scannability
- Enhanced invoice template text color and visuals for improved readability
- Reorganized Stripe payment link input position in the form for better flow
- Improved user feedback during invoice item deletion with better toast notification handling
- Enhanced error handling to reset invoice metadata to defaults on errors
- Clearer error messages when invoice sharing fails
- Tooltip on the "Add invoice item" button for contextual guidance
- Sentry error tracking integration for invoice sharing and GitHub stars features
### Fixed
- i18n issue when generating PDF via the API route
- Delete invoice item flow not working correctly
- Item name field validation too strict (now optional for flexibility)
## [1.0.1] - 2026-01-12
### Added
- Stripe-inspired invoice template with professional styling and layout optimizations
- Dynamic template selection in the invoice form
- Logo upload capability for the Stripe template with validation
- Stripe payment URL field for enhanced invoice functionality
- Customizable Tax/VAT label text (e.g., "VAT", "GST", "Sales Tax")
- Customizable Tax Number label in buyer and seller information sections
- Dynamic tax label updates based on selected invoice language
### Changed
- Landing page cleanup: refined About section and footer for better layout and accessibility
- Call-to-action toasts: added custom, randomized CTA toasts encouraging user support
- Added support for more currencies with improved date handling
- Enhanced tooltips with detailed explanations and improved styling
- Enhanced validation for VAT input to accept both numeric values and specific strings
- Improved user interface messages for clarity regarding VAT input requirements
### Fixed
- Bug with accordion component
- Error message for invoice link generation now includes a refresh suggestion
## [1.0.0] - 2025-11-19
### Added
- Initial release of EasyInvoicePDF — a free, open-source invoice generator
- Live preview: invoice updates in real-time as you make changes
- Shareable links: generate secure links to share invoices directly with clients
- Instant PDF download with one click
- Multi-language support (English, Polish, German, Spanish, Portuguese, Russian, Ukrainian, French, Italian, Dutch)
- Support for all major currencies with automatic locale-based formatting
- European VAT calculation and formatting compliant with EU tax requirements
- Complete seller and buyer information management with save-for-future-use
- Detailed invoice items with descriptions, quantities, and pricing
- Automatic tax calculations and totals
- Invoice numbering, dating, and payment terms
- No sign-up required — fully browser-based with no server-side data storage
[1.0.3]: https://github.com/VladSez/easy-invoice-pdf/compare/v1.0.2...v1.0.3
[1.0.2]: https://github.com/VladSez/easy-invoice-pdf/compare/EasyInvoicePDF-1.0.1...v1.0.2
[1.0.1]: https://github.com/VladSez/easy-invoice-pdf/compare/EasyInvoicePDF-v1.0.0...EasyInvoicePDF-1.0.1
[1.0.0]: https://github.com/VladSez/easy-invoice-pdf/releases/tag/EasyInvoicePDF-v1.0.0

View file

@ -94,16 +94,52 @@ _**Choose between multiple professional templates** (Default and Stripe) to matc
</div>
## 📢 What's New
**v1.0.3 — Seller & Buyer Improvements (Mar 2026)**
- **Email visibility toggle** — control whether email addresses appear in the generated PDF
- **Confirm discard dialog** — warns about unsaved changes when closing buyer/seller dialogs
- **Improved seller & buyer forms** — reworked layout, locked-state banners, and cleaner field grouping
[Full release notes for v1.0.3](https://github.com/VladSez/easy-invoice-pdf/releases/tag/v1.0.3)
---
**v1.0.2 — QR Codes & Multi-Page PDFs (Mar 2026)**
- **QR code support** — add payment QR codes with custom descriptions to both templates
- **Logo upload for default template** — previously available only in the Stripe template
- **Searchable currency combobox** — grouped categories replace the native dropdown
- **Improved multi-page PDFs** — automatic pagination and page breaks for large invoices
[Full release notes for v1.0.2](https://github.com/VladSez/easy-invoice-pdf/releases/tag/v1.0.2)
---
**v1.0.1 — Stripe Template & Tax Customization (Jan 2026)**
- **Stripe-inspired invoice template** — professional styling with logo upload and payment URL field
- **Customizable tax labels** — set VAT, GST, Sales Tax, or any custom label per invoice language
- **Improved i18n** — dynamic tax label updates and better locale-based currency handling
[Full release notes for v1.0.1](https://github.com/VladSez/easy-invoice-pdf/releases/tag/EasyInvoicePDF-1.0.1)
---
**v1.0.0 — Initial Release (Nov 2025)**
- **Live preview** — invoice updates in real-time as you type
- **Instant PDF download** — one-click, no sign-up required
- **Shareable links** — send invoices directly to clients without attachments
- **10 languages & 120+ currencies** — full multi-language and currency support out of the box
[Full release notes for v1.0.0](https://github.com/VladSez/easy-invoice-pdf/releases/tag/EasyInvoicePDF-v1.0.0)
## 🌟 Star History
[![RepoStars](https://repostars.dev/api/embed?repo=VladSez%2Feasy-invoice-pdf&theme=dark)](https://repostars.dev/?repos=VladSez%2Feasy-invoice-pdf&theme=dark)
## 📢 News & Updates
- **Mar 10, 2026**: Added QR code support, logo upload for the default template, searchable currency combobox, and improved multi-page PDF support. [Release notes for v1.0.2](https://github.com/VladSez/easy-invoice-pdf/releases/tag/v1.0.2)
- **Jan 11, 2026**: Added customizable tax/VAT labels, improved internationalization (i18n) translations, enhanced overall performance, and fixed multiple bugs. [Release notes for v1.0.1](https://github.com/VladSez/easy-invoice-pdf/releases/tag/EasyInvoicePDF-1.0.1)
- **Nov 19, 2025**: EasyInvoicePDF version 1.0.0 released! Create professional invoices in seconds. Welcome to try EasyInvoicePDF. [Release notes for v1.0.0](https://github.com/VladSez/easy-invoice-pdf/releases/tag/EasyInvoicePDF-v1.0.0)
## 🎥 Demo Video
Watch a quick demo of EasyInvoicePDF in action to see how easy it is to create professional invoices in seconds. The video demonstrates key features like **Live Preview**, **Instant PDF Download**, and **Customization Options**.

View file

@ -1099,61 +1099,4 @@ test.describe("Buyer management", () => {
expect(parsedData).toHaveLength(1);
expect(parsedData[0].name).toBe("Globex Corp");
});
test("drops invalid localStorage entries and preserves valid ones on save", async ({
page,
}) => {
// Seed: one valid + one corrupt buyer (component already mounted with empty localStorage,
// so "New Buyer" button is still visible — onSubmit reads localStorage fresh)
await page.addInitScript(() => {
const buyers = [
{
id: "pre-seeded-valid",
name: "Pre-seeded Valid Buyer",
address: "1 Valid Avenue",
email: "valid@pre-seeded.com",
emailFieldIsVisible: true,
vatNo: "VATPRE",
vatNoLabelText: "Tax Number",
vatNoFieldIsVisible: true,
notes: "",
notesFieldIsVisible: true,
},
{ corrupt: true, notABuyer: 42 },
];
localStorage.setItem("EASY_INVOICE_PDF_BUYERS", JSON.stringify(buyers));
});
await page.goto("/?template=default");
await expect(page).toHaveURL("/?template=default");
await page.getByRole("button", { name: "New Buyer" }).click();
const manageBuyerDialog = page.getByTestId("manage-buyer-dialog");
await manageBuyerDialog
.getByRole("textbox", { name: "Name" })
.fill("Brand New Buyer");
await manageBuyerDialog
.getByRole("textbox", { name: "Address" })
.fill("99 Fresh Boulevard");
await manageBuyerDialog.getByRole("button", { name: "Save Buyer" }).click();
// Submit succeeds — no error toast
await expect(
page.getByText("Buyer added and applied to invoice", { exact: true }),
).toBeVisible();
// localStorage: valid entry preserved, corrupt entry dropped, new entry added
const storedData = (await page.evaluate(() =>
localStorage.getItem("EASY_INVOICE_PDF_BUYERS"),
)) as string;
const parsedData = JSON.parse(storedData) as BuyerData[];
expect(parsedData).toHaveLength(2);
expect(parsedData.some((b) => b.name === "Pre-seeded Valid Buyer")).toBe(
true,
);
expect(parsedData.some((b) => b.name === "Brand New Buyer")).toBe(true);
});
});

View file

@ -2,10 +2,7 @@ import { INITIAL_INVOICE_DATA } from "@/app/constants";
import { INVOICE_PDF_TRANSLATIONS } from "@/app/(app)/pdf-i18n-translations/pdf-translations";
import fs from "node:fs";
import path from "node:path";
import {
SMALL_TEST_IMAGE_BASE64,
uploadBase64LogoAsFile,
} from "../stripe-invoice-template/utils";
import { uploadLogoFile } from "../stripe-invoice-template/utils";
// IMPORTANT: we use custom extended test fixture that provides a temporary download directory for each test
import { test, expect } from "../utils/extended-playwright-test";
@ -19,7 +16,7 @@ test.describe("Default Invoice Template", () => {
// we set the system time to a fixed date, so that the invoice number and other dates are consistent across tests
await page.clock.setSystemTime(new Date("2025-12-17T00:00:00Z"));
await page.goto("/");
await page.goto("/?template=default");
await expect(page).toHaveURL("/?template=default");
});
@ -1579,7 +1576,7 @@ test.describe("Default Invoice Template", () => {
const generalInfoSection = page.getByTestId("general-information-section");
// Upload a valid logo
await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64);
await uploadLogoFile(page);
// Verify logo preview is visible
await expect(page.getByText("Logo uploaded successfully!")).toBeVisible();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 146 KiB

BIN
e2e/fixtures/app-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View file

@ -1165,67 +1165,4 @@ test.describe("Seller management", () => {
expect(parsedData).toHaveLength(1);
expect(parsedData[0].name).toBe("Acme Corp");
});
test("drops invalid localStorage entries and preserves valid ones on save", async ({
page,
}) => {
// Seed: one valid + one corrupt seller (component already mounted with empty localStorage,
// so "New Seller" button is still visible — onSubmit reads localStorage fresh)
await page.evaluate(() => {
const sellers = [
{
id: "pre-seeded-valid",
name: "Pre-seeded Valid Seller",
address: "1 Valid Road",
email: "valid@pre-seeded.com",
emailFieldIsVisible: true,
vatNo: "VATPRE",
vatNoLabelText: "Tax Number",
vatNoFieldIsVisible: true,
accountNumber: "ACCTPRE",
accountNumberFieldIsVisible: true,
swiftBic: "SWIFTPRE",
swiftBicFieldIsVisible: true,
notes: "",
notesFieldIsVisible: true,
},
{ corrupt: true, notASeller: 42 },
];
localStorage.setItem("EASY_INVOICE_PDF_SELLERS", JSON.stringify(sellers));
});
await page.goto("/?template=default");
await expect(page).toHaveURL("/?template=default");
await page.getByRole("button", { name: "New Seller" }).click();
const manageSellerDialog = page.getByTestId("manage-seller-dialog");
await manageSellerDialog
.getByRole("textbox", { name: "Name" })
.fill("Brand New Seller");
await manageSellerDialog
.getByRole("textbox", { name: "Address" })
.fill("99 Fresh Lane");
await manageSellerDialog
.getByRole("button", { name: "Save Seller" })
.click();
// Submit succeeds — no error toast
await expect(
page.getByText("Seller added and applied to invoice", { exact: true }),
).toBeVisible();
// localStorage: valid entry preserved, corrupt entry dropped, new entry added
const storedData = (await page.evaluate(() =>
localStorage.getItem("EASY_INVOICE_PDF_SELLERS"),
)) as string;
const parsedData = JSON.parse(storedData) as SellerData[];
expect(parsedData).toHaveLength(2);
expect(parsedData.some((s) => s.name === "Pre-seeded Valid Seller")).toBe(
true,
);
expect(parsedData.some((s) => s.name === "Brand New Seller")).toBe(true);
});
});

View file

@ -1,10 +1,10 @@
import { PDF_DATA_LOCAL_STORAGE_KEY, type InvoiceData } from "@/app/schema";
import { expect, test } from "@playwright/test";
import { SMALL_TEST_IMAGE_BASE64, uploadBase64LogoAsFile } from "./utils";
import { uploadLogoFile } from "./utils";
test.describe("Stripe Invoice Sharing Logic", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
await page.goto("/?template=default");
});
test("can share invoice with Stripe template and *WITHOUT* logo", async ({
@ -67,13 +67,13 @@ test.describe("Stripe Invoice Sharing Logic", () => {
// Verify logo upload section is visible (but empty since no logo was shared)
await expect(
newPageGeneralInfoSection.getByText("Company Logo (Optional)"),
newPageGeneralInfoSection.getByText("Company Logo", { exact: true }),
).toBeVisible();
// Verify payment URL section is visible
await expect(
newPageGeneralInfoSection.getByRole("textbox", {
name: "Payment Link URL (Optional)",
name: "Payment Link URL",
}),
).toBeVisible();
@ -109,7 +109,7 @@ test.describe("Stripe Invoice Sharing Logic", () => {
.selectOption("stripe");
// Upload logo
await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64);
await uploadLogoFile(page);
// Wait for logo to be uploaded
const generalInfoSection = page.getByTestId("general-information-section");
@ -148,7 +148,7 @@ test.describe("Stripe Invoice Sharing Logic", () => {
.selectOption("stripe");
// Upload logo
await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64);
await uploadLogoFile(page);
// Wait debounce timeout
// eslint-disable-next-line playwright/no-wait-for-timeout
@ -223,7 +223,7 @@ test.describe("Stripe Invoice Sharing Logic", () => {
.selectOption("stripe");
// Upload logo
await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64);
await uploadLogoFile(page);
// Verify share button becomes disabled
await expect(shareButton).toHaveAttribute("data-disabled", "true");
@ -259,7 +259,7 @@ test.describe("Stripe Invoice Sharing Logic", () => {
await page.waitForURL("/?template=stripe");
// Upload logo
await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64);
await uploadLogoFile(page);
// Wait for upload and verify share button is disabled
const generalInfoSection = page.getByTestId("general-information-section");
@ -287,9 +287,7 @@ test.describe("Stripe Invoice Sharing Logic", () => {
const parsedData = JSON.parse(storedData) as InvoiceData;
expect(parsedData).toMatchObject({
logo: SMALL_TEST_IMAGE_BASE64,
} satisfies Pick<InvoiceData, "logo">);
expect(parsedData.logo).toBeTruthy();
// Reload the page
await page.reload();
@ -330,7 +328,7 @@ test.describe("Stripe Invoice Sharing Logic", () => {
.selectOption("stripe");
// Upload logo
await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64);
await uploadLogoFile(page);
// Wait for logo to be uploaded
// eslint-disable-next-line playwright/no-wait-for-timeout

View file

@ -6,7 +6,7 @@ import {
} from "@/app/schema";
import fs from "node:fs";
import path from "node:path";
import { SMALL_TEST_IMAGE_BASE64, uploadBase64LogoAsFile } from "./utils";
import { uploadLogoFile } from "./utils";
// IMPORTANT: we use custom extended test fixture that provides a temporary download directory for each test
import { expect, test } from "../utils/extended-playwright-test";
@ -21,7 +21,7 @@ test.describe("Stripe Invoice Template", () => {
// we set the system time to a fixed date, so that the invoice number and other dates are consistent across tests
await page.clock.setSystemTime(new Date("2025-12-17T00:00:00Z"));
await page.goto("/");
await page.goto("/?template=default");
});
test("displays correct OG meta tags for Stripe template", async ({
@ -83,7 +83,7 @@ test.describe("Stripe Invoice Template", () => {
// Logo section is visible on default template
await expect(
generalInfoSection.getByText("Company Logo (Optional)"),
generalInfoSection.getByText("Company Logo", { exact: true }),
).toBeVisible();
await expect(
generalInfoSection.getByTestId("logo-upload-input"),
@ -92,7 +92,7 @@ test.describe("Stripe Invoice Template", () => {
// Payment URL section should not be visible on default template
await expect(
generalInfoSection.getByRole("textbox", {
name: "Payment Link URL (Optional)",
name: "Payment Link URL",
}),
).toBeHidden();
@ -112,7 +112,7 @@ test.describe("Stripe Invoice Template", () => {
).toBeVisible();
await expect(
generalInfoSection.getByText("Company Logo (Optional)"),
generalInfoSection.getByText("Company Logo", { exact: true }),
).toBeVisible();
await expect(
generalInfoSection.getByText("Click to upload your company logo"),
@ -124,7 +124,7 @@ test.describe("Stripe Invoice Template", () => {
// Payment URL section should now be visible on Stripe template
await expect(
generalInfoSection.getByRole("textbox", {
name: "Payment Link URL (Optional)",
name: "Payment Link URL",
}),
).toBeVisible();
@ -135,7 +135,7 @@ test.describe("Stripe Invoice Template", () => {
// Logo section remains visible on default template
await expect(
generalInfoSection.getByText("Company Logo (Optional)"),
generalInfoSection.getByText("Company Logo", { exact: true }),
).toBeVisible();
await expect(
@ -145,7 +145,7 @@ test.describe("Stripe Invoice Template", () => {
// Payment URL section should be hidden again on default template
await expect(
generalInfoSection.getByRole("textbox", {
name: "Payment Link URL (Optional)",
name: "Payment Link URL",
}),
).toBeHidden();
});
@ -226,7 +226,7 @@ test.describe("Stripe Invoice Template", () => {
const generalInfoSection = page.getByTestId("general-information-section");
// Upload a valid small image
await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64);
await uploadLogoFile(page);
// Should show success toast
await expect(page.getByText("Logo uploaded successfully!")).toBeVisible();
@ -259,7 +259,7 @@ test.describe("Stripe Invoice Template", () => {
.selectOption("stripe");
// Upload a valid small image
await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64);
await uploadLogoFile(page);
const generalInfoSection = page.getByTestId("general-information-section");
@ -304,7 +304,7 @@ test.describe("Stripe Invoice Template", () => {
const generalInfoSection = page.getByTestId("general-information-section");
const paymentUrlInput = generalInfoSection.getByRole("textbox", {
name: "Payment Link URL (Optional)",
name: "Payment Link URL",
});
// Try invalid URL
@ -337,11 +337,11 @@ test.describe("Stripe Invoice Template", () => {
// Add payment URL
await generalInfoSection
.getByRole("textbox", { name: "Payment Link URL (Optional)" })
.getByRole("textbox", { name: "Payment Link URL" })
.fill("https://buy.stripe.com/test_payment_link");
// Upload logo
await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64);
await uploadLogoFile(page);
// Wait for logo to be uploaded and PDF to regenerate
await expect(page.getByText("Logo uploaded successfully!")).toBeVisible();
@ -369,9 +369,7 @@ test.describe("Stripe Invoice Template", () => {
const parsedData = JSON.parse(storedData) as InvoiceData;
expect(parsedData).toMatchObject({
logo: SMALL_TEST_IMAGE_BASE64,
} satisfies Pick<InvoiceData, "logo">);
expect(parsedData.logo).toBeTruthy();
// Reload page
await page.reload();
@ -386,7 +384,7 @@ test.describe("Stripe Invoice Template", () => {
// Verify payment URL persists
await expect(
generalInfoSection.getByRole("textbox", {
name: "Payment Link URL (Optional)",
name: "Payment Link URL",
}),
).toHaveValue("https://buy.stripe.com/test_payment_link");
@ -900,7 +898,7 @@ test.describe("Stripe Invoice Template", () => {
await expect(page).toHaveURL("/?template=stripe");
// Upload a valid logo
await page.evaluate(uploadBase64LogoAsFile, SMALL_TEST_IMAGE_BASE64);
await uploadLogoFile(page);
// Wait for logo to be uploaded and PDF to regenerate
await expect(page.getByText("Logo uploaded successfully!")).toBeVisible();
@ -917,7 +915,7 @@ test.describe("Stripe Invoice Template", () => {
// Add payment URL
await generalInfoSection
.getByRole("textbox", { name: "Payment Link URL (Optional)" })
.getByRole("textbox", { name: "Payment Link URL" })
.fill("https://buy.stripe.com/test_payment_link");
// Wait a moment for any debounced localStorage updates
@ -933,9 +931,7 @@ test.describe("Stripe Invoice Template", () => {
const parsedData = JSON.parse(storedData) as InvoiceData;
expect(parsedData).toMatchObject({
logo: SMALL_TEST_IMAGE_BASE64,
} satisfies Pick<InvoiceData, "logo">);
expect(parsedData.logo).toBeTruthy();
const downloadPDFButton = page.getByRole("link", {
name: "Download PDF in English",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 149 KiB

View file

@ -1,51 +1,8 @@
/**
* Create a small test image in base64 format (1x1 pixel PNG)
*/
export const SMALL_TEST_IMAGE_BASE64 =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==";
import path from "node:path";
import type { Page } from "@playwright/test";
/**
* Upload a base64 image to the logo input
*/
export const uploadBase64LogoAsFile = (base64Data: string) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const fileInput = document.querySelector(
"#logoUpload",
) as HTMLInputElement | null;
const LOGO_FIXTURE_PATH = path.join(__dirname, "../fixtures/app-logo.png");
if (!fileInput) {
throw new Error('Logo upload input "#logoUpload" was not found');
}
if (!base64Data) {
throw new Error("Base64 data is required");
}
// Convert base64 data to binary string by removing metadata and decoding
const byteString = atob(base64Data.split(",")[1]);
// Extract MIME type from base64 metadata (e.g. "image/png")
const mimeString = base64Data.split(",")[0].split(":")[1].split(";")[0];
// Create ArrayBuffer to store binary data
const ab = new ArrayBuffer(byteString.length);
// Create Uint8Array view to write bytes to ArrayBuffer
const ia = new Uint8Array(ab);
// Convert binary string to bytes and write to array
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
// Create File object from ArrayBuffer with name and MIME type
const file = new File([ab], "test-logo.png", { type: mimeString });
// Create DataTransfer to simulate file input
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
// Set file input's files property and trigger change event
fileInput.files = dataTransfer.files;
fileInput.dispatchEvent(new Event("change", { bubbles: true }));
};
export async function uploadLogoFile(page: Page) {
await page.setInputFiles("#logoUpload", LOGO_FIXTURE_PATH);
}

View file

@ -1,6 +1,6 @@
{
"name": "pdf-invoice-generator",
"version": "1.0.2",
"version": "1.0.3",
"private": true,
"packageManager": "pnpm@10.30.1+sha512.3590e550d5384caa39bd5c7c739f72270234b2f6059e13018f975c313b1eb9fefcc09714048765d4d9efe961382c312e624572c0420762bdc5d5940cdf9be73a",
"pnpm": {

View file

@ -656,7 +656,7 @@ export const GeneralInformation = memo(function GeneralInformation({
{/* Logo Upload */}
<div className="">
<Label htmlFor="logoUpload" className="mb-2">
Company Logo (Optional)
Company Logo
</Label>
{logo ? (
@ -718,7 +718,7 @@ export const GeneralInformation = memo(function GeneralInformation({
{template === "stripe" && (
<div className="">
<Label htmlFor={`stripePayOnlineUrl`} className="">
Payment Link URL (Optional)
Payment Link URL
</Label>
<Controller

View file

@ -73,3 +73,5 @@ Automatically calculate European VAT rates and totals for your invoices. Complia
Start creating professional invoices in seconds with our free, open-source tool at [easyinvoicepdf.com](https://easyinvoicepdf.com)
**GitHub Repository**: [VladSez/easy-invoice-pdf](https://github.com/VladSez/easy-invoice-pdf)
[View Release Notes for v1.0.0 on GitHub](https://github.com/VladSez/easy-invoice-pdf/releases/tag/EasyInvoicePDF-v1.0.0)

View file

@ -57,3 +57,5 @@ export const metadata = {
---
**Full Changelog**: [`EasyInvoicePDF-1.0.1...v1.0.2`](https://github.com/VladSez/easy-invoice-pdf/compare/EasyInvoicePDF-1.0.1...v1.0.2)
[View Release Notes for v1.0.2 on GitHub](https://github.com/VladSez/easy-invoice-pdf/releases/tag/v1.0.2)

View file

@ -0,0 +1,55 @@
export const metadata = {
title: "Seller & Buyer Improvements, Email Visibility Toggle and More",
description:
"Email visibility toggle for seller/buyer sections, reworked form layouts with locked-state banners, ConfirmDiscardDialog, and auto-scroll on mobile",
date: "2026-03-29",
version: "1.0.3",
type: "minor",
};
## ✨ Highlights
- **Email visibility toggle** for seller and buyer sections — control whether the email address appears in the generated PDF
- **`ConfirmDiscardDialog`** component to warn users about unsaved changes when closing the buyer/seller dialogs
- **Reworked seller and buyer information form sections** with improved layout, locked-state banners, and cleaner field grouping
- **Auto-scroll the invoice form on mobile** when switching between tabs (UX improvement)
- **Out-of-Date dates helper** improved with more accurate state detection
<video
src="https://github.com/user-attachments/assets/1b39eb6f-e2be-493f-9825-cbce3dc6fa16"
controls
style={{ width: "100%", borderRadius: "8px" }}
/>
---
### Changed
- Invalid localStorage entries for buyers and sellers are now validated and silently dropped instead of causing errors
- Error message component layout and copy updated for better readability
- GitHub Actions workflows updated to latest action versions; failure handling added to all CI jobs
- Added knip GitHub CI job for automated dead-code and unused-dependency detection
### Fixed
- Pre-fill switch in buyer/seller dialogs no longer retains its state after the dialog is closed and reopened
- Buyer and seller dialogs now reset form values and pre-fill switch to their defaults when closed
- Buyer and seller names are trimmed of whitespace before saving; whitespace-padded duplicates are rejected
### What's Changed
- feat: auto-scroll form on mobile when switching between tabs, fix loading placeholder by @VladSez in [#191](https://github.com/VladSez/easy-invoice-pdf/pull/191)
- refactor: update layout and improve accessibility in invoice-related components by @VladSez in [#192](https://github.com/VladSez/easy-invoice-pdf/pull/192)
- refactor: improve layout in BuyerDialog and SellerDialog components by @VladSez in [#193](https://github.com/VladSez/easy-invoice-pdf/pull/193)
- refactor: streamline toast management in InvoiceForm component by @VladSez in [#194](https://github.com/VladSez/easy-invoice-pdf/pull/194)
- feat: enhance buyer and seller management with new functionality and improved toast notifications by @VladSez in [#195](https://github.com/VladSez/easy-invoice-pdf/pull/195)
- feat: reworked seller/buyer sections, email visibility switch field, shared invoice indicator, improved Out-of-Date helper + minor things by @VladSez in [#197](https://github.com/VladSez/easy-invoice-pdf/pull/197)
- feat: add cancel confirm dialog for seller/buyer dialog, update gh actions, added new script update-github-actions in package.json by @VladSez in [#198](https://github.com/VladSez/easy-invoice-pdf/pull/198)
- fix: update GitHub Actions script and fix buyer/seller dialog pre-fill bug by @VladSez in [#199](https://github.com/VladSez/easy-invoice-pdf/pull/199)
- feat: replaced window.confirm with alert discard dialog for buyer and seller management by @VladSez in [#200](https://github.com/VladSez/easy-invoice-pdf/pull/200)
---
**Full Changelog**: [`v1.0.2...v1.0.3`](https://github.com/VladSez/easy-invoice-pdf/compare/v1.0.2...v1.0.3)
[View Release Notes for v1.0.3 on GitHub](https://github.com/VladSez/easy-invoice-pdf/releases/tag/v1.0.3)

View file

@ -45,3 +45,5 @@ export const metadata = {
---
**Full Changeset**: [`63b097b...7ae733b`](https://github.com/VladSez/easy-invoice-pdf/compare/63b097b...7ae733b)
[View Release Notes for v1.0.1 on GitHub](https://github.com/VladSez/easy-invoice-pdf/releases/tag/EasyInvoicePDF-1.0.1)