# Age Verification for PrestaShop – Blocking Modal for CBD, Alcohol, Vape & Healthcare Pros

> The dfagegate module adds a blocking age-verification modal to your PrestaShop 8 or 9 store. It covers two distinct markets: standard mode for age-restricted products (CBD, alcohol, vape, firearms, lighters,…

- Page: <https://www.datafirefly.com/en/documentation/verification-age-prestashop-modal-cbd-alcool-vape-medical/>
- Language: en
- Last updated: 2026-09-28
- Other languages: [fr](https://www.datafirefly.com/documentation/verification-age-prestashop-modal-cbd-alcool-vape-medical/index.md), [es](https://www.datafirefly.com/es/documentation/verification-age-prestashop-modal-cbd-alcool-vape-medical/index.md), [de](https://www.datafirefly.com/de/documentation/verification-age-prestashop-modal-cbd-alcool-vape-medical/index.md), [it](https://www.datafirefly.com/it/documentation/verification-age-prestashop-modal-cbd-alcool-vape-medical/index.md), [pl](https://www.datafirefly.com/pl/documentation/verification-age-prestashop-modal-cbd-alcool-vape-medical/index.md), [pt](https://www.datafirefly.com/pt/documentation/verification-age-prestashop-modal-cbd-alcool-vape-medical/index.md), [nl](https://www.datafirefly.com/nl/documentation/verification-age-prestashop-modal-cbd-alcool-vape-medical/index.md)
- Index: <https://www.datafirefly.com/en/documentation/llms.txt>

The **dfagegate** module adds a **blocking** age-verification modal to your PrestaShop 8 or 9 store. It covers two distinct markets: **standard mode** for age-restricted products (CBD, alcohol, vape, firearms, lighters, 18+ products), and **medical mode** for medical devices restricted to healthcare professionals (in France, within the meaning of article L5122-9 of the Public Health Code).

**Compatibility** — PrestaShop 1.7.7+, 8.x and 9.0. PHP 7.4 minimum, 8.1+ recommended. Multistore and multilingual (FR/EN/ES/DE pre-filled at installation).

## Installation

Installation follows the standard PrestaShop workflow. After purchasing on DataFirefly, you receive a ZIP file `dfagegate-X.Y.Z.zip`.

### Via the back office (recommended)

1. Log in to your PrestaShop back office
2. Go to **Modules → Module Catalog**
3. Click **Upload a module** at the top right
4. Select the ZIP `dfagegate-X.Y.Z.zip`
5. Once uploaded, click **Install**

### Via FTP

1. Unzip the archive locally
2. Upload the `dfagegate/` folder into your PrestaShop `modules/` directory
3. Go to **Modules → Module Catalog**
4. Search for "DataFirefly Age Gate" and click **Install**

**Important** — After installation, **the module is disabled by default**. This is intentional: it lets you configure texts and mode before blocking your store. Go to the module configuration and enable the toggle in the _General_ tab once everything is set up.

## Initial setup

Access the configuration: **Modules → Installed modules → DataFirefly Age Gate → Configure**.

Configuration is organized into 6 tabs:

- **General** — activation, mode, verification type, minimum age, display scope
- **Content** — multilingual texts (title, message, buttons, legal notices)
- **Design** — logo, colors, backdrop blur
- **Behavior** — cookie, redirect, bypass rules
- **Medical mode** — professions and license number (only applies in medical mode)
- **Logs & GDPR** — logging, statistics and diagnostics

### General tab

- **Enable module** — Yes/No toggle, controls the global display of the modal
- **Mode** — _Standard_ (CBD, alcohol, vape, firearms) or _Medical_ (healthcare professionals)
- **Verification type** — _Yes/no button_, _Date of birth_, or _Profession declaration_
- **Minimum age** — 18 by default, 21 for some markets (US for alcohol, for example)
- **Display scope** — whole store, targeted categories or targeted products (see below)
- **Test the modal** — button that opens the store in preview mode (see the Diagnostics section)

**Which verification type should you choose?** The _yes/no button_ suits most cases (CBD, mainstream alcohol, vape): fast, low friction, sufficient deterrent for most regulatory audits. _Date of birth_ is stricter and recommended for firearms or nicotine liquids. _Profession declaration_ is reserved for medical mode.

### Display scope: targeting categories or products

Since version 1.1.0, the **Display scope** setting in the General tab offers three modes:

- **Whole store** — the modal shows on every page. This is the legacy behaviour and the default value
- **Targeted categories only** — the modal shows on the pages of the checked categories and on the product pages of products belonging to them. Selection happens in a category tree with checkboxes, an instant search filter and a selection counter
- **Targeted products only** — the modal only shows on the pages of the selected products. Search by name or reference, suggestions appear as you type, and each selected product becomes a removable chip

In categories mode, the **Include sub-categories** option (enabled by default) automatically covers the full descendance of the checked categories: check "CBD" and its sub-categories "Oils", "Flowers" or "Resins" are covered without checking them one by one. The expansion relies on PrestaShop nested sets, in a single query.

**Mixed store?** This is the typical targeting use case: a fine grocery that also sells spirits, an online pharmacy with a professional equipment department. Target the regulated department by category: it is protected, and the rest of the store stays modal-free.

**Products mode and listing pages** — In targeted-products mode, listing pages (category, search, new products) do not trigger the modal: only the selected product pages do. To cover a whole department, listings included, prefer category targeting.

The display decision is made **server-side**, not in JavaScript. The diagnostic comment in the head tag shows the active scope: `scope=all`, `scope=categories` or `scope=products`. On save, a targeted scope with an empty selection is rejected with an error message, to avoid locking the configuration in an inconsistent state.

### Content tab

Each language enabled in your PrestaShop has its own text block. Defaults are pre-filled in **FR, EN, ES and DE**. For each language you can configure:

- **Title** — displayed large at the top of the modal (default: "Age Verification")
- **Message** — main explanatory text, line breaks are preserved
- **Confirm button** — positive button label (default: "I am 18 or older")
- **Refuse button** — negative button label
- **Legal notice** — text at the bottom of the modal (e.g. "Please drink responsibly.")
- **Denial message** — screen shown when the user refuses, before redirect

**HTML** — Content is sanitized on save (plain text only). Line breaks are converted to break tags at render time via an automatic `nl2br` filter.

### Design tab

- **Logo** — PNG, JPG, SVG or WEBP, 2 MB max, displayed at the top of the modal
- **Background color** — main card color (white by default)
- **Primary color** — title and main button (black #111111 by default)
- **Text color** — message body
- **Overlay color** — veil behind the modal, accepts CSS `rgba()` and hex formats (default: `rgba(15,15,20,0.85)`)
- **Backdrop blur** — background blur (modern effect, works in all current browsers)

### Behavior tab

- **Cookie duration** — in days (90 by default). Set `0` for a session cookie (deleted when the browser closes)
- **Redirect URL on refusal** — leave empty to only show the denial message, or set an external URL (Google, supplier page, etc.)
- **Bypass IPs** — list of IPs (one per line) for which the modal never shows. Ideal for you and your team during tests
- **Bypass URLs** — excluded partial paths. Pre-filled with `/legal`, `/contact`, and other legal pages
- **Bypass logged-in customers** — if enabled, already-authenticated customers never see the modal (useful if your store is restricted to already-validated accounts)

### Medical mode tab

This tab only applies when **mode** is set to _Medical_ and **verification type** to _Profession declaration_.

- **Profession list** — one per line. Defaults: Doctor, Pharmacist, Nurse, Physiotherapist, Dentist, Veterinarian, Other healthcare professional. Fully customizable
- **Mandatory license number (RPPS/ADELI)** — if enabled, a field appears in the modal. Regex validation accepting 9 to 11 digits. **The number is not stored**, it is only validated server-side

**Legal compliance** — Medical mode materializes a sworn declaration in the spirit of French article L5122-9 of the Public Health Code, which restricts advertising of certain medical devices to authorized healthcare professionals. **This module does not replace a legal review of your catalog by a specialized lawyer**. Consult your counsel to validate your configuration and check the equivalent rules in your jurisdiction.

### Logs & GDPR tab

- **Log refusals** — records each refusal in the `ps_dfagegate_log` table with SHA-256 hashed IP (never plain), date, reason
- **Log retention** — retention period in days (365 by default, `0` for unlimited). Refusals older than this period are deleted automatically every time the configuration page opens

Since version 1.1.0, this tab also shows **refusal statistics**: total over the last 30 days, breakdown by reason, and the 15 latest refusals with date, reason, declared age and profession where applicable. A **Purge all refusal logs** button (with confirmation) empties the table for the current store.

This tab also shows a GDPR summary:

- **Cookie set** — `dfagegate_ok`
- **Category** — strictly necessary (legal access compliance)
- **Data** — value "1", configurable duration, SameSite=Lax, Secure on HTTPS

## Configuring for your market

### Mainstream CBD store

- Mode: **Standard**
- Verification type: **Yes/no button**
- Minimum age: **18**
- Cookie duration: **90 days** (good balance between compliance and UX)
- Redirect URL: _empty_ (denial message only)

### Premium spirits / alcohol store

- Mode: **Standard**
- Verification type: **Date of birth** (stricter control for premium markets)
- Minimum age: **18** (or 21 for US markets)
- Redirect URL: any official information page

### Vape / nicotine e-liquid store

- Mode: **Standard**
- Verification type: **Date of birth** (strongly recommended for nicotine)
- Minimum age: **18**
- Log refusals: **Yes** (useful in case of a regulatory audit)

### Firearms store

- Mode: **Standard**
- Verification type: **Date of birth** (mandatory)
- Minimum age: **18**
- Bypass URLs: add `/regulations`, `/hunting-license`
- Log refusals: **Yes**

### Fine grocery with a spirits department (mixed store)

- Mode: **Standard**
- Verification type: **Yes/no button**
- Display scope: **Targeted categories**, with the Spirits category checked and sub-categories included
- Result: the modal only shows on the spirits department and its product pages, the rest of the grocery stays friction-free

### Medical equipment store (professional devices)

- Mode: **Medical**
- Verification type: **Profession declaration**
- Profession list: Doctor, Pharmacist, Physiotherapist, Osteopath (adapt to your catalog)
- Mandatory license number: **Yes**
- Bypass logged-in customers: **Yes** (if you already validate the profession at registration)

## How date-of-birth verification works

Unlike a simple button, date-of-birth verification performs the calculation **server-side**, not in the browser:

1. The visitor enters day, month and year in the modal
2. JavaScript sends these values to the AJAX controller `DfagegateAjaxModuleFrontController`
3. PHP validates the date with `checkdate()` then computes the age via `DateTimeImmutable::diff()`
4. If the age is under the configured threshold, the server returns a JSON response with `success=false, denied=true` and the error message
5. The modal shows the denial message and redirects after 2 seconds
6. If the age meets the threshold, the `dfagegate_ok` cookie is set and the modal closes

**Why server-side?** A purely JavaScript check can be bypassed via DevTools in under 10 seconds. Server-side calculation guarantees that an underage user cannot access the site, even with technical knowledge. This is essential to withstand a regulatory audit.

Note that **the date of birth is never stored** — it is used for a single calculation and then forgotten. Only the binary validation (accepted / refused) is retained via the cookie.

## How medical mode works

1. The modal shows a dropdown of professions (configurable) and optionally a license number field
2. A mandatory **sworn declaration** checkbox is present
3. The AJAX controller validates that a profession is selected
4. If the license number is mandatory, the server validates the format via a regular expression accepting 9 to 11 consecutive digits
5. No storage: neither the profession nor the number is kept in the database — the `dfagegate_ok` cookie only materializes the successful passage

**Design choice** — The regulation requires materializing the declaration, not necessarily a real-time check against a national registry. Our approach is _minimum viable compliance_: we ask for the declaration, formally validate it, log the refusal if enabled, but collect no unnecessary personal data. If you need real-time registry verification, that is a separate custom development.

## Multistore

The module is **100% multistore compatible**. All settings (mode, verification type, multilingual texts, colors, bypass rules, display scope) are stored per shop context via `id_shop_group` and `id_shop`. This means a single PrestaShop can host:

- A **CBD FR** store in standard mode at 18 with French texts
- A **vape UK** store in standard mode at 18 with English texts
- A **medical equipment DE** store in medical mode with mandatory license number and German texts

To configure a specific sub-store:

1. In the context selector at the top of the back office, pick the target sub-store
2. Open the module configuration
3. Edit the values — they will be persisted for that store only

**Hooks are registered on all stores** at installation time via `Shop::getCompleteListOfShopsID()`, avoiding the classic pitfall of a module that only runs on the current store.

## Compatibility with custom themes

The module uses the standard `displayBeforeBodyClosingTag` hook to inject the modal just before the closing body tag. This hook is supposed to be universal on PrestaShop 1.7.5+.

Unfortunately, **some custom themes do not call this hook** in their layout. For this case, dfagegate ships with a **JavaScript injection fallback**:

1. PHP pre-renders the full modal HTML and passes it to JS via `Media::addJsDef`
2. On `DOMContentLoaded`, the script checks whether the element with ID `dfagegate-modal` exists in the DOM
3. If yes, all good — the hook worked
4. If not, the script injects the modal itself via `insertAdjacentHTML('beforeend', ...)`
5. A `console.info` message confirms the fallback activated: "[dfagegate] modal injected via JS fallback (theme does not trigger displayBeforeBodyClosingTag)."

**Practical result** — The module works on any PrestaShop 1.7.5+ theme, including incomplete custom themes, **without touching the layout**. You can deploy it without coordinating with your theme agency.

## Diagnostics and debugging

Once enabled, dfagegate adds an **HTML comment in the head tag** of every front page, in the form "dfagegate v1.1.0 enabled=1 scope=all should_display=1".

This comment is your first diagnostic checkpoint. View the source of a front page (Ctrl+U or Cmd+U) and search for "dfagegate".

| Comment | Interpretation |
| --- | --- |
| No comment at all | The `displayHeader` hook is not registered — check the module is installed and active |
| `enabled=0` | The "Enable module" toggle is off in the General tab |
| `enabled=1 should_display=0` | A bypass is active (your IP is whitelisted, the current URL matches an excluded path, or you are a logged-in customer with the bypass enabled), or the current page is outside the targeted scope — check the `scope=` value |
| `enabled=1 should_display=1` | Server-side, everything is fine. If the modal does not appear, check the browser console for a fallback or error message |

### The modal still does not appear?

1. Is the diagnostic comment present with `should_display=1`? If not, fix the config
2. Open the store in **private browsing**. The `dfagegate_ok` cookie may have been set in a previous session
3. Check your IP against the bypasses in the _Behavior_ tab
4. With a targeted scope, check that the tested page actually belongs to the selected categories or products
5. Open the browser console (F12). Look for `[dfagegate]` messages
6. Check PrestaShop logs in **Advanced Parameters → Logs**, filter on "dfagegate"
7. **Clear the PrestaShop cache** after every configuration change: _Advanced Parameters → Performance → Clear cache_

### Testing with preview mode

Since version 1.1.0, the **Test the modal** button in the General tab opens the store in a new tab with the `?dfagegate_preview=1` parameter. In this mode:

- The modal shows **even if the dfagegate_ok cookie is already set**
- Confirming sets **no cookie**: the test is repeatable at will, without private browsing or manual deletion

With a targeted scope, navigate to a covered page while keeping the parameter in the URL to see the modal.

### Resetting the cookie in your browser

1. Open DevTools (F12)
2. **Application** tab (Chrome) or **Storage** (Firefox)
3. **Cookies** section → your domain
4. Delete the `dfagegate_ok` row
5. Reload the page

## GDPR compliance

### The `dfagegate_ok` cookie

- **Type** — strictly necessary to comply with a legal access obligation
- **Legal basis** — exempt from prior consent (in France, under article 82 of the Data Protection Act per CNIL guidance; similar exemptions exist under the ePrivacy framework)
- **Value** — binary (`1` = confirmed)
- **Duration** — configurable (90 days by default), or session if set to 0
- **Attributes** — `SameSite=Lax`, automatic `Secure` on HTTPS, `Path=/`

**In practice** — You do **not** need to add this cookie to your consent banner. It falls into the same category as the PrestaShop session cookie or the CSRF cookie: necessary for the legal operation of the site, therefore exempt.

### Refusal logs

If you enable _Log refusals_, every refusal is recorded in the `ps_dfagegate_log` table with:

- `id_shop` — sub-store concerned
- `reason` — refusal reason (`user_refused` or `dob_under_age`)
- `age` — declared age if applicable
- `profession` — declared profession if applicable
- `ip_hash` — **SHA-256 of the IP**, never the plain IP
- `date_add` — timestamp

SHA-256 hashing makes the IP **non-reversible** while still allowing deduplication of attempts (the same IP always produces the same hash). This is the compromise recommended by data-protection authorities for access statistics.

**Retention is automatic** since 1.1.0: entries older than the configured period (365 days by default) are purged every time the configuration page opens. A time-limited retention is exactly what the GDPR minimisation principle expects from this kind of log. The statistics in the Logs tab (30-day total, breakdown by reason, latest refusals) are based on this same hashed data.

### Verification data

- The **date of birth** transits via AJAX for the calculation but is **never stored**
- The **license number** is validated server-side then **forgotten**, never stored
- Only the **binary validation** is retained, via the cookie

## Technical structure and integration

### Hooks used

- `displayHeader` — injects the diagnostic comment
- `actionFrontControllerSetMedia` — registers CSS and JS, passes the config and the pre-rendered modal HTML to JS
- `displayBeforeBodyClosingTag` — renders the modal server-side (JS fallback if absent from the theme)

### AJAX endpoints

The AJAX controller responds at `/module/dfagegate/ajax` and accepts two actions:

- `action=confirm` — with parameters depending on the verification type (nothing for yes/no, day/month/year for DOB, profession/license for medical)
- `action=refuse` — logs the refusal and returns the redirect URL

Responses are JSON. A confirmation returns `success=true`. An underage refusal returns `success=false`, `denied=true` and the appropriate error message. A validation error returns `success=false` with the message. A user refusal returns `success=true` and `redirect_url` containing the configured redirect URL.

### Database schema

A single table is created: `ps_dfagegate_log`. It contains the columns `id_log` (auto-increment primary key), `id_shop`, `reason` (varchar 64), `age` (nullable), `profession` (varchar 128 nullable), `ip_hash` (char 64 for the SHA-256) and `date_add` (datetime). Two secondary indexes optimize reporting queries: `idx_shop_date` on (id_shop, date_add) and `idx_reason` on reason.

## Uninstallation

### Disable without removing

**Modules → Installed modules → DataFirefly Age Gate → Disable**. Configuration and the log table are kept. You can re-enable at any time without reconfiguring.

### Full uninstall

**Modules → Installed modules → DataFirefly Age Gate → Uninstall**. This action:

- Drops the `ps_dfagegate_log` table
- Removes all configuration entries (23 scalar keys + 6 multilingual keys)
- Unregisters the hooks
- Removes the module from the system

**Warning** — Uninstallation is **irreversible**. If you want to keep the refusal log history (e.g. for a data-protection audit), export the table first.

## Support and updates

Every license includes:

- **12 months of updates** — PrestaShop compatibility, fixes, improvements
- **Email technical support** — response within 24 business hours, FR/EN
- **30-day refund**
- **Unencrypted source code** — you are free to adapt the module to your specific needs

For any technical question or bug report, contact us from your DataFirefly account. Please include your PrestaShop version, PHP version, module version (visible at the top of the configuration screen), and if possible the diagnostic comment present in the head tag of your store.
