2020-11-25 17:13:45 +01:00
|
|
|
import React, { FormEvent } from 'react';
|
|
|
|
import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js';
|
|
|
|
import { PaymentMethod } from "@stripe/stripe-js";
|
2020-12-01 17:55:23 +01:00
|
|
|
import PaymentAPI from '../api/payment';
|
|
|
|
import { CartItems, PaymentConfirmation } from '../models/payment';
|
|
|
|
import { useTranslation } from 'react-i18next';
|
2020-12-07 13:49:11 +01:00
|
|
|
import { Reservation } from '../models/reservation';
|
2020-11-25 17:13:45 +01:00
|
|
|
|
|
|
|
interface StripeFormProps {
|
|
|
|
onSubmit: () => void,
|
2020-12-07 17:13:05 +01:00
|
|
|
onSuccess: (result: PaymentMethod|PaymentConfirmation|any) => void,
|
2020-11-25 17:13:45 +01:00
|
|
|
onError: (message: string) => void,
|
2020-11-30 16:52:55 +01:00
|
|
|
className?: string,
|
2020-12-01 17:55:23 +01:00
|
|
|
processPayment?: boolean,
|
|
|
|
cartItems?: CartItems
|
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.
|
|
|
|
* The form validation button must be created elsewhere, using the attribute form="stripe-form".
|
|
|
|
*/
|
2020-12-07 17:13:05 +01:00
|
|
|
export const StripeForm: React.FC<StripeFormProps> = ({ onSubmit, onSuccess, onError, children, className, processPayment = true, cartItems}) => {
|
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',
|
|
|
|
card: cardElement,
|
|
|
|
});
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
onError(error.message);
|
2020-12-01 17:55:23 +01:00
|
|
|
} else {
|
|
|
|
if (processPayment) {
|
|
|
|
// process the full payment pipeline, including SCA validation
|
|
|
|
const res = await PaymentAPI.confirm(paymentMethod.id, cartItems);
|
2020-12-07 13:49:11 +01:00
|
|
|
await handleServerConfirmation(res);
|
2020-12-01 17:55:23 +01:00
|
|
|
} else {
|
|
|
|
// we don't want to process the payment, only return the payment method
|
|
|
|
onSuccess(paymentMethod);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Process the server response about the Strong-customer authentication (SCA)
|
2020-12-07 13:49:11 +01:00
|
|
|
* @param response can be a PaymentConfirmation, or a Reservation (if the reservation succeeded), or a Subscription (if the subscription succeeded)
|
|
|
|
* @see app/controllers/api/payments_controller.rb#on_reservation_success
|
|
|
|
* @see app/controllers/api/payments_controller.rb#on_subscription_success
|
|
|
|
* @see app/controllers/api/payments_controller.rb#generate_payment_response
|
2020-12-01 17:55:23 +01:00
|
|
|
*/
|
2020-12-07 13:49:11 +01:00
|
|
|
const handleServerConfirmation = async (response: PaymentConfirmation|any) => {
|
2020-12-01 17:55:23 +01:00
|
|
|
if (response.error) {
|
|
|
|
if (response.error.statusText) {
|
|
|
|
onError(response.error.statusText);
|
|
|
|
} else {
|
|
|
|
onError(`${t('app.shared.messages.payment_card_error')} ${response.error}`);
|
|
|
|
}
|
|
|
|
} else if (response.requires_action) {
|
|
|
|
// 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 {
|
|
|
|
const confirmation = await PaymentAPI.confirm(result.paymentIntent.id, cartItems);
|
2020-12-07 13:49:11 +01:00
|
|
|
await handleServerConfirmation(confirmation);
|
2020-12-01 17:55:23 +01:00
|
|
|
} catch (e) {
|
|
|
|
onError(e);
|
|
|
|
}
|
|
|
|
}
|
2020-11-25 17:13:45 +01:00
|
|
|
} else {
|
2020-12-07 13:49:11 +01:00
|
|
|
onSuccess(response);
|
2020-11-25 17:13:45 +01: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'
|
|
|
|
},
|
|
|
|
},
|
|
|
|
hidePostalCode: true
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
2020-11-30 16:52:55 +01:00
|
|
|
<form onSubmit={handleSubmit} id="stripe-form" className={className}>
|
2020-11-25 17:13:45 +01:00
|
|
|
<CardElement options={cardOptions} />
|
|
|
|
{children}
|
|
|
|
</form>
|
|
|
|
);
|
|
|
|
}
|