=== Event Tickets with Ticket Scanner ===
Contributors: sasonikolov
Tags: event tickets, ticket scanner, congress, woocommerce tickets, seating plan
Requires at least: 6.0
Requires PHP: 8.1
Stable tag: 3.1.14
Tested up to: 7.1
License: GPLv3
License URI: https://www.gnu.org/licenses/gpl-3.0.html

Sell tickets with WooCommerce: seating plans, PDF tickets with QR codes, door scanner. No per-ticket service fees; free version up to 50 tickets.

== Description ==

**Run your entire ticketing workflow inside WordPress.** No platform service fees, no per-ticket commissions — you only pay your payment processor (Stripe, PayPal, etc.) like any WooCommerce product.

Event Tickets with Ticket Scanner turns any WooCommerce product into a scannable event ticket — complete with QR code, downloadable PDF, and a built-in mobile ticket scanner for your team at the door.

Unlike ticketing platforms that charge service fees on paid tickets, this plugin adds no per-ticket commission — you keep your ticket revenue (minus your payment processor's standard fee). Unlike other WordPress plugins, you get a **visual seating plan designer** and a **ticket PDF designer** included — not as expensive add-ons.

https://youtu.be/uWSdKdOyn70

**Active on 1,000+ WordPress sites** — from small community events to large concert venues.

= Who is this for? =

* **Concert & festival organizers** — sell general admission or assigned seats with interactive seat selection
* **Theaters & venues** — design your seating layout with drag & drop, let customers pick their seats
* **Sports events & arenas** — handle high volumes with offline fallback and team scanner access
* **Clubs, spas, gyms & theme parks** — sell multi-entry passes, family tickets, or memberships with expiration
* **Community events & fundraisers** — get started in minutes with the free version (up to 50 tickets)

= How it works — 3 steps =

1. Install the plugin and create a ticket list under "Event Tickets"
2. Enable "Ticket Sales" on any WooCommerce product
3. Customers receive a unique QR code ticket — scan and redeem at the entrance

That's it. No coding required. [Watch the quickstart video](https://youtu.be/KKLp1Lwqj_U)

= 🎨 Visual Seating Plan Designer =

Create professional venue layouts without any design tools:

* Drag & drop seats, shapes, labels, and text onto your canvas
* Upload venue floor plans as background images
* Rotate, duplicate, and bulk-edit elements
* Color-code seat categories and pricing tiers
* Customers see a **real-time interactive seat map** during checkout
* Seats are automatically blocked during checkout and released on cancel/refund

= 🎟️ Ticket PDF Designer =

Design tickets that match your brand — not generic templates:

* Add your logo, custom colors, header and background images
* Position QR codes exactly where you want them
* Create event badges for staff and VIP passes
* Multi-page PDF support with attached documents
* Full bleed mode for edge-to-edge designs

= 📱 Built-in Ticket Scanner =

No extra app needed. The scanner runs in any mobile browser — or install it as a PWA:

* Scan QR codes with your phone camera — works on iOS and Android
* **Install as PWA** — add to home screen for instant launch without browser chrome
* **Fullscreen mode** — immersive scanning with a single tap
* **Haptic feedback** — vibration confirms valid/invalid tickets instantly
* Grant team members scanner access via Auth Tokens (no WordPress login needed)
* See ticket details, seat position, and venue map instantly after scan
* Voice output confirms valid/invalid tickets for fast processing
* Protection against fake tickets, double redemption, and brute-force attempts
* Supports hardware barcode scanners for high-traffic entrances
* Customizable theme color to match your brand

= 📲 Vollstart Wallet — All Tickets in One App =

Your customers can collect tickets from multiple shops in one free app:

* **Vollstart Wallet** at [wallet.vollstart.com](https://wallet.vollstart.com) — a Progressive Web App that works on iOS, Android, and desktop
* Tickets are added with one click from the ticket page or order email
* QR code always available — even offline
* **Privacy first** — ticket data flows directly from your shop to the customer's browser. Vollstart never receives or stores any data
* [Learn more about security & privacy](https://vollstart.com/vollstart-wallet/)

Enable it in Settings → Digital Wallets. Free for all users.

= 🎓 Congress & Attendee Portal =

Give ticket holders a private event portal — no extra login, no separate app. Assign a **congress** to any ticket product and buyers reach it straight from their ticket ID.

* **Organize content into pages** — a start page plus as many pages as you need (Speakers, Programme, Downloads, Sponsors …), with a sidebar on desktop and a hamburger menu on mobile
* **Rich section types** — formatted text, a day-by-day programme schedule, downloadable files, link lists, image galleries, single images, and embedded videos (paste a YouTube/Vimeo link or upload your own)
* **Password-protect** individual sections for VIP or speaker-only content
* **Fast by design** — the start page loads instantly and other pages are fetched on demand, so even congresses with dozens of speakers stay snappy
* **Installable & offline-friendly** — the portal ships as a mini Progressive Web App
* **Time-controlled access** — open the portal a set number of hours before the event and keep it available for a chosen number of days afterwards
* **Recurring events** — duplicate a congress as a "new edition" in one click

Build it under the Congresses menu, assign it in the product's Event Tickets tab, and optionally add a portal link to the order email. Included free.

= Powerful Ticket Types =

* **Single entry** — classic one-time event ticket
* **Multi-entry passes** — allow multiple scans (configurable limit)
* **Family tickets** — generate multiple tickets per order item
* **Memberships & season passes** — set expiration dates
* **Day chooser tickets** — let customers pick their event date at checkout
* **Purchase allowance codes** — require a valid ticket or access code before a product can be bought. Use it for presale and access codes, or to require a ticket from an earlier event (for example a group-stage ticket in order to buy the final). Per product you decide whether a code unlocks one purchase or several

= Built for WooCommerce =

* Works with product variants (e.g., VIP vs. General Admission)
* Tickets auto-generate on order completion
* Refunded orders automatically recover and recycle ticket numbers
* Compatible with WooCommerce Subscriptions
* Works with WooCommerce PDF Invoices & Packing Slips
* WPML compatible for multilingual stores

= Free vs. Premium =

The free version covers everything you need to start selling tickets — ideal for small events up to **50 tickets**:

* ✅ Ticket generation with QR codes (up to 50 tickets, 5 ticket lists)
* ✅ PDF ticket download (via link in email)
* ✅ Built-in ticket scanner
* ✅ Interactive seating plan designer (1 plan, up to 20 seats)
* ✅ Seat selection at checkout
* ✅ Multi-entry and family tickets
* ✅ Day chooser for date-based events
* ✅ Congress / attendee portal (pages, programme, downloads, media, password-protected sections)
* ✅ Purchase allowance codes (presale/access codes, or require a ticket from an earlier event)
* ✅ Webhooks for third-party integrations

**Premium adds professional features for larger events and removes all ticket limits:**

* 🔓 PDF ticket as email attachment (not just a link)
* 🔓 Team scanner access via Auth Tokens
* 🔓 Calendar invites (ICS files) in emails
* 🔓 Custom flyers and multi-page PDFs
* 🔓 CVV security check on tickets
* 🔓 Brute-force IP blocking
* 🔓 Unlimited tickets (removes the 50-ticket free limit)
* 🔓 Advanced shortcodes for ticket display and validation

[Get Premium](https://vollstart.com/event-tickets-with-ticket-scanner/)

= Links =

* [Documentation](https://vollstart.com/event-tickets-with-ticket-scanner/docs/)
* [Quickstart Video](https://youtu.be/KKLp1Lwqj_U)
* [All Features](https://vollstart.com/event-tickets-with-ticket-scanner/docs/event-tickets-with-ticket-scanner-feature-list/)
* [Premium Plugin](https://vollstart.com/event-tickets-with-ticket-scanner/)
* [Support](mailto:support@vollstart.com)

== Installation ==

= Requirements =
* WordPress 5.0 or greater
* WooCommerce 6.0 or greater
* PHP 8.1 or greater (PHP 8.4 compatible)
* PHP extensions: php-curl, php-imagick

= Installation =

1. Go to **Plugins → Add New** in your WordPress admin
2. Search for "Event Tickets with Ticket Scanner"
3. Click **Install Now**, then **Activate**
4. Go to **Event Tickets** in your admin menu and create your first ticket list
5. Edit any WooCommerce product → open the **Event Tickets** tab → enable ticket sales

= Quick Setup =

The plugin extends WooCommerce so any product can become a ticket. Enable the checkbox on your product, assign a ticket list, and you're ready to sell.

**Tip:** Set the product as "Virtual" so WooCommerce auto-completes the order and triggers ticket generation immediately after payment.

To test: Create a manual order in WooCommerce, set it to "Completed", and check the order email for the ticket link.

**For a step-by-step walkthrough, [watch the quickstart video](https://vollstart.com/event-tickets-with-ticket-scanner/docs/#quickstart).**

== Frequently Asked Questions ==

= Do I need any other plugins? =
You need WooCommerce (free) to handle payments and orders. Everything else is included — no additional ticketing add-ons required.

= Does this work without WooCommerce? =
WooCommerce is required for selling tickets. However, you can use the plugin to manage and validate ticket lists manually without WooCommerce sales.

= Can my team scan tickets without a WordPress account? =
Yes! Premium includes Auth Tokens that give your door staff scanner access via a simple URL — no login required.

= Does the scanner work offline? =
The scanner is browser-based and requires an internet connection. For large events, the plugin includes offline fallback options to prevent interruptions.

= Can customers choose their own seats? =
Yes. Design your venue layout with the drag & drop seating designer, and customers will see an interactive seat map during checkout where they can pick available seats.

= What ticket types can I create? =
Single entry, multi-entry passes, family tickets (multiple tickets per purchase), memberships with expiration dates, and day-chooser tickets where customers pick their event date.

= How are tickets delivered to customers? =
In the free version, the order confirmation email includes a link to download the ticket PDF and view the QR code. Premium allows attaching the PDF directly to the email and adding calendar invites (ICS).

= What happens when an order is refunded? =
The ticket is automatically deactivated, the assigned seat is released, and the ticket number is recovered for reuse.

= Is the plugin compatible with WPML? =
Yes. WPML is supported for multilingual ticket sales. The plugin also ships translations for German, Spanish, French, Italian, Hungarian, Japanese, Dutch, Portuguese, and Chinese.

= What if I exceed the free ticket limit? =
The free version supports up to 50 tickets. If you reach the limit, the plugin shows a notice in the admin area (your customers are never blocked from purchasing). Premium removes the limit entirely.

= Can I use my own QR code or barcode scanner hardware? =
Yes. The built-in scanner page accepts input from hardware barcode scanners in addition to camera-based QR scanning.

= How do I prevent ticket fraud? =
Every ticket number is unique. The scanner detects duplicate redemption attempts. Premium adds CVV verification and brute-force IP blocking for additional security.

= What is the congress / attendee portal? =
It's a private event portal you can attach to a ticket product — organized into pages (start page, speakers, programme, downloads, sponsors, etc.) with text, schedules, files, image galleries and embedded videos. Ticket holders open it directly via their ticket, no separate account needed. You can password-protect individual sections, control when the portal opens and how long it stays available, and the page works offline as a mini PWA. It's included in the free version; build it under the Congresses menu and assign it in the product's Event Tickets tab.

== Screenshots ==

1. **Event Tickets Dashboard** — Manage ticket lists, sold tickets, and everything else from one place in your WordPress admin.
2. **Visual Seating Plan Designer** — Drag & drop seats, rows, shapes and labels to design your venue. No design tools needed.
3. **Interactive Seat Map at Checkout** — Customers pick their own seat on a live seat map. Taken seats are blocked automatically.
4. **Sell Tickets as WooCommerce Products** — Any product becomes a ticket with one checkbox. Works with variations like VIP or General Admission.
5. **PDF Ticket** — Branded PDF ticket with QR code, ready for print or mobile display.
6. **Ticket on Mobile** — Customers see their ticket with QR code, PDF download and calendar file on any device.
7. **Ticket Scanner** — Scan and redeem tickets with any phone. No extra app required; installs as a PWA with fullscreen mode.
8. **Ticket Management** — All generated tickets with status, redemption info, filters and CSV export.
9. **Tickets on the Order Page** — See and quick-scan the tickets of every WooCommerce order directly in the order detail view.
10. **Product Settings** — Enable ticket sales, assign a ticket list and a seating plan on the product's Event Tickets tab.

== Upgrade Notice ==

= 2.8.0 =
Major release: Old premium version will no longer work with this version. Interactive seating plan designer with drag & drop editor. Default ticket template updated to display seat numbers. Please backup your system before upgrading.

= 2.8.10 = 
Old premium version will no longer work with this version. You need to downgrade the basic plugin or get a new license for premium to update your premium plugin too.

== Changelog ==

= 3.1.14 - 2026-09-07 =
* Security: Added nonce verification to the admin-area AJAX endpoint (executeWCBackend) to block cross-site request forgery. The frontend already shipped the nonce; the server now verifies it.
* Security: Restricted WooCommerce order/product data actions (downloadTicketInfosOfProduct, downloadAllTicketsAsOnePDF, removeAllTicketsFromOrder, removeAllNonTicketsFromOrder, downloadPDFTicketBadge) to users with the manage_woocommerce or edit_shop_orders capability. Without this, a low-privilege role granted admin-area access via the "Allow only specific roles" setting could read or delete every customer's ticket data through the plugin-internal role check.
* Security: Removed an `eval(data)` call after `$.getScript()` in the seating-admin loader (`backend.js`, _displaySeatingplanArea, line 1288). `$.getScript()` already executes the loaded script; the extra eval ran every byte of the response through the JS engine again. CWE-95 — a server-side change to `js/seating_admin.js` would have given code-injection on every admin page that opens the seating-plan area. No callers, no behaviour change beyond the redundant execute.
* Security: Ticket codes, scanner auth tokens and ticket idcodes are now generated with a cryptographically secure random source (`random_bytes`) instead of a hash of the current time (`md5(time() ...)`). The old values were predictable: two codes created in the same second shared their time component, so anyone who knew roughly when a ticket was issued could narrow the guessing range dramatically. CWE-338. Five generators were changed — the ticket code and the code-list lock in the WooCommerce assignment path, the per-list and per-order idcode, and the ticket-scanner auth token. The printed format is unchanged (same length and character set), existing tickets and tokens keep working, and no migration is needed.

= 3.1.13 - 2026-09-03 =
* Tweak: Products with variations no longer show the seat selector on the shop overview page. In the overview no variation is chosen, so the selector could not work there anyway - it only pushed the product image out of its card. The "Select options" link leads to the product page, where the full seat selection (and its validation) lives. Simple products keep the quick selector in the overview.


= 3.1.12 - 2026-09-02 =
* Tweak: The premium wizard now says exactly what the "Enable Recommended Settings" button does before you click it: attach tickets as PDF to the purchase email, merge all tickets of an order into one PDF, and raise the attachment limit to 21. Previously the popup only vaguely asked to "enable PDF ticket attachment" and revealed the actual settings only after they had already been changed.
* Tweak: "Skip" in the premium wizard closes the popup instantly instead of waiting for the server. The "don't show again" marker is written exactly once per installation - the first time the wizard is skipped while it is still unset; re-opening the wizard later sends no request at all.
* Fix: In multilingual shops (WPML), the seat check on "add to cart" was silently skipped when the customer bought in a secondary language. The check read the seat settings from the translated product, where they do not exist - a shop could sell numbered-seat tickets without a chosen seat. The check now reads the settings from the original product, like the cart display already did.
* New: A safety net at checkout. If a product with required seat selection reaches the cart or checkout without a seat on its line - possible when the seating plan was still unpublished at add-to-cart, when a seat reservation expired and was removed, or when another plugin rewrote the cart - the order is now blocked with a clear message until a seat is selected. Products with optional seats are unaffected.



= 3.1.11 - 2026-09-01 =
* Fix: Adding a ticket with a chosen seat to the cart failed with "Invalid seat selection" on every attempt since 3.1.8. The seat check compared the chosen seat against a value that the variation-plan rework had left behind, so no seat could ever pass - on products with and without variations. Valid seats are accepted again.

= 3.1.10 - 2026-08-31 =
* Fix: Two errors that blocked a shop after a staging-to-live database migration. (1) The Event Tickets admin page stayed on the loading spinner and showed "#505 auth token not found" as soon as a ticket in the list referenced a scanner token that no longer existed in the new database - the audit column tried to read the token's name and the missing-row error crashed the whole page. (2) Membership products that were never ticket products suddenly demanded a "ticket or access code" in the cart and at checkout, even though their product settings showed no restriction. Both came from values that the database carried over from staging. (1) is caught and falls back to "AUTHTOKEN DELETED" in the audit column; the migration artefact no longer breaks the page. (2) sits behind the same is_ticket gate that already protects the other ticket-specific cart fields (added in 3.1.8): a stale restriction on a non-ticket product is now ignored, both for the input field and for the checkout validation. The restriction still works as before for real ticket products, including variations, where the parent is walked up first.

= 3.1.9 - 2026-08-26 =
* Fix: The "Check License" button in the settings did nothing since 3.1.6 - it showed "Checking..." forever and never contacted the license server, and after saving a license key the page never confirmed the activation. A helper function used by the button was defined in a place the button could not see, so the click stopped with an error before the request was sent. Your license was still checked in the background as before - only the visible check and the confirmation after entering a new key were dead. Moving the helper where every part of the settings page can use it brings both back.

= 3.1.8 - 2026-08-26 =
* New: Fields that another plugin adds to an order line - for example the answers of a product add-on plugin - can now be shown as a column in the ticket list. Switch on "Display a column with the extra fields of the order line" in the settings. Until now those answers were only visible by opening each order, which is why shops with more questions than our two per-ticket fields had to build their own list. A second setting hides individual fields by name, for answers that belong in the order but not in a list. The values belong to the whole order line, not to a single ticket: an order line for three tickets carries one set of answers.
* Fix: A seating plan assigned to a single variation is now actually used in the shop. The product editor has offered a "Seating Plan Override" per variation for a while and saved it correctly, but the shop only ever read the parent product: the seat map on the product page never changed when a customer picked another variation, the cart notice and the seat check when adding to cart used the parent's plan as well. Picking a variation now swaps the map to that variation's plan, and a product whose plan only exists on its variations shows a map at all for the first time. Products without variations are unaffected.
* Tweak: In the variations of a ticket product, the label "This variation is NOT a ticket product" no longer sticks to its checkbox. It now keeps a fixed gap, so the line reads as a label and a box instead of one run-on word.
* New: For plugin developers: fields that your plugin collects at checkout can be listed inside Event Tickets, right where our users write their ticket template. Fill the filter saso_eventtickets_template_order_fields with your fields - key and name are enough - and shop owners no longer have to look up a meta key in the database to print your field on a ticket. The heading above them is your plugin name, taken from the plugin your callback lives in; give your own heading once as the array key if you prefer. Fields of different providers are never merged, not even when two of them pick the same heading. Reading the value itself never needed us: a ticket template prints any order meta with {{ ORDER.get_meta("your_key") }}. The list sits under the replacement tags on the options page and stays empty until a plugin registers something - if yours is missing, ask its provider.
* New: A new setting decides from which order status a seat counts as sold: "Mark seats as sold from this order status". Until now a seat only turned red when the ticket number was created, which is when the order is paid - with a cash voucher or a bank transfer that can be days later, and until then the seat stayed available for everyone else. Set the status to "pending payment" and the seat is gone the moment the order is placed, while the ticket still follows on payment. The default keeps the previous behaviour. The ticket number is written onto the same seat afterwards, so cancelling an order still frees it.
* Fix: A seat now turns red on the seat map when the order is paid, even if the customer took their time or paid later. Selecting a seat reserves it for a while - by default a quarter of an hour - and the plugin marked exactly that reservation as sold when the order was completed. If the reservation had run out in the meantime, and it always has when payment arrives days later through a cash voucher or a bank transfer, there was nothing left to mark, and the plugin treated that as done. The order carried the seat correctly, the seat map kept offering it to everyone else, and the same seat could be sold twice. The sale is now written to the seat map in any case, and a second run of the ticket creation cannot enter the same seat twice. Shops with instant card payment were affected as well whenever a checkout took longer than the reservation.
* Fix: A one-day event no longer prints its date twice on the ticket. When no end date is entered, the plugin fills in the start date internally so the ticket stays valid until the end of the event day - and the printed date treated that filler like an entry, so "12.09.2026 19:00" became "12.09.2026 19:00 - 12.09.2026". Only the date you actually entered is shown now: a start alone stays a start, a real end date still produces a range, and an end time on the same day still shows as "19:00 - 23:00". The same applies to the date picker: a ticket for a chosen day shows that day once instead of "15.06.2026 19:00 - 15.06.2026". For template authors: TICKET.end_date is never empty for the same reason, so testing it tells you nothing. Two new variables do: TICKET.is_end_date_set and TICKET.is_end_time_set.

= 3.1.7 - 2026-08-24 =
* Fix: Removing a single ticket number from an order left that number on the order line. The number was freed in the ticket list, but the order, the email and the invoice PDF kept showing it, and it no longer lined up with the public ticket numbers next to it. The number was compared in its internal spelling against the one printed on the order, which never matched, so nothing was ever removed. Orders with several tickets on one line are the ones affected.
* New: For developers: the decision whether an order line gets a ticket now lives in one place and can be overruled with the filter saso_eventtickets_wc_order_item_is_ticket. It exists for shops where another plugin splits one product into several order lines - split VAT or invoicing plugins - so that only the original line carries the ticket while the other lines stay bookkeeping. Nothing changes unless the filter is used.
* New: For developers: the order manager now answers, per order line, how many ticket numbers a line is supposed to carry and which lines are still short of them (getExpectedTicketAmountForItem, countOrderItemsWithMissingTicketnumbers, orderNeedsTicketNumbers). Ticket creation uses the same calculation, so a tool that looks for missing numbers and the code that draws them can no longer drift apart. The premium bulk assignment is the first user.
* New: The ticket export now has its own column for the value you ask per ticket (the product setting "Request a value for each ticket from dropdown"). Until now only the name per ticket had one, so the answer was in the export but buried inside the raw meta column, where a spreadsheet cannot filter or sort it. The column stays in the file even when a product does not ask for a value, so the header does not change from export to export.
* Tweak: An extra safeguard so a product that is not configured as a ticket can never receive a ticket number, not even when it sits in the same order as tickets. Selling through the shop was never affected - the check was already made for every single order line - but the entry point that other code (the premium plugin, custom code) can call did not make it itself. It does now.

= 3.1.6 - 2026-08-13 =
* Tweak: The admin area was redesigned. The top navigation is now a segmented control, the footer cards sit in a four-column grid, ticket status pills use semantic colors, and the primary action buttons share one style across the whole settings page.
* Tweak: The support footer now has a "Rate this Plugin" card linking to the WordPress.org review page, so the four footer cards fill one row.
* Fix: The "Dismiss" button of the version-notice box sat outside the card; it is now inside the card where it belongs.
* Fix: The Dashicons inside primary (lila) buttons inherited the WordPress default blue and clashed with the background; they are now white.
* Tweak: The ticket scanner and the ticket detail page now feel like one product. A design system was added - CSS custom properties for colors, spacing and radius, a BEM class structure on top of the existing IDs - and the parts that grew ugly over the years were cleaned up (hard-coded black border on the ticket box, the &lt;center&gt; tag on the scanner, the orphaned button_ok.png / button_cancel.png images). Every existing ID and class is still there, so a custom CSS file you wrote for an earlier release keeps working.
* Tweak: On the scanner page the ticket information no longer appears inline after a scan - the event name, date and seat only filled the space between the reader and the buttons, and on a phone the layout jumped around while the answer was on the way. While the scanner is waiting for the answer, the area shows a loading hint; as soon as the ticket is retrieved, a compact summary card appears (heading, title, date) with a color that matches the ticket state - green for valid, orange for expired or not yet redeemable, red for already redeemed. The full details open behind an "Show ticket info" button as a full-screen overlay with a close button, so the scanner display stays clean.
* Tweak: On the scanner the option row was moved up next to the buttons, so the "Scan and Redeem immediately" checkbox and its peers are visible right where the action buttons are. A new "Use classic layout" checkbox sits next to the existing "Use old ticket scanner library" one and toggles the scanner layout via a URL parameter - the same pattern that toggle uses.
* Tweak: The standalone ticket scanner is no longer preceded by a "Ticket Scanner" headline - it looks like a scanner device now, not like a page. On the public ticket view the wrapper no longer carries a hard border; the content sits directly on the page, with only the page's own padding for air. Scanner buttons are now 48 minimum height (56 for primary actions), so the person at the door in January gloves does not have to fight the touch target.
* Fix: The spinner shown while the scanner waits for the server was invisible on the modern layout - the CSS class that animated it lived only inside the legacy HTML page. The animation is now defined in the design system stylesheet, so the spinner shows wherever it is used.
* Fix: Three button styles in the scanner used three different looks - one was a bare HTML button, one used a class the stylesheet had never heard of, and one toggled between gray and green via inline JavaScript. They are now all built on the same BEM class, and the disabled state has a real visual treatment instead of just disappearing.
* Fix: The "Badge" and "PDF" buttons on the scanner did nothing to indicate the download was on the way. They are now disabled while the file is being fetched and show a small spinner inside the button label, restoring the original label as soon as the download is delivered (or fails).
* Tweak: The "AI Support Bot" card is gone from the support view. It answered from a knowledge base we stopped keeping up to date, and an answer about last year's plugin is worse than none. Documentation, release notes and the support address are where they were.
* New: A ticket can be passed along from the ticket page. On a phone the usual share sheet opens - WhatsApp, Telegram, Signal, mail, whatever is installed - on a desktop browser WhatsApp and Telegram are offered as links. That is how the second ticket reaches the friend who is coming along, and how the ticket gets from the laptop to the phone. Your shop sends nothing itself; the customer does. The page says out loud that whoever receives the link can show the QR code. Can be switched off ("Let customers send their ticket on"), and it is shown in every kind of ticket sale, including vouchers.
* Tweak: The last pieces of premium licensing left this plugin. The "Check License Server" button and the update dialog that appeared after entering a license key are gone - both reached out to our server from the free plugin, which is not what a plugin on WordPress.org should do. Updating premium is the premium plugin's own job ("Check license & update" next to the license key, version 1.7.3 and newer). What stays here is the display: the license status and a button to refresh it, which asks the premium plugin to do the checking.
* New: A message you can set for events where tickets are still sold at the door - it is added to the "sales closed" text when the premium version marks a product that way, so a buyer who arrives too late for the online sale is not left with a dead end.
* Fix: Tickets without a WooCommerce order could not be redeemed by typing their number into the scanner - the very way a printed card is meant to be checked at the door. It failed with "#9302" unless a second, separate option was switched on as well. Allowing tickets without an order is now enough.
* Tweak: The scanner said "Ticket is NOT paid ()." for a card that was never sold in the shop - a defect message for the case the feature is made for, with an empty bracket on top. It now says the ticket was handed out directly.
* Tweak: In the product's Event Tickets tab and in the ticket list, option keys no longer appear in customer-facing text. The settings page hides them in the entry view as well and shows them under "All options", where support answers need them.
* Tweak: The "Getting Started" checklist now reads the real state of your installation instead of guessing from counters. Until now it ticked off "assign a list to a product" as soon as any ticket existed anywhere, and it congratulated you for the ticket list that the plugin creates by itself. It also no longer counts a fixed four steps: what needs doing is what is listed.
* Tweak: In the product's Event Tickets tab, the ticket switch and the list now come first and the note about the free ticket limit follows below - it used to be the first thing in the tab, in red, before the setting it belongs to.
* New: After activation the plugin says where to go next, once, with a button - instead of leaving you to find the menu entry at the bottom of the sidebar. Installations that already have tickets are not bothered with it.
* New: The setup wizard now ends by showing what it actually switched on for you ("redemption locked until the event starts", "scan redeems immediately", …). The defaults fit most shops - you just could not see them. It also stops claiming you are all set while listing things you still have to do: what is left is one product, and the button goes straight there.
* Tweak: The warning about "Plain" permalinks was a dialog on every single page load, with no way to act on it. It is now the first step of the setup check, with a button that opens the permalink settings - and it is the one thing that still shows up even after you dismissed the setup check, because without it the ticket page and the scanner cannot be reached.
* Tweak: The setup wizard now remembers which kind of sale you picked. Until now the settings page asked the same question a second time, minutes after the wizard had already asked it.
* Fix: In dialogs, the recommended button is highlighted again instead of whichever button happens to sit on the left. In the setup wizard that meant "Skip" and "Back" looked like the main action while "Start Setup" and "Apply & Finish" looked secondary.
* Fix: A tip in the setup wizard showed "&#9889;" as plain text instead of the symbol, and the product tab said "Make sure your are not selling" instead of "you are".
* Fix: The free version asked for a premium license key. On a plain installation without the premium plugin, a dialog appeared saying "Thank you for using the Premium version!" and asked for a key that such an installation never had — a comparison error made "no premium version" look like "premium version installed, key missing". The dialog now only appears when the premium plugin is really installed.
* New: The plugin page now shows when the plugin was last updated, next to the version, with a link to the changelog of every release. Whether a plugin is still being looked after is a fair question, and the answer belongs where you are, not on a sales page.
* New: A setting that belongs to a different kind of sale but was changed anyway is now shown in the filtered view instead of being hidden - highlighted, with a note that it can make your shop behave unexpectedly and a "reset to default" button right next to it. Those are exactly the leftovers that cause the "it does something I never asked for" cases.
* New: "Restore the defaults of this view" resets only what is currently in front of you, and lists beforehand which settings would change. The existing button that resets all options is untouched.
* New: Existing installations are asked once which kind of ticket sale they run. Whoever answers gets the filtered view, whoever declines keeps all options and is not asked again.
* New: The settings page can now show only the settings that belong to your kind of ticket sale. Pick your event type once - event tickets, day passes, memberships or vouchers - and the page starts with the handful of settings that decide how your shop behaves (14 instead of 306). From there it is one click to everything that belongs to your event type, and one more to all options; nothing is ever hidden for good. The search always looks through every setting and tells you when a match sits outside your event type.
* New: The settings page has a search field. Type two letters and only the matching settings stay on the page - it searches the name, the description and the option key, so you can also paste the key from a support answer or from the documentation ("wcTicketHideTicketAfterEventEnd"). The number of matches is shown, the search term is highlighted, "Esc" clears the field and the "/" key jumps into it. An empty field brings the page back exactly as it was.
* New: Once the setup check has nothing left to report, you can dismiss it for good with the small x. From then on it is not only hidden but no longer worked out either - a finished installation pays nothing for a check it does not need. Clear "setupStatusDismissed" in the settings export to bring it back.
* New: The settings page now opens with a short setup check that reads your actual installation instead of showing you more options: is a product connected to a ticket list, and does your door team have access to the ticket scanner. Every open step has a button that takes you straight to the right place; once both are done the check simply confirms that you can sell and check tickets.
* New: Tickets can now work entirely without a WooCommerce order, for printed cards you hand out yourself. Create the ticket numbers in a ticket list as before, switch on "Allow tickets without a WooCommerce order" — either globally in the WooCommerce settings or for a single ticket list — and the scanner accepts them, shows them and redeems them. Both switches are off by default. Note what you give up for those tickets: the plugin can no longer verify them against a paid order, only that the number exists, is active, has not been redeemed yet and belongs to its list. Tickets sold through WooCommerce are untouched and keep the full check.
* Tweak: This plugin no longer contacts the license server of the premium version. That check now lives in the premium plugin itself, where it belongs — this plugin only stores the answer and uses it to switch premium features on or off. If you use the premium version, update it to 1.8.0 or newer; until you do, your premium features stay switched on and the plugin asks you to update instead of expiring them.
* New: Scanner auth tokens can now be restricted to ticket lists, not only to products ("Bound to ticket list(s)" when editing a token, comma separated list ids, empty means all). A door team then only redeems the lists it is responsible for — for a venue with its own ticket list, one token per venue. This is also the only restriction that can apply to tickets without an order, because those have no product to bind a token to.
* New: A ticket list can carry its own event window ("From" / "Until" with date and time). It applies to tickets without an order, which have no product to take event times from, and is governed by the same scanner options as before — no redemption before the start, none after the end, or none after the start. Empty means no time limit.
* New: Seat order can now be arranged by hand. A "Seat Order" button above the seat list of a simple (dropdown) seating plan opens a drag-and-drop list; the order you save is the order buyers see in the seat dropdown. Until now the order was fixed to the sequence in which the seats were created.
* Tweak: The action buttons of the seating plan list no longer wrap onto a second line — the column is wider and kept on one line.
* Fix: The "Go to Seating Plans" and "Create one first" links in the product panel, and the admin link passed to the frontend seat selector, pointed to a non-existent admin page (`?page=sasoEventTickets`). Following them produced "Sorry, you are not allowed to access this page." The links now use the correct page slug so the seating tab opens as expected.
* Tweak: Plugin description and FAQ now clearly state the 50-ticket free-version limit and the exact free-tier caps (5 ticket lists, 1 seating plan with 20 seats, 3 scanner auth tokens).
* Tweak: Removed unverified social-proof claims ("4.9/5" rating, "1,000+ event organizers") and replaced with the verifiable WordPress.org metric ("Active on 1,000+ WordPress sites").
* Tweak: Removed named-competitor comparison (Eventbrite, Ticketmaster) from the plugin description.
* Tweak: "No per-ticket fees" clarified to "No per-ticket service fees" with payment-processor qualifier.

= 3.1.5 - 2026-08-10 =
* New: Online sales can now be stopped a set number of hours before the event starts ("Stop online sales before the event starts" in the WooCommerce settings, off by default; when switched on it stops sales two hours before the event unless you change the number). Useful when you also sell at the box office and want the online shop to close first. Products without an event date are never affected; for day-chooser products the date the customer picked counts. Buyers are told when they add the product to the cart, and a cart that was filled before the cutoff is blocked at checkout. The message is configurable.
* Tweak: When online sales have closed for a ticket, the buy button is now hidden on the product page and in product lists, and a short line explains why, instead of letting the buyer add the ticket and only failing afterwards. This also covers direct "add to cart" links. Products without an event date, day-chooser products (the buyer picks the date later) and everything that is not a ticket are never affected, and the shop admin still sees the normal buttons in the backend.
* Tweak: If a buyer leaves a ticket in the cart until online sales close, WooCommerce drops it from the cart — the wording is now yours instead of WooCommerce's "please contact us if you need assistance", and can be changed in the settings ("Message when a closed ticket is dropped from the cart"). Items removed for any other reason keep the WooCommerce text.
* Fix: The "Same-day cutoff time" of day-chooser products was evaluated in the server's timezone instead of the shop's, so on a site ahead of UTC it took effect hours too late — with a shop in Central European Summer Time, a cutoff set to 17:00 only blocked today's date from 19:00 on. It now applies at the time you entered, in the timezone configured in WordPress, both in the date picker and when the cart is checked. The date picker previously also used the buyer's own browser clock, which gave visitors abroad a different cutoff than the one enforced at checkout; it now follows the shop's clock as well.

= 3.1.4 - 2026-07-26 =
* New: Optionally require a valid ticket or access code to purchase a product. Pick the required event/code list in the product's Event Tickets tab — until you do, nothing changes for any product. Buyers enter their code in the cart and can only check out with a valid, unused code from that list. Use it for presale and access codes, or to require a ticket from an earlier event — for example a group-stage ticket in order to buy the final. Free.
* New: Per product, the same code can optionally unlock several purchases ("Allow the same code for several purchases"). Off by default, so each code unlocks exactly one purchase.
* New: The cart tells the buyer straight away whether the entered code was accepted, is already used, or is not valid for that product — instead of only failing at checkout.
* Fix: The global switch "Allow requiring a ticket or access code to purchase" now really disables the feature everywhere when switched off. Previously it only controlled the cart script, while checkout still demanded a code for any product that had a list assigned — which, combined with the cart field not submitting the code, could make such a product impossible to buy.
* Fix: On hosting accounts where the server's temporary directory cannot be written to (common with Plesk, open_basedir or PHP-FPM private temp directories), ticket PDFs were never created. The plugin still passed the file path on, so the PDF merge failed and the order email was sent without the ticket and without the calendar/info attachment — with no visible error. The plugin now falls back to a folder inside the WordPress uploads directory, and reports a clear, actionable error if no writable location exists at all. The same fallback now applies to badge PDFs and to all email attachments, which previously failed silently on these hosts as well.
* Fix: Merging ticket PDFs no longer aborts completely when a single source file is missing. Unreadable files are skipped and logged individually, so the remaining tickets are still delivered.
* Fix: The "Delete All Tickets" button on a ticket list failed with a server error instead of deleting anything. The button never worked since it was introduced; it now removes all tickets of the selected list, including their WooCommerce order data. Long lists are deleted in small batches with a progress bar, so even lists with many thousands of tickets no longer run into a server timeout.
* Fix: On hosts that run a different PHP version for WP-CLI than for the website (IONOS and similar split-PHP setups), the plugin's template-engine PHP 8.1 check no longer aborts WP-CLI runs with a fatal error. Ticket and scanner pages are never rendered from WP-CLI, so the check is now skipped in CLI mode. This prevents some hosts from auto-disabling the plugin after a WP-CLI or cache-build run. Website behaviour is unchanged — the actionable PHP 8.1 notice still appears when a ticket or scanner page is opened under an older PHP in the browser.

= 3.1.3 - 2026-07-12 =
* New: Interface translations expanded to the full language set — added Arabic, Czech, Danish, Greek, Hindi, Korean, Norwegian Bokmål, Polish, Romanian, Russian, Swedish, Turkish, Ukrainian, Traditional Chinese, plus corrected Japanese (ja) and French (fr_FR) locale files.
* Fix: When a server runs the plugin folder under PHP below 8.1 (required by the bundled template engine) — which can happen for files opened directly from the plugin folder even when the main site runs a newer PHP — the ticket and scanner pages now show a clear, actionable message instead of a blank 500 error. The message points to the "Ticket detail URL path" option and to enabling PHP 8.1+ for the whole hosting account.

= 3.1.2 - 2026-06-22 =
* Fixed: Support info no longer shows a broken "Own URL" with a double slash when no ticket detail URL path (compatibility mode) is set — both the displayed cards and the copy-to-clipboard text now show "(not set)" instead.

= 3.1.1 - 2026-06-15 =
* New: Event portals — the Congress tool now works for any event (opera, zoo, festival, …).
* New: Optional card-grid landing showing each page as a card with an icon or image and a short description.
* New: Customizable portal label per portal (with a global default, "Infos") — used on the page, in the wallet action and the order email.
* New: Back-to-home control in the portal view.
* New: Insert ticket/order/product/customer variables into info and custom section text; they are filled with the visitor's data when the page is opened.
* Improvement: Internal data and links are unchanged — existing congresses keep working exactly as before.
* Improved: Cleaner admin dashboard — moved the "List of tickets" description into the list card next to its buttons and removed the redundant heading and divider.
* New: Speaker sections — add one or more speakers (photo, name, title, short bio). A single speaker is shown in full; multiple speakers appear as a grid of cards with a detail view and a back control.
* New: "My ticket" entry in the portal always shows the ticket's QR code, so attendees without a printed badge can present it at the entrance.
* New: Portal entries (such as "My ticket") also appear as cards on the card-grid landing.
* Improvement: The portal title is now clickable and returns to the start page; the "Add to Vollstart Wallet" link moved from the header into the "My ticket" view.
* New: View access toggles (security) — each output reachable through the ticket link (ticket detail view, PDF, ICS calendar, badge PDF, all-tickets-in-one-PDF, event portal/congress) can now be switched off individually. When off, the view is no longer rendered and a short "deactivated" notice (or 403 for downloads) is shown. All default to on, so existing installs are unaffected. Unlike the older "hide button" options, this actually blocks the URL.
* New: Optional "Allow redemption by plain ticket number" (off by default). When enabled, a ticket can additionally be redeemed using only the plain ticket number printed on the card; the full ticket id keeps working as before. Note: for the plain number the copy protection does not apply, and it is unsafe together with ticket reuse (a warning is shown on both options).
* Fix: Settings checkboxes whose default is "on" (e.g. the new view-access toggles, the QR scanner on the validation form, the seating-plan/venue buttons) could appear unchecked until the settings page was saved once. The admin UI compared a boolean default too strictly; it now renders the correct checked state. The actual behaviour was always correct (the option was on) — only the checkbox display was wrong.

= 3.1.0 - 2026-06-04 =
* New: Congress mode — organizers create congress objects (programme, downloads, texts, media) and assign them to WooCommerce products.
* New: A dismissible "What's New" banner in the admin introduces the Congress feature (shown once per version).
* New: Ticket holders open the congress page via their ticket ID. Like the ticket detail and scanner pages it is served from the plugin path (`…/ticket/{ticket-id}?congress`, query fallback `…/?code={ticket-id}&congress`), so it is compatibility-mode aware and never collides with shop page slugs.
* New: Offline-capable congress page with a Web App Manifest (installable as a mini PWA).
* New: Sections with optional password protection (server-side transient, 1h TTL).
* New: Programme sections with a day/slot structure, plus download and media sections (WordPress Media Library).
* New: "New edition" button for recurring congresses — duplicates the congress and sets an automatic expiry date on the original.
* New: Option to activate/deactivate the public congress page (default: active); the admin area stays available either way.
* New: Event-relative access window — optional event start/end time per congress; global options control how many hours before the start access opens and how many days after the end the data stays available.
* Improvement: Congress assignment now lives directly in the Event Tickets product tab (instead of a separate tab); the dropdown shows the event/expiry date for easier selection.
* New: `congress_url` in the Wallet API response — the Wallet app shows an "Open congress" button when the product is assigned to an active congress.
* New: Congress URL pattern listed on the Support/System Info page (adapts to the site location); the admin ticket detail shows a direct "Open congress page" link for tickets whose product is assigned to a congress.
* Fix: Congress access now accepts the public ticket ID (as used by the Wallet/QR), not only the internal code — so the Wallet/customer link works reliably.
* New: Congress section editor with full input masks per type — Info/Custom use a visual editor, Download picks files from the media library, URL with internal/external link option, Program with day & time-slot structure, Media gallery.
* New: Congress sections now track and display created/updated date and the editing user.
* New: Visual text editor (TinyMCE) for the "Additional text on PDF ticket" and "Ticket Badge HTML" options instead of a plain textarea.
* New: Tag picker dropdown inside the Ticket Badge editor — insert replacement tags ({TICKET.*}, {ORDER.*}, {PRODUCT.*} …) at the cursor instead of copy-pasting from the list below.
* Improvement: Modernized admin UI — compact buttons, lightweight ghost-style table action buttons grouped together, navigation menu with active highlight, modern DataTables styling.
* Improvement: Congress editor redesigned as a clean card layout; all native browser confirm/prompt dialogs replaced with the plugin's own styled dialogs and loading spinner.
* New: Congress pages — sections are now grouped into pages (the first page is the start page). Visitors get a sidebar (desktop) / hamburger drawer (mobile) navigation; the start page loads instantly and other pages are fetched on demand, so large congresses stay fast.
* New: Congress section types Image (from the media library, with caption) and Video (paste a YouTube/Vimeo link for automatic embedding, or upload a video file).
* New: Image sections have a display-size option — full width (default), original size, or a custom width (px or %).
* New: Image sections can optionally open full size in a lightbox when clicked (default off).
* New: Download files can be set to "open inline" (PDF/images open in the browser) instead of forcing a download.
* New: Admin can create, rename, delete and reorder congress pages (drag & drop) and move a section to another page; sections are drag-sortable within a page.
* Security: All HTML content is sanitized with `wp_kses_post()` — no JavaScript allowed in congress content; video embeds are generated server-side via WordPress oEmbed.
* i18n: All new congress strings (admin editor + visitor page) translated into 11 locales (de_DE, de_CH, es_ES, fr_FR, hu_HU, it_IT, ja_JP, nl_NL, pt_BR, pt_PT, zh_CN), including the JavaScript UI (script-translation JSON).
* Performance: Transient cache (5 min) for access checks; ETag/Last-Modified for browser cache invalidation.
* Fix: Same-day cutoff time was not working when the product also had a fixed start date configured. The absolute start date was applied after the cutoff check and silently overrode it, leaving today selectable past the cutoff time. Cutoff check now runs after all date overrides and correctly disables today in every configuration.
* Fix: Same-day cutoff time is now validated server-side at checkout. A customer who selected today's date before the cutoff and held the cart past it is now blocked at checkout.
* New: Congress can be switched active/inactive per congress (on top of the global on/off) — hide it everywhere without unassigning, e.g. to publish the info only later after purchase.
* New: The Vollstart Wallet shows a congress button on the ticket; it enables/disables on the next refresh based on the global switch, the per-congress active flag, the order status, and the access window (which now derives the event date from the ticket's product). Outside the window it shows a disabled "available from …" hint.
* Changed: Wallet API now returns a generic, extensible `actions` array (future content types reuse it) instead of the single `congress_url` field.
* Changed: Removed the redundant per-congress "Event start/end" fields from the congress editor — the event date comes from the WooCommerce product.

= 3.0.10 - 2026-05-18 =
* Fix: Same-day cutoff time was not working when the product also had a fixed start date configured. The absolute start date was applied after the cutoff check and silently overrode it, leaving today selectable past the cutoff time. Cutoff check now runs after all date overrides and correctly disables today in every configuration.
* Fix: Same-day cutoff time is now validated server-side at checkout. A customer who selected today's date before the cutoff and held the cart past it is now blocked at checkout.

= 3.0.9 - 2026-05-18 =
* New: Day-chooser products can now define a "Same-day cutoff time" (HH:MM) in the product's ticket settings. When the customer's local time is past that value, today's date is automatically disabled in the date picker — only tomorrow and future dates remain selectable. The field has no effect when "Offset days for start date" is already ≥ 1 (today would be excluded anyway).
* Fix: Same-day cutoff time was ineffective on products that also have a fixed start date (ticket_start_date) configured. The absolute start date was applied after the cutoff check and silently overrode it, leaving today selectable past the cutoff. The cutoff check now runs after all min-date overrides and correctly shifts the minimum to tomorrow whenever today would still be reachable.
* New: CVV-at-Scanner — a per-product opt-in second-factor for ticket redemption. Enable "Require security code at scanner" (`saso_eventtickets_require_cvv_at_scanner`) on any ticket product; a unique 4-character alphanumeric code (uppercase A–Z plus 2–9, no visually ambiguous O/0/I/1) is generated automatically at WC order-completion time and stored alongside the ticket. At the scanner, the ticket's public ID is shown immediately on QR scan but all identifying information (name, seat, billing details, product title) is withheld until the bearer reads the code from their ticket and enters it. An incorrect code counts as one strike; after 5 consecutive wrong attempts the ticket locks and the scanner shows a prominent locked-screen warning — the attendant must contact the organiser to reset via the "Reset CVV attempts" button in the admin code-edit modal. The default UI is a two-stage hand-over flow: first stage confirms the public ticket ID and prompts the organiser to hand the device to the attendee; second stage shows the masked code entry field with a show/hide toggle. An optional one-stage flow (global option `wcTicketScannerCVVOneStageFlow`, default off) skips the hand-over prompt for high-trust environments. Input masking is on by default and controlled globally via `wcTicketScannerCVVMaskInput` (default on). The anti-information-leak short-circuit response from REST endpoints exposes only `requires_cvv`, `attempts_remaining`, `locked`, and `public_ticket_id` — no product name, seat, billing, or order metadata is returned before a correct CVV is entered.
* New: Order-ticket QR codes (one QR for an entire order) now honor the per-product CVV requirement on a per-ticket-row basis. When an order QR is scanned and one or more of its tickets is from a CVV-protected product, the scanner UI renders a compact CVV input next to each protected row instead of revealing name/seat/order details for it, while non-protected rows in the same order continue to show full information. A "Confirm codes" button at the bottom submits all entered codes in one round-trip; rows with a correct CVV unlock and become fully redeemable. Wrong-CVV rows decrement the per-ticket attempt counter (same 5-strike lockout as single-ticket scans); attempts on one row do not affect other rows. Anti-information-leak: blocked rows expose only `requires_cvv`, `attempts_remaining`, `locked`, and `public_ticket_id`.
* New: CSV ticket export gains two new columns — `meta_cvv_attempts` (number of failed attempts so far) and `meta_cvv_locked` (boolean, 1 if the 5-strike limit was reached) — so organisers can audit CVV activity across all tickets without opening individual admin screens.

= 3.0.8 - 2026-05-11 =
* Fix: On variable products, the parent's "Request a value per ticket", "...mandatory" and the matching name-per-ticket checkboxes silently turned themselves back ON every time anything on the product was saved (e.g. flipping a variation's stock status). Root cause: each variation row submits an override `<select name="<key>[<i>]">` under the same root name as the parent's checkbox/input, so on a variable-product save `$_POST[<key>]` arrived as an array. The save handler treated "key is present in POST" as "checkbox is checked" and overwrote the parent meta with `yes` — which is also why the cart kept asking customers to fill in a value field even when, in the admin, the boxes looked unchecked. The handler now ignores array values for parent-level fields (`request_name/value_per_ticket(_mandatory|_label|_def)`, `ticket_start/end_date/time`, `ticket_amount_per_item`, and Premium's `expiration_days`). After updating, simply uncheck the affected boxes once and they will stay unchecked. No data migration is required.
* Fix: Fatal TypeError when adding products to cart from a wishlist (or any 3rd-party plugin that calls `WC_Cart::add_to_cart()` without supplying `$variation` as an array). The internal handler used a strict `array` type-hint and aborted the whole cart operation — it now accepts any input and normalizes defensively. Affects sites running WooCommerce Wishlists.
* Fix: Ticket configuration fields (date, dropdown, name-per-ticket, etc.) no longer appear on every variation of every variable product in the admin. They are now only rendered when the parent product is explicitly marked as a ticket — same UX rule that already applies to simple products. Catalogs with thousands of non-ticket variable products no longer have to opt out per variation.
* Fix: Cart and checkout no longer render ticket-specific input fields (name-per-ticket, value-per-ticket, daychooser, seat info) for products whose parent is not (or no longer) a ticket. The `is_ticket` flag on the parent product is now the single source of truth — stale variation/product meta from a previous configuration is ignored. Validation at checkout follows the same rule and no longer blocks orders for non-ticket products with leftover meta. Purchase-restriction codes are unaffected and continue to work on any product type.
* Fix: Cart and checkout input fields for the name-per-ticket, value-per-ticket and purchase-restriction features now use standard WooCommerce form-row markup (`<p class="form-row form-row-wide"><label>…</label><input class="input-text" />`) instead of a custom `<small>`-label wrapper. This lets the active theme's CSS apply normally so the fields look consistent with the rest of the cart/checkout form.

= 3.0.7 - 2026-05-05 =
* Fix: License-key modal no longer nags on every admin page load. Clicking "Later" now silences the prompt for 24 hours (per browser, via localStorage) instead of re-popping on the next click. Customers can still enter the key any time on the plugin's Options page — the field is rendered there.
* New: Eventado public-calendar teaser added to the "What's New" version-notices system on the plugin's settings page (the same release-announcement surface that already carries the Wallet card). Active for all users until "Dismiss" is clicked; dismissing pins it to the current plugin version, so it re-appears on the next plugin update.
* New: Authtoken audit on ticket redemption — when an external scanner team uses an authtoken (no WP login), the token's database id is now stored in the redeem record (`redeemed_via_authtoken_id`). Backend ticket detail view shows "Redeemed via authtoken: NAME (#ID)" alongside any WP-user redeem info; CSV export gains `meta_wc_ticket_redeemed_via_authtoken_id` + `_name` columns. The token name is resolved live with per-request caching, so a renamed token shows the current name everywhere — only the id is persisted.
* i18n: 13 Premium-side strings now ship with translations in all 11 languages (de_DE, de_CH, en, es_ES, fr_FR, hu_HU, it_IT, ja_JP, nl_NL, pt_BR, pt_PT, zh_CN). Strings cover the new Eventado options (publish toggle, per-product exclude, 18+ flag, sync notice, license-expiry banner) and a few Premium options that previously only existed as English source strings.
* i18n: Context-Wizard suggestion-card system gains a new card for the Eventado publishing option (Premium only). Replaces the temporary admin_notices banner from 1.6.8 — uses the existing dismiss-per-user flow, renders inline on the options page next to the Wallet suggestion.
* i18n: Premium plugin now uses the basic plugin's textdomain consistently — strings that lived under `event-tickets-with-woocommmerce-premium` (license-expiry messages) are migrated to `event-tickets-with-ticket-scanner`, so a single .mo file per language covers both plugins.

= 3.0.6 - 2026-05-04 =
* New: "First Activated" date in Support Info — shows when the plugin was first activated, included in the copyable support text. Marked as "(estimate)" for installs predating this version.
* Internal: First-activation timestamp is sent to the license server with the next license check (Premium plugin) for free-to-paid analytics.
* Improvement: Day chooser auto-jumps to the first month with available dates. If the current month has no free slots (all days excluded by weekday rules, blackout dates, or min-date), the picker now opens directly on the next month that has at least one selectable day — customers no longer see a frustrating all-grey calendar. Scans up to 24 months forward.

= 3.0.5 - 2026-04-24 =
* Fix: "Update available" badge stayed visible after auto-upgrade of the premium plugin — the `update_plugins` site transient was only cleared BEFORE the upgrade, not after. WordPress then kept the stale "update available" entry until the next WP-Cron (12h later). The transient is now cleared + re-checked after the upgrade completes, so the red badge disappears on the next page load.
* Improvement: License activation modal now locks the UI while the license check runs — "Activate" button and input field are disabled, a clear spinner with "Validating license — please wait…" replaces the small status text. Prevents multiple clicks and confusion during the 2–5s background check.

= 3.0.4 - 2026-04-23 =
* Fix: License activation flow — after entering a license key, the "Starter plugin installed" and "subscription expired" banners now disappear immediately (via JS fade + 60s server-side suppression) instead of staying visible until the next page load. Page now always reloads after a serial is saved to reflect the new plugin state.
* Fix: "Subscription expired" banner no longer shown when no license key is configured (was confusing new customers who had just installed the starter plugin).
* Fix: PHP warning "Undefined array key SCRIPT_NAME" when plugin runs in CLI or WP-Cron context — index.php:92 and :578 now use null-safe access. Same class of bug as the REQUEST_METHOD fix in 3.0.3 but for SCRIPT_NAME.
* Fix: Hardened `SASO_EVENTTICKETS::issetRPara()` against missing REQUEST_METHOD in CLI/cron context.

= 3.0.3 - 2026-04-20 =
* New: Auto-upgrade premium plugin after valid license key is saved — no more manual "Update" click needed. Customers who install the starter plugin and enter their license now get the real premium installed automatically in the background.
* Fix: PHP 8.2+ fatal error "Call to undefined function trigger_deprecation()" — Twig Composer autoloader conflict when another plugin bundles Twig. Added polyfill before autoload.
* Fix: PHP warning "Undefined array key REQUEST_METHOD" when plugin runs in CLI or WP-Cron context
* Fix: Day chooser label not visible on product and shop pages — empty string prevented DB lookup and rendering
* Fix: Day chooser showed wrong date in cart after re-adding same product — stale session data was not cleaned up on cart item removal. Added session_unset_value() cleanup.
* Fix: Day chooser dates grew unbounded on WC cart merge — old dates were appended instead of correctly filling only new ticket positions
* Fix: Day chooser on shop/archive page now works with WooCommerce Blocks — click interceptor appends date as GET parameter since WC Blocks no longer fires jQuery adding_to_cart event
* Fix: Day chooser input now blocks keyboard typing and paste via JS — users must pick from datepicker. Server-side validation also checks excluded weekdays and specific dates (Premium).
* Improvement: Datepicker wrapper has CSS class `saso-eventtickets-datepicker` and `data-product-id` for custom styling
* New: Variable fields (name input + value dropdown) now configurable per WooCommerce variation — override enable, label, mandatory, and dropdown values per variation with automatic fallback to parent product settings
* New: Attendance tab — "Sold Tickets" calendar view showing booked dates across all date picker products with product filter, month navigation, drill-down to individual tickets, CSV export, and auto-refresh (10s/30s/1min/5min) for live dashboard use
* New: Error logs CSV export in Support Info
* Improvement: Support Info layout — two-column grid for cards, URLs and Libraries side by side
* Improvement: Sold Tickets Calendar button visible on all ticket products (disabled with hint for non-datepicker products)
* Improvement: Print List table column widths fixed (# 1cm, Order/Status 2cm)
* New: "More Plugins by Vollstart" submenu page — discover other free plugins from the same team

= 3.0.2 - 2026-04-12 =
* Fix: Old premium (< 1.5.0) compatibility — ReflectionClass-based method check BEFORE instantiation prevents "ghost hook" crashes. The old premium constructor registers 25+ WordPress hooks on $this; if the class was instantiated and then discarded, WordPress kept the callbacks alive in memory and fired them later, crashing the site on methods removed in the WC manager refactor. Now the compatibility check happens without ever running the constructor.
* Fix: DB migration for redeemed codes no longer loads all rows into PHP memory — replaced SELECT+loop with a single SQL UPDATE, preventing memory exhaustion on large databases (1M+ tickets)
* New: Vollstart Wallet integration — customers can add tickets to the Vollstart Wallet app (wallet.vollstart.com) to collect tickets from multiple shops in one place. Enable in plugin settings under "Digital Wallets". Adds "Add to Vollstart Wallet" button on ticket detail page and in order emails. Includes REST API endpoint for the wallet app.
* New: Wallet option in Setup Wizard — all 4 presets (Event, Day Pass, Membership, Voucher) now include the Vollstart Wallet toggle
* New: Context suggestion for existing installations — prompts to enable Vollstart Wallet when tickets exist
* New: REST API warning in wallet option when WordPress REST API is disabled
* New: Version notice system — shows "What's New" messages after plugin updates, dismissible per version
* New: Wallet test link in admin ticket detail view — quickly import a ticket to Vollstart Wallet for testing
* Fix: Hard-throttled periodic license check via site transient — prevents runaway license requests when admin pages are hit rapidly by bots, monitoring tools, or other plugins
* Improvement: Updated all translations — 11 new wallet strings added to all 11 languages

= 3.0.1 - 2026-04-07 =
* Improvement: Updated all translations — regenerated POT from source, added 188 new strings, all 11 languages at 100% (de, es, fr, hu, it, ja, nl, pt_BR, pt_PT, zh_CN)
* Improvement: Premium plugin labels (Ticket Template, Excluded Days, License Key etc.) now included in translations

= 3.0.0 - 2026-03-31 =
* Fix: Admin CSS was cached by Cloudflare/browsers after plugin updates — added version-based cache buster to dynamically loaded stylesheet
* Fix: Scoped global CSS button selectors to plugin container — no longer affects WP admin sidebar
* Fix: Backend CSS now loaded via wp_enqueue_style instead of JS injection (prevents layout shift)
* Fix: Removed duplicate Settings submenu entry (top-level menu only)
* New: QR code scanner on the validator form — users can scan QR codes with their device camera instead of typing codes manually (enable in plugin settings)
* New: License key prompt dialog on settings page instead of browser alert — with direct link to options page
* Fix: Ticket table button column no longer forces minimum width (cleaner layout on small screens)

= 2.9.9 - 2026-03-23 =
* Improvement: License check resilience — brief server outages no longer disable premium features; raised failure threshold from 5 to 10, extended staleness window from 10 to 21 days, and server errors (5xx, empty body) now count as transient failures instead of genuine rejections
* Fix: "Check License" button now resets failure counter before server check — previously, accumulated failures from a server outage could prevent recovery even after the server was back online
* Improvement: Modern admin UI redesign — card-based layout with consistent spacing, branded purple accent color, professional footer grid, improved DataTable styling, smooth loading animations, and responsive design
* New: Plugin header now includes Requires at least, Tested up to, and Requires PHP — displayed in support info area for easier troubleshooting
* Fix: Corrected 10 English typos in source strings that caused translation mismatches (e.g. successfull, duplicat, choosen)
* Fix: No more flash of unstyled content when loading admin page
* Improvement: Regenerated all German backend translations — from 162 to 451 strings (full coverage)
* New: FOMO banner for expired premium subscriptions — shows new features released since expiration with dismissible 30-day reminder
* Fix: Added missing License header to plugin file to satisfy WordPress.org plugin check

= 2.9.8 - 2026-03-11 =
* Fix: TypeError in WooCommerce email preview and order emails — removed strict type hints from all WooCommerce hook callbacks to prevent crashes with different WC versions

= 2.9.7 - 2026-03-11 =
* Fix: Options migration safety net — premium options (e.g. PDF ticket attachment) are now preserved even if the premium plugin was inactive during the database migration from wp_options to the custom options table

= 2.9.6 - 2026-03-10 =
* New: Daily Redemption Summary — view redeemed tickets across all products for any date range (Premium)
* New: No-Show count in Attendance view showing unredeemed tickets per event (Premium)
* New: Drill-down from Attendance rows to individual ticket codes with order links (Premium)
* New: Export Attendance data as CSV (Premium)
* Improvement: Seating CSV export/import now includes visual designer data — position, rotation, shape, and color survive roundtrip (Premium) (#209)
* Fix: Seating CSV export appended JSON artifact at end of file — missing exit after CSV output (#209)
* New: Calendar view and printable ticket list on product edit page for daychooser products (#191)
* Performance: Lazy-load single options via getOption() instead of bulk-loading all options
* New: License Server Connectivity Check — diagnostic button in Support area tests if vollstart.com license/update server is reachable. Shows connection status, response time, and detailed error messages. Helps diagnose Premium update issues caused by firewalls, DNS problems, or network restrictions.
* Improvement: Subscription expiration warnings now distinguish between license types — lifetime/onetime licenses show no warning (continue working with Basic < 2.8.0), monthly subscriptions warn about payment processing, yearly subscriptions warn about expiration.
* Improvement: Renamed "Serial Key" to "License Key" in all user-facing strings for clarity
* Improvement: Complete translations for all 11 languages (DE, ES, FR, HU, IT, JA, NL, PT_BR, PT_PT, ZH_CN, DE_CH) — 100% coverage

= 2.9.5 - 2026-03-03 =
* Fix: Premium serial key displayed as 0 after options migration — premium features appeared lost. Options added late during initialization (e.g. by the premium plugin) are now loaded correctly from the custom table.

= 2.9.4 - 2026-03-03 =
* Performance: Plugin options migrated from individual wp_options rows to a dedicated custom table — all settings now load in a single query instead of 150+ separate queries (#73)
* New: Options change history — tracks who changed which setting, when, with old and new values. Revert button to restore previous values. Keeps last 10 entries per option, older entries cleaned up automatically. Available in Options > Change History tab (#73)
* New: Context-Wizards — smart suggestions on the options page based on your current configuration. Detects related settings and offers to enable them with one click. Covers email attachments, scanner presets, ticket display, and security settings. Dismissals are per-user (#232)
* New: Export and import seating plan seats via CSV for backup, external editing, or transfer between plans (Premium) (#209)
* Fix: Old premium plugin (< 1.6.0 or without version constant) caused crash instead of being gracefully blocked
* Fix: Options migration is now abort-safe — if interrupted, it resumes on next page load instead of silently losing settings. Existing values are updated correctly on re-run.
* Fix: Database upgrade jobs now run before saving the new DB version, preventing incomplete upgrades from being marked as finished
* Fix: Premium or third-party upgrade hooks that throw exceptions are now caught and logged instead of causing an infinite crash loop
* Security: Activate global ticket counter brake to prevent free limit bypass via delete-and-recreate

= 2.9.3 - 2026-03-02 =
* New: "Check License" button — recheck premium license status on demand from Options page (next to serial field) and Support Info page. Shows status, last check, expiration, failure count.
* Improvement: Saving a serial key now immediately checks the license and shows the result inline. On success, the page reloads automatically so premium features are available without manual refresh.
* Improvement: "Check License Now" button bypasses the 7-day server cache to always get a fresh response
* Fix: Dismiss button for ticket format warnings did not work — two bugs: nonce parameter name mismatch, and wp_redirect() called during admin_notices (after output started). Dismiss handler now runs on admin_init.
* Fix: JavaScript error "cannot call methods on dialog prior to initialization" — closeDialog() tried to destroy all dialog elements on page instead of only the target dialog
* Fix: backend.js could be cached by browser across updates — jQuery.getScript() bypassed WordPress cache busting. Now uses file modification time as additional cache-buster parameter.
* New: Setup Wizard — multi-step dialog guides new users through use-case selection (Event tickets, Day passes, Memberships, Vouchers) and configures optimal settings automatically. Covers redemption rules, scanner behavior, email delivery (ICS, date, order view), and order processing. Premium users also get PDF attachment settings. Re-launchable via "Start Wizard" button.
* New: Premium Wizard — one-time dialog when premium is activated, offers to enable recommended premium defaults (PDF email attachment, merge into one PDF). Re-launchable via "Premium Wizard" button (only visible with premium). (#233)
* New: Export/Import options — backup and restore all plugin settings as JSON file
* New: "Max redeems per day" option per product — limit how many times a multi-redeem ticket can be redeemed on a single day (0 = unlimited, only total max applies)
* Improvement: "First Steps" onboarding box upgraded to interactive card with progress bar, auto-detection of completed steps, and action buttons
* Improvement: Code generation now uses date-based prefix (encoded as 5 letters) to partition address space — virtually eliminates collisions even with short code formats
* Improvement: Increased code generation retry limit from 100 to 500 attempts
* Improvement: Ticket detail view now shows WooCommerce order status, billing email, product name, and variation attributes
* Improvement: Support Info — options list is now hidden behind a button click to reduce page load clutter
* Improvement: Bulk action dropdown now shows a message when no tickets are selected
* Improvement: AJAX calls now properly handle network-level errors (timeout, connection lost)
* Improvement: Cart input fields (name per ticket, restriction code) now auto-save on Enter key (#234)
* New: Auto-update dialog — after entering a serial key with an outdated premium plugin, automatically checks for updates and offers to install them. Shows release notes for users without an active subscription.
* Fix: Value-per-ticket dropdown in cart never saved — selector targeted wrong element type and attribute (#234)
* Fix: Options in-memory cache corruption in _setOptionValuesByKey() — variable name collision caused stale/incorrect values within the same request
* Fix: Restriction code input in cart crashed on PHP 8.x due to wrong constant name case (META_KEY_CODELIST_RESTRICTION_order_item → _ORDER_ITEM)
* Fix: False "Ticket format exhausted" warning when free version ticket limit (32) was reached — #208 exception was incorrectly triggering format warning on first attempt
* Fix: clearFormatWarning() never actually cleared warnings — editList() was called with wrong signature, exception silently caught
* Fix: "Edit list" link in format warning notice and email pointed to non-existent admin page
* Fix: Import options failed silently due to WordPress wp_magic_quotes escaping JSON — now handled via stripslashes
* Fix: Product meta save could trigger PHP warning for undefined array key due to operator precedence bug
* Fix: Admin ping timeout (e.g. when browser tab is frozen) no longer shows repeated error dialogs
* Fix: Typo in displayFirstStepsHelp option description ("activet" → "activated")

= 2.9.2 - 2026-02-23 =
* Fix: Format warning data (attempts, last_email) was never persisted — editList() called with wrong signature and would auto-clear warnings

= 2.9.1 - 2026-02-23 =
* Fix: Crash during checkout when format warning check runs (undefined method _json_encode_with_error_handling)

= 2.9.0 - 2026-02-23 =
* Fix: Crash on PHP 8.x when plugin loading order puts basic before premium (e.g. after auto-update) — now defers premium loading via plugins_loaded hook
* Fix: Crash in get_expiration() on PHP 8.x when license data contains invalid JSON
* New: Old premium versions (< 1.6.0) are now gracefully blocked instead of causing fatal errors — site runs as free with admin warning
* New: Serial key field shown in basic plugin settings when old premium is detected, so users can still manage their license
* Improvement: License check now properly throttles retries when serial key is invalid, preventing excessive server calls
* Improvement: Changing the serial key now immediately triggers a fresh license check instead of waiting for the next cycle
* Fix: Invalid serial key no longer causes license check on every page load
* Fix: All non-German translations (NL, ES, IT, PT_BR, PT_PT, HU, JA, ZH_CN) replaced with proper localized translations — previously contained German text
* Fix: Renewal and reactivation links in admin notices pointed to wrong URL
* Fix: Ticket redemption blocking ("deny redeem before event start") was off by the GMT offset for non-UTC timezones

= 2.8.10 - 2026-02-18 =
* Fix: Premium license recovery — license revalidation now runs even when local license data is stale, preventing permanent premium lockout
* Fix: Stale "notvalid" flag is now cleared when the license server confirms a valid subscription

= 2.8.8 - 2026-02-18 =
* Fix: Option "No login required to access scanner" (wcTicketScannerAllowedRoles) now works correctly again for non-authenticated users
* Fix: PHP 8.4 compatibility — explicit nullable type hints for deprecated implicit nullable parameters
* Improved: Updated translations for all 11 languages (DE, ES, FR, HU, IT, JA, NL, PT-BR, PT-PT, ZH) with 16 new strings from v2.8.6 and v2.8.7 (PWA, scanner options, format warnings)
* New: Ticket scanner shows timestamp of last scan for better tracking
* New: Admin notice when PHP version is below the required 8.1
* Improved: Corrected library version display in system info (FPDF 1.85, Twig PHP requirement note)

= 2.8.7 - 2026-02-16 =
* New: PWA (Progressive Web App) support — install the ticket scanner as a home screen app on mobile devices (optional, enable in settings)
* New: Fullscreen mode button for the ticket scanner — immersive scanning without browser chrome
* New: Haptic feedback (vibration) on scan result — different patterns for success and failure
* New: Customizable scanner theme color — affects PWA status bar, loading spinner, and scanner buttons
* New: Expanded FAQ with 7 categories and 21 questions covering scanner setup, PDF design, WooCommerce, webhooks, and more
* Improved: Scanner HTML now uses proper DOCTYPE, charset, and viewport meta tags
* Improved: Scanner assets (JS libraries, CSS, images) cached by service worker for faster loading
* Security: Hardened premium license revalidation with independent subscription verification
* Security: Added HMAC checksum protection against license data tampering in database
* Fix: Resolved infinite recursion during plugin initialization when premium plugin is active
* Fix: Translation loading no longer triggers WordPress 6.7+ "too early" notice
* Fix: License checksum verification now stable across CLI and web contexts
* Fix: License server response now correctly overrides local data (fixed array_merge order)
* Fix: API connection failures are now tracked; premium deactivates after 5 consecutive failures
* Fix: Admin page loads now trigger license check if last check was >24h ago (cron fallback)
* Fix: License key input field now always visible when premium plugin is installed

= 2.8.6 - 2026-02-11 =
* New: Full bleed mode option for ticket designer — removes ALL margins for edge-to-edge background images.
* New: Separate full bleed option for test ticket designer.
* New: Ticket background color option — set a custom background color for PDF tickets, badges, and flyers.
* New: Ticket number format exhaustion warning — alerts admin when ticket format is running out of combinations (counter-based detection).
* New: Automatic email notifications when ticket format reaches 50% capacity or is exhausted (once per day).
* New: Admin notice with dismiss button for format warnings, includes direct link to edit ticket list.
* New: Format warning auto-clears when ticket list is manually saved (user likely adjusted formatter).
* Security: Fixed unauthenticated REST endpoint that exposed admin dispatcher (CVSS 9 — reported by Patchstack).
* Security: REST permission callback now requires authentication (login or authtoken) by default.
* Security: Added capability guards for sensitive admin actions (defense in depth).
* Security: Removed wp_rest nonce fallback from admin action handler.

= 2.8.5 - 2026-01-26 =
* Fix: Fatal error on non-premium installations when SASO_EVENTTICKETS_PREMIUM_PLUGIN_VERSION constant is undefined.
* Improved: VollstartValidatorDebug URL parameter now passed through to AJAX requests for easier debugging.

= 2.8.4 - 2026-01-26 =
* New: Seating plan visualization in ticket scanner — view venue map with highlighted seat position.
* New: Option to enable/disable seating plan button in scanner.
* New: Option to enable/disable venue image button in scanner.
* Improved: Seating plan data loaded on demand (lazy loading) for better scanner performance.
* New: Shortcode parameter `order_id` for [sasoEventTicketsValidator_code] to display tickets from a specific order.
* New: Shortcode [sasoEventTicketsValidator_ticket_detail] to display ticket detail view on any page.
* Fix: Date localization now correctly displays translated month/day names while maintaining correct timezone handling.
* Fix: Fatal error in removeUsedInformationFromCode() when changing order status.

= 2.8.3 - 2026-01-22 =
* New: Seating Designer — Element rotation (0-359°) with preset buttons.
* New: Seating Designer — Group rotation (rotate multiple selected elements around their common center).
* New: Seating Designer — Bulk property editing when multiple elements are selected.
* New: Seating Designer — Duplicate selection for groups (copy multiple elements at once).
* New: Seating Designer — Enhanced visual feedback for multi-selected elements.
* Improved: Seating API now uses unified bulk operations for better performance.
* Fix: PDF QR code compatibility with third-party FPDI libraries.
* Fix: Event times displayed incorrectly due to double timezone conversion in ticket templates.

= 2.8.2 - 2026-01-21 =
* New: Clone/duplicate seating plans with all seats and layout.
* New: Batch operations for seats (activate, deactivate, delete multiple seats at once).
* New: Premium subscription expiration check with admin warnings and grace period.
* New: Lifetime license support.
* New: Delete All Tickets button on ticket list with double confirmation.
* New: Shortcode parameter download_all_pdf for downloading all user tickets as one PDF.
* New: Safety check when deleting ticket lists — warns if list is still assigned to products.
* Fix: Restored deprecated methods for backward compatibility with older premium plugin versions.

= 2.8.1 - 2026-01-20 =
* Fix: PHP 8.4 compatibility — explicit nullable type for REST API parameter.

= 2.8.0 - 2026-01-20 =
* New: Interactive seating plan designer with drag & drop editor for creating venue layouts.
* New: Let customers choose their seats during checkout with visual seat selection.
* New: Seat information displayed on PDF tickets, in emails, and in the ticket scanner.
* New: Automatic seat blocking during checkout with configurable timeout.
* New: Seat release on order cancel or refund.
* New: Seating plan support on shop/archive pages.
* New: Voice output for the ticket scanner.
* Improved: Security checks for admin area access.
* Improved: Public ticket number now displayed in emails instead of internal ticket number.
* Fix: Ticket list description slashes.
* Fix: Customer search in admin backend.
