Amount
1 month ago
Consent
1 year ago
utils
1 year ago
Checkbox.tsx
1 year ago
Date.tsx
1 year ago
Email.tsx
1 year ago
File.tsx
1 year ago
Gateways.tsx
1 year ago
Hidden.tsx
2 years ago
Honeypot.tsx
1 year ago
MultiSelect.tsx
2 years ago
Password.tsx
2 years ago
Phone.tsx
1 year ago
Radio.tsx
2 years ago
Select.tsx
1 year ago
Text.tsx
1 year ago
TextArea.tsx
2 years ago
Url.tsx
2 years ago
Gateways.tsx
317 lines
| 1 | import {ErrorMessage} from '@hookform/error-message'; |
| 2 | import type {GatewayFieldProps, GatewayOptionProps} from '@givewp/forms/propTypes'; |
| 3 | import {ErrorBoundary} from 'react-error-boundary'; |
| 4 | import {__, sprintf} from '@wordpress/i18n'; |
| 5 | import {createInterpolateElement, useEffect, useMemo} from '@wordpress/element'; |
| 6 | import cx from 'classnames'; |
| 7 | import {isDonationTypeSubscription} from '@givewp/forms/types'; |
| 8 | |
| 9 | interface EmptyMessageProps { |
| 10 | message: string; |
| 11 | } |
| 12 | |
| 13 | interface GatewayMissingMessageProps { |
| 14 | donationAmountMinimumNotReached?: boolean; |
| 15 | currencyNotSupported?: boolean; |
| 16 | subscriptionNotSupported?: boolean; |
| 17 | } |
| 18 | |
| 19 | interface GatewayFieldsErrorFallbackProps { |
| 20 | error: Error; |
| 21 | resetErrorBoundary: () => void; |
| 22 | } |
| 23 | |
| 24 | /** |
| 25 | * Empty message component displayed when no payment gateways are available. |
| 26 | * |
| 27 | * @since 3.20.0 |
| 28 | */ |
| 29 | function EmptyMessage({message}: EmptyMessageProps) { |
| 30 | return ( |
| 31 | <div className="givewp-fields-gateways__gateway--empty"> |
| 32 | <p> |
| 33 | <b>{__('Payment options are not available:', 'give')}</b> |
| 34 | </p> |
| 35 | |
| 36 | <p> |
| 37 | <em>{message}</em> |
| 38 | </p> |
| 39 | </div> |
| 40 | ); |
| 41 | } |
| 42 | |
| 43 | /** |
| 44 | * Component that displays contextual messages when no gateways are available. |
| 45 | * Handles different scenarios like minimum donation amount, currency support, and subscription support. |
| 46 | * |
| 47 | * @since 3.20.0 updated message to account for minimum donation amount |
| 48 | * @since 3.0.0 |
| 49 | */ |
| 50 | function GatewayMissingMessage({ |
| 51 | donationAmountMinimumNotReached, |
| 52 | currencyNotSupported, |
| 53 | subscriptionNotSupported, |
| 54 | }: GatewayMissingMessageProps) { |
| 55 | let message = __( |
| 56 | 'No gateways have been enabled yet. To get started accepting donations, enable a compatible payment gateway in your settings.', |
| 57 | 'give' |
| 58 | ); |
| 59 | |
| 60 | if (donationAmountMinimumNotReached) { |
| 61 | message = __('Donation amount must be greater than zero.', 'give'); |
| 62 | } else if (currencyNotSupported) { |
| 63 | message = __( |
| 64 | 'The selected currency is not supported by any of the available payment gateways. Please select a different currency or contact the site administrator for assistance.', |
| 65 | 'give' |
| 66 | ); |
| 67 | } else if (subscriptionNotSupported) { |
| 68 | message = __( |
| 69 | 'No gateways support recurring payments. Please select a different payment gateway or contact the site administrator for assistance.', |
| 70 | 'give' |
| 71 | ); |
| 72 | } |
| 73 | |
| 74 | return <EmptyMessage message={message} />; |
| 75 | } |
| 76 | |
| 77 | /** |
| 78 | * Error fallback component for gateway fields that fail to render. |
| 79 | * Provides user-friendly error message and reload functionality. |
| 80 | * |
| 81 | * @since 3.0.0 |
| 82 | */ |
| 83 | function GatewayFieldsErrorFallback({error, resetErrorBoundary}: GatewayFieldsErrorFallbackProps) { |
| 84 | return ( |
| 85 | <div role="alert"> |
| 86 | <p> |
| 87 | {__( |
| 88 | 'An error occurred while loading the gateway fields. Please notify the site administrator. The error message is:', |
| 89 | 'give' |
| 90 | )} |
| 91 | </p> |
| 92 | <pre style={{padding: '0.5rem'}}>{error.message}</pre> |
| 93 | <button type="button" onClick={resetErrorBoundary}> |
| 94 | {__('Reload form', 'give')} |
| 95 | </button> |
| 96 | </div> |
| 97 | ); |
| 98 | } |
| 99 | |
| 100 | /** |
| 101 | * Notice component displayed when the donation form is in test mode. |
| 102 | * Informs users that no live donations will be processed. |
| 103 | * |
| 104 | * @since 3.0.0 |
| 105 | */ |
| 106 | const TestModeNotice = () => { |
| 107 | return ( |
| 108 | <div className="givewp-test-mode-notice"> |
| 109 | <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"> |
| 110 | <path |
| 111 | fillRule="evenodd" |
| 112 | clipRule="evenodd" |
| 113 | d="M12 1C5.92487 1 1 5.92487 1 12C1 18.0751 5.92487 23 12 23C18.0751 23 23 18.0751 23 12C23 5.92487 18.0751 1 12 1ZM12 7C11.4477 7 11 7.44772 11 8C11 8.55228 11.4477 9 12 9H12.01C12.5623 9 13.01 8.55228 13.01 8C13.01 7.44772 12.5623 7 12.01 7H12ZM13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12V16C11 16.5523 11.4477 17 12 17C12.5523 17 13 16.5523 13 16V12Z" |
| 114 | fill="#F29718" |
| 115 | /> |
| 116 | </svg>{' '} |
| 117 | <p> |
| 118 | {createInterpolateElement( |
| 119 | __( |
| 120 | 'Test mode is <strong>enabled</strong>. While in test mode no live donations are processed.', |
| 121 | 'give' |
| 122 | ), |
| 123 | { |
| 124 | strong: <strong />, |
| 125 | } |
| 126 | )} |
| 127 | </p> |
| 128 | </div> |
| 129 | ); |
| 130 | }; |
| 131 | |
| 132 | /** |
| 133 | * Main Gateways component that handles payment gateway selection and display. |
| 134 | * |
| 135 | * This component: |
| 136 | * - Filters available gateways based on donation amount, currency, and subscription support |
| 137 | * - Handles currency switcher settings to show only compatible gateways |
| 138 | * - Automatically selects appropriate default gateway when options change |
| 139 | * - Displays contextual messages when no gateways are available |
| 140 | * - Shows test mode notice when applicable |
| 141 | * |
| 142 | * @since 4.4.0 filter gateways based on donation type |
| 143 | * @since 3.0.0 |
| 144 | */ |
| 145 | export default function Gateways({isTestMode, defaultValue, inputProps, gateways}: GatewayFieldProps) { |
| 146 | const {useFormState, useWatch, useFormContext, useDonationFormSettings} = window.givewp.form.hooks; |
| 147 | const {errors} = useFormState(); |
| 148 | const {setValue} = useFormContext(); |
| 149 | const {currencySwitcherSettings} = useDonationFormSettings(); |
| 150 | |
| 151 | // Watch form values that affect gateway availability |
| 152 | const donationAmount = useWatch({name: 'amount'}); |
| 153 | const currency = useWatch({name: 'currency'}); |
| 154 | const activeGatewayId = useWatch({name: 'gatewayId'}); |
| 155 | const donationType = useWatch({name: 'donationType'}); |
| 156 | const isSubscription = isDonationTypeSubscription(donationType); |
| 157 | |
| 158 | const donationAmountMinimumNotReached = donationAmount === 0; |
| 159 | |
| 160 | /** |
| 161 | * Filter gateway options based on currency switcher settings. |
| 162 | * If currency switcher is enabled, only show gateways that support the selected currency. |
| 163 | */ |
| 164 | const gatewayOptionsWithCurrencySettings = useMemo(() => { |
| 165 | if (currencySwitcherSettings.length <= 1) { |
| 166 | return gateways; |
| 167 | } |
| 168 | |
| 169 | const currencySwitcherSetting = currencySwitcherSettings.find(({id}) => id === currency); |
| 170 | |
| 171 | if (!currencySwitcherSetting) { |
| 172 | return []; |
| 173 | } |
| 174 | |
| 175 | return gateways.filter(({id}) => currencySwitcherSetting.gateways.includes(id)); |
| 176 | }, [currency]); |
| 177 | |
| 178 | /** |
| 179 | * Filter gateways that support subscription/recurring donations. |
| 180 | */ |
| 181 | const gatewayOptionsWithSubscriptionSupport = useMemo(() => { |
| 182 | return gatewayOptionsWithCurrencySettings.filter(({supportsSubscriptions}) => supportsSubscriptions); |
| 183 | }, [gatewayOptionsWithCurrencySettings]); |
| 184 | |
| 185 | /** |
| 186 | * Final filtered gateway options based on all criteria: |
| 187 | * - Donation amount must be greater than zero |
| 188 | * - For subscriptions, gateway must support recurring payments |
| 189 | * - Gateway must support the selected currency |
| 190 | */ |
| 191 | const gatewayOptions = useMemo(() => { |
| 192 | if (donationAmountMinimumNotReached) { |
| 193 | return []; |
| 194 | } |
| 195 | |
| 196 | if (isSubscription) { |
| 197 | return gatewayOptionsWithSubscriptionSupport.length > 0 ? gatewayOptionsWithSubscriptionSupport : []; |
| 198 | } |
| 199 | |
| 200 | return gatewayOptionsWithCurrencySettings.length > 0 ? gatewayOptionsWithCurrencySettings : []; |
| 201 | }, [ |
| 202 | donationAmountMinimumNotReached, |
| 203 | gatewayOptionsWithSubscriptionSupport, |
| 204 | gatewayOptionsWithCurrencySettings, |
| 205 | isSubscription, |
| 206 | ]); |
| 207 | |
| 208 | /** |
| 209 | * Automatically set the selected gateway when available options change. |
| 210 | * - If default gateway is still available, keep it selected |
| 211 | * - Otherwise, select the first available gateway |
| 212 | * - If no gateways available, clear the selection |
| 213 | */ |
| 214 | useEffect(() => { |
| 215 | if (gatewayOptions.length > 0) { |
| 216 | const optionsDefaultValue = gatewayOptions.find(option => option.id === defaultValue) |
| 217 | ? defaultValue |
| 218 | : gatewayOptions[0].id; |
| 219 | |
| 220 | setValue(inputProps.name, optionsDefaultValue); |
| 221 | } else { |
| 222 | setValue(inputProps.name, null); |
| 223 | } |
| 224 | }, [gatewayOptions]); |
| 225 | |
| 226 | return ( |
| 227 | <> |
| 228 | {gatewayOptions.length > 0 ? ( |
| 229 | <> |
| 230 | {isTestMode && <TestModeNotice />} |
| 231 | <ul className="givewp-fields-gateways__list" style={{listStyleType: 'none', padding: 0}}> |
| 232 | {gatewayOptions.map((gateway) => ( |
| 233 | <GatewayOption |
| 234 | gateway={gateway} |
| 235 | defaultChecked={gateway.id === defaultValue} |
| 236 | key={gateway.id} |
| 237 | inputProps={inputProps} |
| 238 | isActive={gateway.id === activeGatewayId} |
| 239 | /> |
| 240 | ))} |
| 241 | </ul> |
| 242 | </> |
| 243 | ) : ( |
| 244 | <GatewayMissingMessage |
| 245 | donationAmountMinimumNotReached={donationAmountMinimumNotReached} |
| 246 | currencyNotSupported={gatewayOptionsWithCurrencySettings.length === 0 && !donationAmountMinimumNotReached} |
| 247 | subscriptionNotSupported={isSubscription && gatewayOptionsWithSubscriptionSupport.length === 0} |
| 248 | /> |
| 249 | )} |
| 250 | |
| 251 | <ErrorMessage |
| 252 | errors={errors} |
| 253 | name={'gatewayId'} |
| 254 | render={({message}) => <span className="give-next-gen__error-message">{message}</span>} |
| 255 | /> |
| 256 | </> |
| 257 | ); |
| 258 | } |
| 259 | |
| 260 | /** |
| 261 | * Individual gateway option component that renders a radio button with gateway information. |
| 262 | * |
| 263 | * Features: |
| 264 | * - Radio button input for gateway selection |
| 265 | * - Gateway-specific styling and icons |
| 266 | * - Dynamic icon selection based on gateway type |
| 267 | * - Error boundary for gateway-specific fields |
| 268 | * - Conditional rendering of gateway fields when active |
| 269 | * |
| 270 | * @since 3.0.0 |
| 271 | */ |
| 272 | function GatewayOption({gateway, defaultChecked, inputProps, isActive}: GatewayOptionProps) { |
| 273 | const gatewayClass = `givewp-fields-gateways__gateway--${gateway.id}`; |
| 274 | const className = cx('givewp-fields-gateways__gateway', { |
| 275 | [gatewayClass]: true, |
| 276 | 'givewp-fields-gateways__gateway--active': isActive, |
| 277 | }); |
| 278 | |
| 279 | // Determine appropriate icon based on gateway type |
| 280 | let fontAwesomeClass = 'fa-solid fa-gear'; |
| 281 | if (gateway.id.includes('stripe') || gateway.id.includes('card')) { |
| 282 | fontAwesomeClass = 'fa-solid fa-credit-card'; |
| 283 | } else if (gateway.id.includes('paypal')) { |
| 284 | fontAwesomeClass = 'fa-brands fa-paypal'; |
| 285 | } else if (gateway.id.includes('test')) { |
| 286 | fontAwesomeClass = 'fa-solid fa-screwdriver-wrench'; |
| 287 | } |
| 288 | |
| 289 | return ( |
| 290 | <li className={className}> |
| 291 | <label> |
| 292 | <input |
| 293 | type="radio" |
| 294 | value={gateway.id} |
| 295 | id={gateway.id} |
| 296 | defaultChecked={defaultChecked} |
| 297 | {...inputProps} |
| 298 | /> |
| 299 | <span className="givewp-fields-gateways__gateway__label"> |
| 300 | {sprintf(__('Donate with %s', 'give'), gateway.label)} |
| 301 | </span> |
| 302 | <i className={`givewp-fields-gateways__gateway__icon ${fontAwesomeClass}`}></i> |
| 303 | </label> |
| 304 | <div className="givewp-fields-gateways__gateway__fields"> |
| 305 | <ErrorBoundary |
| 306 | FallbackComponent={GatewayFieldsErrorFallback} |
| 307 | onReset={() => { |
| 308 | window.location.reload(); |
| 309 | }} |
| 310 | > |
| 311 | {isActive && <gateway.Fields />} |
| 312 | </ErrorBoundary> |
| 313 | </div> |
| 314 | </li> |
| 315 | ); |
| 316 | } |
| 317 |