1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-12-01 12:24:28 +01:00

handle stripe errors while local payments

This commit is contained in:
Sylvain 2021-10-07 16:43:51 +02:00
parent 3663f8ab86
commit a3f680964c
10 changed files with 67 additions and 28 deletions

View File

@ -21,8 +21,8 @@ export default class StripeAPI {
return res?.data;
}
static async createSubscription (paymentMethodId: string, cartItems: ShoppingCart): Promise<PaymentConfirmation|PaymentSchedule> {
const res: AxiosResponse = await apiClient.post('/api/stripe/create_subscription', {
static async setupSubscription (paymentMethodId: string, cartItems: ShoppingCart): Promise<PaymentConfirmation|PaymentSchedule> {
const res: AxiosResponse = await apiClient.post('/api/stripe/setup_subscription', {
payment_method_id: paymentMethodId,
cart_items: cartItems
});

View File

@ -1,4 +1,4 @@
import React, { FunctionComponent, ReactNode, useEffect, useState } from 'react';
import React, { FunctionComponent, ReactNode, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import WalletLib from '../../lib/wallet';
import { WalletInfo } from '../wallet-info';
@ -34,6 +34,7 @@ interface AbstractPaymentModalProps {
isOpen: boolean,
toggleModal: () => void,
afterSuccess: (result: Invoice|PaymentSchedule) => void,
onError: (message: string) => void,
cart: ShoppingCart,
currentUser: User,
schedule?: PaymentSchedule,
@ -55,7 +56,7 @@ interface AbstractPaymentModalProps {
* This component must not be called directly but must be extended for each implemented payment gateway
* @see https://reactjs.org/docs/composition-vs-inheritance.html
*/
export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, cart, currentUser, schedule, customer, logoFooter, GatewayForm, formId, className, formClassName, title, preventCgv, preventScheduleInfo, modalSize }) => {
export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, cart, currentUser, schedule, customer, logoFooter, GatewayForm, formId, className, formClassName, title, preventCgv, preventScheduleInfo, modalSize }) => {
// customer's wallet
const [wallet, setWallet] = useState<Wallet>(null);
// server-computed price with all details
@ -74,6 +75,8 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
const [gateway, setGateway] = useState<string>(null);
// the sales conditions
const [cgv, setCgv] = useState<CustomAsset>(null);
// is the component mounted
const mounted = useRef(false);
const { t } = useTranslation('shared');
@ -81,11 +84,14 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
* When the component loads first, get the name of the currently active payment modal
*/
useEffect(() => {
mounted.current = true;
CustomAssetAPI.get(CustomAssetName.CgvFile).then(asset => setCgv(asset));
SettingAPI.get(SettingName.PaymentGateway).then((setting) => {
// we capitalize the first letter of the name
setGateway(setting.value.replace(/^\w/, (c) => c.toUpperCase()));
});
return () => { mounted.current = false; };
}, []);
/**
@ -153,8 +159,12 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
* When the payment form raises an error, it is handled by this callback which display it in the modal.
*/
const handleFormError = (message: string): void => {
setSubmitState(false);
setErrors(message);
if (mounted.current) {
setSubmitState(false);
setErrors(message);
} else {
onError(message);
}
};
/**
@ -201,7 +211,7 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
{errors}
</div>}
{hasPaymentScheduleInfo() && <div className="payment-schedule-info">
<HtmlTranslate trKey="app.shared.payment.payment_schedule_html" options={{ DEADLINES: schedule.items.length, GATEWAY: gateway }} />
<HtmlTranslate trKey="app.shared.payment.payment_schedule_html" options={{ DEADLINES: `${schedule.items.length}`, GATEWAY: gateway }} />
</div>}
{hasCgv() && <div className="terms-of-sales">
<input type="checkbox" id="acceptToS" name="acceptCondition" checked={tos} onChange={toggleTos} required />

View File

@ -17,6 +17,7 @@ interface LocalPaymentModalProps {
isOpen: boolean,
toggleModal: () => void,
afterSuccess: (result: Invoice|PaymentSchedule) => void,
onError: (message: string) => void,
cart: ShoppingCart,
currentUser: User,
schedule?: PaymentSchedule,
@ -26,7 +27,7 @@ interface LocalPaymentModalProps {
/**
* This component enables a privileged user to confirm a local payments.
*/
const LocalPaymentModalComponent: React.FC<LocalPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, cart, currentUser, schedule, customer }) => {
const LocalPaymentModalComponent: React.FC<LocalPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, cart, currentUser, schedule, customer }) => {
const { t } = useTranslation('admin');
/**
@ -71,6 +72,7 @@ const LocalPaymentModalComponent: React.FC<LocalPaymentModalProps> = ({ isOpen,
cart={cart}
customer={customer}
afterSuccess={afterSuccess}
onError={onError}
schedule={schedule}
GatewayForm={renderForm}
modalSize={schedule ? ModalSize.large : ModalSize.medium}
@ -79,12 +81,12 @@ const LocalPaymentModalComponent: React.FC<LocalPaymentModalProps> = ({ isOpen,
);
};
export const LocalPaymentModal: React.FC<LocalPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, currentUser, schedule, cart, customer }) => {
export const LocalPaymentModal: React.FC<LocalPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule, cart, customer }) => {
return (
<Loader>
<LocalPaymentModalComponent isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} currentUser={currentUser} schedule={schedule} cart={cart} customer={customer} />
<LocalPaymentModalComponent isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} onError={onError} currentUser={currentUser} schedule={schedule} cart={cart} customer={customer} />
</Loader>
);
};
Application.Components.component('localPaymentModal', react2angular(LocalPaymentModal, ['isOpen', 'toggleModal', 'afterSuccess', 'currentUser', 'schedule', 'cart', 'customer']));
Application.Components.component('localPaymentModal', react2angular(LocalPaymentModal, ['isOpen', 'toggleModal', 'afterSuccess', 'onError', 'currentUser', 'schedule', 'cart', 'customer']));

View File

@ -47,6 +47,7 @@ const PaymentModalComponent: React.FC<PaymentModalProps> = ({ isOpen, toggleModa
return <StripeModal isOpen={isOpen}
toggleModal={toggleModal}
afterSuccess={afterSuccess}
onError={onError}
cart={cart}
currentUser={currentUser}
schedule={schedule}
@ -60,6 +61,7 @@ const PaymentModalComponent: React.FC<PaymentModalProps> = ({ isOpen, toggleModa
return <PayZenModal isOpen={isOpen}
toggleModal={toggleModal}
afterSuccess={afterSuccess}
onError={onError}
cart={cart}
currentUser={currentUser}
schedule={schedule}

View File

@ -14,6 +14,7 @@ interface PayZenModalProps {
isOpen: boolean,
toggleModal: () => void,
afterSuccess: (result: Invoice|PaymentSchedule) => void,
onError: (message: string) => void,
cart: ShoppingCart,
currentUser: User,
schedule?: PaymentSchedule,
@ -27,7 +28,7 @@ interface PayZenModalProps {
* This component should not be called directly. Prefer using <PaymentModal> which can handle the configuration
* of a different payment gateway.
*/
export const PayZenModal: React.FC<PayZenModalProps> = ({ isOpen, toggleModal, afterSuccess, cart, currentUser, schedule, customer }) => {
export const PayZenModal: React.FC<PayZenModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, cart, currentUser, schedule, customer }) => {
/**
* Return the logos, shown in the modal footer.
*/
@ -71,6 +72,7 @@ export const PayZenModal: React.FC<PayZenModalProps> = ({ isOpen, toggleModal, a
cart={cart}
customer={customer}
afterSuccess={afterSuccess}
onError={onError}
schedule={schedule}
GatewayForm={renderForm} />
);

View File

@ -44,8 +44,8 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
const res = await StripeAPI.confirmMethod(paymentMethod.id, cart);
await handleServerConfirmation(res);
} else {
const res = await StripeAPI.createSubscription(paymentMethod.id, cart);
await handleServerConfirmation(res);
const res = await StripeAPI.setupSubscription(paymentMethod.id, cart);
await handleServerConfirmation(res, paymentMethod.id);
}
} catch (err) {
// catch api errors
@ -57,9 +57,10 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
/**
* Process the server response about the Strong-customer authentication (SCA)
* @param response can be a PaymentConfirmation, or an Invoice/PaymentSchedule (if the payment succeeded)
* @param paymentMethodId ID of the payment method, required only when confirming a payment schedule
* @see app/controllers/api/stripe_controller.rb#confirm_payment
*/
const handleServerConfirmation = async (response: PaymentConfirmation|Invoice|PaymentSchedule) => {
const handleServerConfirmation = async (response: PaymentConfirmation|Invoice|PaymentSchedule, paymentMethodId?: string) => {
if ('error' in response) {
if (response.error.statusText) {
onError(response.error.statusText);
@ -67,24 +68,34 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
onError(`${t('app.shared.messages.payment_card_error')} ${response.error}`);
}
} else if ('requires_action' in response) {
// 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 {
if (response.type === 'payment') {
if (response.type === 'payment') {
// 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 StripeAPI.confirmIntent(result.paymentIntent.id, cart);
await handleServerConfirmation(confirmation);
} catch (e) {
onError(e);
}
if (response.type === 'subscription') {
}
} else if (response.type === 'subscription') {
const result = await stripe.confirmCardPayment(response.payment_intent_client_secret, {
payment_method: paymentMethodId
});
if (result.error) {
onError(result.error.message);
} else {
try {
const confirmation = await StripeAPI.confirmSubscription(response.subscription_id, cart);
await handleServerConfirmation(confirmation);
} catch (e) {
onError(e);
}
} catch (e) {
onError(e);
}
}
} else if ('id' in response) {

View File

@ -15,6 +15,7 @@ interface StripeModalProps {
isOpen: boolean,
toggleModal: () => void,
afterSuccess: (result: Invoice|PaymentSchedule) => void,
onError: (message: string) => void,
cart: ShoppingCart,
currentUser: User,
schedule?: PaymentSchedule,
@ -28,7 +29,7 @@ interface StripeModalProps {
* This component should not be called directly. Prefer using <PaymentModal> which can handle the configuration
* of a different payment gateway.
*/
export const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuccess, cart, currentUser, schedule, customer }) => {
export const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, cart, currentUser, schedule, customer }) => {
/**
* Return the logos, shown in the modal footer.
*/
@ -75,6 +76,7 @@ export const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, a
cart={cart}
customer={customer}
afterSuccess={afterSuccess}
onError={onError}
schedule={schedule}
GatewayForm={renderForm} />
);

View File

@ -162,6 +162,7 @@ export const ProposePacksModal: React.FC<ProposePacksModalProps> = ({ isOpen, to
<LocalPaymentModal isOpen={localPaymentModal}
toggleModal={toggleLocalPaymentModal}
afterSuccess={handlePackBought}
onError={onError}
cart={cart}
currentUser={operator}
customer={customer} />

View File

@ -362,6 +362,14 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
afterPayment(invoice);
};
/**
* Invoked when something wrong occurred after the payment dialog has been closed
* @param message {string}
*/
$scope.onLocalPaymentError = (message) => {
growl.error(message);
};
/**
* Invoked when something wrong occurred during the payment dialog initialization
* @param message {string}

View File

@ -214,6 +214,7 @@
<local-payment-modal is-open="localPayment.showModal"
toggle-modal="toggleLocalPaymentModal"
after-success="afterLocalPaymentSuccess"
on-error="onLocalPaymentError"
cart="localPayment.cartItems"
current-user="currentUser"
customer="user"