Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
38f906b
fields settings licence block logic and licence fetching
Mar 16, 2026
dc360ed
feat: generate field access token endpoint and logic implementation
Mar 16, 2026
b5adaaa
feat: add Capture option to 3D Secure failure behavior setting
Mar 16, 2026
6b92b9f
remove upgrade
Mar 16, 2026
4e7eb47
feat: add Order reference on payment page toggle to control Descripti…
Mar 17, 2026
64baef3
feat: add ConfigSet field validation, move hosted field style to gene…
Mar 17, 2026
a766c17
feat: add accessibility improvements for EAA compliance
Mar 20, 2026
54c05a0
fix(a11y): add aria-label to mobile switches and tab triggers
May 8, 2026
04934a5
fix(a11y): improve color contrast for WCAG 2.1 AA compliance
May 8, 2026
2da0b7d
fix(a11y): add aria-label to hosted field style combobox
May 8, 2026
baca7a3
fix(a11y): add focus trap to log details modal
May 8, 2026
3456d45
fix(a11y): improve contrast on SaferpayOfficial admin order buttons
May 8, 2026
a78459d
fix(a11y): add scope=col to saved cards table headers
May 8, 2026
ce8d69f
fix(a11y): improve Remove link contrast on saved cards table
May 8, 2026
634beca
revert(a11y): drop PS theme contrast overrides
May 8, 2026
3476949
fix: prevent silent save with empty or invalid API credentials
May 8, 2026
07ba05d
fix: hide Saferpay Fields section per active environment license
May 8, 2026
65152b6
fix: log license fetch failures and surface honest warning toast on save
May 8, 2026
efbf236
Merge pull request #322 from Invertus/BUGFIX/disable-save-empty-crede…
TLabutis May 15, 2026
0fd7498
Merge pull request #323 from Invertus/BUGFIX/saferpay-fields-env-lice…
TLabutis May 15, 2026
1c91872
Merge pull request #324 from Invertus/INTERNAL/license-fetch-warning-…
TLabutis May 15, 2026
5338eea
fix: break out of SaferPay iframe on payment status redirect
May 15, 2026
b558f03
fix: use location.replace and guard window.top access
May 15, 2026
b94589a
BUGFIX: payment methods default to all countries/currencies and dropd…
May 15, 2026
216b7a0
Merge pull request #326 from Invertus/BUGFIX/payment-methods-all-rest…
TLabutis May 15, 2026
e0570d9
Merge pull request #325 from Invertus/BUGFIX/iframe-redirect-top-window
TLabutis May 15, 2026
20263c3
BUGFIX: redirect customer to cart instead of order history after Safe…
May 15, 2026
bb3e081
BUGFIX: clarify Hosted field style info banner to mention Custom form…
May 15, 2026
9147de8
Merge branch 'SL-346/accessibility-eaa-compliance' into BUGFIX/abort-…
TLabutis May 15, 2026
1319947
Merge pull request #327 from Invertus/BUGFIX/abort-redirect-secure-key
TLabutis May 15, 2026
bfab294
Merge pull request #328 from Invertus/BUGFIX/clarify-hosted-field-sty…
TLabutis May 15, 2026
7743a2d
BUGFIX: validate Merchant Emails field on save
May 15, 2026
1c5f676
Merge pull request #329 from Invertus/BUGFIX/merchant-emails-validation
TLabutis May 26, 2026
15841de
BUGFIX: ensure 3DS-fail behavior overrides default payment behavior
May 26, 2026
60d1e5a
BUGFIX: de-duplicate ApiRequest error logs
May 26, 2026
e1959fd
Merge remote-tracking branch 'origin/SL-358/payment-field' into SL-34…
Jul 9, 2026
a6e3e4d
Merge pull request #320 from Invertus/SL-346/accessibility-eaa-compli…
justelis22 Jul 9, 2026
2001b75
Merge pull request #319 from Invertus/SL-358/payment-field
justelis22 Jul 9, 2026
87bcdd1
Merge pull request #318 from Invertus/SL-356/configurable-payment-des…
justelis22 Jul 9, 2026
84f1437
Merge pull request #317 from Invertus/SL-355/payment-capture-3ds
justelis22 Jul 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,7 @@
## [2.0.2]
- Remove WL Crypto payment method
- Added setting to toggle order confirmation email sending
- Added feature to group card payment methods into unified "Card" payment method
- Added feature to group card payment methods into unified "Card" payment method
- Fixed issue when newly enabled payment methods did not appear in checkout because default "all countries/currencies" restriction was not created on save
- Fixed issue when payment method country/currency dropdowns showed "0" instead of indicating that all countries/currencies are allowed
- BO : Added validation for Merchant Emails field (frontend + backend) to prevent saving invalid addresses
90 changes: 0 additions & 90 deletions controllers/admin/AdminSaferPayOfficialFieldsController.php

