=== FDesign Withdrawal Button for WooCommerce ===
Contributors: doeri
Tags: woocommerce, withdrawal, eu, compliance, consumer rights
Requires at least: 6.9
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 1.0.1
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Helps your WooCommerce shop meet EU Directive 2023/2673: a two step withdrawal form guests use without an account, and a timestamped acknowledgement.

== Description ==

Since 19 June 2026, shops selling online to consumers in the EU have had to offer an electronic withdrawal function under Article 11a of the Consumer Rights Directive, added by Directive (EU) 2023/2673. The function must be clearly labelled, easy to find and available for the whole withdrawal period. The customer gives their name, the contract and an email address, confirms in a separate step, and receives an acknowledgement on a durable medium stating what was declared and when.

This plugin builds that function into WooCommerce. It is a technical tool, not legal advice, and installing it does not by itself make a shop compliant. The wording on your terms and returns pages, and how your team handles returns when they arrive, remain yours. Confirm both against the law as it applies in your country.

= What it does =

* Adds a clearly labelled withdrawal link to the footer of every page of the shop, on by default.
* Lets guests withdraw without an account. The customer enters the order number and the email used for the order; when both match, a link valid for thirty minutes is sent to that address.
* Puts the same link on each order in My Account, where the customer is already looking, and only on orders that can still be withdrawn from.
* Offers the `[fdwb_button]` shortcode to place the link on any other page.
* Runs a genuine two step flow: the customer reviews exactly what they are declaring, then confirms as a separate action.
* Supports withdrawing from part of an order, per item and per quantity, and counts refunds already issued.
* Sends a timestamped acknowledgement of receipt on a durable medium, carrying an integrity code, then tells the customer what you decided.
* Gives the shop a request list with status filters, a full audit trail, and accept or reject with a note that goes to the customer.
* Lets you reword every email: the closing paragraph of each customer email is a setting, and all five carry WooCommerce's standard Additional content box.
* Writes the declared items onto the order as an order note, so the merchant sees the request where the money is.
* Lets you exclude product categories, and virtual or downloadable products, that carry no right of withdrawal.
* Asks for nothing beyond a name, an order number and a contact email. No IBAN, no mandatory reason.
* Ready for translation. Greek and Polish translations are complete and submitted to translate.wordpress.org, from where WordPress installs them automatically once approved.
* Works with Loco Translate, WPML and Polylang, including a separate withdrawal page for each language.

= Built to survive a real shop =

The parts nobody sees until they go wrong:

* **Access links are stored only as hashes**, never in readable form, work for one purpose only, and expire after thirty minutes. A leaked database backup contains nothing usable.
* **Every form carries a WordPress nonce**, so a form submitted from another website is refused before anything is recorded or sent.
* **The lookup form is rate limited** per email address and per visitor, with a hidden field for bots and a signed timing check, so it cannot be used to search for other people's orders. A wrong order number and a wrong email produce exactly the same message as a correct pair.
* **Refunds you have already issued reduce what is left to withdraw.** Withdraw one of three, refund another through WooCommerce, and the customer is offered one, not two.
* **The withdrawal page excludes itself from caching**, using the signal WP Rocket, LiteSpeed Cache, W3 Total Cache, WP Super Cache and WP Fastest Cache all respect. A cached withdrawal page would show one customer another customer's session.
* **A double click cannot file two declarations.** The session is claimed before anything is written, so one submission is recorded and the other lands on the same confirmation.
* **Nothing is stored that would be wrong later.** No monetary amount, because what you owe depends on delivery costs, discounts spread across several items and the condition of the goods.

= Setup and support =

The plugin's page on fdesign.com.gr describes every part of the plugin and its settings in detail: [FDesign Withdrawal Button for WooCommerce](https://fdesign.com.gr/fdesign-withdrawal-button-for-woocommerce/).

FDesign, the author of the plugin, also offers installation, a review of the email wording against your returns process, and monthly care. These services are optional. The plugin is free and complete without them.

For questions and bug reports, use the support forum on WordPress.org.

