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:
parent
3663f8ab86
commit
a3f680964c
@ -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
|
||||
});
|
||||
|
@ -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 />
|
||||
|
@ -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']));
|
||||
|
@ -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}
|
||||
|
@ -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} />
|
||||
);
|
||||
|
@ -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) {
|
||||
|
@ -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} />
|
||||
);
|
||||
|
@ -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} />
|
||||
|
@ -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}
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user