diff --git a/src/Service/SettingsTranslationService.php b/src/Service/SettingsTranslationService.php index ae2de999..29772358 100644 --- a/src/Service/SettingsTranslationService.php +++ b/src/Service/SettingsTranslationService.php @@ -109,6 +109,9 @@ private function getApiCredentialsTranslations() 'live' => $this->module->l('Live', self::FILE_NAME), 'apiCredentials' => $this->module->l('API Credentials', self::FILE_NAME), 'enterSaferpayCredentials' => $this->module->l('Enter your Saferpay %s environment API credentials.', self::FILE_NAME), + 'credentialsHint' => html_entity_decode($this->module->l('You can generate your API credentials inside the [backoffice_link] under Settings > JSON API Basic authentication. [more_info_link]', self::FILE_NAME), ENT_QUOTES, 'UTF-8'), + 'credentialsBackofficeLinkText' => $this->module->l('Saferpay Backoffice', self::FILE_NAME), + 'credentialsMoreInfoLinkText' => $this->module->l('More information', self::FILE_NAME), 'jsonApiUsername' => $this->module->l('JSON API Username', self::FILE_NAME), 'enterApiUsername' => $this->module->l('Enter %s API username', self::FILE_NAME), 'jsonApiPassword' => $this->module->l('JSON API Password', self::FILE_NAME), @@ -176,11 +179,11 @@ 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('Behavior when 3D Secure Payer Authentication was not successful and liability shift was not granted', self::FILE_NAME), + 'behaviourWhen3dsFails' => $this->module->l('Behavior when liability shift through 3D Secure has not been 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), + 'continueWithout3ds' => $this->module->l('Cancel or Capture manually', 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), @@ -205,13 +208,13 @@ private function getEmailTranslations() { return [ 'emailSending' => $this->module->l('Email Sending', self::FILE_NAME), - 'emailSendingDescription' => $this->module->l('Configure which emails are sent during the payment process.', self::FILE_NAME), + 'emailSendingDescription' => $this->module->l('Configure which emails are sent during the payment process. Merchant notifications sent by Saferpay use the Merchant Email(s) field on the API Credentials tab.', self::FILE_NAME), 'saferpayCustomerMail' => $this->module->l('Send an email from Saferpay on payment completion', self::FILE_NAME), - 'saferpayCustomerMailDescription' => $this->module->l('With this setting enabled an email from the Saferpay system will be sent to the customer.', self::FILE_NAME), + 'saferpayCustomerMailDescription' => $this->module->l('Saferpay sends a payment confirmation email directly to the customer.', self::FILE_NAME), 'newOrderMail' => $this->module->l('Send new order mail on authorization', self::FILE_NAME), - 'newOrderMailDescription' => $this->module->l('Receive a notification when an order is authorized by Saferpay (Using the Mail alert module).', self::FILE_NAME), + 'newOrderMailDescription' => $this->module->l('Notify the shop owner when an order is authorized (requires the Mail Alert module).', self::FILE_NAME), 'orderConfMail' => $this->module->l('Send order confirmation mail on payment completion', self::FILE_NAME), - 'orderConfMailDescription' => $this->module->l('Send an email from Saferpay on payment completion.', self::FILE_NAME), + 'orderConfMailDescription' => $this->module->l('Send the shop\'s order confirmation email to the customer, only once payment is authorized by Saferpay.', self::FILE_NAME), 'emailConfInfo' => $this->module->l('When this feature is enabled, a confirmation email will be only sent once the payment is authorized by Saferpay.', self::FILE_NAME), 'emailConfMailAlert' => $this->module->l('For this feature to be functioning you need to have the Mail Alert module configured.', self::FILE_NAME), ]; @@ -230,7 +233,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' => 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'), + 'hostedFieldInfo' => html_entity_decode($this->module->l('"Hosted field style" sets how the card input form looks on the Saferpay-hosted payment page. It doesn\'t affect the custom form or saved-card payments.', 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/js/admin/settings-app/src/components/settings/api-credentials.tsx b/views/js/admin/settings-app/src/components/settings/api-credentials.tsx index d806211b..70896142 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 @@ -12,6 +12,10 @@ import type { TerminalOption } from '@/types' type CredentialStatus = 'idle' | 'checking' | 'valid' | 'invalid' +// Mirrors AdminSaferPayOfficialSettingsController::PASSWORD_PLACEHOLDER. The real +// password is never sent to the browser; a stored credential arrives as this mask. +const PASSWORD_PLACEHOLDER = '********' + export function ApiCredentials() { const { settings, updateSettings, saveCredentials, fetchTerminals, generateFieldAccessToken, savingSections } = useSettings() const saving = savingSections.has('credentials') @@ -26,9 +30,14 @@ export function ApiCredentials() { const prefix = isTest ? 'test' : 'live' const envLabel = isTest ? t('test') : t('live') const environment = isTest ? 'test' : 'live' + const backofficeUrl = isTest ? 'https://test.saferpay.com/bo/login' : 'https://www.saferpay.com/bo/login' + const jsonApiBasicAuthDocsUrl = 'https://docs.saferpay.com/home/interfaces/backoffice/settings/json-api-basic-client-certificate-authentication#basic-authentication' const username = isTest ? settings.testUsername : settings.liveUsername const password = isTest ? settings.testPassword : settings.livePassword + // While the field still holds the untouched stored-credential mask there is nothing + // to reveal, so the show/hide toggle is hidden until the user types a new password. + const isStoredPasswordMasked = password === PASSWORD_PLACEHOLDER const terminalId = isTest ? settings.testTerminalId : settings.liveTerminalId const merchantEmails = isTest ? settings.testMerchantEmails : settings.liveMerchantEmails const fieldAccessToken = isTest ? settings.testFieldAccessToken : settings.liveFieldAccessToken @@ -150,6 +159,26 @@ export function ApiCredentials() {
+ {/* Credentials generation hint */} +
+ +

+ {t('credentialsHint').split(/(\[backoffice_link\]|\[more_info_link\])/).map((part, index) => { + if (part === '[backoffice_link]') { + return ( + {t('credentialsBackofficeLinkText')} + ) + } + if (part === '[more_info_link]') { + return ( + {t('credentialsMoreInfoLinkText')} + ) + } + return part + })} +

+
+ {/* Username & Password */}
@@ -173,22 +202,24 @@ export function ApiCredentials() {
setField('password', e.target.value)} - className="sp-pr-10" + className={isStoredPasswordMasked ? undefined : 'sp-pr-10'} required aria-required="true" /> - + {!isStoredPasswordMasked && ( + + )}
@@ -343,7 +374,7 @@ export function ApiCredentials() { value={fieldJsUrl} onChange={(e) => setField('fieldJsUrl', e.target.value)} /> - + {t('findLibraryUrlHere')}