From cbca0d9f4132665fd49fa47995efd30b852e1150 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Wed, 1 Jun 2022 15:12:43 -0400 Subject: [PATCH 01/10] feature: scaffold NextGenStripeGateway using Stripes new payment element api --- .../DonationFormBlock/app/form/Form.tsx | 20 +- src/NextGen/DonationForm/resources/types.ts | 7 +- .../NextGenCreditCardGateway.php | 102 ------- .../NextGenStripeGateway.php | 279 ++++++++++++++++++ .../nextGenStripeGateway.tsx} | 33 ++- src/NextGen/ServiceProvider.php | 4 +- webpack.config.js | 4 +- 7 files changed, 329 insertions(+), 120 deletions(-) delete mode 100644 src/NextGen/Gateways/Stripe/NextGenCreditCardGateway/NextGenCreditCardGateway.php create mode 100644 src/NextGen/Gateways/Stripe/NextGenStripeGateway/NextGenStripeGateway.php rename src/NextGen/Gateways/Stripe/{NextGenCreditCardGateway/nextGenCreditCardGateway.tsx => NextGenStripeGateway/nextGenStripeGateway.tsx} (73%) diff --git a/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/form/Form.tsx b/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/form/Form.tsx index 1ed306a0a..394b2c693 100644 --- a/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/form/Form.tsx +++ b/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/form/Form.tsx @@ -12,7 +12,7 @@ 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'; const messages = getFieldErrorMessages(); @@ -44,11 +44,11 @@ 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}); @@ -56,11 +56,19 @@ const handleSubmitRequest = async (values, setError, gateway: Gateway) => { const request = await axios.post(donateUrl, { ...values, - ...gatewayResponse, + ...beforeCreatePaymentGatewayResponse, }); - if (request.status === 200) { - alert('Thank You!'); + if (request.status !== 200) { + return setError('FORM_ERROR', "An error occurred"); + } + + try { + if (gateway.afterCreatePayment) { + await gateway.afterCreatePayment(values); + } + } catch (error) { + return setError('FORM_ERROR', {message: error.message}); } }; diff --git a/src/NextGen/DonationForm/resources/types.ts b/src/NextGen/DonationForm/resources/types.ts index 5aa96abdc..78f094a21 100644 --- a/src/NextGen/DonationForm/resources/types.ts +++ b/src/NextGen/DonationForm/resources/types.ts @@ -10,7 +10,7 @@ export interface FormData { firstName: string; lastName?: string; email: string; - amount: Currency; + amount: number; company?: string; } @@ -75,6 +75,11 @@ export interface Gateway { * A hook before the form is submitted. */ beforeCreatePayment?(values: FormData): Promise | Error; + + /** + * A hook before the form is submitted. + */ + afterCreatePayment?(values: FormData): Promise | Error; } export interface Template {} diff --git a/src/NextGen/Gateways/Stripe/NextGenCreditCardGateway/NextGenCreditCardGateway.php b/src/NextGen/Gateways/Stripe/NextGenCreditCardGateway/NextGenCreditCardGateway.php deleted file mode 100644 index 5a4d60ff6..000000000 --- a/src/NextGen/Gateways/Stripe/NextGenCreditCardGateway/NextGenCreditCardGateway.php +++ /dev/null @@ -1,102 +0,0 @@ -getId(), - 'build/nextGenCreditCardGateway.js', - GIVE_NEXT_GEN_DIR, - GIVE_NEXT_GEN_URL, - 'give' - ); - } - - /** - * @unreleased - */ - public function formSettings($formId): array - { - give_stripe_set_app_info($formId); - - $stripePublishableKey = give_stripe_get_publishable_key($formId); - $stripeConnectedAccountKey = give_stripe_get_connected_account_id($formId); - $currency = give_get_currency($formId); - $formDefaultAmount = give_get_default_form_amount($formId); - $defaultAmount = Money::fromDecimal(!empty($formDefaultAmount) ? $formDefaultAmount : '50', $currency); - $stripePaymentIntent = $this->generateStripePaymentIntent( - $stripeConnectedAccountKey, - $defaultAmount - ); - - return [ - 'stripeKey' => $stripePublishableKey, - 'stripeClientSecret' => $stripePaymentIntent->client_secret, - 'stripeConnectedAccountKey' => $stripeConnectedAccountKey, - 'successUrl' => give_get_success_page_uri(), - ]; - } - - /** - * Mocking Payment Intent while building out api - * - * @unreleased - */ - private function generateStripePaymentIntent($accountId, Money $amount): PaymentIntent - { - return PaymentIntent::create( - [ - 'amount' => $amount->formatToMinorAmount(), - 'currency' => $amount->getCurrency()->getCode(), - 'automatic_payment_methods' => ['enabled' => true], - ], - ['stripe_account' => $accountId] - ); - } -} diff --git a/src/NextGen/Gateways/Stripe/NextGenStripeGateway/NextGenStripeGateway.php b/src/NextGen/Gateways/Stripe/NextGenStripeGateway/NextGenStripeGateway.php new file mode 100644 index 000000000..164715f19 --- /dev/null +++ b/src/NextGen/Gateways/Stripe/NextGenStripeGateway/NextGenStripeGateway.php @@ -0,0 +1,279 @@ +getId(), + 'build/nextGenStripeGateway.js', + GIVE_NEXT_GEN_DIR, + GIVE_NEXT_GEN_URL, + 'give' + ); + } + + /** + * @unreleased + */ + public function formSettings($formId): array + { + give_stripe_set_app_info($formId); + + $stripePublishableKey = give_stripe_get_publishable_key($formId); + $stripeConnectedAccountKey = give_stripe_get_connected_account_id($formId); + $currency = give_get_currency($formId); + $formDefaultAmount = give_get_default_form_amount($formId); + $defaultAmount = Money::fromDecimal(!empty($formDefaultAmount) ? $formDefaultAmount : '50', $currency); + $stripePaymentIntent = $this->generateStripePaymentIntent( + $stripeConnectedAccountKey, + $defaultAmount + ); + + return [ + 'stripeKey' => $stripePublishableKey, + 'stripeClientSecret' => $stripePaymentIntent->client_secret, + 'stripeConnectedAccountKey' => $stripeConnectedAccountKey, + 'successUrl' => give_get_success_page_uri(), + 'stripePaymentIntentId' => $stripePaymentIntent->id, + 'updatePaymentIntentUrl' => $this->generateGatewayRouteUrl( + 'updatePaymentIntent', + [ + 'stripePaymentIntentId' => $stripePaymentIntent->id, + 'stripeConnectedAccountKey' => $stripeConnectedAccountKey + ] + ) + ]; + } + + /** + * Mocking Payment Intent while building out api + * + * @unreleased + */ + private function generateStripePaymentIntent($accountId, Money $amount): PaymentIntent + { + return PaymentIntent::create( + [ + 'amount' => $amount->formatToMinorAmount(), + 'currency' => $amount->getCurrency()->getCode(), + 'automatic_payment_methods' => ['enabled' => true], + ], + ['stripe_account' => $accountId] + ); + } + + /** + * @unreleased + * + * @throws ApiErrorException + */ + public function updatePaymentIntent(array $queryParams): JsonResponse + { + /** + * Get data from client request + */ + $request = $this->request(); + + $stripeConnectedAccountKey = $queryParams['stripeConnectedAccountKey']; + $stripePaymentIntentId = $queryParams['stripePaymentIntentId']; + $amount = $request->get('amount') * 100; + $firstName = $request->get('firstName'); + $lastName = $request->get('lastName'); + $email = $request->get('email'); + $customerId = give_stripe_get_customer_id($email) ?? ''; + + /** + * Get or create a Stripe customer + */ + $customer = $this->getOrCreateStripeCustomer( + $customerId, + $stripeConnectedAccountKey, + $email, + "$firstName $lastName" + ); + + $intent = $this->updateStripePaymentIntent( + $stripePaymentIntentId, + [ + 'amount' => $amount, + 'customer' => $customer->id + ] + ); + + return response()->json([ + 'status' => $intent->status + ]); + } + + /** + * Get or create Stripe Customer + * + * @unreleased + */ + public function getOrCreateStripeCustomer( + string $customerId, + string $connectAccountId, + string $email, + string $name + ): Customer { + // make sure customerId still exists in connect account + if ($customerId) { + $customer = Customer::retrieve($customerId, ['stripe_account' => $connectAccountId]); + } + + // create a new customer if necessary + if (!$customerId || !$customer) { + $customer = Customer::create( + [ + 'name' => $name, + 'email' => $email, + ], + ['stripe_account' => $connectAccountId] + ); + } + + return $customer; + } + + /** + * @inheritDoc + */ + public function createPayment(Donation $donation): GatewayCommand + { + $stripePaymentIntentId = $this->request()->get('stripePaymentIntentId'); + $donation->gatewayTransactionId = $stripePaymentIntentId; + $donation->save(); + + // @todo: payment method may need to be saved on the webhook side + $paymentIntent = PaymentIntent::retrieve($stripePaymentIntentId); + $paymentMethodId = $paymentIntent->payment_method; + + give_update_meta($donation->id, '_give_stripe_source_id', $paymentMethodId); + + DonationNote::create([ + 'donationId' => $donation->id, + 'content' => sprintf(__('Stripe Source/Payment Method ID: %s', 'give'), $paymentMethodId) + ]); + + $donationSummary = Call::invoke(SaveDonationSummary::class, $donation); + + $intent = $this->updateStripePaymentIntent( + $stripePaymentIntentId, + [ + 'description' => $donationSummary->getSummaryWithDonor(), + 'metadata' => give_stripe_prepare_metadata($donation->id), + ] + ); + + DonationNote::create([ + 'donationId' => $donation->id, + 'content' => sprintf(__('Stripe Charge/Payment Intent ID: %s', 'give'), $stripePaymentIntentId) + ]); + + DonationNote::create([ + 'donationId' => $donation->id, + 'content' => sprintf(__('Stripe Payment Intent Client Secret: %s', 'give'), $intent->client_secret) + ]); + + give_update_meta( + $donation->id, + '_give_stripe_payment_intent_client_secret', + $intent->client_secret + ); + + return new RespondToBrowser([ + 'status' => 200 + ]); + } + + /** + * @inheritDoc + */ + public function getLegacyFormFieldMarkup(int $formId, array $args): string + { + return 'Legacy Stripe Fields Not Supported.'; + } + + /** + * @inheritDoc + */ + public function refundDonation(Donation $donation) + { + // TODO: Implement refundDonation() method. + } + + /** + * @unreleased + */ + private function updateStripePaymentIntent(string $id, array $data): PaymentIntent + { + return PaymentIntent::update( + $id, + $data + ); + } +} diff --git a/src/NextGen/Gateways/Stripe/NextGenCreditCardGateway/nextGenCreditCardGateway.tsx b/src/NextGen/Gateways/Stripe/NextGenStripeGateway/nextGenStripeGateway.tsx similarity index 73% rename from src/NextGen/Gateways/Stripe/NextGenCreditCardGateway/nextGenCreditCardGateway.tsx rename to src/NextGen/Gateways/Stripe/NextGenStripeGateway/nextGenStripeGateway.tsx index c55627022..1f97957bf 100644 --- a/src/NextGen/Gateways/Stripe/NextGenCreditCardGateway/nextGenCreditCardGateway.tsx +++ b/src/NextGen/Gateways/Stripe/NextGenStripeGateway/nextGenStripeGateway.tsx @@ -1,6 +1,7 @@ import {loadStripe, Stripe, StripeElements} from '@stripe/stripe-js'; import {Elements, PaymentElement, useElements, useStripe} from '@stripe/react-stripe-js'; import type {Gateway, GatewaySettings} from '@givewp/forms/types'; +import axios from 'axios'; const StripeFields = ({gateway}) => { const stripe = useStripe(); @@ -20,6 +21,8 @@ interface StripeSettings extends GatewaySettings { stripeConnectAccountId: string; stripeClientSecret: string; successUrl: string; + updatePaymentIntentUrl: string; + stripePaymentIntentId: string; } interface StripeGateway extends Gateway { @@ -49,18 +52,34 @@ const stripeGateway: StripeGateway = { }; }, beforeCreatePayment: async function (values): Promise { - window.alert('create payment with gateway'); - if (!this.stripe || !this.elements) { // Stripe.js has not yet loaded. // Make sure to disable form submission until Stripe.js has loaded. return; } + // update the payment intent on the server + const updatePaymentIntentResponse = await axios.post(this.settings.updatePaymentIntentUrl, { + ...values + }); + + // tell elements to fetch updates + if (updatePaymentIntentResponse.data.data.status === 'requires_payment_method') { + const {error} = await this.elements.fetchUpdates(); + + if (error) { + throw new Error(error.message); + } + } + + return { + stripePaymentIntentId: this.settings.stripePaymentIntentId + } + }, + afterCreatePayment: async function (values): Promise { const {error} = await this.stripe.confirmPayment({ elements: this.elements, confirmParams: { - // Make sure to change this to your payment completion page return_url: this.settings.successUrl, }, }); @@ -71,15 +90,15 @@ const stripeGateway: StripeGateway = { // be redirected to an intermediate site first to authorize the payment, then // redirected to the `return_url`. if (error.type === 'card_error' || error.type === 'validation_error') { - console.log(error.message); - } else { - console.log('An unexpected error occurred.'); + throw new Error(error.message); + } else if (error) { + throw new Error(error.message); } }, Fields() { return ( - + ); }, diff --git a/src/NextGen/ServiceProvider.php b/src/NextGen/ServiceProvider.php index c4b7a3f04..7a4dd13cf 100644 --- a/src/NextGen/ServiceProvider.php +++ b/src/NextGen/ServiceProvider.php @@ -4,7 +4,7 @@ use Give\Framework\PaymentGateways\PaymentGatewayRegister; use Give\NextGen\Gateways\NextGenTestGateway\NextGenTestGateway; -use Give\NextGen\Gateways\Stripe\NextGenCreditCardGateway\NextGenCreditCardGateway; +use Give\NextGen\Gateways\Stripe\NextGenStripeGateway\NextGenStripeGateway; use Give\ServiceProviders\ServiceProvider as ServiceProviderInterface; /** @@ -26,7 +26,7 @@ public function boot() { add_action('givewp_register_payment_gateway', function (PaymentGatewayRegister $registrar) { $registrar->registerGateway(NextGenTestGateway::class); - $registrar->registerGateway(NextGenCreditCardGateway::class); + $registrar->registerGateway(NextGenStripeGateway::class); }); } } diff --git a/webpack.config.js b/webpack.config.js index 9ac8b333c..60c5d24c1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -24,8 +24,8 @@ module.exports = { donationFormBlock: srcPath('NextGen/DonationForm/Blocks/DonationFormBlock/block.js'), donationFormBlockApp: srcPath('NextGen/DonationForm/Blocks/DonationFormBlock/app/DonationFormBlockApp.tsx'), donationFormRegistrars: srcPath('NextGen/DonationForm/Registrars/resources/registrars.ts'), - nextGenCreditCardGateway: srcPath( - 'NextGen/Gateways/Stripe/NextGenCreditCardGateway/nextGenCreditCardGateway.tsx' + nextGenStripeGateway: srcPath( + 'NextGen/Gateways/Stripe/NextGenStripeGateway/nextGenStripeGateway.tsx' ), nextGenTestGateway: srcPath('NextGen/Gateways/NextGenTestGateway/nextGenTestGateway.tsx'), }, From 486222e8ac5fc38654f2626a2dd5b95e75ab1dc6 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Wed, 1 Jun 2022 15:45:37 -0400 Subject: [PATCH 02/10] refactor: move intent updates into createPayment --- .../DonationFormBlock/app/form/Form.tsx | 2 +- src/NextGen/DonationForm/resources/types.ts | 2 +- .../NextGenStripeGateway.php | 92 ++++++------------- .../nextGenStripeGateway.tsx | 28 ++---- 4 files changed, 38 insertions(+), 86 deletions(-) diff --git a/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/form/Form.tsx b/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/form/Form.tsx index 394b2c693..4e5142507 100644 --- a/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/form/Form.tsx +++ b/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/form/Form.tsx @@ -65,7 +65,7 @@ const handleSubmitRequest = async (values, setError, gateway: Gateway) => { try { if (gateway.afterCreatePayment) { - await gateway.afterCreatePayment(values); + await gateway.afterCreatePayment({...request.data.data}); } } catch (error) { return setError('FORM_ERROR', {message: error.message}); diff --git a/src/NextGen/DonationForm/resources/types.ts b/src/NextGen/DonationForm/resources/types.ts index 78f094a21..8c8940ee5 100644 --- a/src/NextGen/DonationForm/resources/types.ts +++ b/src/NextGen/DonationForm/resources/types.ts @@ -79,7 +79,7 @@ export interface Gateway { /** * A hook before the form is submitted. */ - afterCreatePayment?(values: FormData): Promise | Error; + afterCreatePayment?(response: object): Promise | Error; } export interface Template {} diff --git a/src/NextGen/Gateways/Stripe/NextGenStripeGateway/NextGenStripeGateway.php b/src/NextGen/Gateways/Stripe/NextGenStripeGateway/NextGenStripeGateway.php index 164715f19..5f36af41e 100644 --- a/src/NextGen/Gateways/Stripe/NextGenStripeGateway/NextGenStripeGateway.php +++ b/src/NextGen/Gateways/Stripe/NextGenStripeGateway/NextGenStripeGateway.php @@ -5,7 +5,6 @@ use Give\Donations\Models\Donation; use Give\Donations\Models\DonationNote; use Give\Framework\EnqueueScript; -use Give\Framework\Http\Response\Types\JsonResponse; use Give\Framework\PaymentGateways\Commands\GatewayCommand; use Give\Framework\PaymentGateways\Commands\RespondToBrowser; use Give\Framework\PaymentGateways\PaymentGateway; @@ -14,11 +13,8 @@ use Give\Helpers\Call; use Give\PaymentGateways\Gateways\Stripe\Actions\SaveDonationSummary; use Stripe\Customer; -use Stripe\Exception\ApiErrorException; use Stripe\PaymentIntent; -use function Give\Framework\Http\Response\response; - /** * @unreleased */ @@ -99,13 +95,6 @@ public function formSettings($formId): array 'stripeConnectedAccountKey' => $stripeConnectedAccountKey, 'successUrl' => give_get_success_page_uri(), 'stripePaymentIntentId' => $stripePaymentIntent->id, - 'updatePaymentIntentUrl' => $this->generateGatewayRouteUrl( - 'updatePaymentIntent', - [ - 'stripePaymentIntentId' => $stripePaymentIntent->id, - 'stripeConnectedAccountKey' => $stripeConnectedAccountKey - ] - ) ]; } @@ -126,49 +115,6 @@ private function generateStripePaymentIntent($accountId, Money $amount): Payment ); } - /** - * @unreleased - * - * @throws ApiErrorException - */ - public function updatePaymentIntent(array $queryParams): JsonResponse - { - /** - * Get data from client request - */ - $request = $this->request(); - - $stripeConnectedAccountKey = $queryParams['stripeConnectedAccountKey']; - $stripePaymentIntentId = $queryParams['stripePaymentIntentId']; - $amount = $request->get('amount') * 100; - $firstName = $request->get('firstName'); - $lastName = $request->get('lastName'); - $email = $request->get('email'); - $customerId = give_stripe_get_customer_id($email) ?? ''; - - /** - * Get or create a Stripe customer - */ - $customer = $this->getOrCreateStripeCustomer( - $customerId, - $stripeConnectedAccountKey, - $email, - "$firstName $lastName" - ); - - $intent = $this->updateStripePaymentIntent( - $stripePaymentIntentId, - [ - 'amount' => $amount, - 'customer' => $customer->id - ] - ); - - return response()->json([ - 'status' => $intent->status - ]); - } - /** * Get or create Stripe Customer * @@ -204,26 +150,39 @@ public function getOrCreateStripeCustomer( */ public function createPayment(Donation $donation): GatewayCommand { - $stripePaymentIntentId = $this->request()->get('stripePaymentIntentId'); - $donation->gatewayTransactionId = $stripePaymentIntentId; - $donation->save(); + /** + * Get data from client request + */ + $request = $this->request(); - // @todo: payment method may need to be saved on the webhook side - $paymentIntent = PaymentIntent::retrieve($stripePaymentIntentId); - $paymentMethodId = $paymentIntent->payment_method; + $stripeConnectedAccountKey = $request->get('stripeConnectedAccountKey'); + $stripePaymentIntentId = $request->get('stripePaymentIntentId'); + $amount = $request->get('amount') * 100; + $firstName = $request->get('firstName'); + $lastName = $request->get('lastName'); + $email = $request->get('email'); + $customerId = give_stripe_get_customer_id($email) ?? ''; - give_update_meta($donation->id, '_give_stripe_source_id', $paymentMethodId); + /** + * Get or create a Stripe customer + */ + $customer = $this->getOrCreateStripeCustomer( + $customerId, + $stripeConnectedAccountKey, + $email, + "$firstName $lastName" + ); - DonationNote::create([ - 'donationId' => $donation->id, - 'content' => sprintf(__('Stripe Source/Payment Method ID: %s', 'give'), $paymentMethodId) - ]); + $donation->gatewayTransactionId = $stripePaymentIntentId; + $donation->save(); $donationSummary = Call::invoke(SaveDonationSummary::class, $donation); $intent = $this->updateStripePaymentIntent( $stripePaymentIntentId, [ + 'amount' => $amount, + 'customer' => $customer->id, 'description' => $donationSummary->getSummaryWithDonor(), 'metadata' => give_stripe_prepare_metadata($donation->id), ] @@ -246,7 +205,8 @@ public function createPayment(Donation $donation): GatewayCommand ); return new RespondToBrowser([ - 'status' => 200 + 'status' => 200, + 'intentStatus' => $intent->status ]); } diff --git a/src/NextGen/Gateways/Stripe/NextGenStripeGateway/nextGenStripeGateway.tsx b/src/NextGen/Gateways/Stripe/NextGenStripeGateway/nextGenStripeGateway.tsx index 1f97957bf..34cf7429a 100644 --- a/src/NextGen/Gateways/Stripe/NextGenStripeGateway/nextGenStripeGateway.tsx +++ b/src/NextGen/Gateways/Stripe/NextGenStripeGateway/nextGenStripeGateway.tsx @@ -1,7 +1,6 @@ import {loadStripe, Stripe, StripeElements} from '@stripe/stripe-js'; import {Elements, PaymentElement, useElements, useStripe} from '@stripe/react-stripe-js'; import type {Gateway, GatewaySettings} from '@givewp/forms/types'; -import axios from 'axios'; const StripeFields = ({gateway}) => { const stripe = useStripe(); @@ -21,7 +20,6 @@ interface StripeSettings extends GatewaySettings { stripeConnectAccountId: string; stripeClientSecret: string; successUrl: string; - updatePaymentIntentUrl: string; stripePaymentIntentId: string; } @@ -58,25 +56,19 @@ const stripeGateway: StripeGateway = { return; } - // update the payment intent on the server - const updatePaymentIntentResponse = await axios.post(this.settings.updatePaymentIntentUrl, { - ...values - }); - - // tell elements to fetch updates - if (updatePaymentIntentResponse.data.data.status === 'requires_payment_method') { - const {error} = await this.elements.fetchUpdates(); - - if (error) { - throw new Error(error.message); - } - } - return { - stripePaymentIntentId: this.settings.stripePaymentIntentId + ...this.settings } }, - afterCreatePayment: async function (values): Promise { + afterCreatePayment: async function (response: { status: string, intentStatus: string }): Promise { + if (response.intentStatus === 'requires_payment_method') { + const {error: fetchUpdatesError} = await this.elements.fetchUpdates(); + + if (fetchUpdatesError) { + throw new Error(fetchUpdatesError.message); + } + } + const {error} = await this.stripe.confirmPayment({ elements: this.elements, confirmParams: { From 249c4362220efb2896a42a93375dce95b97f9608 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Thu, 2 Jun 2022 10:48:03 -0400 Subject: [PATCH 03/10] refactor: cleanup gateway transactions with repository trait methods --- .../NextGenStripeGateway.php | 129 +++------------ .../NextGenStripeRepository.php | 147 ++++++++++++++++++ 2 files changed, 171 insertions(+), 105 deletions(-) create mode 100644 src/NextGen/Gateways/Stripe/NextGenStripeGateway/NextGenStripeRepository.php diff --git a/src/NextGen/Gateways/Stripe/NextGenStripeGateway/NextGenStripeGateway.php b/src/NextGen/Gateways/Stripe/NextGenStripeGateway/NextGenStripeGateway.php index 5f36af41e..f9e572c46 100644 --- a/src/NextGen/Gateways/Stripe/NextGenStripeGateway/NextGenStripeGateway.php +++ b/src/NextGen/Gateways/Stripe/NextGenStripeGateway/NextGenStripeGateway.php @@ -3,17 +3,13 @@ namespace Give\NextGen\Gateways\Stripe\NextGenStripeGateway; use Give\Donations\Models\Donation; -use Give\Donations\Models\DonationNote; use Give\Framework\EnqueueScript; use Give\Framework\PaymentGateways\Commands\GatewayCommand; use Give\Framework\PaymentGateways\Commands\RespondToBrowser; use Give\Framework\PaymentGateways\PaymentGateway; use Give\Framework\PaymentGateways\Traits\HasRequest; use Give\Framework\Support\ValueObjects\Money; -use Give\Helpers\Call; -use Give\PaymentGateways\Gateways\Stripe\Actions\SaveDonationSummary; -use Stripe\Customer; -use Stripe\PaymentIntent; +use Stripe\Exception\ApiErrorException; /** * @unreleased @@ -21,10 +17,7 @@ class NextGenStripeGateway extends PaymentGateway { use HasRequest; - - public $routeMethods = [ - 'updatePaymentIntent' - ]; + use NextGenStripeRepository; /** * @inheritDoc @@ -47,7 +40,7 @@ public function getId(): string */ public function getName(): string { - return __('Next Gen Stripe - Credit Card', 'give'); + return __('Next Gen Stripe', 'give'); } /** @@ -55,7 +48,7 @@ public function getName(): string */ public function getPaymentMethodLabel(): string { - return __('Next Gen Stripe - Credit Card', 'give'); + return __('Next Gen Stripe', 'give'); } /** @@ -74,6 +67,7 @@ public function enqueueScript(): EnqueueScript /** * @unreleased + * @throws ApiErrorException */ public function formSettings($formId): array { @@ -90,63 +84,17 @@ public function formSettings($formId): array ); return [ + 'successUrl' => give_get_success_page_uri(), 'stripeKey' => $stripePublishableKey, 'stripeClientSecret' => $stripePaymentIntent->client_secret, 'stripeConnectedAccountKey' => $stripeConnectedAccountKey, - 'successUrl' => give_get_success_page_uri(), 'stripePaymentIntentId' => $stripePaymentIntent->id, ]; } - /** - * Mocking Payment Intent while building out api - * - * @unreleased - */ - private function generateStripePaymentIntent($accountId, Money $amount): PaymentIntent - { - return PaymentIntent::create( - [ - 'amount' => $amount->formatToMinorAmount(), - 'currency' => $amount->getCurrency()->getCode(), - 'automatic_payment_methods' => ['enabled' => true], - ], - ['stripe_account' => $accountId] - ); - } - - /** - * Get or create Stripe Customer - * - * @unreleased - */ - public function getOrCreateStripeCustomer( - string $customerId, - string $connectAccountId, - string $email, - string $name - ): Customer { - // make sure customerId still exists in connect account - if ($customerId) { - $customer = Customer::retrieve($customerId, ['stripe_account' => $connectAccountId]); - } - - // create a new customer if necessary - if (!$customerId || !$customer) { - $customer = Customer::create( - [ - 'name' => $name, - 'email' => $email, - ], - ['stripe_account' => $connectAccountId] - ); - } - - return $customer; - } - /** * @inheritDoc + * @throws ApiErrorException */ public function createPayment(Donation $donation): GatewayCommand { @@ -154,56 +102,38 @@ public function createPayment(Donation $donation): GatewayCommand * Get data from client request */ $request = $this->request(); - $stripeConnectedAccountKey = $request->get('stripeConnectedAccountKey'); $stripePaymentIntentId = $request->get('stripePaymentIntentId'); - $amount = $request->get('amount') * 100; - $firstName = $request->get('firstName'); - $lastName = $request->get('lastName'); - $email = $request->get('email'); - $customerId = give_stripe_get_customer_id($email) ?? ''; /** * Get or create a Stripe customer */ - $customer = $this->getOrCreateStripeCustomer( - $customerId, + $customer = $this->getOrCreateStripeCustomerFromDonation( $stripeConnectedAccountKey, - $email, - "$firstName $lastName" + $donation ); - $donation->gatewayTransactionId = $stripePaymentIntentId; - $donation->save(); - - $donationSummary = Call::invoke(SaveDonationSummary::class, $donation); + /** + * Setup Stripe Payment Intent args + */ + $intentArgs = $this->getPaymentIntentArgsFromDonation($donation, $customer); + /** + * Update Payment Intent + */ $intent = $this->updateStripePaymentIntent( $stripePaymentIntentId, - [ - 'amount' => $amount, - 'customer' => $customer->id, - 'description' => $donationSummary->getSummaryWithDonor(), - 'metadata' => give_stripe_prepare_metadata($donation->id), - ] + $intentArgs ); - DonationNote::create([ - 'donationId' => $donation->id, - 'content' => sprintf(__('Stripe Charge/Payment Intent ID: %s', 'give'), $stripePaymentIntentId) - ]); - - DonationNote::create([ - 'donationId' => $donation->id, - 'content' => sprintf(__('Stripe Payment Intent Client Secret: %s', 'give'), $intent->client_secret) - ]); - - give_update_meta( - $donation->id, - '_give_stripe_payment_intent_client_secret', - $intent->client_secret - ); + /** + * Update Donation Meta + */ + $this->updateDonationMetaFromPaymentIntent($donation, $intent); + /** + * Return response to client + */ return new RespondToBrowser([ 'status' => 200, 'intentStatus' => $intent->status @@ -225,15 +155,4 @@ public function refundDonation(Donation $donation) { // TODO: Implement refundDonation() method. } - - /** - * @unreleased - */ - private function updateStripePaymentIntent(string $id, array $data): PaymentIntent - { - return PaymentIntent::update( - $id, - $data - ); - } } diff --git a/src/NextGen/Gateways/Stripe/NextGenStripeGateway/NextGenStripeRepository.php b/src/NextGen/Gateways/Stripe/NextGenStripeGateway/NextGenStripeRepository.php new file mode 100644 index 000000000..3b15312e9 --- /dev/null +++ b/src/NextGen/Gateways/Stripe/NextGenStripeGateway/NextGenStripeRepository.php @@ -0,0 +1,147 @@ + $amount->formatToMinorAmount(), + 'currency' => $amount->getCurrency()->getCode(), + 'automatic_payment_methods' => ['enabled' => true], + ], + ['stripe_account' => $accountId] + ); + } + + /** + * Get or create Stripe Customer from Donation + * + * @unreleased + * @throws ApiErrorException + * @throws Exception + */ + public function getOrCreateStripeCustomerFromDonation( + string $connectAccountId, + Donation $donation + ): Customer { + $donorCustomerId = give_stripe_get_customer_id($donation->email) ?? ''; + + // make sure customerId still exists in connect account + if ($donorCustomerId) { + $customer = Customer::retrieve($donorCustomerId, ['stripe_account' => $connectAccountId]); + } + + // create a new customer if necessary + if (!$donorCustomerId || !$customer) { + $customer = Customer::create( + [ + 'name' => "$donation->firstName $donation->lastName", + 'email' => $donation->email, + ], + ['stripe_account' => $connectAccountId] + ); + } + + DonationNote::create([ + 'donationId' => $donation->id, + 'content' => sprintf(__('Stripe Customer ID: %s', 'give'), $customer->id) + ]); + + if ($customer->id !== $donorCustomerId) { + give()->donor_meta->update_meta($donation->donorId, give_stripe_get_customer_key(), $customer->id); + } + + give_update_meta($donation->id, give_stripe_get_customer_key(), $customer->id); + + return $customer; + } + + /** + * @unreleased + * @throws ApiErrorException + */ + protected function updateStripePaymentIntent(string $id, array $data): PaymentIntent + { + return PaymentIntent::update( + $id, + $data + ); + } + + /** + * @unreleased + * + * @throws InvalidPropertyName + */ + protected function getPaymentIntentArgsFromDonation(Donation $donation, Customer $customer) + { + // Collect intent args to be updated + $intentArgs = [ + 'amount' => $donation->amount->formatToMinorAmount(), + 'customer' => $customer->id, + 'description' => (Call::invoke(SaveDonationSummary::class, $donation))->getSummaryWithDonor(), + 'metadata' => give_stripe_prepare_metadata($donation->id), + ]; + + // Add application fee, if the Stripe premium add-on is not active. + if (ApplicationFee::canAddfee()) { + $intentArgs['application_fee_amount'] = give_stripe_get_application_fee_amount( + $donation->amount->getAmount() + ); + } + + // Add statement descriptor + $intentArgs['statement_descriptor'] = give_stripe_get_statement_descriptor(); + + // Send Stripe Receipt emails when enabled. + if (give_is_setting_enabled(give_get_option('stripe_receipt_emails'))) { + $intentArgs['receipt_email'] = $donation->email; + } + + return $intentArgs; + } + + /** + * @return void + * @throws Exception + */ + protected function updateDonationMetaFromPaymentIntent(Donation $donation, PaymentIntent $intent) + { + $donation->gatewayTransactionId = $intent->id; + $donation->save(); + + DonationNote::create([ + 'donationId' => $donation->id, + 'content' => sprintf(__('Stripe Charge/Payment Intent ID: %s', 'give'), $intent->id) + ]); + + DonationNote::create([ + 'donationId' => $donation->id, + 'content' => sprintf(__('Stripe Payment Intent Client Secret: %s', 'give'), $intent->client_secret) + ]); + + give_update_meta( + $donation->id, + '_give_stripe_payment_intent_client_secret', + $intent->client_secret + ); + } +} From 4d75913322cff9e0e5c7cc66a5b6ad3bab61ab63 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Thu, 2 Jun 2022 11:57:18 -0400 Subject: [PATCH 04/10] refactor: replace axios with fetch --- package-lock.json | 17 ------------- package.json | 1 - .../DonationFormBlock/app/form/Form.tsx | 8 +++--- .../app/utilities/postData.ts | 25 +++++++++++++++++++ src/NextGen/DonationForm/resources/types.ts | 2 +- 5 files changed, 30 insertions(+), 23 deletions(-) create mode 100644 src/NextGen/DonationForm/Blocks/DonationFormBlock/app/utilities/postData.ts diff --git a/package-lock.json b/package-lock.json index b1cf2bb36..3b26a4237 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,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", @@ -7348,14 +7347,6 @@ "node": ">=4" } }, - "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "dependencies": { - "follow-redirects": "^1.14.0" - } - }, "node_modules/axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -31901,14 +31892,6 @@ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.4.1.tgz", "integrity": "sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw==" }, - "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "requires": { - "follow-redirects": "^1.14.0" - } - }, "axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", diff --git a/package.json b/package.json index 076d5bbe1..a9faf4492 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/form/Form.tsx b/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/form/Form.tsx index 4e5142507..0d36b6343 100644 --- a/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/form/Form.tsx +++ b/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/form/Form.tsx @@ -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 {Field as FieldInterface, Gateway} from '@givewp/forms/types'; +import postData from "../utilities/postData"; const messages = getFieldErrorMessages(); @@ -54,18 +54,18 @@ const handleSubmitRequest = async (values, setError, gateway: Gateway) => { return setError('FORM_ERROR', {message: error.message}); } - const request = await axios.post(donateUrl, { + const request = await postData(donateUrl, { ...values, ...beforeCreatePaymentGatewayResponse, }); - if (request.status !== 200) { + if (!request.response.ok) { return setError('FORM_ERROR', "An error occurred"); } try { if (gateway.afterCreatePayment) { - await gateway.afterCreatePayment({...request.data.data}); + await gateway.afterCreatePayment(request.data); } } catch (error) { return setError('FORM_ERROR', {message: error.message}); diff --git a/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/utilities/postData.ts b/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/utilities/postData.ts new file mode 100644 index 000000000..a3e8fc568 --- /dev/null +++ b/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/utilities/postData.ts @@ -0,0 +1,25 @@ +/** + * @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: 'cors', // no-cors, *cors, same-origin + cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached + credentials: 'include', // include, *same-origin, omit + headers: { + 'Content-Type': 'application/json' + // 'Content-Type': 'application/x-www-form-urlencoded', + }, + 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() + }; +} diff --git a/src/NextGen/DonationForm/resources/types.ts b/src/NextGen/DonationForm/resources/types.ts index 8c8940ee5..6e3c14bec 100644 --- a/src/NextGen/DonationForm/resources/types.ts +++ b/src/NextGen/DonationForm/resources/types.ts @@ -77,7 +77,7 @@ export interface Gateway { beforeCreatePayment?(values: FormData): Promise | Error; /** - * A hook before the form is submitted. + * A hook after the form is submitted. */ afterCreatePayment?(response: object): Promise | Error; } From a62307debb647e1d516ac320eb731774f494442a Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Fri, 3 Jun 2022 11:33:23 -0400 Subject: [PATCH 05/10] fix: pass message key to setError --- .../DonationForm/Blocks/DonationFormBlock/app/form/Form.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/form/Form.tsx b/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/form/Form.tsx index 0d36b6343..2face4ce4 100644 --- a/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/form/Form.tsx +++ b/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/form/Form.tsx @@ -60,7 +60,7 @@ const handleSubmitRequest = async (values, setError, gateway: Gateway) => { }); if (!request.response.ok) { - return setError('FORM_ERROR', "An error occurred"); + return setError('FORM_ERROR', {message: "Something went wrong, please try again or contact support."}); } try { From ae3063830cd4ce87d5fe64d40bcc3ec0ca78a58d Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Fri, 3 Jun 2022 11:55:53 -0400 Subject: [PATCH 06/10] feature: add a next gen gateway interface --- .../NextGenPaymentGatewayInterface.php | 20 +++++++++++++++++++ .../NextGenStripeGateway.php | 5 +++-- 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 src/Framework/PaymentGateways/Contracts/NextGenPaymentGatewayInterface.php diff --git a/src/Framework/PaymentGateways/Contracts/NextGenPaymentGatewayInterface.php b/src/Framework/PaymentGateways/Contracts/NextGenPaymentGatewayInterface.php new file mode 100644 index 000000000..27128a44b --- /dev/null +++ b/src/Framework/PaymentGateways/Contracts/NextGenPaymentGatewayInterface.php @@ -0,0 +1,20 @@ + Date: Tue, 7 Jun 2022 16:46:12 -0400 Subject: [PATCH 07/10] refactor: use same-origin mode in fetch --- .../app/utilities/postData.ts | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/utilities/postData.ts b/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/utilities/postData.ts index a3e8fc568..bb102c2da 100644 --- a/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/utilities/postData.ts +++ b/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/utilities/postData.ts @@ -5,17 +5,16 @@ 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: 'cors', // no-cors, *cors, same-origin - cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached - credentials: 'include', // include, *same-origin, omit - headers: { - 'Content-Type': 'application/json' - // 'Content-Type': 'application/x-www-form-urlencoded', - }, - 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 + 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: 'include', // 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 { From 0f13c2e490388cb380d95c9c826ebca713070abd Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Tue, 7 Jun 2022 16:49:44 -0400 Subject: [PATCH 08/10] refactor: use same-origin for credentials as well --- .../Blocks/DonationFormBlock/app/utilities/postData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/utilities/postData.ts b/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/utilities/postData.ts index bb102c2da..9f6cd3c09 100644 --- a/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/utilities/postData.ts +++ b/src/NextGen/DonationForm/Blocks/DonationFormBlock/app/utilities/postData.ts @@ -8,7 +8,7 @@ export default async function postData(url: string, data: object = {}) { 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: 'include', // include, *same-origin, omit + credentials: 'same-origin', // include, *same-origin, omit headers: { 'Content-Type': 'application/json' }, From fa730b4ecfb172775019ec8713728867e6ef4603 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Tue, 7 Jun 2022 17:06:32 -0400 Subject: [PATCH 09/10] refactor: update to use sessions on donate controller for current receipt functionality --- .../Controllers/DonateController.php | 35 ++++-- .../DataTransferObjects/DonateFormData.php | 111 +++++++++++------- .../DonationForm/Routes/DonateRoute.php | 4 +- 3 files changed, 96 insertions(+), 54 deletions(-) diff --git a/src/NextGen/DonationForm/Controllers/DonateController.php b/src/NextGen/DonationForm/Controllers/DonateController.php index 762ae5e4a..579159d4c 100644 --- a/src/NextGen/DonationForm/Controllers/DonateController.php +++ b/src/NextGen/DonationForm/Controllers/DonateController.php @@ -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 @@ -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); } @@ -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); + } + } } diff --git a/src/NextGen/DonationForm/DataTransferObjects/DonateFormData.php b/src/NextGen/DonationForm/DataTransferObjects/DonateFormData.php index f0d778d40..d0d78bfcc 100644 --- a/src/NextGen/DonationForm/DataTransferObjects/DonateFormData.php +++ b/src/NextGen/DonationForm/DataTransferObjects/DonateFormData.php @@ -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; } } diff --git a/src/NextGen/DonationForm/Routes/DonateRoute.php b/src/NextGen/DonationForm/Routes/DonateRoute.php index 68dcf7d8a..7a7dbbf16 100644 --- a/src/NextGen/DonationForm/Routes/DonateRoute.php +++ b/src/NextGen/DonationForm/Routes/DonateRoute.php @@ -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); From c53b02253f5db5a3e2255f6e14f30f92b1b7c675 Mon Sep 17 00:00:00 2001 From: Jon Waldstein Date: Tue, 7 Jun 2022 17:09:56 -0400 Subject: [PATCH 10/10] refactor: replace Call invoke with native php --- .../Stripe/NextGenStripeGateway/NextGenStripeRepository.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/NextGen/Gateways/Stripe/NextGenStripeGateway/NextGenStripeRepository.php b/src/NextGen/Gateways/Stripe/NextGenStripeGateway/NextGenStripeRepository.php index 3b15312e9..61ef6d421 100644 --- a/src/NextGen/Gateways/Stripe/NextGenStripeGateway/NextGenStripeRepository.php +++ b/src/NextGen/Gateways/Stripe/NextGenStripeGateway/NextGenStripeRepository.php @@ -6,7 +6,6 @@ use Give\Donations\Models\DonationNote; use Give\Framework\Exceptions\Primitives\Exception; use Give\Framework\Support\ValueObjects\Money; -use Give\Helpers\Call; use Give\PaymentGateways\Exceptions\InvalidPropertyName; use Give\PaymentGateways\Gateways\Stripe\Actions\SaveDonationSummary; use Give\PaymentGateways\Stripe\ApplicationFee; @@ -97,7 +96,7 @@ protected function getPaymentIntentArgsFromDonation(Donation $donation, Customer $intentArgs = [ 'amount' => $donation->amount->formatToMinorAmount(), 'customer' => $customer->id, - 'description' => (Call::invoke(SaveDonationSummary::class, $donation))->getSummaryWithDonor(), + 'description' => (new SaveDonationSummary)($donation)->getSummaryWithDonor(), 'metadata' => give_stripe_prepare_metadata($donation->id), ];