This file was deleted.

137 changes: 130 additions & 7 deletions controllers/admin/AdminSaferPayOfficialSettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use Invertus\SaferPay\Repository\SaferPaySavedCreditCardRepository;
use Invertus\SaferPay\Adapter\Configuration as SaferPayConfiguration;
use Invertus\SaferPay\Service\SaferPayFieldCreator;
use Invertus\SaferPay\Service\SaferPayGenerateFieldAccessToken;
use Invertus\SaferPay\Service\SaferPayGetLicense;
use Invertus\SaferPay\Service\SaferPayGetTerminals;
use Invertus\SaferPay\Service\SaferPayLogoCreator;
Expand All @@ -39,6 +40,7 @@
use Invertus\SaferPay\Service\SaferPayRestrictionCreator;
use Invertus\SaferPay\Exception\Api\SaferPayApiException;
use Invertus\SaferPay\Exception\Restriction\RestrictionException;
use Invertus\SaferPay\Logger\LoggerInterface;

require_once dirname(__FILE__) . '/../../vendor/autoload.php';

Expand All @@ -58,6 +60,7 @@ class AdminSaferPayOfficialSettingsController extends ModuleAdminController
'saveGeneralSettings',
'savePaymentMethods',
'getTerminals',
'generateFieldAccessToken',
'refreshData',
];

Expand Down Expand Up @@ -176,6 +179,17 @@ public function ajaxProcessSaveCredentials()
}
}

$testMerchantEmails = $this->getStringValue($data, 'testMerchantEmails');
$liveMerchantEmails = $this->getStringValue($data, 'liveMerchantEmails');
$invalidEmail = $this->findInvalidEmail($testMerchantEmails) ?: $this->findInvalidEmail($liveMerchantEmails);
if ($invalidEmail !== null) {
$this->ajaxResponse(false, sprintf(
$this->module->l('Invalid merchant email address: %s', self::FILE_NAME),
$invalidEmail
));
return;
}

// Credentials validated — now save
$configuration->set(SaferPayConfig::TEST_MODE, $isTestMode ? 1 : 0);

Expand Down Expand Up @@ -207,8 +221,8 @@ public function ajaxProcessSaveCredentials()

// Auto-detect license features from Saferpay Management API
$suffix = $isTestMode ? SaferPayConfig::TEST_SUFFIX : '';
$licenseMessage = '';
$hasBusinessLicense = false;
$licenseFetchFailed = false;

if (!empty($activeUsername) && !empty($activePassword) && !empty($activeCustomerId)) {
try {
Expand All @@ -225,17 +239,29 @@ public function ajaxProcessSaveCredentials()
$configuration->set(SaferPayConfig::BUSINESS_LICENSE . $suffix, $hasBusinessLicense ? 1 : 0);
} catch (\Exception $e) {
$configuration->set(SaferPayConfig::BUSINESS_LICENSE . $suffix, 0);
$licenseMessage = ' ' . $this->module->l('Could not retrieve license information. Please verify your credentials.', self::FILE_NAME);
$licenseFetchFailed = true;

/** @var LoggerInterface $logger */
$logger = $this->module->getService(LoggerInterface::class);
$logger->error('License fetch failed on credentials save: ' . $e->getMessage(), [
'context' => ['exception_class' => get_class($e)],
]);
}
} else {
$configuration->set(SaferPayConfig::BUSINESS_LICENSE . $suffix, 0);
}

$message = $licenseFetchFailed
? $this->module->l('Settings saved, but Saferpay Fields availability could not be confirmed. Please try again later or check the module Logs for details.', self::FILE_NAME)
: $this->module->l('Settings saved successfully.', self::FILE_NAME);

$this->ajaxResponse(
true,
$this->module->l('API Credentials saved successfully', self::FILE_NAME) . $licenseMessage,
$message,
[
'hasBusinessLicense' => $hasBusinessLicense,
'testHasBusinessLicense' => (bool) $configuration->get(SaferPayConfig::BUSINESS_LICENSE . SaferPayConfig::TEST_SUFFIX),
'liveHasBusinessLicense' => (bool) $configuration->get(SaferPayConfig::BUSINESS_LICENSE),
'warning' => $licenseFetchFailed,
]
);
}
Expand Down Expand Up @@ -309,7 +335,19 @@ public function ajaxProcessSaveGeneralSettings()