= Privacy and GDPR =

The plugin registers a personal data exporter and eraser, so withdrawal declarations appear in the tools under Tools, Export Personal Data and Tools, Erase Personal Data, and it contributes suggested wording to your privacy policy draft.

Erasure is off by default, and says so in the report rather than failing quietly. A withdrawal declaration is the record of a legal act and your evidence that you honoured it, which is a recognised ground for keeping it. WooCommerce treats orders the same way. Switch it on under Withdrawal, Advanced and the name, address and free text are replaced while the declaration itself survives, so you keep the proof without keeping the personal data.

= What it deliberately does not do =

The plugin records the legal declaration and timestamps it. It does not issue refunds. Refunds stay in your normal WooCommerce flow, through your existing gateway, decided by you after you have inspected the returned goods. The rules on return shipping, inspection and refund deadlines are unchanged by the directive and unchanged by this plugin.

It also stores no monetary amount against a request. What a shop owes depends on delivery costs, discounts spread across several items and the condition of the goods. A figure stored at declaration time would look authoritative and be wrong.

= A note on sealed goods =

Sealed health, hygiene and cosmetic products lose the right of withdrawal only once the customer has opened them. An unopened item can still be withdrawn. The plugin therefore does not let you exclude such products automatically, because no software can tell whether a package has been opened. That check belongs to your team when the return arrives.

= What is stored =

The name, email address and order reference a customer submits, the items declared, the time of receipt, and anything the customer chose to write. Visitor IP addresses are used only for rate limiting, kept as a salted hash, and never stored against a declaration.

The plugin contacts no external server. Nothing is sent anywhere, no usage is tracked, and no data leaves your site.

== Installation ==

1. Install and activate the plugin. A page called "Withdraw from contract" is created automatically, holding the form.
2. Go to WooCommerce, Settings, Withdrawal to set the withdrawal window and choose which order status starts the clock.
3. Check the wording of the five emails under WooCommerce, Settings, Emails. The two decision emails end in a highlighted paragraph you should make your own: it is where the customer is told what happens next in your shop.
4. Add a mention of the withdrawal function, and where to find it, to your terms and returns pages. Article 6(1)(h) requires the information given before purchase to state that the function exists and where it is.
5. Place a test order and run the flow from start to finish, including the acknowledgement email.

== Frequently Asked Questions ==

= Can I label the link "Cancel" or "Returns"? =

Not safely. Article 11a asks for the words "withdraw from contract here" or an equally clear phrase. A label a customer does not recognise as the withdrawal function is unlikely to meet the requirement, and "Cancel" reads as abandoning an order that has not shipped yet.

= Can I change what the emails say? =

Yes, and you should. Each of the five has WooCommerce's standard Additional content box, and the three the customer reads, the acknowledgement and the two decisions, also let you replace the highlighted paragraph at the end, the one that tells the customer where they stand. The default wording promises that you will write separately about returning the goods and about the refund, which is true of most shops and not of all; if your process differs, say what yours is. Leave the field empty and the built in wording comes back, in the customer's language.

On the acknowledgement, keep whatever wording you choose to the point that receiving a declaration is not the same as accepting it. That sentence is what stops a customer reading the email as a decision you have not made yet.

Separately, the Accept and Reject buttons sit under a note field. Whatever you write there is sent to the customer with the decision and kept on the request. It is optional when accepting. When rejecting it is worth the thirty seconds: a refusal with no reason is one the customer cannot answer and you cannot defend later.

= What happens if I do not offer a compliant withdrawal function? =

Beyond any national penalty, one consequence applies across the EU. When the shop does not give the information Article 6(1)(h) requires, which now includes the existence and placement of the withdrawal function, Article 10 extends the withdrawal period by twelve months. If the information is supplied later, the period ends fourteen days after the customer receives it.

= Does it work with Loco Translate, WPML or Polylang? =

Yes, all three.

Loco Translate reads the bundled template and any language pack WordPress has installed, so you can change any wording in the plugin without touching a file. Save to Loco's Custom location: it survives both plugin updates and language pack updates.

