Mandatory B2C withdrawal button for thirty bees - EU Directive 2023/2673.
From 19 June 2026 every webshop selling to consumers in the EU must offer an easy, always-available online withdrawal function: a clearly labelled button, a two-step confirmation, and an automatic acknowledgement of receipt on a durable medium (an e-mail with the date and time). This module adds exactly that to a thirty bees shop, and turns each request into a support ticket your staff can act on.
- Module / repo / folder name:
tbwithdrawalbutton - Version: 1.6.0
- Author: thirty bees
- License: AFL-3.0
- Requires: thirty bees 1.x · PHP 7.4+
- Adds a standalone public page -
index.php?fc=module&module=tbwithdrawalbutton&controller=withdrawal- linked in the footer (and optionally in My account), so it is reachable from anywhere and works for guests too (no login required). - Presents the legally required two-step flow:
- Step 1 - statement: the consumer enters their name, order reference, e-mail, and an optional reason. The confirmation is sent to that e-mail.
- Step 2 - confirmation: a summary screen with a "Confirm withdrawal" button.
- On confirmation it:
- writes a row to
PREFIX_tbwd_request- the legal evidence (who, which order, date and time, IP, where it was routed); - creates a ticket in either the built-in Customer Service or the tbticketsystem module (configurable, with automatic fallback to core);
- sends the consumer a dated confirmation e-mail (
withdrawal_ack) - the statutory durable-medium acknowledgement - to the e-mail address they entered; - optionally notifies your staff with a separate internal e-mail
(
withdrawal_staff) that carries the customer's details and an action reminder - distinct from the customer's confirmation.
- writes a row to
- It does not automatically refund or change order status. A human handles the return and refund from the resulting ticket - the module's job is to collect the request and prove receipt.
- It does not check or enforce the 14-day cooling-off deadline. The button is always available (as the law requires) and your staff judge validity per order. The directive forbids auto-refusing a withdrawal, so there is no countdown timer.
- It does not auto-refund or auto-create credit slips.
Customers → Withdrawal requests lists every request: date, name, e-mail, order reference, where it was routed, ticket reference, whether the confirmation e-mail was sent (a green Sent / red FAILED badge), and IP. You can filter, search, open a single record (full reason + all fields), and Export to CSV for audits or disputes.
- There is no "Add" - rows are only ever created by a real consumer request.
- Delete is available for GDPR erasure requests.
- A failed confirmation e-mail is flagged red so staff can follow up.
| Setting | What it controls |
|---|---|
| Ticket destination | Core Customer Service (default) or tbticketsystem. Auto-falls back to core if tbticketsystem is missing/disabled. |
| Intro text | A short text shown above the form, per language, HTML allowed. |
| Colours | Step-1 button (blue), focus highlight, e-mail accent stripe, plus a separate set for the step-2 "Confirm withdrawal" button (dark red by default). |
| Staff notification e-mail | Optional. A copy of every request is sent here. |
| Show in footer + Footer style | Yes/No to add the "Right of withdrawal" entry to the shop footer, shown as a plain text link or a coloured button (using your button colours). Turn off to place the button yourself. |
| Order reference must exist | Yes/No (default No). When on, the order reference must match a real order with the same e-mail before the withdrawal proceeds. Off is recommended - the law does not allow refusing a withdrawal for a lookup mismatch. |
| Anti-spam | Toggle the shared IP-block list and set the minimum seconds before submit. Rate / volume protection is delegated to the tbcontactformipblock module. |
The configure page also shows a status panel (is tbticketsystem detected? is the shared IP-block module detected?), a direct link to the request list, and a pasteable HTML button you can drop anywhere on your site. The button's colours (including the text colour) follow your colour settings, and a language picker lets you copy the button with the label and link in any installed language.
The form and the e-mail intentionally reuse the tbticketsystem card and e-mail chrome, so the two modules feel like one product. All colours are driven by CSS custom properties from the settings.
Anti-spam: two cheap bot guards of the module's own, plus delegated rate/volume protection (and CSRF):
- Honeypot - a hidden field real users never fill.
- Render timestamp - submissions faster than a human can type are rejected.
- Shared IP block list - when
tbcontactformipblock(v1.1.0+) is installed, this module calls itsactionTbIpBlockVerifyhook, which registers the IP and applies that module's own auto-block thresholds, so a blocked IP is blocked on both forms. This module no longer touches thecfipb_*tables directly - the hook is the only contract. There is no second rate limit here; if the hook is absent the form simply runs without IP protection (honeypot + timing still apply). Install tbcontactformipblock to get rate protection.
A per-visitor CSRF token (stored in the shop cookie) protects both steps. The confirm step additionally runs a read-only rate/IP check so a script cannot skip the form and POST the confirmation directly.
Anti-abuse posture: hard-block (by design). When a guard trips, the submission is refused (the consumer is shown a message and, for IP/rate, pointed to e-mail). This is the strongest spam protection. Note the trade-off: in theory a genuine withdrawal could be refused for a technical reason. If your legal preference is to never refuse, the guards can be switched to "accept + flag for review" - ask the maintainer.
One withdrawal per order reference + e-mail is enough (an order reference identifies exactly one order in thirty bees). A repeat - or a page refresh of the confirm step - does not create a second record/ticket/e-mail. Instead the consumer sees a neutral "This order was already withdrawn - contact customer service if you need more information." message (no date is revealed).
If the original confirmation e-mail had failed to send, a repeat attempt re-sends it, so a consumer is never left without their proof.
Customer-facing content is fully localised in 26 languages: the original nl, en, de, fr, es, it, da, sv, no, plus all common EU official languages pl, pt, cs, hu, ro, el, fi, sk, hr, sl, bg, et, lv, lt and the major non-EU languages ru, uk, tr. (Maltese and Irish are intentionally omitted, as those markets transact in English.)
Each language provides:
- the customer
withdrawal_acke-mail (HTML and plain text); - the internal
withdrawal_staffnotification e-mail (HTML and plain text); - the front-office UI via
translations/<iso>.php; - both e-mail subject lines.
English is the source language. The back-office (admin list / settings) is English. The 17 added languages were machine-translated with care; a native-speaker review is recommended before relying on them for legal-critical wording.
Everything is editable in the Back Office. All on-screen text, the e-mail
subjects, and the ticket subject/body are normal module translations
(Translations > Module translations, source files under translations/); the
e-mail bodies are edited under Translations > Email templates translations.
Nothing user-facing is hard-coded.
- Download
tbwithdrawalbutton-vX.Y.Z.zip(or build it withgit archive --format=zip --prefix=tbwithdrawalbutton/ -o tbwithdrawalbutton.zip HEAD). - Back office → Modules → Upload a module → drop the zip → Install.
- Open Configure to set colours, intro text, ticket destination and the staff e-mail. Defaults are sensible - the module works immediately after install.
- Confirm the "Right of withdrawal" link is visible in your theme's footer.
Requirements: thirty bees 1.x, PHP 7.4+, and working outgoing mail (so the durable-medium acknowledgement can be sent). The tbticketsystem and tbcontactformipblock modules are optional integrations, not requirements.
A normal uninstall keeps the tbwd_request table (it is legal evidence) and only
removes the menu tab and settings. A full uninstall (remove data) drops the table.
| Directive 2023/2673 requirement | How this module meets it |
|---|---|
| Clearly labelled withdrawal button, prominent and continuously available | Footer + account link to a standalone page; page heading "Withdraw from contract here" |
| Two-step: statement then a confirmation button | Step 1 form → Step 2 "Confirm withdrawal" |
| Collect identity, contract identification, electronic contact method | Name + order reference + e-mail (the e-mail is the electronic contact method) |
| Automatic acknowledgement of receipt, on a durable medium, with date and time | withdrawal_ack e-mail sent on confirm, with submitted_at date + time |
| No reason required; accept even if data is imperfect | Reason is optional and sanitised, never blocks; an unmatched order is still accepted |
| Proof of receipt | tbwd_request audit row, timestamped, kept as evidence |
This module is a technical aid for compliance, not legal advice. Confirm your overall setup (terms, model withdrawal form, refund process) with your own legal counsel.
- User manual:
Handleiding-tbwithdrawalbutton-v1.0.0-en.docx(English) /-nl.docx(Nederlands), anddocs/MANUAL-en.md/docs/MANUAL-nl.md. - Changelog:
CHANGELOG.md.
tbwithdrawalbutton.php install / config / hooks (footer + account)
config.xml module metadata
controllers/front/withdrawal.php the public 2-step page
controllers/admin/Admin…Controller back-office request list + CSV export
classes/ Request, backends + factory, SpamGuard, Mailer, Logger, ObjectModel
views/templates/front/ form / confirm / success / shared CSS
views/templates/hook/footer-link the footer link
mails/<iso>/withdrawal_ack.* customer confirmation e-mail (26 languages, html + txt)
mails/<iso>/withdrawal_staff.* internal staff notification (26 languages, html + txt)
translations/<iso>.php front-office UI strings (25 languages; en is source)
sql/install.sql · uninstall.sql the tbwd_request audit table
docs/ manuals