{"id":328079,"date":"2026-06-21T10:34:22","date_gmt":"2026-06-21T10:34:22","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/pay4all-form\/"},"modified":"2026-07-14T18:53:43","modified_gmt":"2026-07-14T18:53:43","slug":"pay4all-form","status":"publish","type":"plugin","link":"https:\/\/fi.wordpress.org\/plugins\/pay4all-form\/","author":23516902,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"2.3.0","stable_tag":"2.3.0","tested":"7.0.1","requires":"5.9","requires_php":"7.4","requires_plugins":null,"header_name":"Pay4All Shop","header_author":"Pay4All","header_description":"Create order forms with quantity rules, delivery options and order management. Compatible with Pay4All Pro for TWINT payments.","assets_banners_color":"cabde9","last_updated":"2026-07-14 18:53:43","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"","header_author_uri":"https:\/\/pay4all.ch\/","rating":0,"author_block_rating":0,"active_installs":0,"downloads":492,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.0.0":{"tag":"1.0.0","author":"pay4all","date":"2026-06-21 10:06:39"},"1.1.0":{"tag":"1.1.0","author":"pay4all","date":"2026-06-21 16:33:10"},"1.1.1":{"tag":"1.1.1","author":"pay4all","date":"2026-06-21 16:39:53"},"1.1.2":{"tag":"1.1.2","author":"pay4all","date":"2026-06-21 17:17:23"},"1.1.3":{"tag":"1.1.3","author":"pay4all","date":"2026-06-22 11:23:10"},"1.1.4":{"tag":"1.1.4","author":"pay4all","date":"2026-06-22 11:40:48"},"1.2.0":{"tag":"1.2.0","author":"pay4all","date":"2026-06-25 06:46:51"},"1.2.1":{"tag":"1.2.1","author":"pay4all","date":"2026-06-25 06:55:52"},"1.2.2":{"tag":"1.2.2","author":"pay4all","date":"2026-06-25 07:04:46"},"1.3.0":{"tag":"1.3.0","author":"pay4all","date":"2026-06-29 14:16:35"},"1.3.1":{"tag":"1.3.1","author":"pay4all","date":"2026-06-29 14:52:22"},"1.4.0":{"tag":"1.4.0","author":"pay4all","date":"2026-07-01 15:53:35"},"1.4.1":{"tag":"1.4.1","author":"pay4all","date":"2026-07-04 18:04:03"},"1.4.2":{"tag":"1.4.2","author":"pay4all","date":"2026-07-06 15:20:14"},"1.4.3":{"tag":"1.4.3","author":"pay4all","date":"2026-07-06 15:50:33"},"2.0.0":{"tag":"2.0.0","author":"pay4all","date":"2026-07-10 14:09:52"},"2.3.0":{"tag":"2.3.0","author":"pay4all","date":"2026-07-14 18:53:43"}},"upgrade_notice":{"2.3.0":"<p>Adds a \u00ab Virement bancaire \u00bb payment method with a per-language IBAN \/ BIC \/ beneficiary block, shown at checkout and injected into the customer confirmation e-mail in the shopper&#039;s own language. No schema change, no manual action.<\/p>","2.2.0":"<p>Adds a per-form photo size picker (Petite \/ Moyenne \/ Grande \/ Personnaliser). No schema change, no manual action \u2014 existing forms keep their previous look until the merchant picks a size.<\/p>","2.1.0":"<p>Adds an opt-in \u00ab Remember my information \u00bb checkbox (client-side localStorage, no cookie) and auto-skips the Pay4All Age step when the cart carries no age-restricted product. No schema change, no manual action.<\/p>","2.0.0":"<p>Breaking : Categories, Brands, Tickets, Abandoned Cart and filter widget move to Pay4All Pro. On upgrade the legacy tables <code>wp_p4all_tickets<\/code> + <code>wp_p4all_abandoned_carts<\/code> are dropped, ticket posts deleted, cron cancelled. Back up first. XLSX export replaced by CSV.<\/p>","1.4.3":"<p>Adds a \u00ab \ud83d\udd17 Search \u00bb button next to each per-language URL field on products to pick a page, post, WooCommerce product or media (PDFs, images\u2026) from the site. Detects WPML \/ Polylang automatically. Full DE\/IT\/EN translations. Also clears three Plugin Check nits.<\/p>","1.4.2":"<p>Adds a per-language \u00ab Learn more \u00bb link on every product (URL + label, defaults FR\/DE\/IT\/EN) and a full-size photo lightbox on the form. Security pass: path-traversal fix on virtual downloads, SAMEORIGIN default on shortcode pages, extension allowlist on virtual uploads.<\/p>","1.4.1":"<p>Adds client-side product filtering (per form), multilingual category \/ brand labels, and moves the <em>Montant minimum de commande<\/em> control to each form (previously global). Existing global threshold stays effective as fallback until you configure per-form values. No schema change, no manual action.<\/p>","1.4.0":"<p>Adds optional <em>Protection des mineurs<\/em> (age verification) with mobile-side ID + selfie capture, AES-256-GCM encryption at rest, admin-only decrypted view and auto-purge on order deletion. No schema change, no manual action; opt-in per product via the new <em>Requires age verification<\/em> checkbox.<\/p>","1.3.1":"<p>Adds a Timezone selector in <em>Settings \u203a General<\/em>, fixes abandoned-cart date display via <code>get_date_from_gmt()<\/code>, and clears Plugin Check warnings. No schema change, no manual action.<\/p>","1.3.0":"<p>Adds abandoned-cart capture + automatic recovery e-mail (configurable delay, default 10 days), new admin page, GDPR-aware. Please deactivate \/ reactivate once so the new table and hourly cron are installed.<\/p>","1.2.0":"<p>Adds <em>Hide this product<\/em> and optional per-product stock management: auto out-of-stock at 0, restock on order delete, low-stock e-mail alert (threshold in <em>Settings<\/em>) and a <em>Stock<\/em> admin column. No schema change, no manual action.<\/p>","1.1.2":"<p>Change name plugin and order menu<\/p>","1.1.0":"<p>Adds ticket QR codes, virtual file downloads, out-of-stock flag, per-delivery payment methods and pre-translated default fields. Please deactivate and reactivate the plugin once after upgrading so the new <code>wp_p4all_tickets<\/code> table and the <code>\/p4all-ticket\/...<\/code> rewrite rule are installed.<\/p>","1.0.0":"<p>First public release.<\/p>"},"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3581455,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3581455,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3581466,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3581466,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["1.0.0","1.1.0","1.1.1","1.1.2","1.1.3","1.1.4","1.2.0","1.2.1","1.2.2","1.3.0","1.3.1","1.4.0","1.4.1","1.4.2","1.4.3","2.0.0","2.3.0"],"block_files":[],"assets_screenshots":{"screenshot-1.png":{"filename":"screenshot-1.png","revision":3580503,"resolution":"1","location":"assets","locale":"","width":1518,"height":2425},"screenshot-2.png":{"filename":"screenshot-2.png","revision":3580503,"resolution":"2","location":"assets","locale":"","width":1518,"height":2425},"screenshot-3.png":{"filename":"screenshot-3.png","revision":3580503,"resolution":"3","location":"assets","locale":"","width":1518,"height":2425},"screenshot-4.png":{"filename":"screenshot-4.png","revision":3580503,"resolution":"4","location":"assets","locale":"","width":1518,"height":2425},"screenshot-5.png":{"filename":"screenshot-5.png","revision":3580503,"resolution":"5","location":"assets","locale":"","width":1518,"height":2425}},"screenshots":{"1":"Example of an order form and delivery options.","2":"Payment method selection, confirmation and payment via TWINT.","3":"Dashboard, order list and language selection.","4":"Order form configuration."}},"plugin_section":[],"plugin_tags":[359,1887,702,193458,268242],"plugin_category":[45],"plugin_contributors":[268195],"plugin_business_model":[],"class_list":["post-328079","plugin","type-plugin","status-publish","hentry","plugin_tags-order-form","plugin_tags-payments","plugin_tags-products","plugin_tags-twint","plugin_tags-woocommerce-twint","plugin_category-ecommerce","plugin_contributors-pay4all","plugin_committers-pay4all"],"banners":{"banner":"https:\/\/ps.w.org\/pay4all-form\/assets\/banner-772x250.png?rev=3581466","banner_2x":"https:\/\/ps.w.org\/pay4all-form\/assets\/banner-1544x500.png?rev=3581466","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/pay4all-form\/assets\/icon-128x128.png?rev=3581455","icon_2x":"https:\/\/ps.w.org\/pay4all-form\/assets\/icon-256x256.png?rev=3581455","generated":false},"screenshots":[{"src":"https:\/\/ps.w.org\/pay4all-form\/assets\/screenshot-1.png?rev=3580503","caption":"Example of an order form and delivery options."},{"src":"https:\/\/ps.w.org\/pay4all-form\/assets\/screenshot-2.png?rev=3580503","caption":"Payment method selection, confirmation and payment via TWINT."},{"src":"https:\/\/ps.w.org\/pay4all-form\/assets\/screenshot-3.png?rev=3580503","caption":"Dashboard, order list and language selection."},{"src":"https:\/\/ps.w.org\/pay4all-form\/assets\/screenshot-4.png?rev=3580503","caption":"Order form configuration."},{"src":"https:\/\/ps.w.org\/pay4all-form\/assets\/screenshot-5.png?rev=3580503","caption":""}],"raw_content":"<!--section=description-->\n<p><strong>Pay4All Shop<\/strong> is a simple, focused order-form plugin for small shops, wineries, caterers, takeaways and clubs. It is intentionally lighter than WooCommerce: you create a form, pick products and delivery methods, and embed it via a shortcode.<\/p>\n\n<h4>Core features<\/h4>\n\n<ul>\n<li>Drag-and-drop <strong>form builder<\/strong> with all common field types \u2014 default fields (first name, last name, e-mail, phone, address, etc.) are pre-translated in every activated language<\/li>\n<li><strong>Products<\/strong> with photo, price, sale price, min\/max\/step quantities, short description<\/li>\n<li><strong>Per-form photo size<\/strong> \u2014 pick <em>Small (60 px)<\/em>, <em>Medium (120 px)<\/em>, <em>Large (200 px)<\/em> or <em>Custom<\/em> (any pixel value between 20 and 1000) from the form editor. In <em>List<\/em> layout the value constrains the photo's <strong>width<\/strong>, in <em>Grid<\/em> layout its <strong>height<\/strong> \u2014 so the cards keep tidy regardless of the source image dimensions.<\/li>\n<li><strong>Two product types<\/strong> \u2014 <em>Physical<\/em> (default) and <em>Virtual<\/em> (downloadable file delivered after purchase)<\/li>\n<li><strong>Hide a product<\/strong> \u2014 tick <em>Hide this product<\/em> to remove it from the order form entirely (useful for archiving a seasonal product without deleting it)<\/li>\n<li><strong>Out-of-stock flag<\/strong> \u2014 the product stays visible in the form but the quantity field is greyed out and disabled<\/li>\n<li><strong>Stock management<\/strong> (optional, per product) \u2014 tick <em>Manage stock<\/em> to track quantity available; stock is decremented when an order is placed and restored when an order is deleted; the customer cannot order more than the stock left; product flips to <em>out of stock<\/em> automatically at 0; an admin column shows the current stock at a glance<\/li>\n<li><strong>Low-stock e-mail alert<\/strong> \u2014 set a global threshold in <em>Settings \u203a General<\/em>; when any tracked product crosses it after a sale, an alert is e-mailed to the notification address<\/li>\n<li><strong>Delivery methods<\/strong> \u2014 pickup, flat-rate, free, free over an amount, or no delivery; <strong>accepted payment methods are set per delivery<\/strong> so each delivery can expose its own payment choices to the customer<\/li>\n<li><strong>Per-delivery customer-facing title and description<\/strong> in every activated language<\/li>\n<li><strong>Virtual file delivery<\/strong> \u2014 the file is stored in a private folder (<code>.htaccess<\/code> protected), the customer receives a one-time link valid for 60 days, served by the plugin's download endpoint<\/li>\n<li><strong>\u00ab Remember my information \u00bb opt-in<\/strong> \u2014 a checkbox at the bottom of the customer step lets the shopper store their contact details locally in the browser (via the native <code>localStorage<\/code> API \u2014 no server-side cookie, no data leaves the browser) so a repeat visit pre-fills every field with one click. Unchecking the box and continuing wipes the memory immediately.<\/li>\n<li><strong>Order management<\/strong> \u2014 custom statuses, payment statuses, internal notes<\/li>\n<li><strong>E-mails<\/strong> to the customer and the merchant \u2014 full content (subject, heading, intro, outro) editable per language<\/li>\n<li><strong>CSV export<\/strong> of orders, filterable by date, form, status<\/li>\n<li><strong>Privacy API integration<\/strong> for GDPR export\/erase<\/li>\n<li>Honeypot anti-spam, nonces, server-side validation<\/li>\n<li>Internationalised (FR, DE, IT, EN)<\/li>\n<\/ul>\n\n<h4>Payments included for free<\/h4>\n\n<ul>\n<li>Pickup payment<\/li>\n<li>Cash on delivery<\/li>\n<li><strong>Bank transfer<\/strong> \u2014 merchant-configurable IBAN \/ BIC \/ beneficiary block, translated in every activated language, shown at checkout AND included in the customer confirmation e-mail. A fr \/ de \/ it \/ en shopper receives the bank details in the language they placed the order in.<\/li>\n<li>TWINT manual (textual instructions to the customer)<\/li>\n<\/ul>\n\n<h4>Pay4All Pro (optional)<\/h4>\n\n<p>Install <strong><a href=\"https:\/\/pay4all.ch\/\">Pay4All Pro<\/a><\/strong> to add <strong>automated TWINT payments<\/strong>: the customer pays through their TWINT app at checkout, and the order is marked as Paid automatically without merchant intervention.<\/p>\n\n<p>Pay4All Shop works fully without Pay4All Pro. Pay4All Pro is sold separately at https:\/\/pay4all.ch\/.<\/p>\n\n<h4>External services<\/h4>\n\n<p>This plugin does not connect to any external service. Orders, customer data, virtual downloads and e-mails are all handled locally.<\/p>\n\n<h4>Use cases<\/h4>\n\n<ul>\n<li>Winemakers selling in the shop or at events<\/li>\n<li>Cheese makers with pickup ordering<\/li>\n<li>Caterers with weekly order windows<\/li>\n<li>Local shops with pickup-only ordering<\/li>\n<li>Clubs taking online registrations with payment<\/li>\n<li>Digital sellers delivering PDFs, mp3s or course material as virtual products<\/li>\n<\/ul>\n\n<!--section=installation-->\n<ol>\n<li>Upload the plugin to <code>\/wp-content\/plugins\/pay4all-form\/<\/code> or install via <em>Plugins \u203a Add New<\/em>.<\/li>\n<li>Activate the plugin from the <em>Plugins<\/em> menu.<\/li>\n<li>Open <em>Pay4All Shop<\/em> in the admin sidebar.<\/li>\n<li>Create your products, delivery methods, then a form.<\/li>\n<li>Copy the form shortcode into any page.<\/li>\n<\/ol>\n\n<!--section=faq-->\n<dl>\n<dt id=\"do%20i%20need%20woocommerce%3F\"><h3>Do I need WooCommerce?<\/h3><\/dt>\n<dd><p>No. Pay4All Shop is standalone. It does not depend on WooCommerce in any way.<\/p><\/dd>\n<dt id=\"does%20it%20support%20online%20payments%3F\"><h3>Does it support online payments?<\/h3><\/dt>\n<dd><p>The free version supports manual payments only (pickup, on delivery, bank transfer, TWINT instructions). Automated TWINT payments are available via Pay4All Pro.<\/p><\/dd>\n<dt id=\"is%20the%20plugin%20gdpr-friendly%3F\"><h3>Is the plugin GDPR-friendly?<\/h3><\/dt>\n<dd><p>Yes. Customer data is included in WordPress's <em>Tools \u203a Export \/ Erase Personal Data<\/em> workflow.<\/p><\/dd>\n<dt id=\"will%20my%20data%20be%20deleted%20if%20i%20uninstall%3F\"><h3>Will my data be deleted if I uninstall?<\/h3><\/dt>\n<dd><p>No, unless you explicitly enable <em>R\u00e9glages \u203a Donn\u00e9es \u203a Supprimer les donn\u00e9es lors de la d\u00e9sinstallation<\/em>.<\/p><\/dd>\n<dt id=\"does%20the%20plugin%20use%20cookies%3F\"><h3>Does the plugin use cookies?<\/h3><\/dt>\n<dd><p>No HTTP cookies are set by the plugin. When the shopper ticks the <em>\u00ab Se souvenir de mes informations pour ma prochaine commande \u00bb<\/em> checkbox on the customer step, their contact fields are stored in the browser's own <code>localStorage<\/code> (client-side only, never uploaded to the server, one-year retention, scoped per form). Unticking the checkbox and clicking <em>Suivant<\/em> \u2014 or the browser's <em>Clear site data<\/em> action \u2014 wipes the entry immediately. Nothing is stored when the checkbox stays off.<\/p><\/dd>\n<dt id=\"how%20do%20virtual%20products%20work%3F\"><h3>How do virtual products work?<\/h3><\/dt>\n<dd><p>When you create a product of type <em>Virtual<\/em>, you upload one file per product. The file is stored in <code>wp-content\/uploads\/p4all-virtual\/<\/code>, protected by an automatically generated <code>.htaccess<\/code> (deny from all). After purchase, the customer's e-mail contains a one-time download link served by the plugin endpoint (<code>?p4all_dl={token}<\/code>). The link is valid for 60 days.<\/p><\/dd>\n<dt id=\"how%20does%20%2Aout%20of%20stock%2A%20work%3F\"><h3>How does *Out of stock* work?<\/h3><\/dt>\n<dd><p>Tick the <em>Rupture de stock<\/em> checkbox on a product. The product stays visible in the form so the customer still sees what you usually offer, but the quantity field is greyed and disabled. A red badge labels the product as out of stock. Server-side validation rejects any tampered submission that tries to order an out-of-stock item.<\/p><\/dd>\n<dt id=\"how%20does%20%2Astock%20management%2A%20work%3F\"><h3>How does *Stock management* work?<\/h3><\/dt>\n<dd><p>Tick <em>Manage stock<\/em> on a product and enter the quantity available. From that point on:<\/p>\n\n<ul>\n<li>Each order decrements the stock by the quantity ordered (one decrement per ticket \/ per unit). The decrement happens as soon as the customer validates the order, regardless of the payment status.<\/li>\n<li>The order form prevents the customer from ordering more than what is left \u2014 the quantity input's <em>max<\/em> attribute is capped at the stock available, and a server-side check rejects any tampered submission.<\/li>\n<li>When the stock reaches 0, the product is automatically flagged <em>out of stock<\/em>: it stays visible but cannot be ordered.<\/li>\n<li>When an order is deleted from the admin (definitive delete, not just trashed), the stock is restored for every product still in the order. Status changes (paid, cancelled\u2026) do NOT restore stock; only deletion does.<\/li>\n<li>If you set a <em>Notify me when stock reaches<\/em> threshold in <em>Settings \u203a General<\/em> (under the shop address), an e-mail alert is sent to the notification address as soon as a tracked product crosses the threshold downwards after a sale.<\/li>\n<li>A <em>Stock<\/em> column in the products admin list shows the current quantity (or <code>\u2014<\/code> when stock management is off), highlighted in orange below the threshold and red when out of stock.<\/li>\n<\/ul><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>2.3.0<\/h4>\n\n<ul>\n<li>New <strong>Bank transfer<\/strong> payment method with a per-language description block. The merchant edits the IBAN \/ BIC \/ beneficiary in <em>R\u00e9glages \u203a Paiement<\/em> under each language accordion \u2014 the input is a multi-line textarea with a fake-data placeholder so an empty field at least shows the expected shape at checkout. The description is rendered with <code>white-space:pre-wrap<\/code> at checkout (so the IBAN \/ BIC \/ beneficiary stay on their own lines) AND injected into the customer confirmation e-mail in the language the customer used to place the order (resolved via the <code>_p4all_lang<\/code> meta persisted at submission time).<\/li>\n<li>Internal : <code>Mailer::payment_instructions()<\/code> now takes an explicit <code>$lang<\/code> argument and delegates to <code>Schema::payment_description($method, $lang)<\/code> for every method \u2014 so any future manual method inherits the same per-language override chain (per-lang \u2192 primary-lang \u2192 localised default \u2192 FR default).<\/li>\n<li>Translations : \u00ab Virement bancaire \u00bb label already shipped in the 2.1.0 batch \u2014 de : \u00ab Bank\u00fcberweisung \u00bb, it : \u00ab Bonifico bancario \u00bb, en : \u00ab Bank transfer \u00bb. The multi-line default templates live in <code>Schema::PAYMENT_DESC_DEFAULTS['bank_transfer']<\/code> as per-language constants.<\/li>\n<\/ul>\n\n<h4>2.2.0<\/h4>\n\n<ul>\n<li>New <strong>Per-form photo size picker<\/strong> on the form editor (<em>Affichage \u203a Taille<\/em>). Choose <em>Petite (60 px)<\/em>, <em>Moyenne (120 px)<\/em>, <em>Grande (200 px)<\/em> or <em>Personnaliser\u2026<\/em> \u2014 the last one reveals a pixel input clamped between 20 and 1000. In <em>List<\/em> layout the value constrains the photo's width; in <em>Grid<\/em> layout its height. Implemented via two CSS custom properties (<code>--p4all-photo-w<\/code>, <code>--p4all-photo-h<\/code>) set on the form wrapper, no inline <code>&lt;style&gt;<\/code> block emitted.<\/li>\n<li>Documentation : previously introduced <em>\u00ab Remember my information \u00bb<\/em> checkbox on the customer step is now clearly listed under Core features (persisted in <code>localStorage<\/code> for 1 year, per form, no HTTP cookie).<\/li>\n<\/ul>\n\n<h4>2.1.0<\/h4>\n\n<ul>\n<li>New <strong>\u00ab Remember my information \u00bb opt-in checkbox<\/strong> on the customer step. When ticked, the customer's contact fields are persisted in the browser's own <code>localStorage<\/code> (scoped per form, retention 1 year, no HTTP cookie set) and pre-filled on the next visit. Unticking + clicking <em>Suivant<\/em> clears the memory. Ships with FR \/ DE \/ IT \/ EN translations.<\/li>\n<li>Pay4All Age integration : the <em>\u00ab V\u00e9rification d'\u00e2ge \u00bb<\/em> step now auto-skips when the current cart carries no age-restricted product. The customer no longer has to click <em>Suivant<\/em> on an empty age-check step just because the form's product catalogue happens to include an adult item they didn't add to their order. Implemented via a generic <code>data-p4all-skip=\"1\"<\/code> contract on any <code>.p4all-step<\/code>, so companion plugins can use the same escape hatch.<\/li>\n<li>Checkbox alignment polish : the standalone <em>\u00ab Remember me \u00bb<\/em> checkbox uses the same inline-flex layout as the existing checkbox fields (defensive against themes that turn every input into <code>display:block<\/code>, e.g. Elementor \/ Astra Ecommerce).<\/li>\n<\/ul>\n\n<h4>2.0.0<\/h4>\n\n<ul>\n<li><strong>Breaking change \u2014 five features moved to a separate Pay4All Pro plugin.<\/strong> The free version keeps the form builder, products, deliveries, virtual downloads, orders and e-mails; the following move out:\n\n<ul>\n<li>Product filter widget on the public form (categories \/ brands \/ sort dropdowns).<\/li>\n<li><em>Categories<\/em> taxonomy (<code>p4all_product_cat<\/code>) \u2014 including per-language names and per-category quantity rules (min \/ max \/ multiple \/ exact).<\/li>\n<li><em>Brands<\/em> taxonomy (<code>p4all_product_brand<\/code>) \u2014 same shape as categories.<\/li>\n<li><em>Abandoned cart<\/em> capture, recovery e-mail and admin page.<\/li>\n<li><em>Ticket<\/em> product type (QR codes for events) \u2014 including the <em>Billets<\/em> admin page and the public <code>\/p4all-ticket\/\u2026<\/code> validation URL.<\/li>\n<\/ul><\/li>\n<li><strong>Automatic clean-up on upgrade<\/strong> \u2014 activating 2.0.0 once drops the <code>wp_p4all_tickets<\/code> and <code>wp_p4all_abandoned_carts<\/code> tables, deletes every term in <code>p4all_product_cat<\/code> \/ <code>p4all_product_brand<\/code>, deletes every ticket post, and un-schedules the hourly <code>p4all_abandoned_cart_tick<\/code> cron event. Idempotent, guarded by an option so re-installs skip it.<\/li>\n<li><strong>XLSX export removed<\/strong> on both the orders CSV screen and the products import\/export screen. CSV export is unchanged. XLSX moves to Pro.<\/li>\n<li>Product edit screen : the <em>Billet (QR code)<\/em> option is removed from the product-type dropdown. Existing tickets keep rendering as <em>Physical<\/em> until edited.<\/li>\n<li>Settings \u203a General : <em>D\u00e9lai d'envoi du mail d'abandon de panier<\/em> setting removed (no longer used).<\/li>\n<li>Help page : reworded to reflect the trimmed feature set.<\/li>\n<li>Internal : new <code>Domain\\Language<\/code> helper replaces <code>Domain\\Category::current_language()<\/code> at every call site.<\/li>\n<\/ul>\n\n<h4>1.4.3<\/h4>\n\n<ul>\n<li>New <strong>Content picker next to each URL field<\/strong> \u2014 a <em>\u00ab \ud83d\udd17 Rechercher \u00bb<\/em> button opens a mini search panel that queries pages, posts, WooCommerce products, every public custom post type, and media attachments (PDFs, images, mp3\u2026). Clicking a result drops the full URL into the input. WPML \/ Polylang are detected automatically: on the DE tab the search only returns DE pages, on IT only IT, etc. Media stays language-agnostic (a PDF is the same file in every locale). AJAX endpoint requires <code>edit_posts<\/code> + a nonce.<\/li>\n<li>Full <strong>DE \/ IT \/ EN translations<\/strong> for the eleven new admin strings introduced by the \u00ab Learn more \u00bb link, the picker, and the photo lightbox (labels, placeholders, help text, status messages).<\/li>\n<li><strong>Plugin Check compliance polish<\/strong> \u2014 <code>phpcs:disable<\/code> \/ <code>phpcs:enable<\/code> block around the <code>$_FILES<\/code> sanitisation in <code>MetaBoxes::store_virtual_upload()<\/code> (nonce is verified upstream in <code>save()<\/code>); documented ignores for WPML's public <code>wpml_switch_language<\/code> hook (must be invoked verbatim to trigger their language switcher) and for the intentional <code>suppress_filters<\/code> on the media query used by the picker (language-agnostic search is the product decision).<\/li>\n<\/ul>\n\n<h4>1.4.2<\/h4>\n\n<ul>\n<li>New <strong>\u00ab Learn more \u00bb link per product<\/strong> \u2014 two new fields (URL + link label) on each language tab of the product edit screen. When the URL is set, a link is rendered under the short description in the order form and opens in a new tab. The <em>Link label<\/em> field is pre-filled with a language-specific default on every tab (FR <em>\u00ab D\u00e9couvrez en plus\u2026 \u00bb<\/em>, DE <em>\u00ab Erfahren Sie mehr\u2026 \u00bb<\/em>, IT <em>\u00ab Scopri di pi\u00f9\u2026 \u00bb<\/em>, EN <em>\u00ab Learn more\u2026 \u00bb<\/em>) regardless of the admin's own locale; the merchant is free to overwrite any of them. Same default kicks in on the front-end so a visitor in DE (or IT \/ EN) always sees the label in their language, even when the merchant left the field blank. URLs are validated at save with <code>esc_url_raw()<\/code> (http \/ https \/ mailto \/ tel only, <code>javascript:<\/code> rejected).<\/li>\n<li>New <strong>Product photo lightbox<\/strong> \u2014 clicking the product thumbnail on the first step of the form now opens the full-size photo in an overlay. Keyboard-accessible (Enter \/ Space to open, ESC to close), non-destructive to the form (body scroll is temporarily locked). No dependency added; pure vanilla JS.<\/li>\n<li><strong>Security hardening<\/strong> \u2014 three fixes triggered by an internal security review:\n\n<ul>\n<li><em>Path traversal fix<\/em> in the virtual-download endpoint (<code>?p4all_dl=<\/code>). The customer-facing streamer now rejects any filename containing directory separators, <code>..<\/code>, or NULL bytes, and enforces a <code>realpath()<\/code> boundary check that guarantees the resolved path lives inside the private uploads folder. Protects against manipulated <code>_p4all_virtual_downloads<\/code> meta on stores with a compromised admin account or a SQL injection introduced by a third-party plugin.<\/li>\n<li><em>X-Frame-Options \/ CSP frame-ancestors<\/em> now emitted on every front-end page rendering the <code>[pay4all_form]<\/code> shortcode. Default: <code>SAMEORIGIN<\/code> \u2014 blocks brand impersonation via cross-origin iframes and closes the per-domain-licence bypass some agencies used by iframing a licensed shop on many client sites. Configurable via a new <em>R\u00e9glages \u203a G\u00e9n\u00e9ral<\/em> option (<code>sameorigin<\/code> \/ <code>deny<\/code> \/ <code>off<\/code>).<\/li>\n<li><em>Virtual-product upload allowlist<\/em> \u2014 merchant-configurable list of accepted file extensions (default: <code>pdf, mp3, mp4, m4a, epub, zip, jpg, jpeg, png, gif, svg, doc(x), xls(x), ppt(x), txt, csv, html<\/code>). Blocks executable uploads (<code>.php<\/code>, <code>.phtml<\/code>, <code>.exe<\/code>, <code>.htaccess<\/code>\u2026) BEFORE <code>wp_handle_upload()<\/code> runs. Set the option to an empty value to restore the legacy behaviour of allowing anything. The private uploads folder is now also guarded by a <code>web.config<\/code> deny rule for IIS hosts, in addition to the existing <code>.htaccess<\/code>.<\/li>\n<\/ul><\/li>\n<li><strong>Admin menu badge polish<\/strong> \u2014 the \"Commandes\" count badge next to <em>Pay4All SHOP<\/em> now uses the same red (<code>#d63638<\/code>) and compact sizing as WordPress core's <em>update available<\/em> pill, and stays visible on every admin screen (previously the CSS only loaded on Pay4All Form pages, so the badge disappeared on the Dashboard).<\/li>\n<li><strong>Admin menu i18n<\/strong> \u2014 sub-entries <em>Param\u00e8tres<\/em> and <em>Licences<\/em> under Pay4All AGE, plus the WooCommerce entry-point label, are now localised in DE \/ IT \/ EN based on the admin locale.<\/li>\n<li><strong>KYC tab visual polish<\/strong> \u2014 the accordion widget on <em>R\u00e9glages \u203a Protection des mineurs<\/em> now matches the visual language of the <em>Paiements<\/em> tab (rounded card, <code>\u25be<\/code> chevron, language pill), and the accordions honour the languages selected in <em>R\u00e9glages \u203a Langue<\/em> (so a DE-only shop only shows the DE accordion here).<\/li>\n<li>Plugin Check clean-up \u2014 nonce comments, <code>sanitize_text_field( wp_unslash() )<\/code> on remaining <code>$_POST<\/code> reads, <code>esc_url()<\/code> on the licence page form action, <code>wp_delete_file()<\/code> in the KYC storage purge, and Plugin Check ignore blocks documented for WPML hooks (<code>wpml_element_trid<\/code>, <code>wpml_get_element_translations<\/code>) that intentionally do not carry the plugin prefix.<\/li>\n<\/ul>\n\n<h4>1.4.1<\/h4>\n\n<ul>\n<li>New <strong>Client-side product filtering<\/strong> \u2014 an optional filter bar can be enabled per form (Form editor \u203a <em>Affichage des produits<\/em> \u203a <em>Afficher les filtres<\/em>). Customers can narrow the product list on the first step by category, brand and sub-taxonomies, with an optional item-count next to each option. Fully client-side, no page reload.<\/li>\n<li><strong>Multilingual category \/ brand labels<\/strong> \u2014 every category and brand can carry a customer-facing name in each activated language, plus a <em>\u00ab Tous les\u2026 \u00bb<\/em> label for filter dropdowns. Empty fields fall back to the WordPress canonical term name. Drag-and-drop reorder from the taxonomy list.<\/li>\n<li><strong>Minimum order amount is now per-form<\/strong> \u2014 the <em>Montant minimum de commande<\/em> panel has moved from <em>R\u00e9glages \u203a Commandes<\/em> to each form's edit screen (right above <em>Affichage des produits<\/em>), so different forms can have different thresholds. Includes the per-language error message (with <code>%s<\/code> for the amount) and live validation on the product step: the <em>Suivant<\/em> button is blocked and a red alert appears as soon as the subtotal drops below the floor. Existing global setting is honoured as fallback for forms that haven't set their own.<\/li>\n<li>Compatibility with Pay4All Age plugin improved.<\/li>\n<\/ul>\n\n<h4>1.4.0<\/h4>\n\n<ul>\n<li>New Compatible with Pay4All AGE <strong>Protection of minors<\/strong> (age verification) \u2014 enable per product with the <em>Requires age verification<\/em> checkbox on the product edit screen. At checkout the customer scans a QR code, opens a mobile page and captures their ID (recto + verso) and a selfie; the order cannot be validated until every required slot is present.<\/li>\n<li><strong>Cross-device flow<\/strong> \u2014 the QR code embeds a short-lived session token (20 min); the desktop cart polls the server and unlocks automatically as soon as the mobile capture is complete, without asking the customer to reload.<\/li>\n<li><strong>Encryption at rest<\/strong> \u2014 every photo is encrypted with AES-256-GCM using a per-order subkey derived via HMAC-SHA256 from a master key stored in <code>wp-content\/uploads\/p4all-secure\/kyc.key<\/code> (outside the database backup). Ciphertexts live in a private folder guarded by <code>.htaccess<\/code> + <code>index.php<\/code> + <code>web.config<\/code>.<\/li>\n<li><strong>Admin-only decrypted view<\/strong> \u2014 a <em>Protection des mineurs<\/em> section on the order page streams the decrypted photos through a nonce-protected admin endpoint (never served publicly, never written back to disk).<\/li>\n<li><strong>Yellow warning banner<\/strong> in the admin new-order e-mail when the order requires age verification, with a <em>Voir la commande<\/em> button that links straight to the order edit screen.<\/li>\n<li><strong>Auto-purge<\/strong> \u2014 deleting an order also deletes its KYC directory. A daily cron purges session buckets older than 24 h.<\/li>\n<li>All KYC-related strings translated in FR \/ DE \/ IT \/ EN.<\/li>\n<\/ul>\n\n<h4>1.3.1<\/h4>\n\n<ul>\n<li>New <strong>Timezone<\/strong> setting in <em>Settings \u203a General<\/em> \u2014 surfaces the WordPress global timezone selector for admins who don't know where to find it. Defaults to Europe\/Paris when not configured. Modifying it updates the standard WP <code>timezone_string<\/code> option.<\/li>\n<li>Dates in <em>Pay4All Shop \u203a Abandon panier<\/em> admin page now use <code>get_date_from_gmt()<\/code> for accurate conversion of UTC-stored timestamps to the site's configured timezone.<\/li>\n<li>Renamed menu label from <em>Abandon de panier<\/em> to <em>Abandon panier<\/em> for compactness; sub-menu translated in all four languages.<\/li>\n<li>Plugin Check \/ WP.org compliance: escaped <code>$cart_id<\/code> output in the abandoned-cart view dialog, and added <code>WordPress.DB.PreparedSQL.InterpolatedNotPrepared<\/code> + <code>PluginCheck.Security.DirectDB.UnescapedDBParameter<\/code> ignores on the <code>wp_p4all_abandoned_carts<\/code> custom-table queries (table name built from <code>$wpdb-&gt;prefix<\/code> + literal suffix).<\/li>\n<\/ul>\n\n<h4>1.3.0<\/h4>\n\n<ul>\n<li>New <strong>Abandoned cart<\/strong> capture and recovery \u2014 when a customer fills the contact-info step (with their e-mail) and clicks <em>Next<\/em> but never validates, the cart is stored. A recovery e-mail with a one-time link is automatically sent after a configurable delay (default 10 days). Clicking the link returns the customer to the form with all their items, contact fields, delivery and payment method already pre-filled.<\/li>\n<li>New admin page <em>Pay4All Shop \u203a Abandon panier<\/em> (above <em>Billets<\/em>) \u2014 lists every captured cart with filters (pending \/ e-mail sent \/ recovered \/ converted), totals, dates and a delete action.<\/li>\n<li>New setting <em>R\u00e9glages \u203a G\u00e9n\u00e9ral \u203a D\u00e9lai d'envoi du mail d'abandon de panier (jours)<\/em> \u2014 0 disables, default 10. Carts older than 90 days are automatically purged (GDPR).<\/li>\n<li>New e-mail template <em>Abandoned cart recovery<\/em> in <em>R\u00e9glages \u203a E-mails<\/em>, editable per language (subject \/ heading \/ intro \/ outro), with <code>{site_name}<\/code>, <code>{customer_firstname}<\/code>, <code>{customer_email}<\/code> and <code>{recover_url}<\/code> placeholders.<\/li>\n<li>GDPR exporter and eraser hooks include abandoned-cart rows in WordPress's <em>Tools \u203a Export \/ Erase Personal Data<\/em>.<\/li>\n<li>Hourly cron <code>p4all_abandoned_cart_tick<\/code> sends due recovery e-mails and purges old rows.<\/li>\n<\/ul>\n\n<h4>1.2.0<\/h4>\n\n<ul>\n<li>New <strong>Hide a product<\/strong> checkbox on the product edit screen \u2014 the product is omitted from the form entirely (no badge, no greyed-out field). Server-side rejects any tampered order containing a hidden product. Useful for archiving seasonal items without deleting them.<\/li>\n<li>Stock-related checkboxes reordered for clarity: <em>Hide this product<\/em> \u2192 <em>Out of stock<\/em> \u2192 <em>Manage stock<\/em> (+ quantity field).<\/li>\n<li>New <strong>Stock management<\/strong> (optional, per product) \u2014 tick <em>Manage stock<\/em> on a product to track its quantity. Stock is decremented at every order (one unit per ticket \/ per item), the customer cannot order more than what is left, and the product is automatically flagged <em>out of stock<\/em> when the stock hits 0.<\/li>\n<li><strong>Stock restored on order deletion<\/strong> \u2014 deleting an order definitively (from trash or via the delete button) restores the stock of every product still present in that order. Status changes (paid, cancelled, etc.) do not restore stock; only deletion does.<\/li>\n<li><strong>Low-stock e-mail alert<\/strong> \u2014 set <em>Settings \u203a General \u203a Notify me when stock reaches<\/em> (under the shop address) to a non-zero threshold; an alert is e-mailed to the notification address each time a tracked product crosses the threshold downwards after a sale.<\/li>\n<li>New <strong>Stock<\/strong> column in the products admin list \u2014 shows <code>\u2014<\/code> when stock management is off, the current quantity otherwise (orange if at or below threshold, red <code>0 Out of stock<\/code> when empty).<\/li>\n<\/ul>\n\n<h4>1.1.0<\/h4>\n\n<ul>\n<li>New product type <strong>Virtual<\/strong> \u2014 upload one downloadable file per product, customer receives a one-time link valid 60 days, served by the plugin's protected endpoint (files stored in a private folder with deny-all <code>.htaccess<\/code>).<\/li>\n<li>New product type <strong>Ticket<\/strong> \u2014 one QR code per seat ordered, sent in the order email, scannable from any smartphone, with a public validation page showing green (available) or red (already used).<\/li>\n<li>New admin page <em>Pay4All Shop \u203a Billets<\/em> \u2014 list every QR ticket, search by first\/last name \/ city \/ e-mail, validate manually, export to CSV.<\/li>\n<li><strong>Out of stock<\/strong> flag on every product \u2014 product stays visible, quantity field is greyed and disabled, server-side rejects tampered orders.<\/li>\n<li><strong>Default form fields<\/strong> (first name, last name, e-mail, phone, address, etc.) are now pre-translated in every activated language at form creation time.<\/li>\n<li><strong>Per-delivery payment methods<\/strong> \u2014 accepted payment methods are now configured per delivery method (a delivery edit screen has a <em>Modes de paiement accept\u00e9s<\/em> section), so each delivery can show its own payment choices to the customer. Forms without any delivery still use the legacy per-form payment picker.<\/li>\n<\/ul>\n\n<h4>1.0.0<\/h4>\n\n<ul>\n<li>Initial release.<\/li>\n<\/ul>","raw_excerpt":"Simple order forms with products, deliveries and Twint payment - no WooCommerce required.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/fi.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/328079","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/fi.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/fi.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/fi.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=328079"}],"author":[{"embeddable":true,"href":"https:\/\/fi.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/pay4all"}],"wp:attachment":[{"href":"https:\/\/fi.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=328079"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/fi.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=328079"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/fi.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=328079"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/fi.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=328079"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/fi.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=328079"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/fi.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=328079"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}