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' })