diff --git a/package-lock.json b/package-lock.json index 695422603..8c9923d05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "react-ace": "^10.1.0", "react-currency-input-field": "^3.6.10", "react-dom": "^18.2.0", + "react-error-boundary": "^3.1.4", "react-hook-form": "^7.42.1", "react-select": "^5.7.0", "swr": "^2.0.1" @@ -22426,6 +22427,21 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" }, + "node_modules/react-error-boundary": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-hook-form": { "version": "7.42.1", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.42.1.tgz", @@ -45051,6 +45067,14 @@ } } }, + "react-error-boundary": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", + "requires": { + "@babel/runtime": "^7.12.5" + } + }, "react-hook-form": { "version": "7.42.1", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.42.1.tgz", diff --git a/package.json b/package.json index f21193f04..5d89153fb 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "react-ace": "^10.1.0", "react-currency-input-field": "^3.6.10", "react-dom": "^18.2.0", + "react-error-boundary": "^3.1.4", "react-hook-form": "^7.42.1", "react-select": "^5.7.0", "swr": "^2.0.1" diff --git a/src/NextGen/DonationForm/Repositories/DonationFormRepository.php b/src/NextGen/DonationForm/Repositories/DonationFormRepository.php index 819db9808..4ed7fbe73 100644 --- a/src/NextGen/DonationForm/Repositories/DonationFormRepository.php +++ b/src/NextGen/DonationForm/Repositories/DonationFormRepository.php @@ -317,12 +317,14 @@ public function getFormDataGateways(int $formId): array foreach ($this->getEnabledPaymentGateways($formId) as $gateway) { $gatewayId = $gateway::id(); + $settings = $this->getGatewayFormSettings($formId, $gateway); + $label = give_get_gateway_checkout_label($gatewayId) ?? $gateway->getPaymentMethodLabel(); $formDataGateways[$gatewayId] = array_merge( [ - 'label' => give_get_gateway_checkout_label($gatewayId) ?? $gateway->getPaymentMethodLabel(), + 'label' => $label, ], - method_exists($gateway, 'formSettings') ? $gateway->formSettings($formId) : [] + $settings ); } @@ -420,4 +422,29 @@ public function isLegacyForm(int $formId): bool return empty($form->data); } + + /** + * Get gateway form settings and handle any exceptions. + * + * @since 0.2.0 + */ + private function getGatewayFormSettings(int $formId, PaymentGateway $gateway): array + { + if (!method_exists($gateway, 'formSettings')) { + return []; + } + + try { + return $gateway->formSettings($formId); + } catch (\Exception $exception) { + $gatewayName = $gateway->getName(); + Log::error("Failed getting gateway ($gatewayName) form settings", [ + 'formId' => $formId, + 'gateway' => $gatewayName, + 'error' => $exception->getMessage(), + ]); + + return []; + } + } } diff --git a/src/NextGen/DonationForm/resources/registrars/gateways/index.ts b/src/NextGen/DonationForm/resources/registrars/gateways/index.ts index 17cf38e9b..1efd5eb89 100644 --- a/src/NextGen/DonationForm/resources/registrars/gateways/index.ts +++ b/src/NextGen/DonationForm/resources/registrars/gateways/index.ts @@ -1,30 +1,58 @@ -import {Gateway, GatewaySettings} from '@givewp/forms/types'; +import {Gateway} from '@givewp/forms/types'; +const {gatewaySettings} = window.giveNextGenExports; + +/** + * @since 0.1.0 + */ interface GatewayRegistrar { register(gateway: Gateway): void; + getAll(): Gateway[]; + get(id: string): Gateway | undefined; } -const {gatewaySettings} = window.giveNextGenExports; - +/** + * @since 0.1.0 + */ export default class Registrar implements GatewayRegistrar { + /** + * @since 0.1.0 + */ private gateways: Gateway[] = []; + /** + * @since 0.1.0 + */ public get(id: string): Gateway | undefined { return this.gateways.find((gateway) => gateway.id === id); } + /** + * @since 0.1.0 + */ public getAll(): Gateway[] { return this.gateways; } + /** + * @since 0.1.0 + */ public register(gateway: Gateway): void { - const settings: GatewaySettings = gatewaySettings[gateway.id]; - gateway.settings = settings; - - if (gateway.initialize) { - gateway.initialize(); + gateway.settings = gatewaySettings[gateway.id]; + + if (gateway.hasOwnProperty('initialize')) { + try { + gateway.initialize(); + } catch (e) { + console.error(`Error initializing ${gateway.id} gateway:`, e); + // TODO: decide what to do if a gateway fails to initialize + // we can hide the fields from the list or display an error message. + // for now we will just display the error message, but in the future + // it might be better to hide the fields all together by returning early here. + //return; + } } this.gateways.push(gateway); diff --git a/src/NextGen/DonationForm/resources/registrars/templates/fields/Gateways.tsx b/src/NextGen/DonationForm/resources/registrars/templates/fields/Gateways.tsx index 6c4e28bac..7afb56eb8 100644 --- a/src/NextGen/DonationForm/resources/registrars/templates/fields/Gateways.tsx +++ b/src/NextGen/DonationForm/resources/registrars/templates/fields/Gateways.tsx @@ -1,16 +1,44 @@ import {ErrorMessage} from '@hookform/error-message'; import type {GatewayFieldProps, GatewayOptionProps} from '@givewp/forms/propTypes'; +import {ErrorBoundary} from 'react-error-boundary'; +import {__} from '@wordpress/i18n'; + +function GatewayFieldsErrorFallback({error, resetErrorBoundary}) { + return ( +
+

+ {__( + 'An error occurred while loading the gateway fields. Please notify the site administrator. The error message is:', + 'give' + )} +

+
{error.message}
+ +
+ ); +} export default function Gateways({inputProps, gateways}: GatewayFieldProps) { const {errors} = window.givewp.form.hooks.useFormState(); return ( <> - + {gateways.length > 0 ? ( + + ) : ( + + {__( + 'No gateways have been enabled yet. To get started accepting donations, enable a compatible payment gateway in your settings.', + 'give' + )} + + )}
- + { + window.location.reload(); + }} + > + +
); diff --git a/src/NextGen/Gateways/Stripe/NextGenStripeGateway/nextGenStripeGateway.tsx b/src/NextGen/Gateways/Stripe/NextGenStripeGateway/nextGenStripeGateway.tsx index 20a8b6282..f8dac9b41 100644 --- a/src/NextGen/Gateways/Stripe/NextGenStripeGateway/nextGenStripeGateway.tsx +++ b/src/NextGen/Gateways/Stripe/NextGenStripeGateway/nextGenStripeGateway.tsx @@ -38,6 +38,10 @@ const stripeGateway: StripeGateway = { initialize() { const {stripeKey, stripeConnectAccountId, stripeClientSecret} = this.settings; + if (!stripeKey || !stripeConnectAccountId || !stripeClientSecret) { + throw new Error('Stripe gateway settings are missing. Check your Stripe settings.'); + } + /** * Create the Stripe object and pass our api keys */ @@ -64,7 +68,7 @@ const stripeGateway: StripeGateway = { data: { intentStatus: string; returnUrl: string; - } + }; }): Promise { if (response.data.intentStatus === 'requires_payment_method') { const {error: fetchUpdatesError} = await this.elements.fetchUpdates(); @@ -93,6 +97,10 @@ const stripeGateway: StripeGateway = { } }, Fields() { + if (!stripePromise) { + throw new Error('Stripe library was not able to load. Check your Stripe settings.'); + } + return (