Internal/ci build react settings#330
Conversation
The admin settings screen is a Vite/React app whose compiled bundle (views/js/admin/dist) is gitignored. The packaging workflows only ran composer, so released ZIPs shipped without saferpay-settings.js/.css, leaving the Settings page blank. Add a pnpm install + build step (pinned to pnpm 9 so esbuild's build script runs) before zipping in release.yml, deploy.yml and create_zip.yml, then strip node_modules/src. Mirrors the Mollie module pipeline.
There was a problem hiding this comment.
Code Review
This pull request migrates the Saferpay module settings page to a modern React-based application, refactoring the admin controllers to handle AJAX requests for credentials, payment processing, email notifications, and payment methods. It also introduces a new service to fetch terminal IDs directly from the Saferpay API. The review feedback highlights several critical issues, including incorrect credential sanitization using pSQL() which can corrupt passwords, a state-handling bug in the React app when switching environments, a potential PHP 8 type error in payment method saving, type mismatches in multi-select ID comparisons, duplicate build steps in the Makefile, and unsafe object property access in the terminal retrieval service.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| */ | ||
| private function getStringValue($data, $key) | ||
| { | ||
| return isset($data[$key]) ? pSQL((string) $data[$key]) : ''; | ||
| } |
There was a problem hiding this comment.
Using pSQL() to sanitize configuration values is incorrect and dangerous. pSQL() is specifically designed for escaping SQL queries to prevent SQL injection. When saving values to PrestaShop's Configuration database table via $configuration->set(), PrestaShop handles the database escaping internally. Applying pSQL() here will corrupt credentials (such as API passwords, usernames, or tokens) if they contain special characters like single quotes or backslashes, leading to authentication failures.
private function getStringValue($data, $key)
{
return isset($data[$key]) ? (string) $data[$key] : '';
}|
|
||
| useEffect(() => { | ||
| if (hasCredentials && terminals.length === 0) { | ||
| handleFetchTerminals() | ||
| } |
There was a problem hiding this comment.
The useEffect hook only triggers when environment changes, but it checks terminals.length === 0. Since the terminals state is not cleared when switching environments, switching from 'Test' to 'Live' (or vice versa) will retain the previous environment's terminals. This prevents the new environment's terminals from being fetched, causing the user to see and potentially save incorrect terminal IDs. Resetting and refetching the terminals properly on environment change fixes this.
useEffect(() => {
setTerminals([])
if (username && password && customerId) {
const fetchForEnv = async () => {
setIsLoadingTerminals(true)
try {
const result = await fetchTerminals(environment, username, password, customerId)
setTerminals(result)
} finally {
setIsLoadingTerminals(false)
}
}
fetchForEnv()
}
}, [environment])
| if (!$data || !isset($data['paymentMethods'])) { | ||
| $this->ajaxResponse(false, 'Invalid request data'); | ||
| return; | ||
| } |
There was a problem hiding this comment.
In ajaxProcessSavePaymentMethods, isset($data['paymentMethods']) is checked, but it is not verified to be an array. If the request payload contains a non-array value for paymentMethods, the subsequent foreach loop will throw a TypeError in PHP 8. Adding an is_array check makes this more robust.
if (!$data || !isset($data['paymentMethods']) || !is_array($data['paymentMethods'])) {
$this->ajaxResponse(false, 'Invalid request data');
return;
}| function MultiSelect({ | ||
| options, | ||
| selected, | ||
| onChange, | ||
| placeholder, | ||
| label, | ||
| }: { | ||
| options: Array<{ id: number; name: string }> | ||
| selected: number[] | ||
| onChange: (values: number[]) => void | ||
| placeholder: string | ||
| label: string | ||
| }) { | ||
| const [search, setSearch] = useState('') | ||
|
|
||
| const filteredOptions = useMemo( | ||
| () => options.filter((opt) => opt.name.toLowerCase().includes(search.toLowerCase())), | ||
| [options, search], | ||
| ) | ||
|
|
||
| const toggle = useCallback( | ||
| (value: number) => { | ||
| onChange( | ||
| selected.includes(value) | ||
| ? selected.filter((s) => s !== value) | ||
| : [...selected, value], | ||
| ) | ||
| }, | ||
| [selected, onChange], | ||
| ) | ||
|
|
There was a problem hiding this comment.
Mismatch between string IDs from the database and numeric IDs in React can cause strict comparison (includes) to fail. Normalizing selected to numbers ensures reliable matching.
function MultiSelect({
options,
selected,
onChange,
placeholder,
label,
}: {
options: Array<{ id: number; name: string }>
selected: Array<number | string>
onChange: (values: number[]) => void
placeholder: string
label: string
}) {
const [search, setSearch] = useState('')
const selectedNumbers = useMemo(() => selected.map(Number), [selected])
const filteredOptions = useMemo(
() => options.filter((opt) => opt.name.toLowerCase().includes(search.toLowerCase())),
[options, search],
)
const toggle = useCallback(
(value: number) => {
onChange(
selectedNumbers.includes(value)
? selectedNumbers.filter((s) => s !== value)
: [...selectedNumbers, value],
)
},
[selectedNumbers, onChange],
)
| rm -rf vendor && \ | ||
| composer install --no-dev --optimize-autoloader && \ | ||
| cp .github/.htaccess vendor/.htaccess && \ | ||
| cd views/js/admin/settings-app && pnpm install && pnpm run build && cd ../../../.. && \ |
| ); | ||
|
|
||
| $terminals = []; | ||
| $terminalList = isset($response->Terminals) ? $response->Terminals : []; |
There was a problem hiding this comment.
Accessing $response->Terminals directly can trigger warnings or errors if $response is not an object (e.g., if the API call failed or returned an unexpected non-JSON response). It is safer to verify that $response is an object before checking for the property.
$terminalList = (is_object($response) && isset($response->Terminals)) ? $response->Terminals : [];
Self-Checks
JIRA task link
Summary
QA Checklist Labels
QA Checklist
Additional Context
Frontend Changes