Skip to content
This repository has been archived by the owner on Oct 16, 2023. It is now read-only.

Commit

Permalink
feature: add basic error boundaries for gateways
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon Waldstein committed Feb 28, 2023
1 parent 223f866 commit 1bcc5b6
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 19 deletions.
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
31 changes: 29 additions & 2 deletions src/NextGen/DonationForm/Repositories/DonationFormRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}

Expand Down Expand Up @@ -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 [];
}
}
}
44 changes: 36 additions & 8 deletions src/NextGen/DonationForm/resources/registrars/gateways/index.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<div role="alert">
<p>
{__(
'An error occurred while loading the gateway fields. Please notify the site administrator. The error message is:',
'give'
)}
</p>
<pre style={{padding: '0.5rem'}}>{error.message}</pre>
<button type="button" onClick={resetErrorBoundary}>
{__('Reload form', 'give')}
</button>
</div>
);
}

export default function Gateways({inputProps, gateways}: GatewayFieldProps) {
const {errors} = window.givewp.form.hooks.useFormState();

return (
<>
<ul style={{listStyleType: 'none', padding: 0}}>
{gateways.map((gateway, index) => (
<GatewayOption gateway={gateway} index={index} key={gateway.id} inputProps={inputProps} />
))}
</ul>
{gateways.length > 0 ? (
<ul style={{listStyleType: 'none', padding: 0}}>
{gateways.map((gateway, index) => (
<GatewayOption gateway={gateway} index={index} key={gateway.id} inputProps={inputProps} />
))}
</ul>
) : (
<em>
{__(
'No gateways have been enabled yet. To get started accepting donations, enable a compatible payment gateway in your settings.',
'give'
)}
</em>
)}

<ErrorMessage
errors={errors}
Expand All @@ -22,14 +50,19 @@ export default function Gateways({inputProps, gateways}: GatewayFieldProps) {
}

function GatewayOption({gateway, index, inputProps}: GatewayOptionProps) {
const Fields = gateway.Fields;

return (
<li>
<input type="radio" value={gateway.id} id={gateway.id} defaultChecked={index === 0} {...inputProps} />
<label htmlFor={gateway.id}> Donate with {gateway.settings.label}</label>
<div className="givewp-fields-payment-gateway">
<Fields />
<ErrorBoundary
FallbackComponent={GatewayFieldsErrorFallback}
onReset={() => {
window.location.reload();
}}
>
<gateway.Fields />
</ErrorBoundary>
</div>
</li>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -64,7 +68,7 @@ const stripeGateway: StripeGateway = {
data: {
intentStatus: string;
returnUrl: string;
}
};
}): Promise<void> {
if (response.data.intentStatus === 'requires_payment_method') {
const {error: fetchUpdatesError} = await this.elements.fetchUpdates();
Expand Down Expand Up @@ -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 (
<Elements stripe={stripePromise} options={stripeElementOptions}>
<StripeFields gateway={stripeGateway} />
Expand Down

0 comments on commit 1bcc5b6

Please sign in to comment.