From 56c577c7031a6a7ee0692263fc703066655efdb2 Mon Sep 17 00:00:00 2001 From: Tadas Labutis Date: Wed, 29 Jul 2026 08:57:59 +0300 Subject: [PATCH] Fix misleading payment methods warning in settings A freshly installed module has no API credentials yet, but the Payment Methods settings called the Saferpay account anyway. The call failed and wrote an error to the merchant's log before anything had been configured. The bootstrap failure flag was also never cleared by a later refresh, so the "Could not reach your Saferpay account" warning kept showing after the payment methods grid had already loaded successfully. - Skip the account call until username, password, customer ID and terminal ID are all set for the active environment - Mirror the refresh outcome onto the failure flag so a resolved failure stops warning - Reset the toast latch when the flag clears, so a later genuine failure is not swallowed --- changelog.md | 2 + ...dminSaferPayOfficialSettingsController.php | 45 ++++++++++++++++--- .../components/settings/payment-methods.tsx | 9 +++- .../src/context/settings-context.tsx | 13 +++++- 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/changelog.md b/changelog.md index db3cbe3b..aeef6e50 100644 --- a/changelog.md +++ b/changelog.md @@ -214,3 +214,5 @@ - BO/FO : Accessibility improvements for EAA / WCAG 2.1 AA compliance - Added configurable payment description and order reference on payment page - API update to V1.50: added WERO and GIFTCARD payment methods, removed deprecated GIROPAY/PAYDIREKT/SOFORT +- BO : Fixed issue when a freshly installed module logged an account error before any API credentials were entered +- BO : Fixed issue when the "Could not reach your Saferpay account" warning kept showing after payment methods had loaded successfully diff --git a/controllers/admin/AdminSaferPayOfficialSettingsController.php b/controllers/admin/AdminSaferPayOfficialSettingsController.php index 4ec41417..713e8e56 100755 --- a/controllers/admin/AdminSaferPayOfficialSettingsController.php +++ b/controllers/admin/AdminSaferPayOfficialSettingsController.php @@ -566,6 +566,9 @@ private function collectSettingsData() /** @var SaferPayConfiguration $configuration */ $configuration = $this->module->getService(SaferPayConfiguration::class); + // Resolved before the payload is built because it sets $paymentMethodsFetchFailed. + $paymentMethodsData = $this->getPaymentMethodsData(); + $data = [ // Environment 'testMode' => (bool) $configuration->get(SaferPayConfig::TEST_MODE), @@ -615,7 +618,7 @@ private function collectSettingsData() 'orderStates' => $this->getOrderStates(), 'countries' => $this->getCountries(), 'currencies' => $this->getCurrencies(), - 'paymentMethods' => $this->getPaymentMethodsData(), + 'paymentMethods' => $paymentMethodsData, 'paymentMethodsFetchFailed' => $this->paymentMethodsFetchFailed, // Endpoints @@ -698,14 +701,21 @@ private function getPaymentMethodsData() /** @var SaferPayPaymentRepository $paymentRepository */ $paymentRepository = $this->module->getService(SaferPayPaymentRepository::class); + // A fresh install has no credentials yet, so calling the account would fail and + // write a misleading error to the merchant's log. Skip the account entirely until + // the credentials needed to build the request are present. + $hasCredentials = $this->hasApiCredentials(); + try { // Re-read the account and reconcile the stored list when the Payment Methods // settings open, so methods added/removed on the Saferpay account are reflected // (and persisted for the front office) without requiring a Save click. Enabled // flags are preserved by the refresh; newly added methods default to disabled. - /** @var SaferPayRefreshPaymentsService $refreshPaymentsService */ - $refreshPaymentsService = $this->module->getService(SaferPayRefreshPaymentsService::class); - $refreshPaymentsService->refreshPayments(); + if ($hasCredentials) { + /** @var SaferPayRefreshPaymentsService $refreshPaymentsService */ + $refreshPaymentsService = $this->module->getService(SaferPayRefreshPaymentsService::class); + $refreshPaymentsService->refreshPayments(); + } // The refresh persists the account's methods, so read them back from storage // instead of calling the API a second time. @@ -713,7 +723,7 @@ private function getPaymentMethodsData() // refreshPayments() is a no-op when nothing is active yet (e.g. a fresh setup), // so fall back to the live account list to still surface newly available methods. - if (empty($paymentMethods)) { + if (empty($paymentMethods) && $hasCredentials) { /** @var SaferPayObtainPaymentMethods $obtainMethods */ $obtainMethods = $this->module->getService(SaferPayObtainPaymentMethods::class); $paymentMethods = $obtainMethods->obtainPaymentMethodsNamesAsArray(); @@ -768,6 +778,31 @@ private function getPaymentMethodsData() return $result; } + /** + * Whether every credential the payment methods request is built from is configured + * for the active environment. + * + * @return bool + */ + private function hasApiCredentials() + { + $suffix = SaferPayConfig::getConfigSuffix(); + $required = [ + SaferPayConfig::USERNAME, + SaferPayConfig::PASSWORD, + SaferPayConfig::CUSTOMER_ID, + SaferPayConfig::TERMINAL_ID, + ]; + + foreach ($required as $key) { + if (!Configuration::get($key . $suffix)) { + return false; + } + } + + return true; + } + /** * Get JSON input from request body */ 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 a8f26338..5a939ff1 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 @@ -154,7 +154,14 @@ export function PaymentMethods() { // The account check runs while the page bootstraps, so a failure arrives via // the initial settings data rather than a refresh response. useEffect(() => { - if (settings.paymentMethodsFetchFailed && !fetchFailedToastShown) { + // Clearing the latch keeps a later, genuine failure from being swallowed. + if (!settings.paymentMethodsFetchFailed) { + fetchFailedToastShown = false + + return + } + + if (!fetchFailedToastShown) { fetchFailedToastShown = true toast({ title: t('paymentMethodsUnreachable'), variant: 'warning' }) } 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 7e05ea6e..7069b1db 100644 --- a/views/js/admin/settings-app/src/context/settings-context.tsx +++ b/views/js/admin/settings-app/src/context/settings-context.tsx @@ -150,8 +150,17 @@ export function SettingsProvider({ children }: { children: React.ReactNode }) { if (result.success && Array.isArray(result.data?.paymentMethods)) { setPaymentMethods(result.data.paymentMethods as PaymentMethodData[]) } - if (result.data?.paymentMethodsFetchFailed === true) { - toast({ title: t('paymentMethodsUnreachable'), variant: 'warning' }) + + // Mirror the refreshed outcome onto the bootstrap flag, otherwise a failure from + // page load keeps warning after a later refresh has already succeeded. Only a + // response that actually reports the flag may clear it. + if (result.data && 'paymentMethodsFetchFailed' in result.data) { + const fetchFailed = result.data.paymentMethodsFetchFailed === true + setSettings((prev) => ({ ...prev, paymentMethodsFetchFailed: fetchFailed })) + + if (fetchFailed) { + toast({ title: t('paymentMethodsUnreachable'), variant: 'warning' }) + } } } catch { toast({ title: t('errorRefreshingPaymentMethods'), variant: 'destructive' })