$configuration->set(SaferPayConfig::SAFERPAY_ORDER_STATE_CHOICE_AWAITING_PAYMENT, $this->getIntValue($data, 'orderStateAwaitingPayment'));
$configuration->set(SaferPayConfig::SAFERPAY_PAYMENT_DESCRIPTION, $this->getStringValue($data, 'paymentDescription'));
$configuration->set(SaferPayConfig::CONFIGURATION_NAME, $this->getStringValue($data, 'configurationName'));

$configurationName = $this->getStringValue($data, 'configurationName');
if ($configurationName !== '' && (strlen($configurationName) > 20 || !preg_match('/^[A-Za-z0-9.:\-_]+$/', $configurationName))) {
$this->ajaxResponse(false, $this->module->l('Only letters, numbers, dots, colons, hyphens, and underscores are allowed. Max 20 characters.', self::FILE_NAME));
return;
}
$configuration->set(SaferPayConfig::CONFIGURATION_NAME, $configurationName);
$hostedFieldsTemplate = $this->getIntValue($data, 'hostedFieldsTemplate');
if ($hostedFieldsTemplate < 1 || $hostedFieldsTemplate > 3) {
$hostedFieldsTemplate = SaferPayConfig::HOSTED_FIELDS_TEMPLATE_DEFAULT;
}
$configuration->set(SaferPayConfig::HOSTED_FIELDS_TEMPLATE, $hostedFieldsTemplate);
$configuration->set(SaferPayConfig::SAFERPAY_ORDER_ID_OPTION, $this->getIntValue($data, 'orderIdOption'));
$configuration->set(SaferPayConfig::SAFERPAY_DEBUG_MODE, !empty($data['debugMode']) ? 1 : 0);

$this->ajaxResponse(true, $this->module->l('General settings saved successfully', self::FILE_NAME));
Expand Down Expand Up @@ -363,6 +401,13 @@ public function ajaxProcessSavePaymentMethods()
$countries = isset($method['countries']) ? $method['countries'] : [];
$currencies = isset($method['currencies']) ? $method['currencies'] : [];

if (empty($countries)) {
$countries = [SaferPayRestrictionCreator::RESTRICTION_ALL];
}
if (empty($currencies)) {
$currencies = [SaferPayRestrictionCreator::RESTRICTION_ALL];
}

$success = $restrictionCreator->updateRestriction(
$paymentName,
SaferPayRestrictionCreator::RESTRICTION_COUNTRY,
Expand Down Expand Up @@ -425,6 +470,60 @@ public function ajaxProcessGetTerminals()
}
}

/**
* AJAX: Generate Saferpay Fields access token
*/
public function ajaxProcessGenerateFieldAccessToken()
{
$data = $this->getJsonInput();
$isTestMode = isset($data['env']) && $data['env'] === 'test';
$suffix = $isTestMode ? SaferPayConfig::TEST_SUFFIX : '';

$username = isset($data['username']) ? trim($data['username']) : '';
$password = isset($data['password']) ? $data['password'] : '';
$terminalId = isset($data['terminalId']) ? trim($data['terminalId']) : '';
$customerId = $this->parseCustomerIdFromUsername($username);

if ($password === self::PASSWORD_PLACEHOLDER) {
/** @var SaferPayConfiguration $configuration */
$configuration = $this->module->getService(SaferPayConfiguration::class);
$password = (string) $configuration->get(SaferPayConfig::PASSWORD . $suffix);
}

if (empty($username) || empty($password) || empty($customerId) || empty($terminalId)) {
$this->ajaxResponse(false, $this->module->l('Please enter valid credentials and select a terminal first.', self::FILE_NAME));
return;
}

try {
/** @var SaferPayGenerateFieldAccessToken $tokenGenerator */
$tokenGenerator = $this->module->getService(SaferPayGenerateFieldAccessToken::class);
$shopUrl = $this->context->link->getBaseLink();
$token = $tokenGenerator->generateWithCredentials($username, $password, $customerId, $terminalId, $isTestMode, $shopUrl);

/** @var SaferPayConfiguration $configuration */
$configuration = $this->module->getService(SaferPayConfiguration::class);
$configuration->set(SaferPayConfig::FIELDS_ACCESS_TOKEN . $suffix, $token);

$this->sendJsonResponse([
'success' => true,
'message' => $this->module->l('Access token generated successfully.', self::FILE_NAME),
'token' => $token,
]);
} catch (\Exception $e) {
\PrestaShopLogger::addLog(
'SaferPay: Failed to generate field access token - ' . $e->getMessage(),
3,
null,
null,
null,
true
);

$this->ajaxResponse(false, $this->module->l('Failed to generate access token.', self::FILE_NAME));
}
}

/**
* AJAX: Refresh all data
*/
Expand Down Expand Up @@ -465,8 +564,9 @@ private function collectSettingsData()
'liveFieldAccessToken' => (string) $configuration->get(SaferPayConfig::FIELDS_ACCESS_TOKEN),
'liveFieldJsUrl' => (string) $configuration->get(SaferPayConfig::FIELDS_LIBRARY),

