=== 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.0
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 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 principle is that withdrawing should be no harder than buying: if a customer can order in a few clicks, they must be able to withdraw in a few clicks.

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, are yours. Confirm both against the law as transposed in your country.

= What it does =

* Adds a clearly labelled withdrawal link to every page of the shop, on by default.
* Lets guests withdraw without an account, by order number and email, verified through a one-time link sent to the address on the order.
* 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.
* 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.
* 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 queue 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 decision email is a setting, and all five carry WooCommerce's standard Additional content block.
* Writes the declared items onto the order as an order note, so the merchant sees the request where the money is.
* Asks for nothing beyond a name, an order number and a contact email. No IBAN, no mandatory reason.
* Translation-ready. Greek, German, French, Italian, Spanish, Polish, Romanian and Czech translations are being contributed to translate.wordpress.org, from where WordPress installs them automatically.
* Works with Loco Translate, WPML and Polylang, including per-language withdrawal pages.

= Built to survive a real shop =

The parts nobody sees until they go wrong:

* **Access links are stored as one-way hashes**, never in readable form, are single purpose, and expire in thirty minutes. A leaked database backup hands over nothing usable.
* **The lookup form is rate limited** per address and per visitor, with a honeypot field and a timing check, so it cannot be used to fish 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 hand one customer another customer's session.
* **A double click cannot file two declarations.** The session is claimed before anything is written, so one submission wins 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, apportioned discounts and the condition of the goods.

= 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 person.

= 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, apportioned discounts 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 is still fully withdrawable. 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 for rate limiting and are 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 requires the pre-contractual information to disclose that the function exists.
5. Place a test order and run the flow end to end, including the acknowledgement email.

== Frequently Asked Questions ==

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

Not safely. The directive points to unambiguous wording along the lines of "withdraw from contract". A label a customer does not recognise as the withdrawal function is unlikely to discharge the obligation, and "Cancel" reads as abandoning an unshipped order.

= 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 shipped 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, the consequence that bites first is EU-wide: the cooling-off period can extend from 14 days to twelve months and 14 days.

= 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 page-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 hands 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.

Of the six comparable plugins, only one offers deletion at all; it also logs the record first and notes it on the order. The other five have 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 signed-in customer? =

On each order under My Account, alongside View and Order again, 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 one-time link 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. If you run a CDN with a "cache everything" rule in front of the site, add a bypass rule for that page as well.

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

Yes. The plugin declares HPOS compatibility and uses only WooCommerce order APIs.

== 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 queue, 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.0 =
* First release.
