Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions src/Service/SettingsTranslationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand All @@ -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),
];
Expand All @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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
Expand Down Expand Up @@ -150,6 +159,26 @@ export function ApiCredentials() {
</CardHeader>
<CardContent>
<div className="sp-grid sp-gap-5">
{/* Credentials generation hint */}
<div className="sp-flex sp-items-center sp-gap-3 sp-rounded-lg sp-bg-[#294e57]/5 sp-border sp-border-[#294e57]/30 sp-border-l-[3px] sp-border-l-[#294e57] sp-px-4 sp-py-3 sp-text-sm sp-text-foreground">
<Info className="sp-h-4 sp-w-4 sp-shrink-0 sp-text-[#294e57]" />
<p className="sp-mb-0">
{t('credentialsHint').split(/(\[backoffice_link\]|\[more_info_link\])/).map((part, index) => {
if (part === '[backoffice_link]') {
return (
<a key={index} href={backofficeUrl} target="_blank" rel="noopener noreferrer" className="sp-text-[#294e57] sp-underline hover:sp-no-underline">{t('credentialsBackofficeLinkText')}</a>
)
}
if (part === '[more_info_link]') {
return (
<a key={index} href={jsonApiBasicAuthDocsUrl} target="_blank" rel="noopener noreferrer" className="sp-text-[#294e57] sp-underline hover:sp-no-underline">{t('credentialsMoreInfoLinkText')}</a>
)
}
return part
})}
</p>
Comment on lines +165 to +179

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To support the single translation string pattern and avoid sentence splitting, split the translated string by the placeholders and map them to the corresponding React link elements.

              <p className="sp-mb-0">
                {t('credentialsHint').split(/(\[backoffice_link\]|\[more_info_link\])/).map((part, index) => {
                  if (part === '[backoffice_link]') {
                    return (
                      <a key={index} href={backofficeUrl} target="_blank" rel="noopener noreferrer" className="sp-text-[#294e57] sp-underline hover:sp-no-underline">
                        {t('credentialsBackofficeLinkText')}
                      </a>
                    )
                  }
                  if (part === '[more_info_link]') {
                    return (
                      <a key={index} href={jsonApiBasicAuthDocsUrl} target="_blank" rel="noopener noreferrer" className="sp-text-[#294e57] sp-underline hover:sp-no-underline">
                        {t('credentialsMoreInfoLinkText')}
                      </a>
                    )
                  }
                  return part
                })}
              </p>

</div>

{/* Username & Password */}
<div className="sp-grid sp-gap-5 md:sp-grid-cols-2">
<div className="sp-flex sp-flex-col sp-gap-2">
Expand All @@ -173,22 +202,24 @@ export function ApiCredentials() {
<div className="sp-relative">
<Input
id="api-password"
type={showApiPassword ? 'text' : 'password'}
type={showApiPassword && !isStoredPasswordMasked ? 'text' : 'password'}
placeholder={t('enterApiPassword', envLabel.toLowerCase())}
value={password}
onChange={(e) => setField('password', e.target.value)}
className="sp-pr-10"
className={isStoredPasswordMasked ? undefined : 'sp-pr-10'}
required
aria-required="true"
/>
<button
type="button"
onClick={() => setShowApiPassword(!showApiPassword)}
className="sp-absolute sp-right-3 sp-top-1/2 sp--translate-y-1/2 sp-text-muted-foreground hover:sp-text-foreground sp-transition-colors sp-p-1 sp-min-w-[24px] sp-min-h-[24px] sp-flex sp-items-center sp-justify-center"
aria-label={showApiPassword ? t('hidePassword') : t('showPassword')}
>
{showApiPassword ? <EyeOff className="sp-h-4 sp-w-4" /> : <Eye className="sp-h-4 sp-w-4" />}
</button>
{!isStoredPasswordMasked && (
<button
type="button"
onClick={() => setShowApiPassword(!showApiPassword)}
className="sp-absolute sp-right-3 sp-top-1/2 sp--translate-y-1/2 sp-text-muted-foreground hover:sp-text-foreground sp-transition-colors sp-p-1 sp-min-w-[24px] sp-min-h-[24px] sp-flex sp-items-center sp-justify-center"
aria-label={showApiPassword ? t('hidePassword') : t('showPassword')}
>
{showApiPassword ? <EyeOff className="sp-h-4 sp-w-4" /> : <Eye className="sp-h-4 sp-w-4" />}
</button>
)}
</div>
</div>
</div>
Expand Down Expand Up @@ -343,7 +374,7 @@ export function ApiCredentials() {
value={fieldJsUrl}
onChange={(e) => setField('fieldJsUrl', e.target.value)}
/>
<a href="https://docs.saferpay.com/home/integration-guide/licences-and-interfaces/saferpay-fields#javascript-library-url" target="_blank" rel="noopener noreferrer" className="sp-text-xs sp-text-[#294e57] sp-underline hover:sp-no-underline">
<a href="https://docs.saferpay.com/home/integration-guide/licences-and-interfaces/saferpay-fields#include-the-saferpay-fields-javascript-library-into-your-site" target="_blank" rel="noopener noreferrer" className="sp-text-xs sp-text-[#294e57] sp-underline hover:sp-no-underline">
{t('findLibraryUrlHere')}
</a>
</div>
Expand Down
Loading