initial module

This commit is contained in:
2026-09-30 14:52:38 +02:00
parent 60317152e5
commit 972b8d965b
15 changed files with 911 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
vendor/
+28
View File
@@ -0,0 +1,28 @@
{
"name": "websmid/trustpilotafsbcc",
"description": "BCCs the order confirmation e-mail to Trustpilot's AFS invitation address",
"type": "prestashop-module",
"authors": [
{
"name": "Isabelle Oving-Anno - De Websmid BV",
"email": "isabelle@dewebsmid.nl"
}
],
"require": {
"php": ">=8.1.0"
},
"autoload": {
"psr-4": {
"Websmid\\TrustpilotAfsBcc\\": "src/"
},
"classmap": [
"ws_trustpilotafsbcc.php"
],
"exclude-from-classmap": []
},
"config": {
"preferred-install": "dist",
"prepend-autoloader": false
},
"license": "MIT"
}
Generated
+20
View File
@@ -0,0 +1,20 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "e29c932400094498561eeb7d4c1bf468",
"packages": [],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=8.1.0"
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+93
View File
@@ -0,0 +1,93 @@
<?php
namespace Websmid\TrustpilotAfsBcc\Config;
class Config
{
const HOOKS = [
'actionEmailSendBefore',
'actionGetExtraMailTemplateVars',
'actionEmailAddAfterContent',
'actionFrontControllerSetMedia',
'displayTopUsp',
'displayBanner',
];
const TABS = [];
// AFS invitations
const CONFIG_BCC_EMAIL = 'WS_TRUSTPILOT_AFS_BCC_EMAIL';
const CONFIG_TRIGGER_TEMPLATES = 'WS_TRUSTPILOT_AFS_TRIGGER_TEMPLATES';
const DEFAULT_TRIGGER_TEMPLATES = 'order_conf';
const CONFIG_AFS_LOCALE = 'WS_TRUSTPILOT_AFS_LOCALE';
const CONFIG_AFS_TEMPLATE_ID = 'WS_TRUSTPILOT_AFS_TEMPLATE_ID';
// Mail template variable holding the JSON body of Trustpilot's AFS structured data snippet
const AFS_SNIPPET_VAR = '{ws_trustpilot_afs_snippet}';
// TrustBox widget
const CONFIG_TRUSTBOX_ENABLED = 'WS_TRUSTPILOT_TRUSTBOX_ENABLED';
const CONFIG_TRUSTBOX_BUSINESS_UNIT_ID = 'WS_TRUSTPILOT_TRUSTBOX_BUSINESS_UNIT_ID';
const CONFIG_TRUSTBOX_TEMPLATE_ID = 'WS_TRUSTPILOT_TRUSTBOX_TEMPLATE_ID';
const CONFIG_TRUSTBOX_THEME = 'WS_TRUSTPILOT_TRUSTBOX_THEME';
const CONFIG_TRUSTBOX_LOCALE = 'WS_TRUSTPILOT_TRUSTBOX_LOCALE';
const CONFIG_TRUSTBOX_PROFILE_URL = 'WS_TRUSTPILOT_TRUSTBOX_PROFILE_URL';
// Trustpilot's public "Micro Review Count" TrustBox template
const DEFAULT_TRUSTBOX_TEMPLATE_ID = '5419b6a8b0d04a076446a9ad';
const DEFAULT_TRUSTBOX_THEME = 'dark';
const TRUSTBOX_THEMES = [
'Dark' => 'dark',
'Light' => 'light',
];
const TRUSTBOX_SCRIPT_URL = 'https://widget.trustpilot.com/bootstrap/v5/tp.widget.bootstrap.min.js';
const TRUSTBOX_TEMPLATE_DIR = 'module:ws_trustpilotafsbcc/views/templates/hook/';
// Display hook => template rendered in it
const TRUSTBOX_HOOK_TEMPLATES = [
'displayTopUsp' => 'trustbox_desktop.tpl',
'displayBanner' => 'trustbox_mobile.tpl',
];
// Front controller (php_self) the TrustBox is shown on
const TRUSTBOX_PAGE = 'index';
// Language defaults, keyed by PrestaShop iso_code
const DEFAULT_LOCALES = [
'nl' => 'nl-BE',
'fr' => 'fr-BE',
'en' => 'en-GB',
'de' => 'de-DE',
];
const PROFILE_HOSTS = [
'nl' => 'nl-be.trustpilot.com',
'fr' => 'fr-be.trustpilot.com',
'en' => 'www.trustpilot.com',
'de' => 'de.trustpilot.com',
];
const DEFAULT_PROFILE_HOST = 'www.trustpilot.com';
// Validation patterns
const PATTERN_TRUSTPILOT_ID = '/^[a-f0-9]{24}$/i';
const PATTERN_LOCALE = '/^[a-z]{2}-[A-Z]{2}$/';
}
+292
View File
@@ -0,0 +1,292 @@
<?php
namespace Websmid\TrustpilotAfsBcc\Module;
use Websmid\TrustpilotAfsBcc\Config\Config;
use Configuration;
use Language;
use Tools;
use Validate;
trait AdminHooks
{
public function isUsingNewTranslationSystem(): bool
{
return true;
}
public function getContent()
{
$confirmation = '';
if (Tools::isSubmit('submitWsTrustpilotAfsBcc')) {
$confirmation = $this->saveConfiguration();
}
$this->context->smarty->assign(array_merge(
$this->loadConfigurationValues(),
[
'languages' => Language::getLanguages(false),
'trustbox_themes' => Config::TRUSTBOX_THEMES,
'form_action' => $this->context->link->getAdminLink('AdminModules') . '&configure=' . $this->name,
'confirmation' => $confirmation,
]
));
return $this->fetch('module:ws_trustpilotafsbcc/views/templates/admin/configuration.tpl');
}
private function loadConfigurationValues(): array
{
return [
'bcc_email' => Configuration::get(Config::CONFIG_BCC_EMAIL),
'trigger_templates' => Configuration::get(Config::CONFIG_TRIGGER_TEMPLATES),
'afs_locales' => Configuration::getConfigInMultipleLangs(Config::CONFIG_AFS_LOCALE),
'afs_template_ids' => Configuration::getConfigInMultipleLangs(Config::CONFIG_AFS_TEMPLATE_ID),
'trustbox_enabled' => (bool) Configuration::get(Config::CONFIG_TRUSTBOX_ENABLED),
'trustbox_business_unit_id' => Configuration::get(Config::CONFIG_TRUSTBOX_BUSINESS_UNIT_ID),
'trustbox_template_id' => Configuration::get(Config::CONFIG_TRUSTBOX_TEMPLATE_ID),
'trustbox_theme' => Configuration::get(Config::CONFIG_TRUSTBOX_THEME),
'trustbox_locales' => Configuration::getConfigInMultipleLangs(Config::CONFIG_TRUSTBOX_LOCALE),
'trustbox_profile_urls' => Configuration::getConfigInMultipleLangs(Config::CONFIG_TRUSTBOX_PROFILE_URL),
];
}
private function saveConfiguration(): string
{
$values = $this->buildSubmittedValues();
$errors = $this->validateSubmittedValues($values);
if (!empty($errors)) {
return $this->displayError($errors);
}
foreach ($values as $key => $value) {
Configuration::updateValue($key, $value);
}
$this->clearTrustboxCache();
return $this->displayConfirmation($this->trans('Settings saved.', [], 'Admin.Notifications.Success'));
}
private function buildSubmittedValues(): array
{
$theme = (string) Tools::getValue('trustbox_theme');
return [
Config::CONFIG_BCC_EMAIL => trim((string) Tools::getValue('bcc_email')),
Config::CONFIG_TRIGGER_TEMPLATES => trim((string) Tools::getValue('trigger_templates')),
Config::CONFIG_AFS_LOCALE => $this->buildSubmittedLangValues('afs_locale'),
Config::CONFIG_AFS_TEMPLATE_ID => $this->buildSubmittedLangValues('afs_template_id'),
Config::CONFIG_TRUSTBOX_ENABLED => (int) (bool) Tools::getValue('trustbox_enabled'),
Config::CONFIG_TRUSTBOX_BUSINESS_UNIT_ID => trim((string) Tools::getValue('trustbox_business_unit_id')),
Config::CONFIG_TRUSTBOX_TEMPLATE_ID => trim((string) Tools::getValue('trustbox_template_id')),
Config::CONFIG_TRUSTBOX_THEME => in_array($theme, Config::TRUSTBOX_THEMES, true) ? $theme : Config::DEFAULT_TRUSTBOX_THEME,
Config::CONFIG_TRUSTBOX_LOCALE => $this->buildSubmittedLangValues('trustbox_locale'),
Config::CONFIG_TRUSTBOX_PROFILE_URL => $this->buildSubmittedLangValues('trustbox_profile_url'),
];
}
private function buildSubmittedLangValues(string $field): array
{
$submitted = Tools::getValue($field);
$values = [];
foreach (Language::getLanguages(false) as $language) {
$idLang = (int) $language['id_lang'];
$values[$idLang] = is_array($submitted) ? trim((string) ($submitted[$idLang] ?? '')) : '';
}
return $values;
}
private function validateSubmittedValues(array $values): array
{
$errors = [];
$bccEmail = $values[Config::CONFIG_BCC_EMAIL];
if ($bccEmail !== '' && !Validate::isEmail($bccEmail)) {
$errors[] = $this->trans('Please enter a valid e-mail address.', [], 'Modules.Wstrustpilotafsbcc.Admin');
}
if (!$this->isValidTrustpilotId($values[Config::CONFIG_TRUSTBOX_BUSINESS_UNIT_ID])
|| !$this->isValidTrustpilotId($values[Config::CONFIG_TRUSTBOX_TEMPLATE_ID])
|| !$this->areValidTrustpilotIds($values[Config::CONFIG_AFS_TEMPLATE_ID])
) {
$errors[] = $this->trans('Trustpilot IDs must be 24 characters (0-9, a-f).', [], 'Modules.Wstrustpilotafsbcc.Admin');
}
if (!$this->areValidLocales($values[Config::CONFIG_AFS_LOCALE])
|| !$this->areValidLocales($values[Config::CONFIG_TRUSTBOX_LOCALE])
) {
$errors[] = $this->trans('Locales must look like nl-BE.', [], 'Modules.Wstrustpilotafsbcc.Admin');
}
foreach ($values[Config::CONFIG_TRUSTBOX_PROFILE_URL] as $profileUrl) {
if ($profileUrl !== '' && !Validate::isAbsoluteUrl($profileUrl)) {
$errors[] = $this->trans('Profile URLs must be full URLs starting with https://.', [], 'Modules.Wstrustpilotafsbcc.Admin');
break;
}
}
if ($values[Config::CONFIG_TRUSTBOX_ENABLED] && $values[Config::CONFIG_TRUSTBOX_BUSINESS_UNIT_ID] === '') {
$errors[] = $this->trans('The TrustBox needs a business unit ID before it can be enabled.', [], 'Modules.Wstrustpilotafsbcc.Admin');
}
return $errors;
}
private function isValidTrustpilotId(string $value): bool
{
return $value === '' || preg_match(Config::PATTERN_TRUSTPILOT_ID, $value) === 1;
}
private function areValidTrustpilotIds(array $values): bool
{
foreach ($values as $value) {
if (!$this->isValidTrustpilotId($value)) {
return false;
}
}
return true;
}
private function areValidLocales(array $values): bool
{
foreach ($values as $value) {
if ($value !== '' && preg_match(Config::PATTERN_LOCALE, $value) !== 1) {
return false;
}
}
return true;
}
private function clearTrustboxCache(): void
{
foreach (Config::TRUSTBOX_HOOK_TEMPLATES as $template) {
$this->_clearCache(Config::TRUSTBOX_TEMPLATE_DIR . $template);
}
}
// In the PS core Mail::send() dispatches actionEmailSendBefore before it validates $bcc, passing
// bcc' => &$bcc by reference. Writing $params['bcc'] here rebinds that reference and
// propagates back to Mail::send()'s own $bcc variable
public function hookActionEmailSendBefore(array $params)
{
if (!$this->isAfsActiveFor((string) ($params['template'] ?? ''))) {
return;
}
$params['bcc'] = $this->buildBccValue($params['bcc'] ?? null, (string) Configuration::get(Config::CONFIG_BCC_EMAIL));
}
// Mail::send() passes 'extra_template_vars' => &$extraTemplateVars and merges it into the
// template vars right before strtr() fills in the HTML, so the JSON lands in the placeholder
// injected by hookActionEmailAddAfterContent below
public function hookActionGetExtraMailTemplateVars(array $params)
{
if (!$this->isAfsActiveFor((string) ($params['template'] ?? ''))) {
return;
}
$params['extra_template_vars'][Config::AFS_SNIPPET_VAR] = $this->buildAfsSnippetJson(
(array) ($params['template_vars'] ?? []),
(int) ($params['id_lang'] ?? 0)
);
}
// Mail::send() passes 'template_html' => &$templateHtml after loading the template file and
// before the template vars are applied. Only the HTML part gets the snippet
public function hookActionEmailAddAfterContent(array $params)
{
if (!$this->isAfsActiveFor((string) ($params['template'] ?? ''))) {
return;
}
$params['template_html'] = $this->buildHtmlWithSnippet(
(string) ($params['template_html'] ?? ''),
$this->renderAfsSnippet()
);
}
private function isAfsActiveFor(string $template): bool
{
$bccEmail = (string) Configuration::get(Config::CONFIG_BCC_EMAIL);
if ($bccEmail === '' || !Validate::isEmail($bccEmail)) {
return false;
}
return in_array($template, $this->loadTriggerTemplates(), true);
}
private function loadTriggerTemplates(): array
{
$raw = (string) Configuration::get(Config::CONFIG_TRIGGER_TEMPLATES);
return array_filter(array_map('trim', explode(',', $raw)));
}
private function buildBccValue($existingBcc, string $bccEmail)
{
if (is_array($existingBcc)) {
$existingBcc[] = $bccEmail;
return $existingBcc;
}
if (!empty($existingBcc)) {
return [$existingBcc, $bccEmail];
}
return $bccEmail;
}
private function buildAfsSnippetJson(array $templateVars, int $idLang): string
{
$firstname = (string) ($templateVars['{firstname}'] ?? '');
$lastname = (string) ($templateVars['{lastname}'] ?? '');
$data = array_filter([
'recipientEmail' => (string) ($templateVars['{email}'] ?? ''),
'recipientName' => trim($firstname . ' ' . $lastname),
'referenceId' => (string) ($templateVars['{order_name}'] ?? ''),
'locale' => (string) Configuration::get(Config::CONFIG_AFS_LOCALE, $idLang),
'templateId' => (string) Configuration::get(Config::CONFIG_AFS_TEMPLATE_ID, $idLang),
], static function (string $value) {
return $value !== '';
});
// HEX flags keep <, >, &, ' and " out of the output, so it is safe inside a <script> block
return (string) json_encode(
$data,
JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
);
}
// Absolute path instead of a 'module:' resource: order e-mails can also be sent from the back
// office or a cron, and the 'module:' resource registration there could not be verified
private function renderAfsSnippet(): string
{
$this->context->smarty->assign(['snippet_placeholder' => Config::AFS_SNIPPET_VAR]);
return $this->fetch($this->getLocalPath() . 'views/templates/hook/afs_snippet.tpl');
}
private function buildHtmlWithSnippet(string $html, string $snippet): string
{
$bodyClosePosition = strripos($html, '</body>');
if ($bodyClosePosition === false) {
return $html . $snippet;
}
return substr_replace($html, $snippet, $bodyClosePosition, 0);
}
}
+83
View File
@@ -0,0 +1,83 @@
<?php
namespace Websmid\TrustpilotAfsBcc\Module;
use Websmid\TrustpilotAfsBcc\Config\Config;
use Configuration;
trait DisplayHooks
{
public function hookActionFrontControllerSetMedia(array $params)
{
if (!$this->isTrustboxActiveOnPage()) {
return;
}
$this->context->controller->registerJavascript(
'ws-trustpilotafsbcc-trustbox',
Config::TRUSTBOX_SCRIPT_URL,
['server' => 'remote', 'position' => 'head', 'priority' => 200, 'attributes' => 'async']
);
$this->context->controller->registerStylesheet(
'ws-trustpilotafsbcc-front',
'modules/' . $this->name . '/views/css/front.css',
['media' => 'all', 'priority' => 200]
);
}
public function renderWidget($hookName, array $configuration)
{
$template = $this->loadTrustboxTemplate((string) $hookName);
// Page check stays outside the Smarty cache: the cache is per hook + language, not per page
if ($template === null || !$this->isTrustboxActiveOnPage()) {
return '';
}
$cacheId = $this->getCacheId('ws_trustpilotafsbcc_' . $hookName);
if (!$this->isCached($template, $cacheId)) {
$this->context->smarty->assign($this->getWidgetVariables($hookName, $configuration));
}
return $this->fetch($template, $cacheId);
}
public function getWidgetVariables($hookName, array $configuration)
{
$idLang = (int) $this->context->language->id;
return [
'trustbox' => [
'business_unit_id' => (string) Configuration::get(Config::CONFIG_TRUSTBOX_BUSINESS_UNIT_ID),
'template_id' => (string) Configuration::get(Config::CONFIG_TRUSTBOX_TEMPLATE_ID),
'theme' => (string) Configuration::get(Config::CONFIG_TRUSTBOX_THEME),
'locale' => (string) Configuration::get(Config::CONFIG_TRUSTBOX_LOCALE, $idLang),
'profile_url' => (string) Configuration::get(Config::CONFIG_TRUSTBOX_PROFILE_URL, $idLang),
],
];
}
private function loadTrustboxTemplate(string $hookName): ?string
{
if (!isset(Config::TRUSTBOX_HOOK_TEMPLATES[$hookName])) {
return null;
}
return Config::TRUSTBOX_TEMPLATE_DIR . Config::TRUSTBOX_HOOK_TEMPLATES[$hookName];
}
private function isTrustboxActiveOnPage(): bool
{
if (!Configuration::get(Config::CONFIG_TRUSTBOX_ENABLED)
|| (string) Configuration::get(Config::CONFIG_TRUSTBOX_BUSINESS_UNIT_ID) === ''
) {
return false;
}
$controller = $this->context->controller;
return isset($controller->php_self) && $controller->php_self === Config::TRUSTBOX_PAGE;
}
}
+108
View File
@@ -0,0 +1,108 @@
<?php
namespace Websmid\TrustpilotAfsBcc\Module;
use Websmid\TrustpilotAfsBcc\Config\Config;
use Configuration;
use Language;
trait Install
{
public function install()
{
if (!parent::install()) {
return false;
}
if (!$this->registerHooks()) {
return false;
}
if (!Configuration::updateValue(Config::CONFIG_TRIGGER_TEMPLATES, Config::DEFAULT_TRIGGER_TEMPLATES)) {
return false;
}
if (!$this->installLanguageDefaults()) {
return false;
}
if (!$this->installTrustboxDefaults()) {
return false;
}
return true;
}
public function registerHooks()
{
foreach (Config::HOOKS as $hook) {
if (!$this->registerHook($hook)) {
return false;
}
}
return true;
}
// Also called from upgrade/upgrade-1.1.0.php, so it only touches keys introduced in 1.1.0.
// Every language key gets a row per language here (even an empty one): Configuration::get()
// only reads a key per language once it has rows in configuration_lang (isLangKey())
public function installLanguageDefaults(): bool
{
$locales = [];
$templateIds = [];
$profileUrls = [];
foreach (Language::getLanguages(false) as $language) {
$idLang = (int) $language['id_lang'];
$iso = (string) $language['iso_code'];
$locales[$idLang] = Config::DEFAULT_LOCALES[$iso] ?? (string) $language['locale'];
$templateIds[$idLang] = '';
$profileUrls[$idLang] = $this->buildDefaultProfileUrl($iso);
}
if (!Configuration::updateValue(Config::CONFIG_AFS_LOCALE, $locales)) {
return false;
}
if (!Configuration::updateValue(Config::CONFIG_AFS_TEMPLATE_ID, $templateIds)) {
return false;
}
if (!Configuration::updateValue(Config::CONFIG_TRUSTBOX_LOCALE, $locales)) {
return false;
}
if (!Configuration::updateValue(Config::CONFIG_TRUSTBOX_PROFILE_URL, $profileUrls)) {
return false;
}
return true;
}
public function installTrustboxDefaults(): bool
{
if (!Configuration::updateValue(Config::CONFIG_TRUSTBOX_ENABLED, 0)) {
return false;
}
if (!Configuration::updateValue(Config::CONFIG_TRUSTBOX_TEMPLATE_ID, Config::DEFAULT_TRUSTBOX_TEMPLATE_ID)) {
return false;
}
if (!Configuration::updateValue(Config::CONFIG_TRUSTBOX_THEME, Config::DEFAULT_TRUSTBOX_THEME)) {
return false;
}
return true;
}
private function buildDefaultProfileUrl(string $iso): string
{
$host = Config::PROFILE_HOSTS[$iso] ?? Config::DEFAULT_PROFILE_HOST;
$shopDomain = preg_replace('/^www\./i', '', (string) Configuration::get('PS_SHOP_DOMAIN'));
return 'https://' . $host . '/review/' . $shopDomain;
}
}
+29
View File
@@ -0,0 +1,29 @@
/* Only loaded on pages where the TrustBox is shown (see DisplayHooks::hookActionFrontControllerSetMedia) */
.ws-trustbox .trustpilot-widget a {
color: #fff;
font-size: .85em;
}
/* Desktop: sits in the theme's black header bar (nav.header-nav, displayTopUsp) */
.ws-trustbox--desktop {
width: 260px;
}
header#header nav.header-nav div.usp_list {
max-width: calc(100% - 185px - 260px);
}
/* Mobile: the theme hides nav.header-nav below md, so this bar renders in displayBanner instead.
Background matches the header bar (_custom.scss: --native-color-black) */
.ws-trustbox--mobile {
background: var(--native-color-black, #0A1433);
padding: 6px 0;
}
.ws-trustbox--mobile .trustpilot-widget {
margin: 0 auto;
max-width: 280px;
}
+181
View File
@@ -0,0 +1,181 @@
{if $confirmation}
{$confirmation nofilter}
{/if}
<form method="post" action="{$form_action|escape:'htmlall':'UTF-8'}">
<div class="panel">
<div class="panel-heading">
<i class="icon-envelope"></i>
{l s='Review invitations (Automatic Feedback Service)' d='Modules.Wstrustpilotafsbcc.Admin'}
</div>
<div class="panel-body">
<p class="help-block">
{l s='Enter the unique invitation address from this site\'s Trustpilot Business Portal (AFS / invitation setup). It will be BCC\'d on every e-mail matching one of the trigger templates below.' d='Modules.Wstrustpilotafsbcc.Admin'}
</p>
<div class="form-group">
<label for="bcc_email" class="control-label">
{l s='Trustpilot AFS invitation address' d='Modules.Wstrustpilotafsbcc.Admin'}
</label>
<input
type="email"
id="bcc_email"
name="bcc_email"
class="form-control"
placeholder="xxxxxxxxxx@invite.trustpilot.com"
value="{$bcc_email|escape:'htmlall':'UTF-8'}"
>
</div>
<div class="form-group">
<label for="trigger_templates" class="control-label">
{l s='Trigger e-mail templates (comma-separated)' d='Modules.Wstrustpilotafsbcc.Admin'}
</label>
<input
type="text"
id="trigger_templates"
name="trigger_templates"
class="form-control"
value="{$trigger_templates|escape:'htmlall':'UTF-8'}"
>
<p class="help-block">
{l s='Mail template name(s) as used by Mail::send(), e.g. order_conf. ' d='Modules.Wstrustpilotafsbcc.Admin'}
</p>
</div>
<h4>{l s='Invitation language per shop language' d='Modules.Wstrustpilotafsbcc.Admin'}</h4>
<p class="help-block">
{l s='Added to the trigger e-mail as Trustpilot structured data, so each customer gets the invitation in their own language. The template ID is optional: leave it empty to use your default template for that locale.' d='Modules.Wstrustpilotafsbcc.Admin'}
</p>
<table class="table">
<thead>
<tr>
<th>{l s='Language' d='Modules.Wstrustpilotafsbcc.Admin'}</th>
<th>{l s='Trustpilot locale (e.g. nl-BE)' d='Modules.Wstrustpilotafsbcc.Admin'}</th>
<th>{l s='Invitation template ID (optional)' d='Modules.Wstrustpilotafsbcc.Admin'}</th>
</tr>
</thead>
<tbody>
{foreach $languages as $language}
{assign var='idLang' value=$language.id_lang}
<tr>
<td>{$language.name|escape:'htmlall':'UTF-8'}</td>
<td>
<input
type="text"
name="afs_locale[{$idLang|intval}]"
class="form-control"
value="{if isset($afs_locales[$idLang])}{$afs_locales[$idLang]|escape:'htmlall':'UTF-8'}{/if}"
>
</td>
<td>
<input
type="text"
name="afs_template_id[{$idLang|intval}]"
class="form-control"
value="{if isset($afs_template_ids[$idLang])}{$afs_template_ids[$idLang]|escape:'htmlall':'UTF-8'}{/if}"
>
</td>
</tr>
{/foreach}
</tbody>
</table>
</div>
</div>
<div class="panel">
<div class="panel-heading">
<i class="icon-star"></i>
{l s='TrustBox widget (homepage header)' d='Modules.Wstrustpilotafsbcc.Admin'}
</div>
<div class="panel-body">
<p class="help-block">
{l s='Shown on the homepage only: in the header bar on desktop, and in its own bar at the top on mobile. Copy the IDs from the TrustBox code in your Trustpilot Business Portal (Integrations > TrustBox library).' d='Modules.Wstrustpilotafsbcc.Admin'}
</p>
<div class="form-group">
<label for="trustbox_enabled" class="control-label">
{l s='Show TrustBox' d='Modules.Wstrustpilotafsbcc.Admin'}
</label>
<select id="trustbox_enabled" name="trustbox_enabled" class="form-control">
<option value="0"{if !$trustbox_enabled} selected{/if}>{l s='No' d='Admin.Global'}</option>
<option value="1"{if $trustbox_enabled} selected{/if}>{l s='Yes' d='Admin.Global'}</option>
</select>
</div>
<div class="form-group">
<label for="trustbox_business_unit_id" class="control-label">
{l s='Business unit ID (data-businessunit-id)' d='Modules.Wstrustpilotafsbcc.Admin'}
</label>
<input
type="text"
id="trustbox_business_unit_id"
name="trustbox_business_unit_id"
class="form-control"
value="{$trustbox_business_unit_id|escape:'htmlall':'UTF-8'}"
>
</div>
<div class="form-group">
<label for="trustbox_template_id" class="control-label">
{l s='TrustBox template ID (data-template-id)' d='Modules.Wstrustpilotafsbcc.Admin'}
</label>
<input
type="text"
id="trustbox_template_id"
name="trustbox_template_id"
class="form-control"
value="{$trustbox_template_id|escape:'htmlall':'UTF-8'}"
>
</div>
<div class="form-group">
<label for="trustbox_theme" class="control-label">
{l s='Theme' d='Modules.Wstrustpilotafsbcc.Admin'}
</label>
<select id="trustbox_theme" name="trustbox_theme" class="form-control">
{foreach $trustbox_themes as $themeLabel => $themeValue}
<option value="{$themeValue|escape:'htmlall':'UTF-8'}"{if $trustbox_theme == $themeValue} selected{/if}>
{$themeLabel|escape:'htmlall':'UTF-8'}
</option>
{/foreach}
</select>
</div>
<h4>{l s='Widget language and profile link per shop language' d='Modules.Wstrustpilotafsbcc.Admin'}</h4>
<table class="table">
<thead>
<tr>
<th>{l s='Language' d='Modules.Wstrustpilotafsbcc.Admin'}</th>
<th>{l s='Trustpilot locale (e.g. nl-BE)' d='Modules.Wstrustpilotafsbcc.Admin'}</th>
<th>{l s='Trustpilot profile URL' d='Modules.Wstrustpilotafsbcc.Admin'}</th>
</tr>
</thead>
<tbody>
{foreach $languages as $language}
{assign var='idLang' value=$language.id_lang}
<tr>
<td>{$language.name|escape:'htmlall':'UTF-8'}</td>
<td>
<input
type="text"
name="trustbox_locale[{$idLang|intval}]"
class="form-control"
value="{if isset($trustbox_locales[$idLang])}{$trustbox_locales[$idLang]|escape:'htmlall':'UTF-8'}{/if}"
>
</td>
<td>
<input
type="url"
name="trustbox_profile_url[{$idLang|intval}]"
class="form-control"
value="{if isset($trustbox_profile_urls[$idLang])}{$trustbox_profile_urls[$idLang]|escape:'htmlall':'UTF-8'}{/if}"
>
</td>
</tr>
{/foreach}
</tbody>
</table>
</div>
<div class="panel-footer">
<button type="submit" name="submitWsTrustpilotAfsBcc" class="btn btn-default pull-right">
<i class="process-icon-save"></i>
{l s='Save' d='Modules.Wstrustpilotafsbcc.Admin'}
</button>
</div>
</div>
</form>
@@ -0,0 +1,11 @@
<div
class="trustpilot-widget"
data-locale="{$trustbox.locale}"
data-template-id="{$trustbox.template_id}"
data-businessunit-id="{$trustbox.business_unit_id}"
data-style-height="24px"
data-style-width="100%"
data-theme="{$trustbox.theme}"
>
<a href="{$trustbox.profile_url}" target="_blank" rel="noopener">Trustpilot</a>
</div>
+1
View File
@@ -0,0 +1 @@
<script type="application/json+trustpilot">{$snippet_placeholder nofilter}</script>
@@ -0,0 +1,3 @@
<div class="ws-trustbox ws-trustbox--desktop col-auto">
{include file='module:ws_trustpilotafsbcc/views/templates/hook/_partials/trustbox.tpl'}
</div>
+5
View File
@@ -0,0 +1,5 @@
<div class="ws-trustbox ws-trustbox--mobile d-md-none">
<div class="container">
{include file='module:ws_trustpilotafsbcc/views/templates/hook/_partials/trustbox.tpl'}
</div>
</div>
+56
View File
@@ -0,0 +1,56 @@
<?php
/**
* 2007-2026 De Websmid BV
*
* NOTICE OF LICENSE
*
* This file is not open source! Each license that you purchased is only available for 1 website only.
* If you want to use this file on more websites (or projects), you need to purchase additional licenses.
* You are not allowed to redistribute, resell, lease, license, sub-license or offer our resources to any third party.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please contact us for extra customization service at an affordable price
*
* @author Isabelle Oving-Anno | De Websmid <isabelle@dewebsmid.nl>
* @copyright 2007-2026 De Websmid BV
* @license Valid for 1 website (or project) for each purchase of license
* International Registered Trademark & Property of De Websmid BV
*/
use Websmid\TrustpilotAfsBcc\Module\Install;
use Websmid\TrustpilotAfsBcc\Module\AdminHooks;
use Websmid\TrustpilotAfsBcc\Module\DisplayHooks;
use PrestaShop\PrestaShop\Core\Module\WidgetInterface;
if (!defined('_PS_VERSION_')) {
exit;
}
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
require_once __DIR__ . '/vendor/autoload.php';
}
class Ws_TrustpilotAfsBcc extends Module implements WidgetInterface
{
use Install;
use AdminHooks;
use DisplayHooks;
public function __construct()
{
$this->name = 'ws_trustpilotafsbcc';
$this->version = '1.1.0';
$this->author = 'Isabelle Oving-Anno | De Websmid BV';
$this->need_instance = 0;
$this->bootstrap = true;
parent::__construct();
$this->displayName = $this->trans('Trustpilot AFS + TrustBox', [], 'Modules.Wstrustpilotafsbcc.Admin');
$this->description = $this->trans('Sends Trustpilot review invitations in the customer\'s language by BCCing the order confirmation to Trustpilot\'s Automatic Feedback Service, and shows a TrustBox widget in the shop header.', [], 'Modules.Wstrustpilotafsbcc.Admin');
$this->ps_versions_compliancy = ['min' => '9.0.0', 'max' => _PS_VERSION_];
}
}