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

Commit

Permalink
Refactor: Update recurring support for PayPal Donations (#153)
Browse files Browse the repository at this point in the history
  • Loading branch information
kjohnson authored Apr 6, 2023
1 parent ff524eb commit 33dd440
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 89 deletions.
4 changes: 4 additions & 0 deletions src/NextGen/Gateways/PayPalCommerce/PayPalCommerceGateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@

namespace Give\NextGen\Gateways\PayPalCommerce;

use Give\Donations\Models\Donation;
use Give\Framework\EnqueueScript;
use Give\Framework\PaymentGateways\Commands\GatewayCommand;
use Give\Framework\PaymentGateways\Commands\PaymentComplete;
use Give\Framework\PaymentGateways\Contracts\NextGenPaymentGatewayInterface;
use Give\NextGen\Framework\PaymentGateways\Traits\HandleHttpResponses;
use Give\PaymentGateways\PayPalCommerce\Models\MerchantDetail;
use Give\PaymentGateways\PayPalCommerce\PayPalCommerce;
use Give\PaymentGateways\PayPalCommerce\Repositories\MerchantDetails;
use Give\Subscriptions\Models\Subscription;

/**
* An extension of the PayPalCommerce gateway in GiveWP that supports the NextGenPaymentGatewayInterface.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Give\NextGen\Gateways\PayPalCommerce;

use Give\Donations\Models\Donation;
use Give\Framework\PaymentGateways\Commands\GatewayCommand;
use Give\Framework\PaymentGateways\Commands\SubscriptionProcessing;
use Give\Framework\PaymentGateways\Exceptions\PaymentGatewayException;
use Give\Framework\PaymentGateways\SubscriptionModule;
use Give\Subscriptions\Models\Subscription;
use Give\Subscriptions\ValueObjects\SubscriptionStatus;
use GiveRecurring\PaymentGateways\PayPalCommerce\Repositories\Subscription as SubscriptionRepository;

class PayPalCommerceSubscriptionModule extends SubscriptionModule
{
/**
* @unreleased
*/
public function createSubscription(
Donation $donation,
Subscription $subscription,
$gatewayData = null
): GatewayCommand {

$subscriptionId = $gatewayData['payPalSubscriptionId'];

return new SubscriptionProcessing($subscriptionId);
}

public function cancelSubscription(Subscription $subscription)
{
try {
give(SubscriptionRepository::class)
->updateStatus($subscription->gatewaySubscriptionId, 'cancel');

$subscription->status = SubscriptionStatus::CANCELLED();
$subscription->save();
} catch (\Exception $exception) {
throw new PaymentGatewayException(
sprintf('Unable to cancel subscription with PayPal. %s', $exception->getMessage()),
$exception->getCode(),
$exception
);
}
}
}
230 changes: 142 additions & 88 deletions src/NextGen/Gateways/PayPalCommerce/payPalCommerceGateway.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ import {CSSProperties, useEffect, useState} from 'react';
let payPalOrderId;
let payPalSubscriptionId;

let subscriptionFrequency;
let subscriptionInstallments;
let subscriptionPeriod;

const buttonsStyle = {
color: 'gold' as 'gold' | 'blue' | 'silver' | 'white' | 'black',
label: 'paypal' as 'paypal' | 'checkout' | 'buynow' | 'pay' | 'installment' | 'subscribe' | 'donate',
Expand Down Expand Up @@ -61,12 +65,21 @@ import {CSSProperties, useEffect, useState} from 'react';

const getFormData = () => {
const formData = new FormData();

formData.append('give-form-id', payPalDonationsSettings.donationFormId);
formData.append('give-form-hash', payPalDonationsSettings.donationFormNonce);

formData.append('give-amount', amount);

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);
formData.append('give-form-id', payPalDonationsSettings.donationFormId);
formData.append('give-form-hash', payPalDonationsSettings.donationFormNonce);

return formData;
};

Expand All @@ -92,7 +105,7 @@ import {CSSProperties, useEffect, useState} from 'react';

