mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-03-01 23:29:23 +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;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async createSubscription (paymentMethodId: string, cartItems: ShoppingCart): Promise<PaymentConfirmation|PaymentSchedule> {
|
static async setupSubscription (paymentMethodId: string, cartItems: ShoppingCart): Promise<PaymentConfirmation|PaymentSchedule> {
|
||||||
const res: AxiosResponse = await apiClient.post('/api/stripe/create_subscription', {
|
const res: AxiosResponse = await apiClient.post('/api/stripe/setup_subscription', {
|
||||||
payment_method_id: paymentMethodId,
|
payment_method_id: paymentMethodId,
|
||||||
cart_items: cartItems
|
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 { useTranslation } from 'react-i18next';
|
||||||
import WalletLib from '../../lib/wallet';
|
import WalletLib from '../../lib/wallet';
|
||||||
import { WalletInfo } from '../wallet-info';
|
import { WalletInfo } from '../wallet-info';
|
||||||
@ -34,6 +34,7 @@ interface AbstractPaymentModalProps {
|
|||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
toggleModal: () => void,
|
toggleModal: () => void,
|
||||||
afterSuccess: (result: Invoice|PaymentSchedule) => void,
|
afterSuccess: (result: Invoice|PaymentSchedule) => void,
|
||||||
|
onError: (message: string) => void,
|
||||||
cart: ShoppingCart,
|
cart: ShoppingCart,
|
||||||
currentUser: User,
|
currentUser: User,
|
||||||
schedule?: PaymentSchedule,
|
schedule?: PaymentSchedule,
|
||||||
@ -55,7 +56,7 @@ interface AbstractPaymentModalProps {
|
|||||||
* This component must not be called directly but must be extended for each implemented payment gateway
|
* 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
|
* @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
|
// customer's wallet
|
||||||
const [wallet, setWallet] = useState<Wallet>(null);
|
const [wallet, setWallet] = useState<Wallet>(null);
|
||||||
// server-computed price with all details
|
// server-computed price with all details
|
||||||
@ -74,6 +75,8 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
|
|||||||
const [gateway, setGateway] = useState<string>(null);
|
const [gateway, setGateway] = useState<string>(null);
|
||||||
// the sales conditions
|
// the sales conditions
|
||||||
const [cgv, setCgv] = useState<CustomAsset>(null);
|
const [cgv, setCgv] = useState<CustomAsset>(null);
|
||||||
|
// is the component mounted
|
||||||
|
const mounted = useRef(false);
|
||||||
|
|
||||||
const { t } = useTranslation('shared');
|
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
|
* When the component loads first, get the name of the currently active payment modal
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
mounted.current = true;
|
||||||
CustomAssetAPI.get(CustomAssetName.CgvFile).then(asset => setCgv(asset));
|
CustomAssetAPI.get(CustomAssetName.CgvFile).then(asset => setCgv(asset));
|
||||||
SettingAPI.get(SettingName.PaymentGateway).then((setting) => {
|
SettingAPI.get(SettingName.PaymentGateway).then((setting) => {
|
||||||
// we capitalize the first letter of the name
|
// we capitalize the first letter of the name
|
||||||
setGateway(setting.value.replace(/^\w/, (c) => c.toUpperCase()));
|
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.
|
* When the payment form raises an error, it is handled by this callback which display it in the modal.
|
||||||
*/
|
*/
|
||||||
const handleFormError = (message: string): void => {
|
const handleFormError = (message: string): void => {
|
||||||
|
if (mounted.current) {
|
||||||
setSubmitState(false);
|
setSubmitState(false);
|
||||||
setErrors(message);
|
setErrors(message);
|
||||||
|
} else {
|
||||||
|
onError(message);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -201,7 +211,7 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
|
|||||||
{errors}
|
{errors}
|
||||||
</div>}
|
</div>}
|
||||||
{hasPaymentScheduleInfo() && <div className="payment-schedule-info">
|
{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>}
|
</div>}
|
||||||
{hasCgv() && <div className="terms-of-sales">
|
{hasCgv() && <div className="terms-of-sales">
|
||||||
<input type="checkbox" id="acceptToS" name="acceptCondition" checked={tos} onChange={toggleTos} required />
|
<input type="checkbox" id="acceptToS" name="acceptCondition" checked={tos} onChange={toggleTos} required />
|
||||||
|
@ -17,6 +17,7 @@ interface LocalPaymentModalProps {
|
|||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
toggleModal: () => void,
|
toggleModal: () => void,
|
||||||
afterSuccess: (result: Invoice|PaymentSchedule) => void,
|
afterSuccess: (result: Invoice|PaymentSchedule) => void,
|
||||||
|
onError: (message: string) => void,
|
||||||
cart: ShoppingCart,
|
cart: ShoppingCart,
|
||||||
currentUser: User,
|
currentUser: User,
|
||||||
schedule?: PaymentSchedule,
|
schedule?: PaymentSchedule,
|
||||||
@ -26,7 +27,7 @@ interface LocalPaymentModalProps {
|
|||||||
/**
|
/**
|
||||||
* This component enables a privileged user to confirm a local payments.
|
* 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');
|
const { t } = useTranslation('admin');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -71,6 +72,7 @@ const LocalPaymentModalComponent: React.FC<LocalPaymentModalProps> = ({ isOpen,
|
|||||||
cart={cart}
|
cart={cart}
|
||||||
customer={customer}
|
customer={customer}
|
||||||
afterSuccess={afterSuccess}
|
afterSuccess={afterSuccess}
|
||||||
|
onError={onError}
|
||||||
schedule={schedule}
|
schedule={schedule}
|
||||||
GatewayForm={renderForm}
|
GatewayForm={renderForm}
|
||||||
modalSize={schedule ? ModalSize.large : ModalSize.medium}
|
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 (
|
return (
|
||||||
<Loader>
|
<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>
|
</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}
|
return <StripeModal isOpen={isOpen}
|
||||||
toggleModal={toggleModal}
|
toggleModal={toggleModal}
|
||||||
afterSuccess={afterSuccess}
|
afterSuccess={afterSuccess}
|
||||||
|
onError={onError}
|
||||||
cart={cart}
|
cart={cart}
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
schedule={schedule}
|
schedule={schedule}
|
||||||
@ -60,6 +61,7 @@ const PaymentModalComponent: React.FC<PaymentModalProps> = ({ isOpen, toggleModa
|
|||||||
return <PayZenModal isOpen={isOpen}
|
return <PayZenModal isOpen={isOpen}
|
||||||
toggleModal={toggleModal}
|
toggleModal={toggleModal}
|
||||||
afterSuccess={afterSuccess}
|
afterSuccess={afterSuccess}
|
||||||
|
onError={onError}
|
||||||
cart={cart}
|
cart={cart}
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
schedule={schedule}
|
schedule={schedule}
|
||||||
|
@ -14,6 +14,7 @@ interface PayZenModalProps {
|
|||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
toggleModal: () => void,
|
toggleModal: () => void,
|
||||||
afterSuccess: (result: Invoice|PaymentSchedule) => void,
|
afterSuccess: (result: Invoice|PaymentSchedule) => void,
|
||||||
|
onError: (message: string) => void,
|
||||||
cart: ShoppingCart,
|
cart: ShoppingCart,
|
||||||
currentUser: User,
|
currentUser: User,
|
||||||
schedule?: PaymentSchedule,
|
schedule?: PaymentSchedule,
|
||||||
@ -27,7 +28,7 @@ interface PayZenModalProps {
|
|||||||
* This component should not be called directly. Prefer using <PaymentModal> which can handle the configuration
|
* This component should not be called directly. Prefer using <PaymentModal> which can handle the configuration
|
||||||
* of a different payment gateway.
|
* 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.
|
* Return the logos, shown in the modal footer.
|
||||||
*/
|
*/
|
||||||
@ -71,6 +72,7 @@ export const PayZenModal: React.FC<PayZenModalProps> = ({ isOpen, toggleModal, a
|
|||||||
cart={cart}
|
cart={cart}
|
||||||
customer={customer}
|
customer={customer}
|
||||||
afterSuccess={afterSuccess}
|
afterSuccess={afterSuccess}
|
||||||
|
onError={onError}
|
||||||
schedule={schedule}
|
schedule={schedule}
|
||||||
GatewayForm={renderForm} />
|
GatewayForm={renderForm} />
|
||||||
);
|
);
|
||||||
|
@ -44,8 +44,8 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
|
|||||||
const res = await StripeAPI.confirmMethod(paymentMethod.id, cart);
|
const res = await StripeAPI.confirmMethod(paymentMethod.id, cart);
|
||||||
await handleServerConfirmation(res);
|
await handleServerConfirmation(res);
|
||||||
} else {
|
} else {
|
||||||
const res = await StripeAPI.createSubscription(paymentMethod.id, cart);
|
const res = await StripeAPI.setupSubscription(paymentMethod.id, cart);
|
||||||
await handleServerConfirmation(res);
|
await handleServerConfirmation(res, paymentMethod.id);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// catch api errors
|
// 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)
|
* 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 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
|
* @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 ('error' in response) {
|
||||||
if (response.error.statusText) {
|
if (response.error.statusText) {
|
||||||
onError(response.error.statusText);
|
onError(response.error.statusText);
|
||||||
@ -67,6 +68,7 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
|
|||||||
onError(`${t('app.shared.messages.payment_card_error')} ${response.error}`);
|
onError(`${t('app.shared.messages.payment_card_error')} ${response.error}`);
|
||||||
}
|
}
|
||||||
} else if ('requires_action' in response) {
|
} else if ('requires_action' in response) {
|
||||||
|
if (response.type === 'payment') {
|
||||||
// Use Stripe.js to handle required card action
|
// Use Stripe.js to handle required card action
|
||||||
const result = await stripe.handleCardAction(response.payment_intent_client_secret);
|
const result = await stripe.handleCardAction(response.payment_intent_client_secret);
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
@ -75,18 +77,27 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
|
|||||||
// The card action has been handled
|
// The card action has been handled
|
||||||
// The PaymentIntent can be confirmed again on the server
|
// The PaymentIntent can be confirmed again on the server
|
||||||
try {
|
try {
|
||||||
if (response.type === 'payment') {
|
|
||||||
const confirmation = await StripeAPI.confirmIntent(result.paymentIntent.id, cart);
|
const confirmation = await StripeAPI.confirmIntent(result.paymentIntent.id, cart);
|
||||||
await handleServerConfirmation(confirmation);
|
await handleServerConfirmation(confirmation);
|
||||||
}
|
|
||||||
if (response.type === 'subscription') {
|
|
||||||
const confirmation = await StripeAPI.confirmSubscription(response.subscription_id, cart);
|
|
||||||
await handleServerConfirmation(confirmation);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
onError(e);
|
onError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if ('id' in response) {
|
} else if ('id' in response) {
|
||||||
onSuccess(response);
|
onSuccess(response);
|
||||||
} else {
|
} else {
|
||||||
|
@ -15,6 +15,7 @@ interface StripeModalProps {
|
|||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
toggleModal: () => void,
|
toggleModal: () => void,
|
||||||
afterSuccess: (result: Invoice|PaymentSchedule) => void,
|
afterSuccess: (result: Invoice|PaymentSchedule) => void,
|
||||||
|
onError: (message: string) => void,
|
||||||
cart: ShoppingCart,
|
cart: ShoppingCart,
|
||||||
currentUser: User,
|
currentUser: User,
|
||||||
schedule?: PaymentSchedule,
|
schedule?: PaymentSchedule,
|
||||||
@ -28,7 +29,7 @@ interface StripeModalProps {
|
|||||||
* This component should not be called directly. Prefer using <PaymentModal> which can handle the configuration
|
* This component should not be called directly. Prefer using <PaymentModal> which can handle the configuration
|
||||||
* of a different payment gateway.
|
* 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.
|
* Return the logos, shown in the modal footer.
|
||||||
*/
|
*/
|
||||||
@ -75,6 +76,7 @@ export const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, a
|
|||||||
cart={cart}
|
cart={cart}
|
||||||
customer={customer}
|
customer={customer}
|
||||||
afterSuccess={afterSuccess}
|
afterSuccess={afterSuccess}
|
||||||
|
onError={onError}
|
||||||
schedule={schedule}
|
schedule={schedule}
|
||||||
GatewayForm={renderForm} />
|
GatewayForm={renderForm} />
|
||||||
);
|
);
|
||||||
|
@ -162,6 +162,7 @@ export const ProposePacksModal: React.FC<ProposePacksModalProps> = ({ isOpen, to
|
|||||||
<LocalPaymentModal isOpen={localPaymentModal}
|
<LocalPaymentModal isOpen={localPaymentModal}
|
||||||
toggleModal={toggleLocalPaymentModal}
|
toggleModal={toggleLocalPaymentModal}
|
||||||
afterSuccess={handlePackBought}
|
afterSuccess={handlePackBought}
|
||||||
|
onError={onError}
|
||||||
cart={cart}
|
cart={cart}
|
||||||
currentUser={operator}
|
currentUser={operator}
|
||||||
customer={customer} />
|
customer={customer} />
|
||||||
|
@ -362,6 +362,14 @@ Application.Directives.directive('cart', ['$rootScope', '$uibModal', 'dialogs',
|
|||||||
afterPayment(invoice);
|
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
|
* Invoked when something wrong occurred during the payment dialog initialization
|
||||||
* @param message {string}
|
* @param message {string}
|
||||||
|
@ -214,6 +214,7 @@
|
|||||||
<local-payment-modal is-open="localPayment.showModal"
|
<local-payment-modal is-open="localPayment.showModal"
|
||||||
toggle-modal="toggleLocalPaymentModal"
|
toggle-modal="toggleLocalPaymentModal"
|
||||||
after-success="afterLocalPaymentSuccess"
|
after-success="afterLocalPaymentSuccess"
|
||||||
|
on-error="onLocalPaymentError"
|
||||||
cart="localPayment.cartItems"
|
cart="localPayment.cartItems"
|
||||||
current-user="currentUser"
|
current-user="currentUser"
|
||||||
customer="user"
|
customer="user"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user