2020-11-25 17:13:45 +01:00
|
|
|
import React, { FormEvent } from 'react';
|
|
|
|
import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js';
|
2020-12-01 17:55:23 +01:00
|
|
|
import { useTranslation } from 'react-i18next';
|
2021-04-28 16:41:15 +02:00
|
|
|
import { GatewayFormProps } from '../abstract-payment-modal';
|
2021-09-10 15:16:08 +02:00
|
|
|
import { PaymentConfirmation, StripeSubscription } from '../../../models/payment';
|
2021-04-12 10:45:41 +02:00
|
|
|
import StripeAPI from '../../../api/stripe';
|
2021-06-01 11:01:38 +02:00
|
|
|
import { Invoice } from '../../../models/invoice';
|
2020-11-25 17:13:45 +01:00
|
|
|
|
2020-11-30 16:52:55 +01:00
|
|
|
/**
|
|
|
|
* A form component to collect the credit card details and to create the payment method on Stripe.
|
2021-04-08 15:21:24 +02:00
|
|
|
* The form validation button must be created elsewhere, using the attribute form={formId}.
|
2020-11-30 16:52:55 +01:00
|
|
|
*/
|
2021-06-01 11:01:38 +02:00
|
|
|
export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule = false, cart, customer, operator, formId }) => {
|
2020-12-01 17:55:23 +01:00
|
|
|
const { t } = useTranslation('shared');
|
2020-11-25 17:13:45 +01:00
|
|
|
|
|
|
|
const stripe = useStripe();
|
|
|
|
const elements = useElements();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle the submission of the form. Depending on the configuration, it will create the payment method on Stripe,
|
|
|
|
* or it will process a payment with the inputted card.
|
|
|
|
*/
|
|
|
|
const handleSubmit = async (event: FormEvent): Promise<void> => {
|
|
|
|
event.preventDefault();
|
|
|
|
onSubmit();
|
|
|
|
|
|
|
|
// Stripe.js has not loaded yet
|
|
|
|
if (!stripe || !elements) { return; }
|
|
|
|
|
|
|
|
const cardElement = elements.getElement(CardElement);
|
|
|
|
const { error, paymentMethod } = await stripe.createPaymentMethod({
|
|
|
|
type: 'card',
|
2021-07-01 12:34:10 +02:00
|
|
|
card: cardElement
|
2020-11-25 17:13:45 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
if (error) {
|
2020-12-09 11:35:49 +01:00
|
|
|
// stripe error
|
2020-11-25 17:13:45 +01:00
|
|
|
onError(error.message);
|
2020-12-01 17:55:23 +01:00
|
|
|
} else {
|
2020-12-09 11:35:49 +01:00
|
|
|
try {
|
|
|
|
if (!paymentSchedule) {
|
|
|
|
// process the normal payment pipeline, including SCA validation
|
2021-06-09 19:40:07 +02:00
|
|
|
const res = await StripeAPI.confirmMethod(paymentMethod.id, cart);
|
2020-12-09 11:35:49 +01:00
|
|
|
await handleServerConfirmation(res);
|
2020-12-08 12:26:03 +01:00
|
|
|
} else {
|
2021-09-08 18:57:10 +02:00
|
|
|
const paymentMethodId = paymentMethod.id;
|
2021-09-10 15:16:08 +02:00
|
|
|
const subscription: StripeSubscription = await StripeAPI.paymentSchedule(paymentMethod.id, cart);
|
2021-09-08 18:57:10 +02:00
|
|
|
if (subscription && subscription.status === 'active') {
|
|
|
|
// Subscription is active, no customer actions required.
|
|
|
|
const res = await StripeAPI.confirmPaymentSchedule(subscription.id, cart);
|
2020-12-09 11:35:49 +01:00
|
|
|
onSuccess(res);
|
|
|
|
}
|
2021-09-08 18:57:10 +02:00
|
|
|
const paymentIntent = subscription.latest_invoice.payment_intent;
|
|
|
|
|
|
|
|
if (paymentIntent.status === 'requires_action') {
|
|
|
|
return stripe
|
|
|
|
.confirmCardPayment(paymentIntent.client_secret, {
|
|
|
|
payment_method: paymentMethodId
|
|
|
|
})
|
|
|
|
.then(async (result) => {
|
|
|
|
if (result.error) {
|
|
|
|
throw result.error;
|
|
|
|
} else {
|
|
|
|
if (result.paymentIntent.status === 'succeeded') {
|
|
|
|
const res = await StripeAPI.confirmPaymentSchedule(subscription.id, cart);
|
|
|
|
onSuccess(res);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch((error) => {
|
|
|
|
onError(error.message);
|
|
|
|
});
|
|
|
|
} else if (paymentIntent.status === 'requires_payment_method') {
|
2021-09-10 15:16:08 +02:00
|
|
|
onError(t('app.shared.messages.payment_card_declined'));
|
2021-09-08 18:57:10 +02:00
|
|
|
}
|
2020-12-08 12:26:03 +01:00
|
|
|
}
|
2020-12-09 11:35:49 +01:00
|
|
|
} catch (err) {
|
|
|
|
// catch api errors
|
|
|
|
onError(err);
|
2020-12-01 17:55:23 +01:00
|
|
|
}
|
|
|
|
}
|
2021-07-01 12:34:10 +02:00
|
|
|
};
|
2020-12-01 17:55:23 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Process the server response about the Strong-customer authentication (SCA)
|
2021-06-01 11:01:38 +02:00
|
|
|
* @param response can be a PaymentConfirmation, or an Invoice (if the payment succeeded)
|
|
|
|
* @see app/controllers/api/stripe_controller.rb#confirm_payment
|
2020-12-01 17:55:23 +01:00
|
|
|
*/
|
2021-06-01 11:01:38 +02:00
|
|
|
const handleServerConfirmation = async (response: PaymentConfirmation|Invoice) => {
|
|
|
|
if ('error' in response) {
|
2020-12-01 17:55:23 +01:00
|
|
|
if (response.error.statusText) {
|
|
|
|
onError(response.error.statusText);
|
|
|
|
} else {
|
|
|
|
onError(`${t('app.shared.messages.payment_card_error')} ${response.error}`);
|
|
|
|
}
|
2021-06-01 11:01:38 +02:00
|
|
|
} else if ('requires_action' in response) {
|
2020-12-01 17:55:23 +01:00
|
|
|
// Use Stripe.js to handle required card action
|
|
|
|
const result = await stripe.handleCardAction(response.payment_intent_client_secret);
|
|
|
|
if (result.error) {
|
|
|
|
onError(result.error.message);
|
|
|
|
} else {
|
|
|
|
// The card action has been handled
|
|
|
|
// The PaymentIntent can be confirmed again on the server
|
|
|
|
try {
|
2021-06-09 19:40:07 +02:00
|
|
|
const confirmation = await StripeAPI.confirmIntent(result.paymentIntent.id, cart);
|
2020-12-07 13:49:11 +01:00
|
|
|
await handleServerConfirmation(confirmation);
|
2020-12-01 17:55:23 +01:00
|
|
|
} catch (e) {
|
|
|
|
onError(e);
|
|
|
|
}
|
|
|
|
}
|
2021-06-01 11:01:38 +02:00
|
|
|
} else if ('id' in response) {
|
2020-12-07 13:49:11 +01:00
|
|
|
onSuccess(response);
|
2021-06-01 11:01:38 +02:00
|
|
|
} else {
|
|
|
|
console.error(`[StripeForm] unknown response received: ${response}`);
|
2020-11-25 17:13:45 +01:00
|
|
|
}
|
2021-07-01 12:34:10 +02:00
|
|
|
};
|
2020-12-01 17:55:23 +01:00
|
|
|
|
2020-11-25 17:13:45 +01:00
|
|
|
/**
|
|
|
|
* Options for the Stripe's card input
|
|
|
|
*/
|
|
|
|
const cardOptions = {
|
|
|
|
style: {
|
|
|
|
base: {
|
|
|
|
fontSize: '16px',
|
|
|
|
color: '#424770',
|
|
|
|
'::placeholder': { color: '#aab7c4' }
|
|
|
|
},
|
|
|
|
invalid: {
|
|
|
|
color: '#9e2146',
|
|
|
|
iconColor: '#9e2146'
|
2021-07-01 12:34:10 +02:00
|
|
|
}
|
2020-11-25 17:13:45 +01:00
|
|
|
},
|
|
|
|
hidePostalCode: true
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
2021-07-01 12:34:10 +02:00
|
|
|
<form onSubmit={handleSubmit} id={formId} className={className || ''}>
|
2020-11-25 17:13:45 +01:00
|
|
|
<CardElement options={cardOptions} />
|
|
|
|
{children}
|
|
|
|
</form>
|
|
|
|
);
|
2021-07-01 12:34:10 +02:00
|
|
|
};
|