const createSubscriptionHandler = async (data, actions) => {
// eslint-disable-next-line
const response = await fetch(`${payPalDonationsSettings.ajaxurl}?action=give_paypal_commerce_create_plan_id`, {
const response = await fetch(`${payPalDonationsSettings.ajaxUrl}?action=give_paypal_commerce_create_plan_id`, {
method: 'POST',
body: getFormData(),
});
Expand All @@ -103,9 +116,9 @@ import {CSSProperties, useEffect, useState} from 'react';
throw responseJson.data.error;
}

payPalSubscriptionId = responseJson.data.id;

return actions.subscription.create({plan_id: payPalSubscriptionId});
return actions.subscription.create({plan_id: responseJson.data.id}).then((orderId) => {
return payPalSubscriptionId = orderId;
});
};

const Divider = ({label, style = {}}) => {
Expand Down Expand Up @@ -152,6 +165,11 @@ import {CSSProperties, useEffect, useState} from 'react';
lastName = useWatch({name: 'lastName'});
email = useWatch({name: 'email'});


subscriptionFrequency = useWatch({name: 'subscriptionFrequency'});
subscriptionInstallments = useWatch({name: 'subscriptionInstallments'});
subscriptionPeriod = useWatch({name: 'subscriptionPeriod'});

return children;
};

Expand All @@ -162,107 +180,140 @@ import {CSSProperties, useEffect, useState} from 'react';

const {isSubmitting, isSubmitSuccessful} = useFormState();

const [{options}, dispatch] = usePayPalScriptReducer();

useEffect(() => {
dispatch({
type: 'resetOptions',
value: {
...options,
currency: currency,
vault: donationType === 'subscription',
intent: donationType === 'subscription' ? 'subscription' : 'capture',
},
});
}, [currency, donationType]);
const props = {
style: buttonsStyle,
disabled: isSubmitting || isSubmitSuccessful,
forceReRender: debounce(() => [amount, firstName, lastName, email, currency], 500),
onApprove: async (data, actions) => {

if(donationType === 'subscription') {
// @ts-ignore
document.forms[0].querySelector('[type="submit"]').click();
return;
}

return actions.order.capture().then((details) => {
// @ts-ignore
document.forms[0].querySelector('[type="submit"]').click();
});
}
}

return (
<PayPalButtons
disabled={isSubmitting || isSubmitSuccessful}
style={buttonsStyle}
// @ts-ignore
forceReRender={debounce(() => [amount, firstName, lastName, email, currency], 500)}
createOrder={createOrderHandler}
// createSubscription={createSubscriptionHandler}
onApprove={async (data, actions) => {
return actions.order.capture().then((details) => {
// @ts-ignore
document.forms[0].querySelector('[type="submit"]').click();
});
}}
/>
);
return donationType === 'subscription'
// @ts-ignore
? <PayPalButtons {...props} createSubscription={createSubscriptionHandler} />
// @ts-ignore
: <PayPalButtons {...props} createOrder={createOrderHandler} />;
};