WPML and Polylang both work. The link to the withdrawal page is resolved per language, so a German visitor is sent to the German page rather than to a copy they cannot read: the plugin asks the wpml_object_id filter, which WPML defines and Polylang implements in its compatibility layer. The cache exclusion covers every translation of the page, not just the original, which matters more than it sounds: a translated copy that slipped through would be cached, and a cached withdrawal page shows one customer another customer's session. A wpml-config.xml exposes the withdrawal link label for translation. The five emails are ordinary WooCommerce emails, so WooCommerce Multilingual already offers their subject, heading and body for translation alongside every other WooCommerce email.

= Can I delete a withdrawal request? =

Yes, from the request's own screen, and it is the right thing to do for spam, duplicates and the test entries every shop leaves behind while setting up.

It never happens silently. Before the row goes, the whole record is written to the WooCommerce log under Status, Logs, and the order it belonged to gets a note naming the reference, the time the declaration was received and who removed it. Neither can be deleted from inside this plugin. That matters, because a withdrawal declaration is the record that a customer exercised a legal right on a particular date, evidence that protects the shop as much as the customer, and a record that can vanish without trace is not evidence at all.

There is deliberately no bulk delete. Removing legal records is not something to do fifty at a time by accident.

When the comparable plugins were checked on 29 August 2026, only one of six offered deletion at all; it also logged the record first and noted it on the order. The other five had no delete button of any kind.

= A customer asked to be deleted. What happens to their withdrawal? =

By default it is kept, and the erasure report says so and why. The declaration is the record of a legal act and your evidence that you honoured it. If you would rather it went, switch on Erasure requests under Withdrawal, Advanced: the name, address and free text are then replaced with anonymised values while the declaration, its reference and its date survive. Any pending access links for that address are deleted either way.

= Where does the link appear for a customer who is signed in? =

On each order in the order list under My Account, and only on orders still within the withdrawal window with something left to withdraw. It is on by default and can be switched off under Withdrawal, General. There is deliberately no separate My Account tab: a tab needs a rewrite endpoint, and a rewrite endpoint that does not flush cleanly on activation produces a 404 exactly where a customer is trying to exercise a legal right.

= Do customers have to create an account? =

No, and they must not have to. The plugin verifies a guest by sending a link, valid for thirty minutes, to the email address on the order, so the order is never shown to somebody who merely typed in an order number.

= Does it work with a caching plugin? =

Yes. The withdrawal page excludes itself from caching using the standard signal that WP Rocket, LiteSpeed Cache, W3 Total Cache, WP Super Cache and WP Fastest Cache all honour. Behind a CDN such as Cloudflare, switch on "Behind a CDN or reverse proxy" under Withdrawal, Advanced, and if the CDN uses a "cache everything" rule, add a bypass rule for the withdrawal page as well.

= Is it compatible with High-Performance Order Storage? =

Yes. The plugin declares HPOS compatibility, uses only WooCommerce order APIs, and is tested with both HPOS and the older posts storage.

= Can someone set it up for me? =

Yes. FDesign, the author of the plugin, offers installation, a review of the email wording against your returns process, and monthly care. The details are on the [plugin's page on fdesign.com.gr](https://fdesign.com.gr/fdesign-withdrawal-button-for-woocommerce/). The plugin works fully without these services.

== Screenshots ==

1. The withdrawal form as a guest sees it: an order number and the email used for the order, nothing more.
2. Choosing what to withdraw, item by item and quantity by quantity, capped at what the order still holds.
3. The request list, with status filters and the pending count in the menu.
4. A single request, showing the declaration, the integrity code and the full audit trail.
5. The settings tab, where the withdrawal window and the exclusions live.

== Changelog ==

= 1.0.1 =
* Adds a "Setup and support" link to the plugin's row on the Plugins screen, pointing to the plugin's page on fdesign.com.gr.
* Readme: corrected the description of the access link and of the My Account link, and added a section on setup and support.

= 1.0.0 =
* First release.
