Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
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
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
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
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
63 changes: 57 additions & 6 deletions controllers/admin/AdminSaferPayOfficialSettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,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 Down Expand Up @@ -178,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 @@ -209,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 @@ -227,18 +239,29 @@ public function ajaxProcessSaveCredentials()
$configuration->set(SaferPayConfig::BUSINESS_LICENSE . $suffix, $hasBusinessLicense ? 1 : 0);
} catch (\Exception $e) {
$configuration->set(SaferPayConfig::BUSINESS_LICENSE . $suffix, 0);
$licenseFetchFailed = true;

$licenseMessage = ' ' . $this->module->l('Could not retrieve license information. Please verify your credentials.', self::FILE_NAME);
/** @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('Settings 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 @@ -378,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 @@ -534,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 Down Expand Up @@ -772,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;
}
}
16 changes: 15 additions & 1 deletion controllers/front/notify.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,19 @@ public function postProcess()
]);

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

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
) {
Expand All @@ -169,6 +181,8 @@ public function postProcess()
'id_order' => $order->id,
],
]);

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

Expand Down
16 changes: 13 additions & 3 deletions controllers/front/return.php
Original file line number Diff line number Diff line change
Expand Up @@ -274,15 +274,17 @@ private function executeTransaction($orderId, $selectedCard)
*/
private function getRedirectionToControllerUrl($controllerName)
{
$cartId = $this->context->cart->id ? $this->context->cart->id : Tools::getValue('cartId');
$cartId = (int) Tools::getValue('cartId') ?: (int) $this->context->cart->id;
$cart = new Cart($cartId);
$secureKey = Validate::isLoadedObject($cart) ? $cart->secure_key : $this->context->cart->secure_key;

return $this->context->link->getModuleLink(
$this->module->name,
$controllerName,
[
'cartId' => $cartId,
'orderId' => Order::getIdByCartId($cartId),
'secureKey' => $this->context->cart->secure_key,
'secureKey' => $secureKey,
'moduleId' => $this->module->id,
]
);
Expand Down Expand Up @@ -339,7 +341,15 @@ private function createAndValidateOrder($assertResponseBody, $transactionStatus,

if ($paymentBehaviorWithout3D === SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D_CANCEL) {
$orderStatusService->cancel($order);
} elseif ($paymentBehaviorWithout3D === SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D_CAPTURE

return;
}

if ($paymentBehaviorWithout3D === SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D_AUTHORIZE) {
return;
}

if ($paymentBehaviorWithout3D === SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D_CAPTURE
&& SaferPayConfig::supportsOrderCapture($order->payment)
&& $transactionStatus !== TransactionStatus::CAPTURED
) {
Expand Down
46 changes: 26 additions & 20 deletions src/Api/ApiRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,16 @@ public function get($url, $params = [])

return json_decode($response->raw_body);
} catch (Exception $exception) {
$this->logger->error($exception->getMessage(), [
'context' => [
'headers' => $this->getHeaders(),
],
'request' => $params,
'response' => json_decode($response->raw_body),
'exceptions' => ExceptionUtility::getExceptions($exception),
]);
if ($response === null) {
$this->logger->error($exception->getMessage(), [
'context' => [
'headers' => $this->getHeaders(),
],
'request' => $params,
'response' => null,
'exceptions' => ExceptionUtility::getExceptions($exception),
]);
}

throw $exception;
}
Expand Down Expand Up @@ -170,12 +172,14 @@ public function getWithCredentials($url, $username, $password, $baseUrl, $params

return json_decode($response->raw_body);
} catch (Exception $exception) {
$this->logger->error($exception->getMessage(), [
'context' => [],
'request' => $params,
'response' => $response ? json_decode($response->raw_body) : null,
'exceptions' => ExceptionUtility::getExceptions($exception),
]);
if ($response === null) {
$this->logger->error($exception->getMessage(), [
'context' => [],
'request' => $params,
'response' => null,
'exceptions' => ExceptionUtility::getExceptions($exception),
]);
}

throw $exception;
}
Expand Down Expand Up @@ -226,12 +230,14 @@ public function postWithCredentials($url, $username, $password, $baseUrl, $param

return json_decode($response->raw_body);
} catch (Exception $exception) {
$this->logger->error($exception->getMessage(), [
'context' => [],
'request' => $params,
'response' => $response ? json_decode($response->raw_body) : null,
'exceptions' => ExceptionUtility::getExceptions($exception),
]);
if ($response === null) {
$this->logger->error($exception->getMessage(), [
'context' => [],
'request' => $params,
'response' => null,
'exceptions' => ExceptionUtility::getExceptions($exception),
]);
}

throw $exception;
}
Expand Down
4 changes: 3 additions & 1 deletion src/Service/SettingsTranslationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ private function getCommonTranslations()
{
return [
'saveChanges' => $this->module->l('Save Changes', self::FILE_NAME),
'saving' => $this->module->l('Saving...', self::FILE_NAME),
'enable' => $this->module->l('Enable', self::FILE_NAME),
'disable' => $this->module->l('Disable', self::FILE_NAME),
'search' => $this->module->l('Search...', self::FILE_NAME),
Expand Down Expand Up @@ -121,6 +122,7 @@ private function getApiCredentialsTranslations()
'merchantEmails' => $this->module->l('Merchant Emails', self::FILE_NAME),
'enterMerchantEmails' => $this->module->l('Enter merchant email addresses (comma-separated)', self::FILE_NAME),
'separateEmails' => $this->module->l('These email addresses receive payment notification emails directly from SaferPay. Separate multiple email addresses with commas.', self::FILE_NAME),
'invalidMerchantEmails' => $this->module->l('Invalid email address', self::FILE_NAME),
'saferpayFields' => $this->module->l('Saferpay Fields', self::FILE_NAME),
'saferpayFieldsDescription' => $this->module->l('Configure Saferpay Fields for inline payment form integration.', self::FILE_NAME),
'fieldAccessTokenInfo' => $this->module->l('Saferpay Field Access Token can be found in Saferpay Backoffice, navigate to', self::FILE_NAME),
Expand Down Expand Up @@ -228,7 +230,7 @@ private function getGeneralSettingsTranslations()
'configName' => $this->module->l('Payment Page configurations name', self::FILE_NAME),
'enterConfigName' => $this->module->l('Enter configuration name', self::FILE_NAME),
'configNameDescription' => html_entity_decode($this->module->l('Name of the Payment Page Configuration created in Saferpay Backoffice (Settings > Payment Page Configuration). Max 20 characters. Allowed: letters, numbers, dots, colons, hyphens, underscores.', self::FILE_NAME), ENT_QUOTES, 'UTF-8'),
'hostedFieldInfo' => $this->module->l('Choose which hosted field will be displayed on payment option selection with supported payment methods.', self::FILE_NAME),
'hostedFieldInfo' => html_entity_decode($this->module->l('This style applies only to payment methods with "Custom form" enabled in the Payment Methods list. Methods without Custom form or paid with saved cards use the Saferpay-hosted payment page, whose appearance is controlled by "Payment Page configurations name".', self::FILE_NAME), ENT_QUOTES, 'UTF-8'),
'hostedFieldStyle' => $this->module->l('Hosted field style', self::FILE_NAME),
'hostedFieldStyleDescription' => $this->module->l('Select the card input form layout for the payment page.', self::FILE_NAME),
'classicLayout' => $this->module->l('Classic Layout', self::FILE_NAME),
Expand Down
23 changes: 22 additions & 1 deletion views/css/admin/logs_tab.css
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,31 @@
border-bottom: solid 1px grey;
pointer-events: all;
display: flex;
justify-content: center;
justify-content: space-between;
align-items: center;
max-height: 10vh;
}

.log-modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
padding: 0.25rem 0.5rem;
line-height: 1;
color: #6b7280;
margin-right: 0.5rem;
}

.log-modal-close:hover {
color: #111827;
}

.log-modal-close:focus {
outline: 2px solid #2196F3;
outline-offset: 2px;
}

.log-modal-content {
padding: 15px;
height: 50vh;
Expand Down
6 changes: 6 additions & 0 deletions views/css/admin/payment_method.css
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@
width: 0;
}

/* Visible focus indicator for keyboard navigation */
.container-checkbox input:focus ~ .checkmark {
outline: 2px solid #2196F3;
outline-offset: 2px;
}

/* Create a custom checkbox */
.checkmark {
position: absolute;
Expand Down
Loading