const HostedFieldsContainer = () => {
const {useWatch} = window.givewp.form.hooks;
const firstName = useWatch({name: 'firstName'});
const lastName = useWatch({name: 'lastName'});
const cardholderDefault = [firstName ?? '', lastName ?? ''].filter((x) => x).join(' ');
const donationType = useWatch({name: 'donationType'});

const cardholderDefault = [firstName ?? '', lastName ?? ''].filter((x) => x).join(' ');
const [_cardholderName, setCardholderName] = useState(null);

useEffect(() => {
cardholderName = _cardholderName ?? cardholderDefault;
});

/**
* Hosted fields are not supported for subscriptions at this time.
*/
const supportsHostedFields = donationType !== 'subscription';

return (
<PayPalHostedFieldsProvider
notEligibleError={<div>Your account is not eligible</div>}
createOrder={createOrderHandler}
>
<Divider label={__('Or pay with card', 'give')} style={{padding: '30px 0'}} />

<TextControl
className="givewp-fields"
label={__('Cardholder Name', 'give')}
hideLabelFromVision={true}
placeholder={'Cardholder Name'}
value={_cardholderName ?? cardholderDefault}
onChange={(value) => setCardholderName(value)}
/>

<PayPalHostedField
id="card-number"
className="card-field"
style={CUSTOM_FIELD_STYLE}
hostedFieldType="number"
options={{
selector: '#card-number',
placeholder: '4111 1111 1111 1111',
}}
/>

<Flex gap="10px">
<PayPalHostedField
id="expiration-date"

<PayPalHostedFieldsProvider
notEligibleError={<div>Your account is not eligible</div>}
createOrder={createOrderHandler}
>
<div style={{ display: supportsHostedFields ? 'initial' : 'none'}}>
<Divider label={__('Or pay with card', 'give')} style={{padding: '30px 0'}} />

<TextControl
className="givewp-fields"
style={CUSTOM_FIELD_STYLE}
hostedFieldType="expirationDate"
options={{
selector: '#expiration-date',
placeholder: 'MM/YYYY',
}}
label={__('Cardholder Name', 'give')}
hideLabelFromVision={true}
placeholder={'Cardholder Name'}
value={_cardholderName ?? cardholderDefault}
onChange={(value) => setCardholderName(value)}
/>

<PayPalHostedField
id="cvv"
id="card-number"
className="card-field"
style={CUSTOM_FIELD_STYLE}
hostedFieldType="cvv"
hostedFieldType="number"
options={{
selector: '#cvv',
placeholder: 'CVV',
maskInput: true,
selector: '#card-number',
placeholder: '4111 1111 1111 1111',
}}
/>
</Flex>
<div style={{display: 'flex', gap: '10px'}}></div>

<HoistHostedFieldContext />
</PayPalHostedFieldsProvider>
<Flex gap="10px">
<PayPalHostedField
id="expiration-date"
className="givewp-fields"
style={CUSTOM_FIELD_STYLE}
hostedFieldType="expirationDate"
options={{
selector: '#expiration-date',
placeholder: 'MM/YYYY',
}}
/>
<PayPalHostedField
id="cvv"
className="card-field"
style={CUSTOM_FIELD_STYLE}
hostedFieldType="cvv"
options={{
selector: '#cvv',
placeholder: 'CVV',
maskInput: true,
}}
/>
</Flex>
<div style={{display: 'flex', gap: '10px'}}></div>

<HoistHostedFieldContext />

</div>
</PayPalHostedFieldsProvider>

);
};

function PaymentMethodsWrapper() {

const {useWatch} = window.givewp.form.hooks;
const currency = useWatch({name: 'currency'});
const donationType = useWatch({name: 'donationType'});

const [{options}, dispatch] = usePayPalScriptReducer();

useEffect(() => {
dispatch({
type: 'resetOptions',
value: {
...options,
currency: currency,
vault: donationType === 'subscription',
intent: donationType === 'subscription' ? 'subscription' : 'capture',
},
});
}, [currency, donationType]);

return (
<>
<SmartButtonsContainer />
<HostedFieldsContainer />
</>
);
}

const payPalCommerceGateway: Gateway = {
id: 'paypal-commerce',
initialize() {
Expand All @@ -276,6 +327,12 @@ import {CSSProperties, useEffect, useState} from 'react';
};
}

if(payPalSubscriptionId) {
return {
payPalSubscriptionId: payPalSubscriptionId,
}
}

if (!validateHostedFields()) {
throw new Error('Invalid hosted fields');
}
Expand All @@ -299,16 +356,13 @@ import {CSSProperties, useEffect, useState} from 'react';
});
},
Fields() {
// Can we get this.settings to be available here?
const {useWatch} = window.givewp.form.hooks;
const donationType = useWatch({name: 'donationType'});
const supportsHostedFields = donationType !== 'subscription';

return (
<FormFieldsProvider>
<PayPalScriptProvider options={payPalDonationsSettings.sdkOptions}>
<SmartButtonsContainer />
{!!supportsHostedFields && <HostedFieldsContainer />}
<PayPalScriptProvider
deferLoading={true}
options={payPalDonationsSettings.sdkOptions}
>
<PaymentMethodsWrapper />
</PayPalScriptProvider>
</FormFieldsProvider>
);
Expand Down
Loading

0 comments on commit 33dd440

Please sign in to comment.