diff --git a/changelog.md b/changelog.md index 4dc83f73a..3fe31cd34 100755 --- a/changelog.md +++ b/changelog.md @@ -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 \ No newline at end of file +- 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 diff --git a/controllers/admin/AdminSaferPayOfficialSettingsController.php b/controllers/admin/AdminSaferPayOfficialSettingsController.php index 0e5883c4e..0d4f5ffcf 100755 --- a/controllers/admin/AdminSaferPayOfficialSettingsController.php +++ b/controllers/admin/AdminSaferPayOfficialSettingsController.php @@ -179,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); @@ -248,7 +259,8 @@ public function ajaxProcessSaveCredentials() true, $message, [ - 'hasBusinessLicense' => $hasBusinessLicense, + 'testHasBusinessLicense' => (bool) $configuration->get(SaferPayConfig::BUSINESS_LICENSE . SaferPayConfig::TEST_SUFFIX), + 'liveHasBusinessLicense' => (bool) $configuration->get(SaferPayConfig::BUSINESS_LICENSE), 'warning' => $licenseFetchFailed, ] ); @@ -389,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, @@ -545,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), @@ -783,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; + } } diff --git a/controllers/front/notify.php b/controllers/front/notify.php index 35bb7d25f..667a1fd4d 100755 --- a/controllers/front/notify.php +++ b/controllers/front/notify.php @@ -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 ) { @@ -169,6 +181,8 @@ public function postProcess() 'id_order' => $order->id, ], ]); + + die($this->module->l('Liability shift is false, capturing order', self::FILE_NAME)); } } diff --git a/controllers/front/return.php b/controllers/front/return.php index f36c7fe22..a65984b32 100755 --- a/controllers/front/return.php +++ b/controllers/front/return.php @@ -274,7 +274,9 @@ 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, @@ -282,7 +284,7 @@ private function getRedirectionToControllerUrl($controllerName) [ 'cartId' => $cartId, 'orderId' => Order::getIdByCartId($cartId), - 'secureKey' => $this->context->cart->secure_key, + 'secureKey' => $secureKey, 'moduleId' => $this->module->id, ] ); @@ -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 ) { diff --git a/src/Api/ApiRequest.php b/src/Api/ApiRequest.php index 72ebc144a..5df54cd59 100755 --- a/src/Api/ApiRequest.php +++ b/src/Api/ApiRequest.php @@ -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; } @@ -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; } @@ -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; } diff --git a/src/Service/SettingsTranslationService.php b/src/Service/SettingsTranslationService.php index 58a315fbd..ae2de9990 100644 --- a/src/Service/SettingsTranslationService.php +++ b/src/Service/SettingsTranslationService.php @@ -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), @@ -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), @@ -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), diff --git a/views/css/admin/logs_tab.css b/views/css/admin/logs_tab.css index 5bb36a92f..702cff1ec 100755 --- a/views/css/admin/logs_tab.css +++ b/views/css/admin/logs_tab.css @@ -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; diff --git a/views/css/admin/payment_method.css b/views/css/admin/payment_method.css index 336eda6f0..9b0b08c91 100755 --- a/views/css/admin/payment_method.css +++ b/views/css/admin/payment_method.css @@ -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; diff --git a/views/js/admin/log.js b/views/js/admin/log.js index 4a8eb769a..a16cecc4b 100644 --- a/views/js/admin/log.js +++ b/views/js/admin/log.js @@ -21,17 +21,65 @@ */ $(document).ready(function () { + function closeModal($modal) { + $modal.removeClass('open'); + var triggerButton = $modal.data('triggerButton'); + if (triggerButton) { + triggerButton.focus(); + } + } + $('.log-modal-overlay').on('click', function (event) { - $('.modal.open').removeClass('open'); + closeModal($(this).closest('.modal')); + event.preventDefault(); + }); + + $('.js-log-modal-close').on('click', function (event) { + closeModal($(this).closest('.modal')); event.preventDefault(); }); + $(document).on('keydown', function (event) { + var $openModal = $('.modal.open'); + if (!$openModal.length) { + return; + } + if (event.key === 'Escape') { + closeModal($openModal); + event.preventDefault(); + return; + } + if (event.key === 'Tab') { + var focusables = $openModal.find('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])').filter(':visible'); + if (!focusables.length) { + event.preventDefault(); + return; + } + var first = focusables.first()[0]; + var last = focusables.last()[0]; + if (event.shiftKey && document.activeElement === first) { + last.focus(); + event.preventDefault(); + } else if (!event.shiftKey && document.activeElement === last) { + first.focus(); + event.preventDefault(); + } else if (!$openModal[0].contains(document.activeElement)) { + first.focus(); + event.preventDefault(); + } + } + }); + $('.js-log-button').on('click', function (event) { var logId = $(this).data('log-id'); var informationType = $(this).data('information-type'); + var $modal = $('#' + $(this).data('target')); + + $modal.data('triggerButton', $(this)); // NOTE: opening modal - $('#' + $(this).data('target')).addClass('open'); + $modal.addClass('open'); + $modal.find('.js-log-modal-close').focus(); // NOTE: if information has been set already we don't need to call ajax again. if (!$('#log-modal-' + logId + '-' + informationType + ' .log-modal-content-data').hasClass('hidden')) { diff --git a/views/js/admin/settings-app/src/components/settings/api-credentials.tsx b/views/js/admin/settings-app/src/components/settings/api-credentials.tsx index 5a3931901..d806211bc 100644 --- a/views/js/admin/settings-app/src/components/settings/api-credentials.tsx +++ b/views/js/admin/settings-app/src/components/settings/api-credentials.tsx @@ -35,6 +35,14 @@ export function ApiCredentials() { const fieldJsUrl = isTest ? settings.testFieldJsUrl : settings.liveFieldJsUrl const hasCredentials = username.length > 0 && password.length > 0 + const hasBusinessLicense = isTest ? settings.testHasBusinessLicense : settings.liveHasBusinessLicense + + const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + const invalidEmails = merchantEmails + .split(',') + .map(e => e.trim()) + .filter(e => e.length > 0 && !EMAIL_RE.test(e)) + const merchantEmailsInvalid = invalidEmails.length > 0 const setField = (field: string, value: string | boolean) => { updateSettings({ [`${prefix}${field.charAt(0).toUpperCase() + field.slice(1)}`]: value } as Record) @@ -145,17 +153,23 @@ export function ApiCredentials() { {/* Username & Password */}
- + setField('username', e.target.value)} + required + aria-required="true" />
- +
setField('password', e.target.value)} className="sp-pr-10" + required + aria-required="true" />
)} {credentialStatus === 'valid' && ( -
+
{t('credentialsValid')}
@@ -233,7 +249,14 @@ export function ApiCredentials() { placeholder={t('enterMerchantEmails')} value={merchantEmails} onChange={(e) => setField('merchantEmails', e.target.value)} + aria-invalid={merchantEmailsInvalid} + className={merchantEmailsInvalid ? 'sp-border-destructive focus-visible:sp-ring-destructive' : ''} /> + {merchantEmailsInvalid && ( +

+ {t('invalidMerchantEmails')}: {invalidEmails.join(', ')} +

+ )}

{t('separateEmails')}

@@ -242,7 +265,7 @@ export function ApiCredentials() { - {settings.hasBusinessLicense && + {hasBusinessLicense && {t('saferpayFields')} {t('saferpayFieldsDescription')} @@ -250,12 +273,12 @@ export function ApiCredentials() {
- +

{t('saferpayFieldsIncluded')}

-

+

{t('saferpayFieldsIncludedDescription')}

@@ -330,7 +353,12 @@ export function ApiCredentials() { }
-
diff --git a/views/js/admin/settings-app/src/components/settings/email-notifications.tsx b/views/js/admin/settings-app/src/components/settings/email-notifications.tsx index 044239c82..d79d331ac 100644 --- a/views/js/admin/settings-app/src/components/settings/email-notifications.tsx +++ b/views/js/admin/settings-app/src/components/settings/email-notifications.tsx @@ -90,7 +90,7 @@ export function EmailNotifications() {
-
diff --git a/views/js/admin/settings-app/src/components/settings/general-settings.tsx b/views/js/admin/settings-app/src/components/settings/general-settings.tsx index fc077be4c..52b795687 100644 --- a/views/js/admin/settings-app/src/components/settings/general-settings.tsx +++ b/views/js/admin/settings-app/src/components/settings/general-settings.tsx @@ -101,7 +101,7 @@ export function GeneralSettings() { value={String(settings.hostedFieldsTemplate)} onValueChange={(val) => updateSettings({ hostedFieldsTemplate: Number(val) })} > - + @@ -221,7 +221,7 @@ export function GeneralSettings() {
-
diff --git a/views/js/admin/settings-app/src/components/settings/payment-methods.tsx b/views/js/admin/settings-app/src/components/settings/payment-methods.tsx index bc3fc449f..99dfd735a 100644 --- a/views/js/admin/settings-app/src/components/settings/payment-methods.tsx +++ b/views/js/admin/settings-app/src/components/settings/payment-methods.tsx @@ -31,15 +31,28 @@ function MultiSelect({ [options, search], ) + const ALL_VALUE = 0 + + const validSelected = useMemo( + () => selected.filter((s) => s !== ALL_VALUE && options.some((o) => o.id === s)), + [selected, options], + ) + + const isAll = selected.includes(ALL_VALUE) || validSelected.length === 0 + const toggle = useCallback( (value: number) => { - onChange( - selected.includes(value) - ? selected.filter((s) => s !== value) - : [...selected, value], - ) + if (value === ALL_VALUE) { + onChange([ALL_VALUE]) + return + } + const base = isAll ? [] : validSelected + const next = base.includes(value) + ? base.filter((s) => s !== value) + : [...base, value] + onChange(next.length === 0 ? [ALL_VALUE] : next) }, - [selected, onChange], + [validSelected, isAll, onChange], ) return ( @@ -48,14 +61,18 @@ function MultiSelect({
- {selected.length > 0 && ( + {validSelected.length > 0 && (
{method.hasCustomForm && ( @@ -251,6 +269,7 @@ export function PaymentMethods() { updatePaymentMethod(method.name, { showCustomForm: checked })} + aria-label={`${t('customForm')} ${method.displayName}`} />
)} @@ -293,7 +312,7 @@ export function PaymentMethods() { {paymentMethods.length > 0 && (
-
diff --git a/views/js/admin/settings-app/src/components/settings/payment-processing.tsx b/views/js/admin/settings-app/src/components/settings/payment-processing.tsx index c19e98a45..3d26f72da 100644 --- a/views/js/admin/settings-app/src/components/settings/payment-processing.tsx +++ b/views/js/admin/settings-app/src/components/settings/payment-processing.tsx @@ -305,7 +305,7 @@ export function PaymentProcessing() {
-
diff --git a/views/js/admin/settings-app/src/components/settings/saferpay-settings.tsx b/views/js/admin/settings-app/src/components/settings/saferpay-settings.tsx index 0d8649911..93b02197c 100644 --- a/views/js/admin/settings-app/src/components/settings/saferpay-settings.tsx +++ b/views/js/admin/settings-app/src/components/settings/saferpay-settings.tsx @@ -24,6 +24,7 @@ export function SaferpaySettings() { @@ -31,6 +32,7 @@ export function SaferpaySettings() { @@ -38,6 +40,7 @@ export function SaferpaySettings() { @@ -45,6 +48,7 @@ export function SaferpaySettings() { @@ -52,6 +56,7 @@ export function SaferpaySettings() { diff --git a/views/js/admin/settings-app/src/context/settings-context.tsx b/views/js/admin/settings-app/src/context/settings-context.tsx index aa54b7876..784cf7fb1 100644 --- a/views/js/admin/settings-app/src/context/settings-context.tsx +++ b/views/js/admin/settings-app/src/context/settings-context.tsx @@ -92,8 +92,12 @@ export function SettingsProvider({ children }: { children: React.ReactNode }) { liveFieldJsUrl: currentSettings.liveFieldJsUrl, }) const data = result as unknown as Record - if (result.success && typeof data.hasBusinessLicense === 'boolean') { - setSettings((prev) => ({ ...prev, hasBusinessLicense: data.hasBusinessLicense as boolean })) + if (result.success) { + setSettings((prev) => ({ + ...prev, + ...(typeof data.testHasBusinessLicense === 'boolean' ? { testHasBusinessLicense: data.testHasBusinessLicense as boolean } : {}), + ...(typeof data.liveHasBusinessLicense === 'boolean' ? { liveHasBusinessLicense: data.liveHasBusinessLicense as boolean } : {}), + })) } return { ...result, warning: data.warning === true } }, 'API Credentials', 'credentials') diff --git a/views/js/admin/settings-app/src/globals.css b/views/js/admin/settings-app/src/globals.css index 084c1fe3b..b222286c7 100644 --- a/views/js/admin/settings-app/src/globals.css +++ b/views/js/admin/settings-app/src/globals.css @@ -14,7 +14,7 @@ --sp-secondary: 180 15% 95%; --sp-secondary-foreground: 220 20% 10%; --sp-muted: 180 12% 95%; - --sp-muted-foreground: 220 10% 46%; + --sp-muted-foreground: 220 12% 38%; --sp-accent: 180 25% 92%; --sp-accent-foreground: 180 51% 22%; --sp-destructive: 0 72% 51%; diff --git a/views/js/admin/settings-app/src/types/index.ts b/views/js/admin/settings-app/src/types/index.ts index 436600c17..79bf8e2ae 100644 --- a/views/js/admin/settings-app/src/types/index.ts +++ b/views/js/admin/settings-app/src/types/index.ts @@ -33,8 +33,9 @@ export interface SaferpaySettingsData { liveFieldAccessToken: string liveFieldJsUrl: string - // License (read-only, auto-detected from API) - hasBusinessLicense: boolean + // License (read-only, auto-detected from API, per environment) + testHasBusinessLicense: boolean + liveHasBusinessLicense: boolean // Payment Processing paymentBehavior: number diff --git a/views/templates/admin/logs/log_modal.tpl b/views/templates/admin/logs/log_modal.tpl index 62cb43dd4..eadfc7fcd 100644 --- a/views/templates/admin/logs/log_modal.tpl +++ b/views/templates/admin/logs/log_modal.tpl @@ -19,7 +19,8 @@ *@copyright SIX Payment Services *@license SIX Payment Services *} -
{l s='View' mod='saferpayofficial'} -
+ -