// License (auto-detected)
'hasBusinessLicense' => (bool) $configuration->get(SaferPayConfig::BUSINESS_LICENSE . SaferPayConfig::getConfigSuffix()),
// License (auto-detected, per environment)
'testHasBusinessLicense' => (bool) $configuration->get(SaferPayConfig::BUSINESS_LICENSE . SaferPayConfig::TEST_SUFFIX),
'liveHasBusinessLicense' => (bool) $configuration->get(SaferPayConfig::BUSINESS_LICENSE),

// Payment Processing
'paymentBehavior' => (int) $configuration->get(SaferPayConfig::PAYMENT_BEHAVIOR),
Expand All @@ -486,6 +586,9 @@ private function collectSettingsData()
'orderStateAwaitingPayment' => (int) $configuration->get(SaferPayConfig::SAFERPAY_ORDER_STATE_CHOICE_AWAITING_PAYMENT),
'paymentDescription' => (string) $configuration->get(SaferPayConfig::SAFERPAY_PAYMENT_DESCRIPTION),
'configurationName' => (string) $configuration->get(SaferPayConfig::CONFIGURATION_NAME),
'hostedFieldsTemplate' => (int) $configuration->get(SaferPayConfig::HOSTED_FIELDS_TEMPLATE),
'modulePath' => $this->module->getPathUri(),
'orderIdOption' => (int) $configuration->get(SaferPayConfig::SAFERPAY_ORDER_ID_OPTION),
'debugMode' => (bool) $configuration->get(SaferPayConfig::SAFERPAY_DEBUG_MODE),

// Reference data
Expand Down Expand Up @@ -700,4 +803,24 @@ private function getIntValue($data, $key)
{
return isset($data[$key]) ? (int) $data[$key] : 0;
}

/**
* Returns the first invalid email in a comma-separated list, or null if all are valid.
*/
private function findInvalidEmail($emails)
{
if ($emails === '') {
return null;
}
foreach (explode(',', $emails) as $email) {
$email = trim($email);
if ($email === '') {
continue;
}
if (!\Validate::isEmail($email)) {
return $email;
}
}
return null;
}
}
51 changes: 37 additions & 14 deletions controllers/front/notify.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,27 +140,50 @@ public function postProcess()
//NOTE must be left below assert action to get newest information.
$order = new Order($orderId);

$paymentBehaviorWithout3D = (int) Configuration::get(SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D);

if (!$assertResponseBody->getLiability()->getLiabilityShift() &&
in_array($order->payment, SaferPayConfig::SUPPORTED_3DS_PAYMENT_METHODS) &&
(int) Configuration::get(SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D) === SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D_CANCEL
in_array($order->payment, SaferPayConfig::SUPPORTED_3DS_PAYMENT_METHODS)
) {
/** @var SaferPayOrderStatusService $orderStatusService */
$orderStatusService = $this->module->getService(SaferPayOrderStatusService::class);
$orderStatusService->cancel($order);

$logger->debug(sprintf('%s - Liability shift is false', self::FILE_NAME), [
'context' => [
'id_order' => $order->id,
],
]);
if ($paymentBehaviorWithout3D === SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D_CANCEL) {
$orderStatusService->cancel($order);

$logger->debug(sprintf('%s - liability shift is false', self::FILE_NAME), [
'context' => [
'id_order' => $order->id,
],
]);
$logger->debug(sprintf('%s - Liability shift is false, canceling order', self::FILE_NAME), [
'context' => [
'id_order' => $order->id,
],
]);

die($this->module->l('Liability shift is false', self::FILE_NAME));
die($this->module->l('Liability shift is false', self::FILE_NAME));
}

if ($paymentBehaviorWithout3D === SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D_AUTHORIZE) {
$logger->debug(sprintf('%s - Liability shift is false, order left authorized', self::FILE_NAME), [
'context' => [
'id_order' => $order->id,
],
]);

die($this->module->l('Liability shift is false, order left authorized', self::FILE_NAME));
}

if ($paymentBehaviorWithout3D === SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D_CAPTURE
&& SaferPayConfig::supportsOrderCapture($order->payment)
&& $transactionStatus !== TransactionStatus::CAPTURED
) {
$orderStatusService->capture($order);

$logger->debug(sprintf('%s - Liability shift is false, capturing order', self::FILE_NAME), [
'context' => [
'id_order' => $order->id,
],
]);

die($this->module->l('Liability shift is false, capturing order', self::FILE_NAME));
}
}

//NOTE to get latest information possible and not override new information.
Expand Down
Loading
Loading