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/AdminSaferPayOfficialFieldsController.php b/controllers/admin/AdminSaferPayOfficialFieldsController.php deleted file mode 100755 index 7508d5986..000000000 --- a/controllers/admin/AdminSaferPayOfficialFieldsController.php +++ /dev/null @@ -1,90 +0,0 @@ - - *@copyright SIX Payment Services - *@license SIX Payment Services - */ - -use Invertus\SaferPay\Config\SaferPayConfig; - -require_once dirname(__FILE__) . '/../../vendor/autoload.php'; - -if (!defined('_PS_VERSION_')) { - exit; -} - -class AdminSaferPayOfficialFieldsController extends ModuleAdminController -{ - public function __construct() - { - parent::__construct(); - $this->bootstrap = true; - - $this->tpl_folder = 'field-option-settings/'; - $this->initOptions(); - } - - public function initContent() - { - parent::initContent(); - } - - public function initOptions() - { - $this->fields_options = [ - 'hosted_fields_settings' => [ - 'title' => $this->module->l('Hosted fields settings'), - 'icon' => 'icon-settings', - 'fields' => [ - SaferPayConfig::HOSTED_FIELDS_TEMPLATE . '_description' => [ - 'type' => 'desc', - 'class' => 'col-lg-12', - 'template' => 'field-hosted-field-template-desc.tpl', - ], - - SaferPayConfig::HOSTED_FIELDS_TEMPLATE => [ - 'type' => 'select-template', - 'name' => SaferPayConfig::HOSTED_FIELDS_TEMPLATE, - 'templateOptions' => [ - "{$this->module->getPathUri()}views/img/hosted-templates/template1.jpg", - "{$this->module->getPathUri()}views/img/hosted-templates/template2.jpg", - "{$this->module->getPathUri()}views/img/hosted-templates/template3.jpg", - ], - ], - ], - 'buttons' => [ - 'save_and_connect' => [ - 'title' => $this->module->l('Save'), - 'icon' => 'process-icon-save', - 'class' => 'btn btn-default pull-right', - 'type' => 'submit', - ], - ], - ], - ]; - } - - public function setMedia($isNewTheme = false) - { - parent::setMedia($isNewTheme); - - $this->addJS('modules/' . $this->module->name . '/views/js/admin/saferpay_fields.js'); - $this->addCSS('modules/' . $this->module->name . '/views/css/admin/saferpay_fields.css'); - } -} diff --git a/controllers/admin/AdminSaferPayOfficialSettingsController.php b/controllers/admin/AdminSaferPayOfficialSettingsController.php index dfce1b086..0d4f5ffcf 100755 --- a/controllers/admin/AdminSaferPayOfficialSettingsController.php +++ b/controllers/admin/AdminSaferPayOfficialSettingsController.php @@ -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; @@ -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'; @@ -58,6 +60,7 @@ class AdminSaferPayOfficialSettingsController extends ModuleAdminController 'saveGeneralSettings', 'savePaymentMethods', 'getTerminals', + 'generateFieldAccessToken', 'refreshData', ]; @@ -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); @@ -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 { @@ -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, ] ); } @@ -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)); @@ -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, @@ -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 */ @@ -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), @@ -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 @@ -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; + } } diff --git a/controllers/front/notify.php b/controllers/front/notify.php index 870464e7c..667a1fd4d 100755 --- a/controllers/front/notify.php +++ b/controllers/front/notify.php @@ -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. diff --git a/controllers/front/return.php b/controllers/front/return.php index b5fc37c69..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, ] ); @@ -329,19 +331,39 @@ private function createAndValidateOrder($assertResponseBody, $transactionStatus, $orderId = Order::getIdByCartId($cartId); $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); + + if ($paymentBehaviorWithout3D === SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D_CANCEL) { + $orderStatusService->cancel($order); + + 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 + ) { + $orderStatusService->capture($order); + + return; + } } //NOTE to get latest information possible and not override new information. - $paymentMethod = $assertResponseBody->getPaymentMeans()->getBrand()->getPaymentMethod();// if payment does not support order capture, it means it always auto-captures it (at least with accountToAccount payment), + $paymentMethod = $assertResponseBody->getPaymentMeans()->getBrand()->getPaymentMethod(); + // if payment does not support order capture, it means it always auto-captures it (at least with accountToAccount payment), // so in this case if status comes back "captured" we just update the order state accordingly if (!SaferPayConfig::supportsOrderCapture($paymentMethod) && $transactionStatus === TransactionStatus::CAPTURED diff --git a/cypress/integration/01_ps1764.Module.Configure.cy.js b/cypress/integration/01_ps1764.Module.Configure.cy.js index b7313cb4c..f8988d2b8 100755 --- a/cypress/integration/01_ps1764.Module.Configure.cy.js +++ b/cypress/integration/01_ps1764.Module.Configure.cy.js @@ -117,14 +117,6 @@ it('04 Fields and Logs tabs are shown OK', () => { cy.get('.pstaggerAddTagInput').type('saferpay') cy.get('#module-search-button').click() cy.get('.btn-group > .btn-primary-reverse').click() //clicking the Congifure - cy.get('#subtab-AdminSaferPayOfficialFields').click() - cy.get('[id="configuration_form"]').should('be.visible') - cy.get('.field-container > :nth-child(1) > img').click() - cy.get(':nth-child(2) > img').click() - cy.get(':nth-child(3) > img').click() - cy.get('[class="alert alert-info"]').should('be.visible') - cy.get('[name="submitOptionsconfiguration"]').click() - cy.get('[class="alert alert-success"]').should('be.visible') cy.get('#subtab-AdminSaferPayOfficialLogs').click() cy.get('[id="form-saferpay_log"]').should('be.visible') }) diff --git a/cypress/integration/01_ps1770.Module.Configure.cy.js b/cypress/integration/01_ps1770.Module.Configure.cy.js index 9696c496c..1de0a18c6 100755 --- a/cypress/integration/01_ps1770.Module.Configure.cy.js +++ b/cypress/integration/01_ps1770.Module.Configure.cy.js @@ -117,14 +117,6 @@ it('04 Fields and Logs tabs are shown OK', () => { cy.get('.pstaggerAddTagInput').type('saferpay') cy.get('#module-search-button').click() cy.get('.btn-group > .btn-primary-reverse').click() //clicking the Congifure - cy.get('#subtab-AdminSaferPayOfficialFields').click() - cy.get('[id="configuration_form"]').should('be.visible') - cy.get('.field-container > :nth-child(1) > img').click() - cy.get(':nth-child(2) > img').click() - cy.get(':nth-child(3) > img').click() - cy.get('[class="alert alert-info"]').should('be.visible') - cy.get('[name="submitOptionsconfiguration"]').click() - cy.get('[class="alert alert-success"]').should('be.visible') cy.get('#subtab-AdminSaferPayOfficialLogs').click() cy.get('[id="form-saferpay_log"]').should('be.visible') }) diff --git a/cypress/integration/01_ps1784.Module.Configure.cy.js b/cypress/integration/01_ps1784.Module.Configure.cy.js index 8da2aae5b..716aa4837 100755 --- a/cypress/integration/01_ps1784.Module.Configure.cy.js +++ b/cypress/integration/01_ps1784.Module.Configure.cy.js @@ -117,14 +117,6 @@ it('04 Fields and Logs tabs are shown OK', () => { cy.get('.pstaggerAddTagInput').type('saferpay') cy.get('#module-search-button').click() cy.get('.btn-group > .btn-primary-reverse').click() //clicking the Congifure - cy.get('#subtab-AdminSaferPayOfficialFields').click() - cy.get('[id="configuration_form"]').should('be.visible') - cy.get('.field-container > :nth-child(1) > img').click() - cy.get(':nth-child(2) > img').click() - cy.get(':nth-child(3) > img').click() - cy.get('[class="alert alert-info"]').should('be.visible') - cy.get('[name="submitOptionsconfiguration"]').click() - cy.get('[class="alert alert-success"]').should('be.visible') cy.get('#subtab-AdminSaferPayOfficialLogs').click() cy.get('[id="form-saferpay_log"]').should('be.visible') }) diff --git a/cypress/integration/01_ps1786.Module.Configure.cy.js b/cypress/integration/01_ps1786.Module.Configure.cy.js index 0c8259a9d..863cb240b 100755 --- a/cypress/integration/01_ps1786.Module.Configure.cy.js +++ b/cypress/integration/01_ps1786.Module.Configure.cy.js @@ -117,14 +117,6 @@ it('04 Fields and Logs tabs are shown OK', () => { cy.get('.pstaggerAddTagInput').type('saferpay') cy.get('#module-search-button').click() cy.get('.btn-group > .btn-primary-reverse').click() //clicking the Congifure - cy.get('#subtab-AdminSaferPayOfficialFields').click() - cy.get('[id="configuration_form"]').should('be.visible') - cy.get('.field-container > :nth-child(1) > img').click() - cy.get(':nth-child(2) > img').click() - cy.get(':nth-child(3) > img').click() - cy.get('[class="alert alert-info"]').should('be.visible') - cy.get('[name="submitOptionsconfiguration"]').click() - cy.get('[class="alert alert-success"]').should('be.visible') cy.get('#subtab-AdminSaferPayOfficialLogs').click() cy.get('[id="form-saferpay_log"]').should('be.visible') }) diff --git a/saferpayofficial.php b/saferpayofficial.php index b0acd4630..dd997aa5e 100755 --- a/saferpayofficial.php +++ b/saferpayofficial.php @@ -55,7 +55,6 @@ class SaferPayOfficial extends PaymentModule const ADMIN_SAFERPAY_MODULE_CONTROLLER = 'AdminSaferPayOfficialModule'; const ADMIN_SETTINGS_CONTROLLER = 'AdminSaferPayOfficialSettings'; const ADMIN_PAYMENTS_CONTROLLER = 'AdminSaferPayOfficialPayment'; - const ADMIN_FIELDS_CONTROLLER = 'AdminSaferPayOfficialFields'; const ADMIN_ORDER_CONTROLLER = 'AdminSaferPayOfficialOrder'; const ADMIN_LOGS_CONTROLLER = 'AdminSaferPayOfficialLogs'; @@ -65,7 +64,7 @@ public function __construct($name = null) { $this->name = 'saferpayofficial'; $this->author = 'Invertus'; - $this->version = '2.0.2'; + $this->version = '2.1.1'; $this->module_key = '3d3506c3e184a1fe63b936b82bda1bdf'; $this->displayName = 'SaferpayOfficial'; $this->description = 'Saferpay Payment module'; diff --git a/src/Api/ApiRequest.php b/src/Api/ApiRequest.php index 70c9323a1..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,13 +172,73 @@ public function getWithCredentials($url, $username, $password, $baseUrl, $params return json_decode($response->raw_body); } catch (Exception $exception) { - $this->logger->error($exception->getMessage(), [ - 'context' => [], + if ($response === null) { + $this->logger->error($exception->getMessage(), [ + 'context' => [], + 'request' => $params, + 'response' => null, + 'exceptions' => ExceptionUtility::getExceptions($exception), + ]); + } + + throw $exception; + } + } + + /** + * API Request Post Method with explicit credentials. + * + * @param string $url + * @param string $username + * @param string $password + * @param string $baseUrl + * @param array|null $params + * @return mixed + * @throws Exception + */ + public function postWithCredentials($url, $username, $password, $baseUrl, $params = null) + { + $response = null; + + try { + $credentials = base64_encode("$username:$password"); + $headers = [ + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'Saferpay-ApiVersion' => SaferPayConfig::API_VERSION, + 'Saferpay-RequestId' => 'false', + 'Authorization' => "Basic $credentials", + ]; + + $body = $params !== null ? json_encode($params) : '{}'; + + $response = Request::post( + $baseUrl . $url, + $headers, + $body + ); + + $this->logger->debug(sprintf('%s - POST (credentials) response: %d', self::FILE_NAME, $response->code), [ + 'context' => [ + 'uri' => $baseUrl . $url, + ], 'request' => $params, - 'response' => $response ? json_decode($response->raw_body) : null, - 'exceptions' => ExceptionUtility::getExceptions($exception), + 'response' => $response->body, ]); + $this->isValidResponse($response); + + return json_decode($response->raw_body); + } catch (Exception $exception) { + if ($response === null) { + $this->logger->error($exception->getMessage(), [ + 'context' => [], + 'request' => $params, + 'response' => null, + 'exceptions' => ExceptionUtility::getExceptions($exception), + ]); + } + throw $exception; } } diff --git a/src/Api/Request/GenerateFieldAccessTokenService.php b/src/Api/Request/GenerateFieldAccessTokenService.php new file mode 100644 index 000000000..3ef46f9a9 --- /dev/null +++ b/src/Api/Request/GenerateFieldAccessTokenService.php @@ -0,0 +1,61 @@ + + *@copyright SIX Payment Services + *@license SIX Payment Services + */ + +namespace Invertus\SaferPay\Api\Request; + +use Invertus\SaferPay\Api\ApiRequest; +use Invertus\SaferPay\DTO\Request\GenerateFieldAccessToken\GenerateFieldAccessTokenRequest; + +if (!defined('_PS_VERSION_')) { + exit; +} + +class GenerateFieldAccessTokenService +{ + /** @var ApiRequest */ + private $apiRequest; + + public function __construct(ApiRequest $apiRequest) + { + $this->apiRequest = $apiRequest; + } + + /** + * @param GenerateFieldAccessTokenRequest $request + * @param string $username + * @param string $password + * @param string $baseUrl + * @param array|null $params + * @return mixed + */ + public function generateToken(GenerateFieldAccessTokenRequest $request, $username, $password, $baseUrl, $params = null) + { + return $this->apiRequest->postWithCredentials( + $request->generateRequestUrl(), + $username, + $password, + $baseUrl, + $params + ); + } +} diff --git a/src/Api/Request/GetLicenseService.php b/src/Api/Request/GetLicenseService.php new file mode 100644 index 000000000..0299875fb --- /dev/null +++ b/src/Api/Request/GetLicenseService.php @@ -0,0 +1,76 @@ + + *@copyright SIX Payment Services + *@license SIX Payment Services + */ + +namespace Invertus\SaferPay\Api\Request; + +use Invertus\SaferPay\Api\ApiRequest; +use Invertus\SaferPay\DTO\Request\GetLicense\GetLicenseRequest; + +if (!defined('_PS_VERSION_')) { + exit; +} + +class GetLicenseService +{ + /** @var ApiRequest */ + private $apiRequest; + + public function __construct(ApiRequest $apiRequest) + { + $this->apiRequest = $apiRequest; + } + + /** + * @param GetLicenseRequest $request + * @param string $username + * @param string $password + * @param string $baseUrl + * @return mixed + */ + public function getLicense(GetLicenseRequest $request, $username, $password, $baseUrl) + { + return $this->apiRequest->getWithCredentials( + $request->generateRequestUrl(), + $username, + $password, + $baseUrl + ); + } + + /** + * @param GetLicenseRequest $request + * @param string $username + * @param string $password + * @param string $baseUrl + * @return mixed + */ + public function getLicenseFallback(GetLicenseRequest $request, $username, $password, $baseUrl) + { + return $this->apiRequest->getWithCredentials( + $request->generateFallbackRequestUrl(), + $username, + $password, + $baseUrl + ); + } +} diff --git a/src/Config/SaferPayConfig.php b/src/Config/SaferPayConfig.php index ae7879997..793d0b8e1 100755 --- a/src/Config/SaferPayConfig.php +++ b/src/Config/SaferPayConfig.php @@ -276,6 +276,9 @@ class SaferPayConfig const PAYMENT_BEHAVIOR_WITHOUT_3D_CANCEL = 0; const PAYMENT_BEHAVIOR_WITHOUT_3D_AUTHORIZE = 1; + const PAYMENT_BEHAVIOR_WITHOUT_3D_CAPTURE = 2; + + const SAFERPAY_ORDER_ID_OPTION = 'SAFERPAY_ORDER_ID_OPTION'; const SAFERPAY_CARDFORM_HOLDERNAME_REQUIRENCE = 'MANDATORY'; const SAFERPAY_DEBUG_MODE = 'SAFERPAY_DEBUG_MODE'; @@ -433,12 +436,13 @@ public static function getDefaultConfiguration() RequestHeader::SPEC_REFUND_VERSION => SaferPayConfig::API_VERSION, RequestHeader::RETRY_INDICATOR => 0, SaferPayConfig::PAYMENT_BEHAVIOR => 1, - SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D => 1, + SaferPayConfig::PAYMENT_BEHAVIOR_WITHOUT_3D => 0, SaferPayConfig::SAFERPAY_ALLOW_SAFERPAY_SEND_CUSTOMER_MAIL => 1, SaferPayConfig::SAFERPAY_PAYMENT_DESCRIPTION => self::SAFERPAY_PAYMENT_DESCRIPTION_DEFAULT_VALUE, SaferPayConfig::FIELDS_LIBRARY => self::FIELDS_LIBRARY_DEFAULT_VALUE, SaferPayConfig::FIELDS_LIBRARY . SaferPayConfig::TEST_SUFFIX => self::FIELDS_LIBRARY_TEST_DEFAULT_VALUE, self::SAFERPAY_ORDER_CREATION_AFTER_AUTHORIZATION => 0, + self::SAFERPAY_ORDER_ID_OPTION => 0, self::TEST_MODE => 1, self::HOSTED_FIELDS_TEMPLATE => self::HOSTED_FIELDS_TEMPLATE_DEFAULT, self::SAFERPAY_ORDER_STATE_CHOICE_AWAITING_PAYMENT => (int) Configuration::get( @@ -479,6 +483,7 @@ public static function getUninstallConfiguration() self::FIELDS_LIBRARY, self::FIELDS_LIBRARY . self::TEST_SUFFIX, self::SAFERPAY_ORDER_CREATION_AFTER_AUTHORIZATION, + self::SAFERPAY_ORDER_ID_OPTION, self::SAFERPAY_SEND_ORDER_CONF_MAIL, self::SAFERPAY_GROUP_CARDS, ]; diff --git a/src/DTO/Request/GenerateFieldAccessToken/GenerateFieldAccessTokenRequest.php b/src/DTO/Request/GenerateFieldAccessToken/GenerateFieldAccessTokenRequest.php new file mode 100644 index 000000000..5eeb4a490 --- /dev/null +++ b/src/DTO/Request/GenerateFieldAccessToken/GenerateFieldAccessTokenRequest.php @@ -0,0 +1,67 @@ + + *@copyright SIX Payment Services + *@license SIX Payment Services + */ + +namespace Invertus\SaferPay\DTO\Request\GenerateFieldAccessToken; + +if (!defined('_PS_VERSION_')) { + exit; +} + +class GenerateFieldAccessTokenRequest +{ + /** @var string */ + private $customerId; + + /** @var string */ + private $terminalId; + + /** + * @param string $customerId + * @param string $terminalId + */ + public function __construct($customerId, $terminalId) + { + if (!preg_match('/^[a-zA-Z0-9\-_]+$/', $customerId)) { + throw new \InvalidArgumentException('Invalid customer ID format'); + } + + if (!preg_match('/^[a-zA-Z0-9\-_]+$/', $terminalId)) { + throw new \InvalidArgumentException('Invalid terminal ID format'); + } + + $this->customerId = $customerId; + $this->terminalId = $terminalId; + } + + /** + * @return string + */ + public function generateRequestUrl() + { + return sprintf( + 'rest/customers/%s/terminals/%s/fields-access-tokens', + $this->customerId, + $this->terminalId + ); + } +} diff --git a/views/css/admin/saferpay_fields.css b/src/DTO/Request/GetLicense/GetLicenseRequest.php old mode 100755 new mode 100644 similarity index 50% rename from views/css/admin/saferpay_fields.css rename to src/DTO/Request/GetLicense/GetLicenseRequest.php index 064c2c333..0cf139810 --- a/views/css/admin/saferpay_fields.css +++ b/src/DTO/Request/GetLicense/GetLicenseRequest.php @@ -1,3 +1,4 @@ +customerId = $customerId; + } + + /** + * @return string + */ + public function generateRequestUrl() + { + return sprintf('rest/customers/%s/license', $this->customerId); + } -.field-container { - display: flex; - flex-wrap: wrap; + /** + * @return string + */ + public function generateFallbackRequestUrl() + { + return sprintf('rest/customers/%s/license-configuration', $this->customerId); + } } diff --git a/src/Entity/index.php b/src/Entity/index.php deleted file mode 100755 index ee6227264..000000000 --- a/src/Entity/index.php +++ /dev/null @@ -1,31 +0,0 @@ - - *@copyright SIX Payment Services - *@license SIX Payment Services - */ -header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); -header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); - -header('Cache-Control: no-store, no-cache, must-revalidate'); -header('Cache-Control: post-check=0, pre-check=0', false); -header('Pragma: no-cache'); - -header('Location: ../'); -exit; diff --git a/src/Install/AbstractInstaller.php b/src/Install/AbstractInstaller.php index cbe8d897d..2188841f0 100755 --- a/src/Install/AbstractInstaller.php +++ b/src/Install/AbstractInstaller.php @@ -63,12 +63,6 @@ public function tabs() 'module_tab' => true, 'visible' => false, ], - [ - 'name' => $this->module->l('Fields'), - 'class_name' => SaferPayOfficial::ADMIN_FIELDS_CONTROLLER, - 'parent_class_name' => SaferPayOfficial::ADMIN_SAFERPAY_MODULE_CONTROLLER, - 'module_tab' => true, - ], [ 'name' => $this->module->l('Order'), 'class_name' => SaferPayOfficial::ADMIN_ORDER_CONTROLLER, diff --git a/src/Service/Request/RequestObjectCreator.php b/src/Service/Request/RequestObjectCreator.php index 6e961489f..21fc39835 100755 --- a/src/Service/Request/RequestObjectCreator.php +++ b/src/Service/Request/RequestObjectCreator.php @@ -118,14 +118,19 @@ public function createPayment(Cart $cart, $totalPrice) $payment = new Payment(); $payment->setValue($totalPrice); $payment->setCurrencyCode($currency['iso_code']); - $payment->setDescription((string) Configuration::get(SaferPayConfig::SAFERPAY_PAYMENT_DESCRIPTION)); - if ((int) \Configuration::get(SaferPayConfig::SAFERPAY_ORDER_CREATION_AFTER_AUTHORIZATION) && empty($order)) { - return $payment; + $description = (string) Configuration::get(SaferPayConfig::SAFERPAY_PAYMENT_DESCRIPTION); + $orderIdOption = (int) Configuration::get(SaferPayConfig::SAFERPAY_ORDER_ID_OPTION); + + if ($orderIdOption === 0 && !empty($order)) { + $payment->setDescription($order->reference); + } else { + $payment->setDescription($description); } - /** This param is not mandatory, but recommended **/ - $payment->setOrderReference($order->reference); + if (!empty($order)) { + $payment->setOrderReference($order->reference); + } return $payment; } diff --git a/src/Service/SaferPayGenerateFieldAccessToken.php b/src/Service/SaferPayGenerateFieldAccessToken.php new file mode 100644 index 000000000..1609cdd37 --- /dev/null +++ b/src/Service/SaferPayGenerateFieldAccessToken.php @@ -0,0 +1,81 @@ + + *@copyright SIX Payment Services + *@license SIX Payment Services + */ + +namespace Invertus\SaferPay\Service; + +use Exception; +use Invertus\SaferPay\Api\Request\GenerateFieldAccessTokenService; +use Invertus\SaferPay\Config\SaferPayConfig; +use Invertus\SaferPay\DTO\Request\GenerateFieldAccessToken\GenerateFieldAccessTokenRequest; + +if (!defined('_PS_VERSION_')) { + exit; +} + +class SaferPayGenerateFieldAccessToken +{ + /** @var GenerateFieldAccessTokenService */ + private $generateFieldAccessTokenService; + + public function __construct(GenerateFieldAccessTokenService $generateFieldAccessTokenService) + { + $this->generateFieldAccessTokenService = $generateFieldAccessTokenService; + } + + /** + * @param string $username + * @param string $password + * @param string $customerId + * @param string $terminalId + * @param bool $isTestMode + * @param string $shopUrl + * + * @return string + * + * @throws Exception + */ + public function generateWithCredentials($username, $password, $customerId, $terminalId, $isTestMode, $shopUrl) + { + $baseUrl = $isTestMode ? SaferPayConfig::TEST_API : SaferPayConfig::API; + $request = new GenerateFieldAccessTokenRequest($customerId, $terminalId); + + $params = [ + 'Description' => 'PrestaShop Module', + 'SourceUrls' => [$shopUrl], + ]; + + $response = $this->generateFieldAccessTokenService->generateToken( + $request, + $username, + $password, + $baseUrl, + $params + ); + + if (!isset($response->AccessToken)) { + throw new Exception('Unexpected API response: no access token returned'); + } + + return $response->AccessToken; + } +} diff --git a/src/Service/SaferPayGetLicense.php b/src/Service/SaferPayGetLicense.php new file mode 100644 index 000000000..a88e410a6 --- /dev/null +++ b/src/Service/SaferPayGetLicense.php @@ -0,0 +1,106 @@ + + *@copyright SIX Payment Services + *@license SIX Payment Services + */ + +namespace Invertus\SaferPay\Service; + +use Exception; +use Invertus\SaferPay\Api\Request\GetLicenseService; +use Invertus\SaferPay\Config\SaferPayConfig; +use Invertus\SaferPay\DTO\Request\GetLicense\GetLicenseRequest; + +if (!defined('_PS_VERSION_')) { + exit; +} + +class SaferPayGetLicense +{ + const FEATURE_HOSTED_ENTRY_FORM = 'HOSTED_ENTRY_FORM'; + + /** @var GetLicenseService */ + private $getLicenseService; + + public function __construct(GetLicenseService $getLicenseService) + { + $this->getLicenseService = $getLicenseService; + } + + /** + * @param string $username + * @param string $password + * @param string $customerId + * @param bool $isTestMode + * + * @return array{hasBusinessLicense: bool, packageName: string, features: array} + * + * @throws Exception + */ + public function fetchLicenseWithCredentials($username, $password, $customerId, $isTestMode) + { + $baseUrl = $isTestMode ? SaferPayConfig::TEST_API : SaferPayConfig::API; + $request = new GetLicenseRequest($customerId); + + $response = $this->fetchWithFallback($request, $username, $password, $baseUrl); + + $packageName = ''; + if (isset($response->Package->DisplayName)) { + $packageName = $response->Package->DisplayName; + } + + $features = []; + $featureList = isset($response->Features) ? $response->Features : []; + if (is_array($featureList)) { + foreach ($featureList as $feature) { + if (isset($feature->Id)) { + $features[] = $feature->Id; + } + } + } + + $hasBusinessLicense = in_array(self::FEATURE_HOSTED_ENTRY_FORM, $features, true); + + return [ + 'hasBusinessLicense' => $hasBusinessLicense, + 'packageName' => $packageName, + 'features' => $features, + ]; + } + + /** + * @param GetLicenseRequest $request + * @param string $username + * @param string $password + * @param string $baseUrl + * + * @return mixed + * + * @throws Exception + */ + private function fetchWithFallback(GetLicenseRequest $request, $username, $password, $baseUrl) + { + try { + return $this->getLicenseService->getLicense($request, $username, $password, $baseUrl); + } catch (Exception $e) { + return $this->getLicenseService->getLicenseFallback($request, $username, $password, $baseUrl); + } + } +} diff --git a/src/Service/SettingsTranslationService.php b/src/Service/SettingsTranslationService.php index 77d03e1be..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), @@ -139,8 +141,8 @@ private function getApiCredentialsTranslations() 'invalidCredentials' => $this->module->l('Invalid credentials. Please check your username and password.', self::FILE_NAME), 'saferpayFieldsIncluded' => $this->module->l('Saferpay Fields is included in your license', self::FILE_NAME), 'saferpayFieldsIncludedDescription' => $this->module->l('You can use hosted payment fields for a seamless checkout experience.', self::FILE_NAME), - 'saferpayFieldsNotIncluded' => $this->module->l('Saferpay Fields is not available', self::FILE_NAME), - 'saferpayFieldsNotIncludedDescription' => $this->module->l('Save valid API credentials to detect your license, or upgrade your Saferpay plan to access this feature.', self::FILE_NAME), + 'tokenGeneratedSuccessfully' => $this->module->l('Access token generated successfully.', self::FILE_NAME), + 'failedToGenerateToken' => $this->module->l('Failed to generate access token.', self::FILE_NAME), ]; } @@ -174,11 +176,12 @@ private function getPaymentProcessingTranslations() 'chargeImmediately' => $this->module->l('Charge immediately', self::FILE_NAME), 'authorize' => $this->module->l('Authorize', self::FILE_NAME), 'reserveAndCaptureLater' => $this->module->l('Reserve and capture later', self::FILE_NAME), - 'behaviourWhen3dsFails' => $this->module->l('Behaviour when 3D Secure fails', self::FILE_NAME), + 'behaviourWhen3dsFails' => $this->module->l('Behavior when 3D Secure Payer Authentication was not successful and liability shift was not granted', self::FILE_NAME), 'behaviourWhen3dsDescription' => $this->module->l('Default payment behavior for payment without 3-D Secure.', self::FILE_NAME), 'cancel' => $this->module->l('Cancel', self::FILE_NAME), 'rejectPayment' => $this->module->l('Reject the payment', self::FILE_NAME), 'continueWithout3ds' => $this->module->l('Continue without 3DS', self::FILE_NAME), + 'captureWithout3ds' => $this->module->l('Charge immediately', self::FILE_NAME), 'restrictRefundAmount' => $this->module->l('Restrict RefundAmount to Captured Amount', self::FILE_NAME), 'restrictRefundDescription' => $this->module->l('If set to true, the refund will be rejected if the sum of authorized refunds exceeds the capture value.', self::FILE_NAME), 'orderCreationRule' => $this->module->l('Order creation rule', self::FILE_NAME), @@ -187,7 +190,7 @@ private function getPaymentProcessingTranslations() 'createWhenAuthorized' => $this->module->l('Create when authorized', self::FILE_NAME), 'beforeAuthorization' => $this->module->l('Before authorization', self::FILE_NAME), 'createBeforePayment' => $this->module->l('Create before payment', self::FILE_NAME), - 'cardDisplaySaving' => $this->module->l('Card Display & Saving', self::FILE_NAME), + 'cardDisplaySaving' => html_entity_decode($this->module->l('Card Display & Saving', self::FILE_NAME), ENT_QUOTES, 'UTF-8'), 'cardDisplayDescription' => $this->module->l('Configure how cards appear at checkout and whether customers can save them.', self::FILE_NAME), 'groupCardsLabel' => $this->module->l('Group debit/credit cards as \'Cards\' in checkout', self::FILE_NAME), 'groupCardsDescription' => $this->module->l('If enabled, all supported card brands will be grouped and shown as a single \'Cards\' payment method at checkout.', self::FILE_NAME), @@ -226,12 +229,22 @@ private function getGeneralSettingsTranslations() 'stylingDescription' => $this->module->l('Customize the appearance of the payment page.', self::FILE_NAME), 'configName' => $this->module->l('Payment Page configurations name', self::FILE_NAME), 'enterConfigName' => $this->module->l('Enter configuration name', self::FILE_NAME), - 'configNameDescription' => $this->module->l('This name is visible in payment page and also in payment confirmation email.', 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' => 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), + 'labeledLayout' => $this->module->l('Labeled Layout', self::FILE_NAME), + 'inlineLayoutWithCard' => $this->module->l('Inline Layout with Card', self::FILE_NAME), 'configuration' => $this->module->l('Configuration', self::FILE_NAME), 'configurationDescription' => $this->module->l('General module configuration settings.', self::FILE_NAME), 'description' => $this->module->l('Description', self::FILE_NAME), 'enterDescription' => $this->module->l('Enter description', self::FILE_NAME), 'descriptionHelp' => $this->module->l('This description is visible in payment page also in payment confirmation email.', self::FILE_NAME), + 'orderReferenceOnPaymentPage' => $this->module->l('Order reference on payment page', self::FILE_NAME), + 'usePrestaShopOrderReference' => $this->module->l('Use PrestaShop Order reference (default)', self::FILE_NAME), + 'useDescriptionFieldValue' => $this->module->l('Use Description field value', self::FILE_NAME), + 'orderReferenceFallbackInfo' => html_entity_decode($this->module->l('When "Use PrestaShop Order reference" is selected and the order is not yet created (e.g. order creation after authorization), the Description field value is used as fallback.', self::FILE_NAME), ENT_QUOTES, 'UTF-8'), 'debugMode' => $this->module->l('Debug mode', self::FILE_NAME), 'debugModeDescription' => $this->module->l('Enable debug mode to see more information in logs.', self::FILE_NAME), ]; diff --git a/translations/en.php b/translations/en.php new file mode 100644 index 000000000..e69de29bb diff --git a/upgrade/install-1.0.3.php b/upgrade/install-1.0.3.php index 2574c76e5..b4938c7f3 100755 --- a/upgrade/install-1.0.3.php +++ b/upgrade/install-1.0.3.php @@ -57,12 +57,5 @@ function upgrade_module_1_0_3($module) ADD COLUMN `authorized` TINYINT(1) DEFAULT 0' ); - $installer = new \Invertus\SaferPay\Install\Installer($module); - $installer->installTab( - SaferPayOfficial::ADMIN_FIELDS_CONTROLLER, - SaferPayOfficial::ADMIN_SAFERPAY_MODULE_CONTROLLER, - $module->l('Fields') - ); - return $result; } diff --git a/views/templates/admin/partials/field-hosted-field-template-desc.tpl b/upgrade/install-2.1.1.php old mode 100755 new mode 100644 similarity index 75% rename from views/templates/admin/partials/field-hosted-field-template-desc.tpl rename to upgrade/install-2.1.1.php index 8314405ef..b7b39ae00 --- a/views/templates/admin/partials/field-hosted-field-template-desc.tpl +++ b/upgrade/install-2.1.1.php @@ -1,4 +1,5 @@ -{** + *@copyright SIX Payment Services *@license SIX Payment Services - *} -
- {l s='Choose which hosted field will be displayed on payment option selection with supported payment methods' mod='saferpayofficial'} -
+ */ + +if (!defined('_PS_VERSION_')) { + exit; +} + +function upgrade_module_2_1_1() +{ + $tabId = Tab::getIdFromClassName('AdminSaferPayOfficialFields'); + if ($tabId) { + $tab = new Tab($tabId); + $tab->delete(); + } + + return true; +} 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/saferpay_settings.js b/views/js/admin/saferpay_settings.js index 6407bc177..72f482b10 100755 --- a/views/js/admin/saferpay_settings.js +++ b/views/js/admin/saferpay_settings.js @@ -20,16 +20,23 @@ *@license SIX Payment Services */ -$(document).ready(function (e) { - $("input[name='SAFERPAY_CONFIGURATION_NAME']").keypress(function (e) { - //disable symbols +$(document).ready(function () { + var $configInput = $("input[name='SAFERPAY_CONFIGURATION_NAME']"); + + $configInput.attr('maxlength', 20); + + $configInput.keypress(function (e) { var txt = String.fromCharCode(e.which); - if (!txt.match(/[A-Za-z0-9&. ]/)) { - return false; - } - // disable space - if (e.keyCode === 32) { + if (!txt.match(/[A-Za-z0-9.:\-_]/)) { return false; } }); + + $configInput.on('paste', function (e) { + var $input = $(this); + setTimeout(function () { + var cleaned = $input.val().replace(/[^A-Za-z0-9.:\-_]/g, '').substring(0, 20); + $input.val(cleaned); + }, 0); + }); }); \ No newline at end of file diff --git a/views/js/admin/settings-app/src/api/client.ts b/views/js/admin/settings-app/src/api/client.ts index 1708bfe30..9fa4a8149 100644 --- a/views/js/admin/settings-app/src/api/client.ts +++ b/views/js/admin/settings-app/src/api/client.ts @@ -67,6 +67,19 @@ export async function getTerminals( }> } +export async function generateFieldAccessToken( + env: string, + username: string, + password: string, + terminalId: string, +): Promise<{ success: boolean; message?: string; token?: string }> { + return postAjax('generateFieldAccessToken', { env, username, password, terminalId }) as Promise<{ + success: boolean + message?: string + token?: string + }> +} + export async function refreshData(): Promise<{ success: boolean; data: Record }> { return postAjax('refreshData') as Promise<{ success: boolean; data: Record }> } 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 2e1aa5ed8..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 @@ -6,15 +6,17 @@ import { Button } from '@/components/ui/button' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { AlertCircle, Eye, EyeOff, Key, Shield, Loader2, Info, CheckCircle2, Wand2, XCircle } from 'lucide-react' import { useSettings } from '@/context/settings-context' +import { toast } from '@/hooks/use-toast' import { t } from '@/utils/translations' import type { TerminalOption } from '@/types' type CredentialStatus = 'idle' | 'checking' | 'valid' | 'invalid' export function ApiCredentials() { - const { settings, updateSettings, saveCredentials, fetchTerminals, savingSections } = useSettings() + const { settings, updateSettings, saveCredentials, fetchTerminals, generateFieldAccessToken, savingSections } = useSettings() const saving = savingSections.has('credentials') const [showApiPassword, setShowApiPassword] = useState(false) + const [generatingToken, setGeneratingToken] = useState(false) const [terminals, setTerminals] = useState([]) const [credentialStatus, setCredentialStatus] = useState('idle') const [credentialError, setCredentialError] = useState('') @@ -33,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) @@ -143,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')}
@@ -231,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')}

@@ -240,39 +265,24 @@ export function ApiCredentials() { - {/* Saferpay Fields Configuration */} - + {hasBusinessLicense && {t('saferpayFields')} {t('saferpayFieldsDescription')}
- {settings.hasBusinessLicense ? ( -
- -
-

- {t('saferpayFieldsIncluded')} -

-

- {t('saferpayFieldsIncludedDescription')} -

-
-
- ) : ( -
- -
-

- {t('saferpayFieldsNotIncluded')} -

-

- {t('saferpayFieldsNotIncludedDescription')} -

-
+
+ +
+

+ {t('saferpayFieldsIncluded')} +

+

+ {t('saferpayFieldsIncludedDescription')} +

- )} +
@@ -293,15 +303,30 @@ export function ApiCredentials() { placeholder={t('enterFieldAccessToken')} value={fieldAccessToken} onChange={(e) => setField('fieldAccessToken', e.target.value)} - disabled={!settings.hasBusinessLicense} className="sp-flex-1" />
@@ -317,7 +342,6 @@ export function ApiCredentials() { placeholder="https://www.saferpay.com/Fields/lib/1/" value={fieldJsUrl} onChange={(e) => setField('fieldJsUrl', e.target.value)} - disabled={!settings.hasBusinessLicense} /> {t('findLibraryUrlHere')} @@ -326,10 +350,15 @@ 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 70f74b738..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 @@ -3,8 +3,9 @@ import { Label } from '@/components/ui/label' import { Input } from '@/components/ui/input' import { Button } from '@/components/ui/button' import { Switch } from '@/components/ui/switch' +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { Settings2, Paintbrush, ClipboardList, Loader2 } from 'lucide-react' +import { Settings2, Paintbrush, ClipboardList, Loader2, Info } from 'lucide-react' import { useSettings } from '@/context/settings-context' import { t } from '@/utils/translations' @@ -71,14 +72,60 @@ export function GeneralSettings() { updateSettings({ configurationName: e.target.value })} + onChange={(e) => { + const cleaned = e.target.value.replace(/[^A-Za-z0-9.:\-_]/g, '') + updateSettings({ configurationName: cleaned }) + }} />

{t('configNameDescription')}

+ + {/* Hosted field info banner */} +
+ +

+ {t('hostedFieldInfo')} +

+
+ + {/* Hosted field style selector */} +
+
+ + +

+ {t('hostedFieldStyleDescription')} +

+
+
+ { +
+
@@ -97,17 +144,60 @@ export function GeneralSettings() {
-
- - updateSettings({ paymentDescription: e.target.value })} - /> -

- {t('descriptionHelp')} + {/* Order reference on payment page */} +

+ + updateSettings({ orderIdOption: Number(val) })} + className="sp-flex sp-flex-col sp-gap-3" + > + + + +
+ + {settings.orderIdOption === 1 && ( +
+ + updateSettings({ paymentDescription: e.target.value })} + /> +

+ {t('descriptionHelp')} +

+
+ )} + + {/* Info banner */} +
+ +

+ {t('orderReferenceFallbackInfo')}

@@ -131,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 a24ad5ba7..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 @@ -113,6 +113,20 @@ export function PaymentProcessing() { {t('continueWithout3ds')}
+
@@ -291,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/components/settings/toast-container.tsx b/views/js/admin/settings-app/src/components/settings/toast-container.tsx index 39d96074d..0b5f905db 100644 --- a/views/js/admin/settings-app/src/components/settings/toast-container.tsx +++ b/views/js/admin/settings-app/src/components/settings/toast-container.tsx @@ -15,7 +15,9 @@ export function ToastContainer() { className={`sp-rounded-lg sp-border sp-px-4 sp-py-3 sp-shadow-lg sp-text-sm sp-animate-in sp-slide-in-from-bottom-2 ${ t.variant === 'destructive' ? 'sp-bg-destructive sp-text-destructive-foreground sp-border-destructive' - : 'sp-bg-emerald-600 sp-text-white sp-border-emerald-700' + : t.variant === 'warning' + ? 'sp-bg-amber-500 sp-text-white sp-border-amber-600' + : 'sp-bg-emerald-600 sp-text-white sp-border-emerald-700' }`} > {t.title &&
{t.title}
} 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 e71802dcc..784cf7fb1 100644 --- a/views/js/admin/settings-app/src/context/settings-context.tsx +++ b/views/js/admin/settings-app/src/context/settings-context.tsx @@ -15,6 +15,7 @@ interface SettingsContextValue { saveGeneralSettings: () => Promise savePaymentMethods: () => Promise fetchTerminals: (env: string, username: string, password: string) => Promise + generateFieldAccessToken: () => Promise<{ success: boolean; message?: string; token?: string }> refreshPaymentMethods: () => Promise paymentMethods: PaymentMethodData[] updatePaymentMethod: (name: string, updates: Partial) => void @@ -47,7 +48,7 @@ export function SettingsProvider({ children }: { children: React.ReactNode }) { }, []) const handleSave = useCallback(async ( - saveFn: () => Promise<{ success: boolean; message?: string }>, + saveFn: () => Promise<{ success: boolean; message?: string; warning?: boolean }>, label: string, section: SavingSection, ) => { @@ -55,7 +56,8 @@ export function SettingsProvider({ children }: { children: React.ReactNode }) { try { const result = await saveFn() if (result.success) { - toast({ title: result.message || t('savedSuccessfully', label), variant: 'default' }) + const variant = result.warning ? 'warning' : 'default' + toast({ title: result.message || t('savedSuccessfully', label), variant }) } else { toast({ title: result.message || t('failedToSave', label), variant: 'destructive' }) } @@ -90,10 +92,14 @@ 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 + return { ...result, warning: data.warning === true } }, 'API Credentials', 'credentials') }, [handleSave]) @@ -125,6 +131,8 @@ export function SettingsProvider({ children }: { children: React.ReactNode }) { orderStateAwaitingPayment: currentSettings.orderStateAwaitingPayment, paymentDescription: currentSettings.paymentDescription, configurationName: currentSettings.configurationName, + hostedFieldsTemplate: currentSettings.hostedFieldsTemplate, + orderIdOption: currentSettings.orderIdOption, debugMode: currentSettings.debugMode, }), 'General Settings', 'generalSettings') }, [handleSave]) @@ -148,6 +156,26 @@ export function SettingsProvider({ children }: { children: React.ReactNode }) { } }, []) + const generateFieldAccessToken = useCallback(async () => { + const s = settingsRef.current + const env = s.testMode ? 'test' : 'live' + const username = s.testMode ? s.testUsername : s.liveUsername + const password = s.testMode ? s.testPassword : s.livePassword + const terminalId = s.testMode ? s.testTerminalId : s.liveTerminalId + + const result = await api.generateFieldAccessToken(env, username, password, terminalId) + if (!result.success) { + throw new Error(result.message || t('failedToGenerateToken')) + } + + if (result.token) { + const fieldKey = s.testMode ? 'testFieldAccessToken' : 'liveFieldAccessToken' + setSettings((prev) => ({ ...prev, [fieldKey]: result.token })) + } + + return result + }, []) + const fetchTerminals = useCallback(async (env: string, username: string, password: string) => { const result = await api.getTerminals(env, username, password) if (!result.success) { @@ -165,6 +193,7 @@ export function SettingsProvider({ children }: { children: React.ReactNode }) { saveGeneralSettings, savePaymentMethods, fetchTerminals, + generateFieldAccessToken, refreshPaymentMethods, paymentMethods, updatePaymentMethod, @@ -178,6 +207,7 @@ export function SettingsProvider({ children }: { children: React.ReactNode }) { saveGeneralSettings, savePaymentMethods, fetchTerminals, + generateFieldAccessToken, refreshPaymentMethods, paymentMethods, updatePaymentMethod, 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/hooks/use-toast.ts b/views/js/admin/settings-app/src/hooks/use-toast.ts index ec2103412..f0af5fe04 100644 --- a/views/js/admin/settings-app/src/hooks/use-toast.ts +++ b/views/js/admin/settings-app/src/hooks/use-toast.ts @@ -7,7 +7,7 @@ export interface ToasterToast { id: string title?: string description?: string - variant?: 'default' | 'destructive' + variant?: 'default' | 'destructive' | 'warning' } type Action = diff --git a/views/js/admin/settings-app/src/types/index.ts b/views/js/admin/settings-app/src/types/index.ts index a8c88e052..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 @@ -54,6 +55,9 @@ export interface SaferpaySettingsData { orderStateAwaitingPayment: number paymentDescription: string configurationName: string + hostedFieldsTemplate: number + modulePath: string + orderIdOption: number debugMode: boolean // Reference data diff --git a/views/templates/admin/field-option-settings/helpers/index.php b/views/templates/admin/field-option-settings/helpers/index.php deleted file mode 100755 index ee6227264..000000000 --- a/views/templates/admin/field-option-settings/helpers/index.php +++ /dev/null @@ -1,31 +0,0 @@ - - *@copyright SIX Payment Services - *@license SIX Payment Services - */ -header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); -header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); - -header('Cache-Control: no-store, no-cache, must-revalidate'); -header('Cache-Control: post-check=0, pre-check=0', false); -header('Pragma: no-cache'); - -header('Location: ../'); -exit; diff --git a/views/templates/admin/field-option-settings/helpers/options/index.php b/views/templates/admin/field-option-settings/helpers/options/index.php deleted file mode 100755 index ee6227264..000000000 --- a/views/templates/admin/field-option-settings/helpers/options/index.php +++ /dev/null @@ -1,31 +0,0 @@ - - *@copyright SIX Payment Services - *@license SIX Payment Services - */ -header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); -header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); - -header('Cache-Control: no-store, no-cache, must-revalidate'); -header('Cache-Control: post-check=0, pre-check=0', false); -header('Pragma: no-cache'); - -header('Location: ../'); -exit; diff --git a/views/templates/admin/field-option-settings/helpers/options/options.tpl b/views/templates/admin/field-option-settings/helpers/options/options.tpl deleted file mode 100755 index 4c100589d..000000000 --- a/views/templates/admin/field-option-settings/helpers/options/options.tpl +++ /dev/null @@ -1,70 +0,0 @@ -{** - *NOTICE OF LICENSE - * - *This source file is subject to the Open Software License (OSL 3.0) - *that is bundled with this package in the file LICENSE.txt. - *It is also available through the world-wide-web at this URL: - *http://opensource.org/licenses/osl-3.0.php - *If you did not receive a copy of the license and are unable to - *obtain it through the world-wide-web, please send an email - *to license@prestashop.com so we can send you a copy immediately. - * - *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 refer to http://www.prestashop.com for more information. - * - *@author INVERTUS UAB www.invertus.eu - *@copyright SIX Payment Services - *@license SIX Payment Services - *} - -{extends file="helpers/options/options.tpl"} - -{block name="input" append} - {if $field['type'] == 'password_input'} -
- -
- {/if} - {if $field['type'] == 'desc'} -
- {if $field['template'] == 'field-javascript-library-desc.tpl'} - {include file="../../../partials/field-javascript-library-desc.tpl"} - {/if} - - {if $field['template'] == 'field-access-token-desc.tpl'} - {include file="../../../partials/field-access-token-desc.tpl"} - {/if} - - {if $field['template'] == 'field-hosted-field-template-desc.tpl'} - {include file="../../../partials/field-hosted-field-template-desc.tpl"} - {/if} - {if $field['template'] == 'field-new-order-mail-desc.tpl'} - {include file="../../../partials/field-new-order-mail-desc.tpl"} - {/if} -
- {/if} - - {if $field['type'] == 'select-template'} - -
- {foreach from=$field['templateOptions'] key=key item=templateUrl} - {assign var='key' value=$key + 1} {* To have normal keys without 0 *} - - {/foreach} -
- - {/if} -{/block} diff --git a/views/templates/admin/field-option-settings/index.php b/views/templates/admin/field-option-settings/index.php deleted file mode 100755 index ee6227264..000000000 --- a/views/templates/admin/field-option-settings/index.php +++ /dev/null @@ -1,31 +0,0 @@ - - *@copyright SIX Payment Services - *@license SIX Payment Services - */ -header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); -header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); - -header('Cache-Control: no-store, no-cache, must-revalidate'); -header('Cache-Control: post-check=0, pre-check=0', false); -header('Pragma: no-cache'); - -header('Location: ../'); -exit; 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'} -
+ -