PluginProbe ʕ •ᴥ•ʔ
GiveWP – Donation Plugin and Fundraising Platform / 3.11.0
GiveWP – Donation Plugin and Fundraising Platform v3.11.0
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
PayPalCommerceGateway.php 2 years ago payPalCommerceGateway.tsx 2 years ago types.ts 2 years ago
payPalCommerceGateway.tsx
584 lines
1 import {
2 PayPalButtons,
3 PayPalHostedField,
4 PayPalHostedFieldsProvider,
5 PayPalScriptProvider,
6 usePayPalHostedFields,
7 usePayPalScriptReducer,
8 } from '@paypal/react-paypal-js';
9 import type {Gateway} from '@givewp/forms/types';
10 import {__, sprintf} from '@wordpress/i18n';
11 import {debounce} from 'react-ace/lib/editorOptions';
12 import {Flex, TextControl} from '@wordpress/components';
13 import {CSSProperties, useEffect, useState} from 'react';
14 import {PayPalSubscriber} from './types';
15
16 (() => {
17 /**
18 * Hoisted values are used to pass data in contexts where hooks cannot be used.
19 * For example, the form uses hooks to get the values of the form fields,
20 * but the callbacks for PayPal cannot use hooks to access those values.
21 *
22 * The <FormFieldsProvider /> component is used to hoist the form field values
23 * which are then collected in createOrderHandler(). This callback is not
24 * a component, so it cannot use hooks to access the form field values.
25 */
26 let amount;
27 let feeRecovery;
28 let firstName;
29 let lastName;
30 let email;
31 let cardholderName;
32 let hostedField;
33 let payPalDonationsSettings;
34 let payPalOrderId;
35 let payPalSubscriptionId;
36
37 let subscriptionFrequency;
38 let subscriptionInstallments;
39 let subscriptionPeriod;
40
41 let country;
42 let state;
43 let city;
44 let addressLine1;
45 let addressLine2;
46 let postalCode;
47
48 let updateOrderAmount = false;
49 let orderCreated = false;
50
51 let currency;
52
53 const buttonsStyle = {
54 color: 'gold' as 'gold' | 'blue' | 'silver' | 'white' | 'black',
55 label: 'paypal' as 'paypal' | 'checkout' | 'buynow' | 'pay' | 'installment' | 'subscribe' | 'donate',
56 layout: 'vertical' as 'vertical' | 'horizontal',
57 shape: 'rect' as 'rect' | 'pill',
58 tagline: false,
59 };
60
61 const CUSTOM_FIELD_STYLE = {
62 height: '50px', // @todo Magic number, but it works
63 borderWidth: '.078rem',
64 borderStyle: 'solid',
65 borderColor: '#666',
66 borderRadius: '.25rem',
67 padding: '0 1.1875rem',
68 width: '100%',
69 marginBottom: '.5rem',
70 boxSizing: 'inherit',
71 inlineSize: '100%',
72 backgroundColor: '#fff',
73 color: '#4d4d4d',
74 fontSize: '1rem',
75 fontFamily: 'inherit',
76 fontWeight: '500',
77 lineHeight: '1.2',
78 } as CSSProperties;
79
80 /**
81 * Get PayPal script options.
82 *
83 * This function return the paypal script options on basis of context.
84 * - If donation type is subscription then remove hosted fields from components.
85 *
86 * @return {object} PayPal script options.
87 */
88 const getPayPalScriptOptions = ({isSubscription}) => {
89 let paypalScriptOptions = {...payPalDonationsSettings.sdkOptions};
90
91 // Remove hosted fields from components if subscription.
92 if (isSubscription && -1 !== paypalScriptOptions.components.indexOf('hosted-fields')) {
93 paypalScriptOptions.components = paypalScriptOptions.components
94 .split(',')
95 .filter((component) => component !== 'hosted-fields')
96 .join(',');
97 }
98
99 return paypalScriptOptions;
100 };
101
102 /**
103 * Get amount with fee (if any).
104 *
105 * @since 3.6.1 Append 'give-cs-form-currency' to formData
106 * @since 3.2.0
107 * @return {number} Amount with fee.
108 */
109 const getAmount = () => {
110 const feeAmount = feeRecovery ? feeRecovery : 0;
111 let amountWithFee = amount + feeAmount;
112 amountWithFee = Math.round(amountWithFee * 100) / 100;
113
114 return amountWithFee;
115 };
116
117 const getFormData = () => {
118 const formData = new FormData();
119
120 formData.append('give-form-id', payPalDonationsSettings.donationFormId);
121 formData.append('give-form-hash', payPalDonationsSettings.donationFormNonce);
122
123 formData.append('give_payment_mode', 'paypal-commerce');
124
125 formData.append('give-amount', getAmount());
126
127 formData.append('give-recurring-period', subscriptionPeriod);
128 formData.append('period', subscriptionPeriod);
129 formData.append('frequency', subscriptionFrequency);
130 formData.append('times', subscriptionInstallments);
131
132 formData.append('give_first', firstName);
133 formData.append('give_last', lastName);
134 formData.append('give_email', email);
135
136 if (country) {
137 formData.append('card_address', addressLine1);
138 formData.append('card_address_2', addressLine2);
139 formData.append('card_city', city);
140 formData.append('card_state', state);
141 formData.append('card_zip', postalCode);
142 formData.append('billing_country', country);
143 }
144
145 /**
146 * Ensure the proper currency will be used when using the Currency Switcher add-on.
147 */
148 formData.append('give-cs-form-currency', currency);
149
150 return formData;
151 };
152
153 const validateHostedFields = () => {
154 return Object.values(hostedField.cardFields.getState().fields).some(
155 (field: {isValid: boolean}) => field.isValid
156 );
157 };
158
159 const createOrderHandler = async (): Promise<string> => {
160 const response = await fetch(`${payPalDonationsSettings.ajaxUrl}?action=give_paypal_commerce_create_order`, {
161 method: 'POST',
162 body: getFormData(),
163 });
164 const responseJson = await response.json();
165
166 if (!responseJson.success) {
167 throw responseJson.data.error;
168 }
169
170 return (payPalOrderId = responseJson.data.id);
171 };
172
173 const createSubscriptionHandler = async (data, actions) => {
174 // eslint-disable-next-line
175 const response = await fetch(`${payPalDonationsSettings.ajaxUrl}?action=give_paypal_commerce_create_plan_id`, {
176 method: 'POST',
177 body: getFormData(),
178 });
179
180 const responseJson = await response.json();
181
182 if (!responseJson.success) {
183 throw responseJson.data.error;
184 }
185
186 const subscriberData: PayPalSubscriber = {
187 name: {
188 given_name: firstName,
189 surname: lastName,
190 },
191 email_address: email,
192 };
193
194 if (country) {
195 subscriberData.shipping_address = {
196 name: {
197 full_name: `${firstName} ${lastName}`.trim(),
198 },
199 address: {
200 address_line_1: addressLine1,
201 address_line_2: addressLine2,
202 admin_area_2: city,
203 admin_area_1: state,
204 postal_code: postalCode,
205 country_code: country,
206 },
207 };
208 }
209
210 return actions.subscription
211 .create({
212 plan_id: responseJson.data.id,
213 subscriber: subscriberData,
214 })
215 .then((orderId) => {
216 return (payPalSubscriptionId = orderId);
217 });
218 };
219
220 const Divider = ({label, style = {}}) => {
221 const styles = {
222 container: {
223 fontSize: '16px',
224 fontStyle: 'italic',
225 display: 'flex',
226 justifyContent: 'center',
227 alignItems: 'center',
228 ...style,
229 },
230 dashedLine: {
231 border: '1px solid #d4d4d4',
232 flexGrow: 1,
233 },
234 label: {
235 padding: '0 6px',
236 fontSize: '14px',
237 color: '#8d8e8e',
238 },
239 };
240
241 return (
242 <div className="separator-with-text" style={styles.container}>
243 <div className="dashed-line" style={styles.dashedLine} />
244 <div className="label" style={styles.label}>
245 {label}
246 </div>
247 <div className="dashed-line" style={styles.dashedLine} />
248 </div>
249 );
250 };
251
252 const HoistHostedFieldContext = () => {
253 hostedField = usePayPalHostedFields();
254 return <></>;
255 };
256
257 const FormFieldsProvider = ({children}) => {
258 const {useWatch} = window.givewp.form.hooks;
259
260 amount = useWatch({name: 'amount'});
261 feeRecovery = useWatch({name: 'feeRecovery'});
262 firstName = useWatch({name: 'firstName'});
263 lastName = useWatch({name: 'lastName'});
264 email = useWatch({name: 'email'});
265
266 subscriptionFrequency = useWatch({name: 'subscriptionFrequency'});
267 subscriptionInstallments = useWatch({name: 'subscriptionInstallments'});
268 subscriptionPeriod = useWatch({name: 'subscriptionPeriod'});
269
270 addressLine1 = useWatch({name: 'address1'});
271 addressLine2 = useWatch({name: 'address2'});
272 city = useWatch({name: 'city'});
273 state = useWatch({name: 'state'});
274 postalCode = useWatch({name: 'zip'});
275 country = useWatch({name: 'country'});
276
277 currency = useWatch({name: 'currency'});
278
279 useEffect(() => {
280 if (orderCreated) {
281 updateOrderAmount = true;
282 }
283 }, [amount]);
284
285 return children;
286 };
287
288 const SmartButtonsContainer = () => {
289 const {useWatch, useFormState} = window.givewp.form.hooks;
290 const donationType = useWatch({name: 'donationType'});
291 const {isSubmitting, isSubmitSuccessful} = useFormState();
292 const {useFormContext} = window.givewp.form.hooks;
293 const {
294 getFieldState,
295 setFocus,
296 getValues,
297 formState: {errors},
298 trigger,
299 setError,
300 } = useFormContext();
301 const gateway = window.givewp.gateways.get('paypal-commerce');
302
303 const props = {
304 style: buttonsStyle,
305 disabled: isSubmitting || isSubmitSuccessful,
306 forceReRender: debounce(() => [amount, feeRecovery, firstName, lastName, email, currency], 500),
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 document.querySelector('#give-next-gen button[type="submit"]').scrollIntoView({behavior: 'smooth'});
325
326 return actions.reject();
327 }
328
329 // Validate the form values before proceeding.
330 const result = await trigger();
331 if (result === false) {
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 orderCreated = true;
342 return actions.resolve();
343 },
344 onApprove: async (data, actions) => {
345 const donationFormWithSubmitButton = Array.from(document.forms).pop();
346 const submitButton: HTMLButtonElement = donationFormWithSubmitButton.querySelector('[type="submit"]');
347 const submitButtonDefaultText = submitButton.textContent;
348 submitButton.textContent = __('Waiting for PayPal...', 'give');
349 submitButton.disabled = true;
350
351 if (payPalOrderId && updateOrderAmount) {
352 const response = await fetch(
353 `${payPalDonationsSettings.ajaxUrl}?action=give_paypal_commerce_update_order_amount&order=${payPalOrderId}`,
354 {
355 method: 'POST',
356 body: getFormData(),
357 }
358 );
359
360 const {data: ajaxResponseData} = await response.json();
361
362 if (ajaxResponseData.hasOwnProperty('error')) {
363 submitButton.disabled = false;
364 submitButton.textContent = submitButtonDefaultText;
365 throw new Error(ajaxResponseData.error);
366 }
367 }
368
369 if (donationType === 'subscription') {
370 submitButton.disabled = false;
371 submitButton.textContent = submitButtonDefaultText;
372 submitButton.click();
373 return;
374 }
375
376 return actions.order.capture().then((details) => {
377 submitButton.disabled = false;
378 submitButton.textContent = submitButtonDefaultText;
379 submitButton.click();
380 });
381 },
382 };
383
384 return donationType === 'subscription' ? (
385 // @ts-ignore
386 <PayPalButtons {...props} createSubscription={createSubscriptionHandler} />
387 ) : (
388 // @ts-ignore
389 <PayPalButtons {...props} createOrder={createOrderHandler} />
390 );
391 };
392
393 const HostedFieldsContainer = () => {
394 const {useWatch} = window.givewp.form.hooks;
395 const firstName = useWatch({name: 'firstName'});
396 const lastName = useWatch({name: 'lastName'});
397
398 const cardholderDefault = [firstName ?? '', lastName ?? ''].filter((x) => x).join(' ');
399 const [_cardholderName, setCardholderName] = useState(null);
400
401 useEffect(() => {
402 cardholderName = _cardholderName ?? cardholderDefault;
403 });
404
405 return (
406 <PayPalHostedFieldsProvider createOrder={createOrderHandler}>
407 <div>
408 <Divider label={__('Or pay with card', 'give')} style={{padding: '30px 0'}} />
409
410 <TextControl
411 className="givewp-fields"
412 label={__('Cardholder Name', 'give')}
413 hideLabelFromVision={true}
414 placeholder={__('Cardholder Name', 'give')}
415 value={_cardholderName ?? cardholderDefault}
416 onChange={(value) => setCardholderName(value)}
417 />
418
419 <PayPalHostedField
420 id="card-number"
421 className="card-field"
422 style={CUSTOM_FIELD_STYLE}
423 hostedFieldType="number"
424 options={{
425 selector: '#card-number',
426 placeholder: '4111 1111 1111 1111',
427 }}
428 />
429
430 <Flex gap="10px">
431 <PayPalHostedField
432 id="expiration-date"
433 className="givewp-fields"
434 style={CUSTOM_FIELD_STYLE}
435 hostedFieldType="expirationDate"
436 options={{
437 selector: '#expiration-date',
438 placeholder: __('MM/YYYY', 'give'),
439 }}
440 />
441 <PayPalHostedField
442 id="cvv"
443 className="card-field"
444 style={CUSTOM_FIELD_STYLE}
445 hostedFieldType="cvv"
446 options={{
447 selector: '#cvv',
448 placeholder: __('CVV', 'give'),
449 maskInput: true,
450 }}
451 />
452 </Flex>
453 <div style={{display: 'flex', gap: '10px'}}></div>
454
455 <HoistHostedFieldContext />
456 </div>
457 </PayPalHostedFieldsProvider>
458 );
459 };
460
461 function PaymentMethodsWrapper() {
462 const {useWatch} = window.givewp.form.hooks;
463 const donationType = useWatch({name: 'donationType'});
464 const [{options}, dispatch] = usePayPalScriptReducer();
465
466 useEffect(() => {
467 const isSubscription = donationType === 'subscription';
468
469 dispatch({
470 type: 'resetOptions',
471 value: {
472 ...getPayPalScriptOptions({isSubscription}),
473 currency: currency,
474 vault: donationType === 'subscription',
475 intent: donationType === 'subscription' ? 'subscription' : 'capture',
476 },
477 });
478 }, [currency, donationType]);
479
480 return (
481 <>
482 <SmartButtonsContainer />
483 {-1 !== options.components.indexOf('hosted-fields') && <HostedFieldsContainer />}
484 </>
485 );
486 }
487
488 const payPalCommerceGateway: Gateway = {
489 id: 'paypal-commerce',
490 initialize() {
491 payPalDonationsSettings = this.settings;
492 },
493 /**
494 * Before create payment.
495 * @since 3.2.0 Handle error response in approveOrderCallback.
496 * @param {Object} values
497 */
498 beforeCreatePayment: async function (values): Promise<object> {
499 if (payPalOrderId) {
500 // If order ID already set by payment buttons then return early.
501 return {
502 payPalOrderId: payPalOrderId,
503 };
504 }
505
506 if (payPalSubscriptionId) {
507 return {
508 payPalSubscriptionId: payPalSubscriptionId,
509 };
510 }
511
512 if (!validateHostedFields()) {
513 throw new Error('Invalid hosted fields');
514 }
515
516 const approveOrderCallback = async (data) => {
517 const response = await fetch(
518 `${payPalDonationsSettings.ajaxUrl}?action=give_paypal_commerce_approve_order&order=${data.orderId}&update_amount=${updateOrderAmount}`,
519 {
520 method: 'POST',
521 body: getFormData(),
522 }
523 );
524
525 const {data: ajaxResponseData} = await response.json();
526
527 if (ajaxResponseData.hasOwnProperty('error')) {
528 throw new Error(ajaxResponseData.error);
529 }
530
531 return {...data, payPalOrderId: data.orderId};
532 };
533
534 try {
535 const result = await hostedField.cardFields.submit({
536 // Trigger 3D Secure authentication
537 contingencies: ['SCA_WHEN_REQUIRED'],
538 cardholderName: cardholderName,
539 });
540
541 if (
542 !result || // Check whether get result from paypal gateway server.
543 (['NO', 'POSSIBLE'].includes(result.liabilityShift) && // Check whether card required 3D secure validation.
544 !(result.liabilityShifted && 'POSSIBLE' === result.liabilityShift)) // Check whether card passed 3D secure validation.
545 ) {
546 throw new Error(
547 __(
548 'There was a problem authenticating your payment method. Please try again. If the problem persists, please try another payment method.',
549 'give'
550 )
551 );
552 }
553
554 return await approveOrderCallback(result);
555 } catch (err) {
556 console.log('paypal donations error', err);
557
558 // Handle PayPal error.
559 const isPayPalDonationError = err.hasOwnProperty('details');
560 if (isPayPalDonationError) {
561 throw new Error(err.details[0].description);
562 }
563
564 throw new Error(sprintf(__('Paypal Donations Error: %s', 'give'), err.message));
565 }
566 },
567 Fields() {
568 const {useWatch} = window.givewp.form.hooks;
569 const donationType = useWatch({name: 'donationType'});
570 const isSubscription = donationType === 'subscription';
571
572 return (
573 <FormFieldsProvider>
574 <PayPalScriptProvider deferLoading={true} options={getPayPalScriptOptions({isSubscription})}>
575 <PaymentMethodsWrapper />
576 </PayPalScriptProvider>
577 </FormFieldsProvider>
578 );
579 },
580 };
581
582 window.givewp.gateways.register(payPalCommerceGateway);
583 })();
584