initial module
This commit is contained in:
@@ -0,0 +1 @@
|
||||
vendor/
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
@@ -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}$/';
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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_];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user