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

New: Next Gen Stripe Gateway #11

Merged
merged 10 commits into from
Jun 8, 2022
Merged
17 changes: 0 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
"@wordpress/i18n": "^3.19.0",
"@wordpress/server-side-render": "^3.1.3",
"accounting": "latest",
"axios": "^0.21.2",
"classnames": "^2.2.6",
"iframe-resizer": "^4.2.10",
"joi": "^17.6.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php
namespace Give\Framework\PaymentGateways\Contracts;

use Give\Framework\EnqueueScript;

/**
* @unreleased
*/
interface NextGenPaymentGatewayInterface {

/**
* @unreleased
*/
public function enqueueScript(): EnqueueScript;

/**
* @unreleased
*/
public function formSettings(int $formId): array;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import Joi from 'joi';
import Field from '../fields/Field';
import getFieldErrorMessages from '../utilities/getFieldErrorMessages';
import FieldSection from '../fields/FieldSection';
import axios from 'axios';
import getWindowData from '../utilities/getWindowData';
import PaymentDetails from '../fields/PaymentDetails';
import DonationReceipt from './DonationReceipt';
import {useGiveDonationFormStore} from '../store';
import type {Gateway, Field as FieldInterface} from '@givewp/forms/types';
import type {Field as FieldInterface, Gateway} from '@givewp/forms/types';
import postData from "../utilities/postData";

const messages = getFieldErrorMessages();

Expand Down Expand Up @@ -44,23 +44,31 @@ type FormInputs = {
};

const handleSubmitRequest = async (values, setError, gateway: Gateway) => {
let gatewayResponse = {};
let beforeCreatePaymentGatewayResponse = {};

try {
if (gateway.beforeCreatePayment) {
gatewayResponse = await gateway.beforeCreatePayment(values);
beforeCreatePaymentGatewayResponse = await gateway.beforeCreatePayment(values);
}
} catch (error) {
return setError('FORM_ERROR', {message: error.message});
}

const request = await axios.post(donateUrl, {
const request = await postData(donateUrl, {
...values,
...gatewayResponse,
...beforeCreatePaymentGatewayResponse,
});

if (request.status === 200) {
alert('Thank You!');
if (!request.response.ok) {
return setError('FORM_ERROR', {message: "Something went wrong, please try again or contact support."});
}

try {
if (gateway.afterCreatePayment) {
await gateway.afterCreatePayment(request.data);
}
} catch (error) {
return setError('FORM_ERROR', {message: error.message});
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* @unreleased
* @see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#supplying_request_options
*/
export default async function postData(url: string, data: object = {}) {
// Default options are marked with *
const response = await fetch(url, {
method: 'POST', // *GET, POST, PUT, DELETE, etc.
mode: 'same-origin', // no-cors, *cors, same-origin
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
credentials: 'same-origin', // include, *same-origin, omit
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow', // manual, *follow, error
referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
body: JSON.stringify(data) // body data type must match "Content-Type" header
});

return {
response,
data: response.json()
};
}
35 changes: 28 additions & 7 deletions src/NextGen/DonationForm/Controllers/DonateController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use Exception;
use Give\Donors\Models\Donor;
use Give\Framework\PaymentGateways\PaymentGateway;
use Give\PaymentGateways\DataTransferObjects\FormData;
use Give\NextGen\DonationForm\DataTransferObjects\DonateFormData;

/**
* @unreleased
Expand All @@ -18,24 +18,26 @@ class DonateController
*
* @unreleased
*
* @param FormData $formData
* @param DonateFormData $formData
* @param PaymentGateway $registeredGateway
*
* @return void
* @throws Exception
*/
public function donate(FormData $formData, PaymentGateway $registeredGateway)
public function donate(DonateFormData $formData, PaymentGateway $registeredGateway)
{
$donor = $this->getOrCreateDonor(
$formData->donorInfo->wpUserId,
$formData->donorInfo->email,
$formData->donorInfo->firstName,
$formData->donorInfo->lastName
$formData->wpUserId,
$formData->email,
$formData->firstName,
$formData->lastName
);

$donation = $formData->toDonation($donor->id);
$donation->save();

$this->setSession($donation->id);

$registeredGateway->handleCreatePayment($donation);
}

Expand Down Expand Up @@ -83,4 +85,23 @@ private function getOrCreateDonor(

return $donor;
}

/**
* Set donation id to purchase session for use in the donation receipt.
*
* @unreleased
*
* @param $donationId
*
* @return void
*/
private function setSession($donationId)
{
$purchaseSession = (array)give()->session->get('give_purchase');

if ($purchaseSession && array_key_exists('purchase_key', $purchaseSession)) {
$purchaseSession['donation_id'] = $donationId;
give()->session->set('give_purchase', $purchaseSession);
}
}
}
111 changes: 66 additions & 45 deletions src/NextGen/DonationForm/DataTransferObjects/DonateFormData.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,71 +2,92 @@

namespace Give\NextGen\DonationForm\DataTransferObjects;

use Give\PaymentGateways\DataTransferObjects\FormData;
use Give\ValueObjects\Address;
use Give\ValueObjects\CardInfo;
use Give\ValueObjects\DonorInfo;
use Give\Donations\Models\Donation;
use Give\Donations\ValueObjects\DonationStatus;
use Give\Framework\Support\ValueObjects\Money;

/**
* @unreleased
*/
class DonateFormData extends FormData
class DonateFormData
{
/**
* @var float
*/
public $amount;
/**
* @var string
*/
public $gatewayId;
/**
* @var string
*/
public $currency;
/**
* @var string
*/
public $firstName;
/**
* @var string
*/
public $lastName;
/**
* @var string
*/
public $email;
/**
* @var int
*/
public $wpUserId;
/**
* @var int
*/
public $formId;
/**
* @var string
*/
public $formTitle;

/**
* Convert data from request into DTO
*
* @unreleased
*
* @return self
* @param array $request
* @return DonateFormData
*/
public static function fromRequest(array $request): FormData
public static function fromRequest(array $request): DonateFormData
{
$self = new static();

$self->price = $request['amount'];
$self->priceId = '';
$self->gatewayId = $request['gatewayId'];
$self->amount = $request['amount'];
$self->date = '';
$self->purchaseKey = '';
$self->currency = $request['currency'];
$self->formTitle = $request['formTitle'];
$self->firstName = $request['firstName'];
$self->lastName = $request['lastName'];
$self->email = $request['email'];
$self->wpUserId = (int)$request['userId'];
$self->formId = (int)$request['formId'];
$self->paymentGateway = $request['gatewayId'];

$self->billingAddress = Address::fromArray([
'line1' => 'line1',
'line2' => 'line2',
'city' => 'city',
'state' => 'state',
'country' => 'country',
'postalCode' => 'postalCode',
]);
$self->formTitle = $request['formTitle'];

$self->donorInfo = DonorInfo::fromArray([
'wpUserId' => $request['userId'],
'firstName' => $request['firstName'],
'lastName' => $request['lastName'],
'email' => $request['email'],
'honorific' => '',
'address' =>[
'line1' => 'line1',
'line2' => 'line2',
'city' => 'city',
'state' => 'state',
'country' => 'country',
'postalCode' => 'postalCode',
]
]);
return $self;
}

$self->cardInfo = CardInfo::fromArray([
'name' => '',
'cvc' => '',
'expMonth' => '',
'expYear' => '',
'number' => '',
/**
* @unreleased
*/
public function toDonation($donorId): Donation
{
return new Donation([
'status' => DonationStatus::PENDING(),
'gatewayId' => $this->gatewayId,
'amount' => Money::fromDecimal($this->amount, $this->currency),
'donorId' => $donorId,
'firstName' => $this->firstName,
'lastName' => $this->lastName,
'email' => $this->email,
'formId' => $this->formId,
'formTitle' => $this->formTitle,
]);

return $self;
}
}
4 changes: 2 additions & 2 deletions src/NextGen/DonationForm/Routes/DonateRoute.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,10 @@ public function __invoke()
$gatewayIds = array_keys($paymentGateways);

// make sure gateway is valid
$this->validateGateway($formData->paymentGateway, $gatewayIds);
$this->validateGateway($formData->gatewayId, $gatewayIds);

/** @var PaymentGateway $gateway */
$gateway = give($paymentGateways[$formData->paymentGateway]);
$gateway = give($paymentGateways[$formData->gatewayId]);

try {
$this->donateController->donate($formData, $gateway);
Expand Down
7 changes: 6 additions & 1 deletion src/NextGen/DonationForm/resources/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface FormData {
firstName: string;
lastName?: string;
email: string;
amount: Currency;
amount: number;
company?: string;
}

Expand Down Expand Up @@ -75,6 +75,11 @@ export interface Gateway {
* A hook before the form is submitted.
*/
beforeCreatePayment?(values: FormData): Promise<object> | Error;

/**
* A hook after the form is submitted.
*/
afterCreatePayment?(response: object): Promise<void> | Error;
Comment on lines 75 to +82
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a more specific name we can give these? What are the gateways intended to do at these points? Perhaps this is fine. It's just not clear to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this was a specific name lol. beforeCreatePayment() fires before the gateway class method createPayment(). afterCreatePayment() gets fired after the gateway class method createPayment(). This is where the gateway could return data to itself using ReturnToBrowser / json and do something with that response.

In the case for Stripe it needs to return a stripe intent status to the front-end and do something with it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps it is a specific as it can be. Right now the naming caters towards the event — that is, the name is clear that it's happening at a point in time and when. That doesn't really inform the gateway as what it ought to be doing at that time. I don't know if this is actually accurate, but something like, prepareForDonation and finishDonation — the point being that it gently guides the gateway as what it ought to be doing.

That said, perhaps what's being done at this event varies too much from gateway to gateway, in which case the current event-based naming is fine.

Copy link
Contributor Author

@jonwaldstein jonwaldstein Jun 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotchya, I think I gravitated toward the event-based naming because it coincides with the backend api naming convention and was easier to conceptualize in what order they get triggered while writing the logic. I know what you're saying about guiding the intention of the methods but as you said, the intention could vary between gateways. Some Gateways might not even need to use these.

Some use cases I see for each:

beforeCreatePayment()

  • sending data to createPayment(), validation, gateway object mounting, pre-fetching something/using custom route

afterCreatePayment()

  • receiving data from createPayment(), gateway confirmation, redirecting (the framework will probably have a general way of redirecting to receipt as well).

}

export interface Template {}
Loading