import {
DISPATCH_ACTION,
PayPalButtons,
PayPalButtonsComponentProps,
PayPalCardFieldsForm,
PayPalCardFieldsProvider,
PayPalScriptProvider,
usePayPalCardFields,
usePayPalScriptReducer,
} from '@paypal/react-paypal-js';
import {__} from '@wordpress/i18n';
import {useEffect} from 'react';
import {PayPalCommerceGateway, PayPalSubscriber} from './types';
import handleValidationRequest from '@givewp/forms/app/utilities/handleValidationRequest';
import createOrder from './resources/js/createOrder';
import type {
CreateSubscriptionRequestBody,
PayPalButtonsComponentOptions,
PayPalCardFieldsComponent,
PayPalCardFieldsComponentBasics,
} from '@paypal/paypal-js';
import createSubscriptionPlan from './resources/js/createSubscriptionPlan';
(() => {
/**
* Hoisted values are used to pass data in contexts where hooks cannot be used.
* For example, the form uses hooks to get the values of the form fields,
* but the callbacks for PayPal cannot use hooks to access those values.
*
* The component is used to hoist the form field values
* which are then collected in createOrderHandler(). This callback is not
* a component, so it cannot use hooks to access the form field values.
*/
let amount;
let firstName;
let lastName;
let email;
let payPalDonationsSettings;
let payPalOrderId;
let payPalSubscriptionId;
let subscriptionFrequency;
let subscriptionInstallments;
let subscriptionPeriod;
let country;
let state;
let city;
let addressLine1;
let addressLine2;
let postalCode;
let currency;
let submitButton;
let payPalCardFieldsForm: PayPalCardFieldsComponent = null;
/**
* @since 4.1.1 updated to reassign the submit button when not assigned yet
* @since 4.1.0
*/
const showOrHideDonateButton = (showOrHide: 'show' | 'hide') => {
submitButton = submitButton || window.givewp.form.hooks.useFormSubmitButton();
if (submitButton) {
submitButton.style.display = showOrHide === 'hide' ? 'none' : '';
}
};
const buttonsStyle = {
color: 'gold' as 'gold' | 'blue' | 'silver' | 'white' | 'black',
label: 'paypal' as 'paypal' | 'checkout' | 'buynow' | 'pay' | 'installment' | 'subscribe' | 'donate',
layout: 'vertical' as 'vertical' | 'horizontal',
shape: 'rect' as 'rect' | 'pill',
tagline: false,
};
/**
* Get PayPal script options.
*
* This function return the paypal script options on basis of context.
* - If donation type is subscription then remove hosted fields from components.
*
* @return {object} PayPal script options.
*/
const getPayPalScriptOptions = ({isSubscription}) => {
let paypalScriptOptions = {...payPalDonationsSettings.sdkOptions};
// Remove hosted fields from components if subscription.
if (isSubscription && -1 !== paypalScriptOptions.components.indexOf('card-fields')) {
paypalScriptOptions.components = paypalScriptOptions.components
.split(',')
.filter((component) => component !== 'card-fields')
.join(',');
}
return paypalScriptOptions;
};
const getFormData = () => {
const formData = new FormData();
formData.append('give-form-id', payPalDonationsSettings.donationFormId);
formData.append('give-form-hash', payPalDonationsSettings.donationFormNonce);
formData.append('give_payment_mode', 'paypal-commerce');
formData.append('give-amount', amount.toString());
formData.append('give-recurring-period', subscriptionPeriod);
formData.append('period', subscriptionPeriod);
formData.append('frequency', subscriptionFrequency);
formData.append('times', subscriptionInstallments);
formData.append('give_first', firstName);
formData.append('give_last', lastName);
formData.append('give_email', email);
if (country && country.length === 2) {
formData.append('card_address', addressLine1);
formData.append('card_address_2', addressLine2);
formData.append('card_city', city);
formData.append('card_state', state);
formData.append('card_zip', postalCode);
formData.append('billing_country', country);
}
/**
* Ensure the proper currency will be used when using the Currency Switcher add-on.
*/
formData.append('give-cs-form-currency', currency);
return formData;
};
const smartButtonsCreateOrderHandler: PayPalButtonsComponentOptions['createOrder'] = async (): Promise => {
return await createOrder(
`${payPalDonationsSettings.ajaxUrl}?action=give_paypal_commerce_create_order`,
payPalCommerceGateway,
getFormData()
);
};
/**
* @since 4.1.0
*/
const cardFieldsOnErrorHandler: PayPalCardFieldsComponentBasics['onError'] = (error) => {
console.error('PayPal Card Fields Error:', error);
throw new Error(__('PayPal Card Fields Error:', 'give') + error.message);
};
/**
* @since 4.1.0
*/
const cardFieldsCreateOrderHandler = async () => {
return await createOrder(
`${payPalDonationsSettings.ajaxUrl}?action=give_paypal_commerce_create_order`,
payPalCommerceGateway,
getFormData()
);
};
/**
* @since 4.1.0
*/
const cardFieldsOnApproveHandler: PayPalCardFieldsComponentBasics['onApprove'] = async (data) => {
// @ts-ignore
const {orderID, liabilityShift} = data;
payPalOrderId = orderID;
if (liabilityShift && !['POSSIBLE', 'YES'].includes(liabilityShift)) {
console.log('Liability shift not possible or not accepted.');
throw new Error(
__('Card type and issuing bank are not ready to complete a 3D Secure authentication.', 'give')
);
}
return;
};
const smartButtonsCreateSubscriptionHandler: PayPalButtonsComponentOptions['createSubscription'] = async (
data,
actions
) => {
const {planId, userAction} = await createSubscriptionPlan(
`${payPalDonationsSettings.ajaxUrl}?action=give_paypal_commerce_create_plan_id`,
payPalCommerceGateway,
getFormData()
);
const subscriberData: PayPalSubscriber = {
name: {
given_name: firstName,
surname: lastName,
},
email_address: email,
};
if (country) {
subscriberData.shipping_address = {
name: {
full_name: `${firstName} ${lastName}`.trim(),
},
address: {
address_line_1: addressLine1,
address_line_2: addressLine2,
admin_area_2: city,
admin_area_1: state,
postal_code: postalCode,
country_code: country,
},
};
}
const createSubscriptionPayload: CreateSubscriptionRequestBody = {
plan_id: planId,
subscriber: subscriberData,
};
if (userAction) {
createSubscriptionPayload.application_context = {
...createSubscriptionPayload.application_context,
user_action: userAction as 'CONTINUE' | 'SUBSCRIBE_NOW',
};
}
return actions.subscription.create(createSubscriptionPayload).then((orderId) => {
return (payPalSubscriptionId = orderId);
});
};
const Divider = ({label, style = {}}) => {
const styles = {
container: {
fontSize: '16px',
fontStyle: 'italic',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
...style,
},
dashedLine: {
border: '1px solid #d4d4d4',
flexGrow: 1,
},
label: {
padding: '0 6px',
fontSize: '14px',
color: '#8d8e8e',
},
};
return (
{label}
);
};
const FormFieldsProvider = ({children}) => {
const formData = window.givewp.form.hooks.useFormData();
amount = formData.amount;
firstName = formData.firstName;
lastName = formData.lastName;
email = formData.email;
subscriptionFrequency = formData.subscriptionFrequency;
subscriptionInstallments = formData.subscriptionInstallments;
subscriptionPeriod = formData.subscriptionPeriod;
addressLine1 = formData.billingAddress.addressLine1;
addressLine2 = formData.billingAddress.addressLine2;
city = formData.billingAddress.city;
state = formData.billingAddress.state;
postalCode = formData.billingAddress.postalCode;
country = formData.billingAddress.country;
currency = formData.currency;
return children;
};
const SmartButtonsContainer = () => {
const {useWatch, useFormState} = window.givewp.form.hooks;
const donationType = useWatch({name: 'donationType'});
const {isSubmitting, isSubmitSuccessful} = useFormState();
const {useFormContext} = window.givewp.form.hooks;
const {
getFieldState,
setFocus,
getValues,
trigger,
setError,
} = useFormContext();
const gateway = window.givewp.gateways.get('paypal-commerce') as PayPalCommerceGateway;
const props: PayPalButtonsComponentProps = {
style: buttonsStyle,
disabled: isSubmitting || isSubmitSuccessful,
forceReRender: [donationType, amount, firstName, lastName, email, currency],
onClick: async (data, actions) => {
// Validate whether payment gateway support subscriptions.
if (donationType === 'subscription' && !gateway.supportsSubscriptions) {
setError(
'FORM_ERROR',
{
message: __(
'This payment gateway does not support recurring payments, please try selecting another payment gateway.',
'give'
),
},
{shouldFocus: true}
);
// Scroll to the top of the form.
// Add this moment we do not have a way to scroll to the error message.
// In the future we can add a way to scroll to the error message and remove this code.
submitButton.scrollIntoView({behavior: 'smooth'});
return actions.reject();
}
// Validate the form values in the client side before proceeding.
const isClientValidationSuccessful = await trigger();
if (!isClientValidationSuccessful) {
// Set focus on first invalid field.
for (const fieldName in getValues()) {
if (getFieldState(fieldName).invalid) {
setFocus(fieldName);
}
}
return actions.reject();
}
/**
* Validate the form values in the server side before proceeding - this is important to prevent problems with some blocks.
*
* Ideally, the client-side validations should be enough. However, in some cases, these validations are reached
* later when the donation is already created on the PayPal side. This way, we need the request below to check
* it earlier and prevent the donation creation on the PayPal side if the required fields are missing.
*
* Know cases:
*
* #1 - Billing Address Block: depending on the selected country, the city, state, and zip fields
* can be required or not and there are custom validation rules on the server side that check it.
*
* #2 - Gift Aid Block: when users opt-in to the gift aid checkbox, it will display some
* required fields that should be filled, but as this block is not a group and even so has
* "children" fields, the validation rules for it live only on the server.
*/
const isServerValidationSuccessful = await handleValidationRequest(
payPalDonationsSettings.validateUrl,
getValues(),
setError,
gateway
);
if (!isServerValidationSuccessful) {
return actions.reject();
}
return actions.resolve();
},
onApprove: async (data, actions) => {
const orderId = data.orderID;
const subscriptionId = data?.subscriptionID;
const submitButtonDefaultText = submitButton.textContent;
submitButton.textContent = __('Waiting for PayPal...', 'give');
submitButton.disabled = true;
if (subscriptionId && donationType === 'subscription') {
payPalSubscriptionId = subscriptionId;
submitButton.disabled = false;
submitButton.textContent = submitButtonDefaultText;
submitButton.click();
return;
}
if (orderId) {
payPalOrderId = orderId;
}
submitButton.disabled = false;
submitButton.textContent = submitButtonDefaultText;
submitButton.click();
return;
},
};
return donationType === 'subscription' ? (
) : (
);
};
/**
* @since 4.1.0
*/
const PayPalGatewayCardFieldsForm = () => {
const {cardFieldsForm} = usePayPalCardFields();
payPalCardFieldsForm = cardFieldsForm;
payPalCommerceGateway.payPalCardFieldsForm = cardFieldsForm;
return ;
};
const CardFieldsContainer = () => {
showOrHideDonateButton('show');
return (
<>
>
);
};
function PaymentMethodsWrapper() {
const {isRecurring} = window.givewp.form.hooks.useFormData();
const [{options}, dispatch] = usePayPalScriptReducer();
const shouldShowCardFields = -1 !== options.components.indexOf('card-fields');
useEffect(() => {
const options = getPayPalScriptOptions({isSubscription: isRecurring});
dispatch({
type: DISPATCH_ACTION.RESET_OPTIONS,
value: {
...options,
currency: currency,
vault: isRecurring,
intent: isRecurring ? 'subscription' : options.intent,
},
});
}, [currency, isRecurring]);
useEffect(() => {
// hide donate buttons if card fields are not expected to be shown
if (!shouldShowCardFields) {
showOrHideDonateButton('hide');
}
return () => {
showOrHideDonateButton('show');
};
}, [shouldShowCardFields]);
return (
<>
{shouldShowCardFields && }
>
);
}
const payPalCommerceGateway: PayPalCommerceGateway = {
id: 'paypal-commerce',
initialize() {
payPalDonationsSettings = this.settings;
},
/**
* Before create payment.
* @since 3.2.0 Handle error response in approveOrderCallback.
* @param {Object} values
*/
beforeCreatePayment: async function (values): Promise