PluginProbe ʕ •ᴥ•ʔ
GiveWP – Donation Plugin and Fundraising Platform / 4.16.6.1
GiveWP – Donation Plugin and Fundraising Platform v4.16.6.1
4.16.7.1 4.16.7 4.16.6.1 4.16.6 4.16.5.1 4.16.5 4.16.4 4.16.3 4.16.2 4.16.1 4.16.0 4.15.5 4.15.4 4.15.3 4.15.2 4.15.1 4.15.0 2.3.0 2.3.1 2.3.2 2.30.0 2.31.0 2.31.1 2.32.0 2.33.0 2.33.1 2.33.2 2.33.3 2.33.4 2.33.5 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.4.7 2.5.0 2.5.1 2.5.10 2.5.11 2.5.12 2.5.13 2.5.2 2.5.3 2.5.4 2.5.5 2.5.6 2.5.7 2.5.8 2.5.9 2.6.0 2.6.1 2.6.2 2.6.3 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.8.0 2.8.1 2.9.0 2.9.1 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.10.0 3.11.0 3.12.0 3.12.1 3.12.2 3.12.3 3.13.0 3.14.0 3.14.1 3.14.2 3.15.0 3.15.1 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.16.5 3.17.0 3.17.1 3.17.2 3.18.0 3.19.0 3.19.1 3.19.2 3.19.3 3.19.4 3.2.0 3.2.1 3.2.2 3.20.0 3.21.0 3.21.1 3.22.0 3.22.1 3.22.2 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.6.0 3.6.1 3.6.2 3.7.0 3.8.0 3.9.0 4.0.0 4.1.0 4.1.1 4.10.0 4.10.1 4.11.0 4.12.0 4.13.0 4.13.1 4.13.2 4.14.0 4.14.1 4.14.2 4.14.3 4.14.4 4.14.5 4.14.6 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.1 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 trunk 1.9.0 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.10.0 2.10.1 2.10.2 2.10.3 2.10.4 2.11.0 2.11.1 2.11.2 2.11.3 2.12.0 2.12.1 2.12.2 2.12.3 2.13.0 2.13.1 2.13.2 2.13.3 2.13.4 2.14.0 2.15.0 2.16.0 2.16.1 2.17.0 2.17.1 2.17.3 2.18.0 2.18.1 2.19.1 2.19.2 2.19.3 2.19.4 2.19.5 2.19.6 2.19.7 2.19.8 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.20.0 2.20.1 2.20.2 2.21.0 2.21.1 2.21.2 2.21.3 2.21.4 2.22.0 2.22.1 2.22.2 2.22.3 2.23.0 2.23.1 2.23.2 2.24.0 2.24.1 2.24.2 2.25.0 2.25.1 2.25.2 2.25.3 2.26.0 2.27.0 2.27.1 2.27.2 2.27.3 2.28.0 2.29.0 2.29.1 2.29.2
give / src / PaymentGateways / Gateways / PayPalCommerce / payPalCommerceGateway.tsx
give / src / PaymentGateways / Gateways / PayPalCommerce Last commit date
resources 1 year ago PayPalCommerceGateway.php 1 year ago payPalCommerceGateway.tsx 1 year ago types.ts 1 year ago
payPalCommerceGateway.tsx
553 lines
1 import {
2 DISPATCH_ACTION,
3 PayPalButtons,
4 PayPalButtonsComponentProps,
5 PayPalCardFieldsForm,
6 PayPalCardFieldsProvider,
7 PayPalScriptProvider,
8 usePayPalCardFields,
9 usePayPalScriptReducer,
10 } from '@paypal/react-paypal-js';
11 import {__} from '@wordpress/i18n';
12 import {useEffect} from 'react';
13 import {PayPalCommerceGateway, PayPalSubscriber} from './types';
14 import handleValidationRequest from '@givewp/forms/app/utilities/handleValidationRequest';
15 import createOrder from './resources/js/createOrder';
16 import type {
17 CreateSubscriptionRequestBody,
18 PayPalButtonsComponentOptions,
19 PayPalCardFieldsComponent,
20 PayPalCardFieldsComponentBasics,
21 } from '@paypal/paypal-js';
22 import createSubscriptionPlan from './resources/js/createSubscriptionPlan';
23
24 (() => {
25 /**
26 * Hoisted values are used to pass data in contexts where hooks cannot be used.
27 * For example, the form uses hooks to get the values of the form fields,
28 * but the callbacks for PayPal cannot use hooks to access those values.
29 *
30 * The <FormFieldsProvider /> component is used to hoist the form field values
31 * which are then collected in createOrderHandler(). This callback is not
32 * a component, so it cannot use hooks to access the form field values.
33 */
34 let amount;
35 let firstName;
36 let lastName;
37 let email;
38 let payPalDonationsSettings;
39 let payPalOrderId;
40 let payPalSubscriptionId;
41
42 let subscriptionFrequency;
43 let subscriptionInstallments;
44 let subscriptionPeriod;
45
46 let country;
47 let state;
48 let city;
49 let addressLine1;
50 let addressLine2;
51 let postalCode;
52
53 let currency;
54 let submitButton;
55
56 let payPalCardFieldsForm: PayPalCardFieldsComponent = null;
57
58 /**
59 * @since 4.1.1 updated to reassign the submit button when not assigned yet
60 * @since 4.1.0
61 */
62 const showOrHideDonateButton = (showOrHide: 'show' | 'hide') => {
63 submitButton = submitButton || window.givewp.form.hooks.useFormSubmitButton();
64
65 if (submitButton) {
66 submitButton.style.display = showOrHide === 'hide' ? 'none' : '';
67 }
68 };
69
70 const buttonsStyle = {
71 color: 'gold' as 'gold' | 'blue' | 'silver' | 'white' | 'black',
72 label: 'paypal' as 'paypal' | 'checkout' | 'buynow' | 'pay' | 'installment' | 'subscribe' | 'donate',
73 layout: 'vertical' as 'vertical' | 'horizontal',
74 shape: 'rect' as 'rect' | 'pill',
75 tagline: false,
76 };
77
78 /**
79 * Get PayPal script options.
80 *
81 * This function return the paypal script options on basis of context.
82 * - If donation type is subscription then remove hosted fields from components.
83 *
84 * @return {object} PayPal script options.
85 */
86 const getPayPalScriptOptions = ({isSubscription}) => {
87 let paypalScriptOptions = {...payPalDonationsSettings.sdkOptions};
88
89 // Remove hosted fields from components if subscription.
90 if (isSubscription && -1 !== paypalScriptOptions.components.indexOf('card-fields')) {
91 paypalScriptOptions.components = paypalScriptOptions.components
92 .split(',')
93 .filter((component) => component !== 'card-fields')
94 .join(',');
95 }
96
97 return paypalScriptOptions;
98 };
99
100 const getFormData = () => {
101 const formData = new FormData();
102
103 formData.append('give-form-id', payPalDonationsSettings.donationFormId);
104 formData.append('give-form-hash', payPalDonationsSettings.donationFormNonce);
105
106 formData.append('give_payment_mode', 'paypal-commerce');
107
108 formData.append('give-amount', amount.toString());
109
110 formData.append('give-recurring-period', subscriptionPeriod);
111 formData.append('period', subscriptionPeriod);
112 formData.append('frequency', subscriptionFrequency);
113 formData.append('times', subscriptionInstallments);
114
115 formData.append('give_first', firstName);
116 formData.append('give_last', lastName);
117 formData.append('give_email', email);
118
119 if (country && country.length === 2) {
120 formData.append('card_address', addressLine1);
121 formData.append('card_address_2', addressLine2);
122 formData.append('card_city', city);
123 formData.append('card_state', state);
124 formData.append('card_zip', postalCode);
125 formData.append('billing_country', country);
126 }
127
128 /**
129 * Ensure the proper currency will be used when using the Currency Switcher add-on.
130 */
131 formData.append('give-cs-form-currency', currency);
132
133 return formData;
134 };
135
136 const smartButtonsCreateOrderHandler: PayPalButtonsComponentOptions['createOrder'] = async (): Promise<string> => {
137 return await createOrder(
138 `${payPalDonationsSettings.ajaxUrl}?action=give_paypal_commerce_create_order`,
139 payPalCommerceGateway,
140 getFormData()
141 );
142 };
143
144 /**
145 * @since 4.1.0
146 */
147 const cardFieldsOnErrorHandler: PayPalCardFieldsComponentBasics['onError'] = (error) => {
148 console.error('PayPal Card Fields Error:', error);
149
150 throw new Error(__('PayPal Card Fields Error:', 'give') + error.message);
151 };
152
153 /**
154 * @since 4.1.0
155 */
156 const cardFieldsCreateOrderHandler = async () => {
157 return await createOrder(
158 `${payPalDonationsSettings.ajaxUrl}?action=give_paypal_commerce_create_order`,
159 payPalCommerceGateway,
160 getFormData()
161 );
162 };
163
164 /**
165 * @since 4.1.0
166 */
167 const cardFieldsOnApproveHandler: PayPalCardFieldsComponentBasics['onApprove'] = async (data) => {
168 // @ts-ignore
169 const {orderID, liabilityShift} = data;
170 payPalOrderId = orderID;
171
172 if (liabilityShift && !['POSSIBLE', 'YES'].includes(liabilityShift)) {
173 console.log('Liability shift not possible or not accepted.');
174 throw new Error(
175 __('Card type and issuing bank are not ready to complete a 3D Secure authentication.', 'give')
176 );
177 }
178
179 return;
180 };
181
182 const smartButtonsCreateSubscriptionHandler: PayPalButtonsComponentOptions['createSubscription'] = async (
183 data,
184 actions
185 ) => {
186 const {planId, userAction} = await createSubscriptionPlan(
187 `${payPalDonationsSettings.ajaxUrl}?action=give_paypal_commerce_create_plan_id`,
188 payPalCommerceGateway,
189 getFormData()
190 );
191
192 const subscriberData: PayPalSubscriber = {
193 name: {
194 given_name: firstName,
195 surname: lastName,
196 },
197 email_address: email,
198 };
199
200 if (country) {
201 subscriberData.shipping_address = {
202 name: {
203 full_name: `${firstName} ${lastName}`.trim(),
204 },
205 address: {
206 address_line_1: addressLine1,
207 address_line_2: addressLine2,
208 admin_area_2: city,
209 admin_area_1: state,
210 postal_code: postalCode,
211 country_code: country,
212 },
213 };
214 }
215
216 const createSubscriptionPayload: CreateSubscriptionRequestBody = {
217 plan_id: planId,
218 subscriber: subscriberData,
219 };
220
221 if (userAction) {
222 createSubscriptionPayload.application_context = {
223 ...createSubscriptionPayload.application_context,
224 user_action: userAction as 'CONTINUE' | 'SUBSCRIBE_NOW',
225 };
226 }
227
228 return actions.subscription.create(createSubscriptionPayload).then((orderId) => {
229 return (payPalSubscriptionId = orderId);
230 });
231 };
232
233 const Divider = ({label, style = {}}) => {
234 const styles = {
235 container: {
236 fontSize: '16px',
237 fontStyle: 'italic',
238 display: 'flex',
239 justifyContent: 'center',
240 alignItems: 'center',
241 ...style,
242 },
243 dashedLine: {
244 border: '1px solid #d4d4d4',
245 flexGrow: 1,
246 },
247 label: {
248 padding: '0 6px',
249 fontSize: '14px',
250 color: '#8d8e8e',
251 },
252 };
253
254 return (
255 <div className="separator-with-text" style={styles.container}>
256 <div className="dashed-line" style={styles.dashedLine} />
257 <div className="label" style={styles.label}>
258 {label}
259 </div>
260 <div className="dashed-line" style={styles.dashedLine} />
261 </div>
262 );
263 };
264
265 const FormFieldsProvider = ({children}) => {
266 const formData = window.givewp.form.hooks.useFormData();
267
268 amount = formData.amount;
269 firstName = formData.firstName;
270 lastName = formData.lastName;
271 email = formData.email;
272
273 subscriptionFrequency = formData.subscriptionFrequency;
274 subscriptionInstallments = formData.subscriptionInstallments;
275 subscriptionPeriod = formData.subscriptionPeriod;
276
277 addressLine1 = formData.billingAddress.addressLine1;
278 addressLine2 = formData.billingAddress.addressLine2;
279 city = formData.billingAddress.city;
280 state = formData.billingAddress.state;
281 postalCode = formData.billingAddress.postalCode;
282 country = formData.billingAddress.country;
283
284 currency = formData.currency;
285
286 return children;
287 };
288
289 const SmartButtonsContainer = () => {
290 const {useWatch, useFormState} = window.givewp.form.hooks;
291 const donationType = useWatch({name: 'donationType'});
292 const {isSubmitting, isSubmitSuccessful} = useFormState();
293 const {useFormContext} = window.givewp.form.hooks;
294 const {
295 getFieldState,
296 setFocus,
297 getValues,
298 trigger,
299 setError,
300 } = useFormContext();
301 const gateway = window.givewp.gateways.get('paypal-commerce') as PayPalCommerceGateway;
302
303 const props: PayPalButtonsComponentProps = {
304 style: buttonsStyle,
305 disabled: isSubmitting || isSubmitSuccessful,
306 forceReRender: [donationType, amount, firstName, lastName, email, currency],
307 onClick: async (data, actions) => {
308 // Validate whether payment gateway support subscriptions.
309 if (donationType === 'subscription' && !gateway.supportsSubscriptions) {
310 setError(
311 'FORM_ERROR',
312 {
313 message: __(
314 'This payment gateway does not support recurring payments, please try selecting another payment gateway.',
315 'give'
316 ),
317 },
318 {shouldFocus: true}
319 );
320
321 // Scroll to the top of the form.
322 // Add this moment we do not have a way to scroll to the error message.
323 // In the future we can add a way to scroll to the error message and remove this code.
324 submitButton.scrollIntoView({behavior: 'smooth'});
325
326 return actions.reject();
327 }
328
329 // Validate the form values in the client side before proceeding.
330 const isClientValidationSuccessful = await trigger();
331 if (!isClientValidationSuccessful) {
332 // Set focus on first invalid field.
333 for (const fieldName in getValues()) {
334 if (getFieldState(fieldName).invalid) {
335 setFocus(fieldName);
336 }
337 }
338 return actions.reject();
339 }
340
341 /**
342 * Validate the form values in the server side before proceeding - this is important to prevent problems with some blocks.
343 *
344 * Ideally, the client-side validations should be enough. However, in some cases, these validations are reached
345 * later when the donation is already created on the PayPal side. This way, we need the request below to check
346 * it earlier and prevent the donation creation on the PayPal side if the required fields are missing.
347 *
348 * Know cases:
349 *
350 * #1 - Billing Address Block: depending on the selected country, the city, state, and zip fields
351 * can be required or not and there are custom validation rules on the server side that check it.
352 *
353 * #2 - Gift Aid Block: when users opt-in to the gift aid checkbox, it will display some
354 * required fields that should be filled, but as this block is not a group and even so has
355 * "children" fields, the validation rules for it live only on the server.
356 */
357 const isServerValidationSuccessful = await handleValidationRequest(
358 payPalDonationsSettings.validateUrl,
359 getValues(),
360 setError,
361 gateway
362 );
363
364 if (!isServerValidationSuccessful) {
365 return actions.reject();
366 }
367
368 return actions.resolve();
369 },
370 onApprove: async (data, actions) => {
371 const orderId = data.orderID;
372 const subscriptionId = data?.subscriptionID;
373
374 const submitButtonDefaultText = submitButton.textContent;
375 submitButton.textContent = __('Waiting for PayPal...', 'give');
376 submitButton.disabled = true;
377
378 if (subscriptionId && donationType === 'subscription') {
379 payPalSubscriptionId = subscriptionId;
380
381 submitButton.disabled = false;
382 submitButton.textContent = submitButtonDefaultText;
383 submitButton.click();
384 return;
385 }
386
387 if (orderId) {
388 payPalOrderId = orderId;
389 }
390
391 submitButton.disabled = false;
392 submitButton.textContent = submitButtonDefaultText;
393 submitButton.click();
394 return;
395 },
396 };
397
398 return donationType === 'subscription' ? (
399 <PayPalButtons {...props} createSubscription={smartButtonsCreateSubscriptionHandler} createOrder={null} />
400 ) : (
401 <PayPalButtons {...props} createOrder={smartButtonsCreateOrderHandler} createSubscription={null} />
402 );
403 };
404
405 /**
406 * @since 4.1.0
407 */
408 const PayPalGatewayCardFieldsForm = () => {
409 const {cardFieldsForm} = usePayPalCardFields();
410 payPalCardFieldsForm = cardFieldsForm;
411 payPalCommerceGateway.payPalCardFieldsForm = cardFieldsForm;
412
413 return <PayPalCardFieldsForm />;
414 };
415
416 const CardFieldsContainer = () => {
417 showOrHideDonateButton('show');
418
419 return (
420 <PayPalCardFieldsProvider
421 createOrder={cardFieldsCreateOrderHandler}
422 onApprove={cardFieldsOnApproveHandler}
423 onError={cardFieldsOnErrorHandler}
424 >
425 <>
426 <Divider label={__('Or pay with card', 'give')} style={{padding: '30px 0'}} />
427 <PayPalGatewayCardFieldsForm />
428 </>
429 </PayPalCardFieldsProvider>
430 );
431 };
432
433 function PaymentMethodsWrapper() {
434 const {isRecurring} = window.givewp.form.hooks.useFormData();
435
436 const [{options}, dispatch] = usePayPalScriptReducer();
437 const shouldShowCardFields = -1 !== options.components.indexOf('card-fields');
438
439 useEffect(() => {
440 const options = getPayPalScriptOptions({isSubscription: isRecurring});
441
442 dispatch({
443 type: DISPATCH_ACTION.RESET_OPTIONS,
444 value: {
445 ...options,
446 currency: currency,
447 vault: isRecurring,
448 intent: isRecurring ? 'subscription' : options.intent,
449 },
450 });
451 }, [currency, isRecurring]);
452
453 useEffect(() => {
454 // hide donate buttons if card fields are not expected to be shown
455 if (!shouldShowCardFields) {
456 showOrHideDonateButton('hide');
457 }
458
459 return () => {
460 showOrHideDonateButton('show');
461 };
462 }, [shouldShowCardFields]);
463
464 return (
465 <>
466 <SmartButtonsContainer />
467 {shouldShowCardFields && <CardFieldsContainer />}
468 </>
469 );
470 }
471
472 const payPalCommerceGateway: PayPalCommerceGateway = {
473 id: 'paypal-commerce',
474 initialize() {
475 payPalDonationsSettings = this.settings;
476 },
477
478 /**
479 * Before create payment.
480 * @since 3.2.0 Handle error response in approveOrderCallback.
481 * @param {Object} values
482 */
483 beforeCreatePayment: async function (values): Promise<object> {
484 if (payPalSubscriptionId) {
485 return {
486 payPalSubscriptionId: payPalSubscriptionId,
487 };
488 }
489
490 if (payPalOrderId) {
491 // If order ID already set by payment buttons then return early.
492 return {
493 payPalOrderId: payPalOrderId,
494 };
495 }
496
497 if (!payPalCardFieldsForm) {
498 throw new Error(__('PayPal Card Fields are not available.', 'give'));
499 }
500
501 const cardFormState = await payPalCardFieldsForm.getState();
502
503 if (!cardFormState.isFormValid) {
504 throw new Error(__('PayPal Card Fields are invalid', 'give'));
505 }
506
507 const submitButtonDefaultText = submitButton.textContent;
508 submitButton.textContent = __('Waiting for PayPal...', 'give');
509 submitButton.disabled = true;
510
511 await payPalCardFieldsForm.submit();
512
513 submitButton.textContent = submitButtonDefaultText;
514
515 if (!payPalOrderId) {
516 submitButton.disabled = false;
517
518 throw new Error(__('Missing PayPal Order ID.', 'give'));
519 }
520
521 return {
522 payPalOrderId,
523 };
524 },
525
526 /**
527 * @since 4.1.1 updated the submit button to assign on mount
528 * @since 4.1.0 updated to use card fields api
529 * @since 3.17.1 Hide submit button when PayPal Commerce is selected.
530 */
531 Fields() {
532 const {isRecurring} = window.givewp.form.hooks.useFormData();
533
534 useEffect(() => {
535 submitButton = window.givewp.form.hooks.useFormSubmitButton();
536 }, []);
537
538 return (
539 <FormFieldsProvider>
540 <PayPalScriptProvider
541 deferLoading={true}
542 options={getPayPalScriptOptions({isSubscription: isRecurring})}
543 >
544 <PaymentMethodsWrapper />
545 </PayPalScriptProvider>
546 </FormFieldsProvider>
547 );
548 },
549 };
550
551 window.givewp.gateways.register(payPalCommerceGateway);
552 })();
553