import React, { FunctionComponent, ReactNode, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import WalletLib from '../../lib/wallet'; import { WalletInfo } from '../wallet-info'; import { FabModal, ModalSize } from '../base/fab-modal'; import { HtmlTranslate } from '../base/html-translate'; import { CustomAsset, CustomAssetName } from '../../models/custom-asset'; import { IFablab } from '../../models/fablab'; import { ShoppingCart } from '../../models/payment'; import { PaymentSchedule } from '../../models/payment-schedule'; import { User } from '../../models/user'; import CustomAssetAPI from '../../api/custom-asset'; import PriceAPI from '../../api/price'; import WalletAPI from '../../api/wallet'; import { Invoice } from '../../models/invoice'; import SettingAPI from '../../api/setting'; import { SettingName } from '../../models/setting'; import { ComputePriceResult } from '../../models/price'; import { Wallet } from '../../models/wallet'; declare var Fablab: IFablab; export interface GatewayFormProps { onSubmit: () => void, onSuccess: (result: Invoice|PaymentSchedule) => void, onError: (message: string) => void, customer: User, operator: User, className?: string, paymentSchedule?: boolean, cart?: ShoppingCart, formId: string, } interface AbstractPaymentModalProps { isOpen: boolean, toggleModal: () => void, afterSuccess: (result: Invoice|PaymentSchedule) => void, cart: ShoppingCart, currentUser: User, schedule?: PaymentSchedule, customer: User, logoFooter: ReactNode, GatewayForm: FunctionComponent, formId: string, className?: string, formClassName?: string, } /** * This component is an abstract modal that must be extended by each payment gateway to include its payment form. * * 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 = ({ isOpen, toggleModal, afterSuccess, cart, currentUser, schedule, customer, logoFooter, GatewayForm, formId, className, formClassName }) => { // customer's wallet const [wallet, setWallet] = useState(null); // server-computed price with all details const [price, setPrice] = useState(null); // remaining price = total price - wallet amount const [remainingPrice, setRemainingPrice] = useState(0); // is the component ready to display? const [ready, setReady] = useState(false); // errors to display in the UI (gateway errors mainly) const [errors, setErrors] = useState(null); // are we currently processing the payment (ie. the form was submit, but the process is still running)? const [submitState, setSubmitState] = useState(false); // did the user accepts the terms of services (CGV)? const [tos, setTos] = useState(false); // currently active payment gateway const [gateway, setGateway] = useState(null); // the sales conditions const [cgv, setCgv] = useState(null); const { t } = useTranslation('shared'); /** * When the component is loaded first, get the name of the currently active payment modal */ useEffect(() => { 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())); }) }, []); /** * On each display: * - Refresh the wallet * - Refresh the price * - Refresh the remaining price */ useEffect(() => { if (!cart) return; WalletAPI.getByUser(cart.customer_id).then((wallet) => { setWallet(wallet); PriceAPI.compute(cart).then((res) => { setPrice(res); const wLib = new WalletLib(wallet); setRemainingPrice(wLib.computeRemainingPrice(res.price)); setReady(true); }) }) }, [cart]); /** * Check if there is currently an error to display */ const hasErrors = (): boolean => { return errors !== null; } /** * Check if the user accepts the Terms of Sales document */ const hasCgv = (): boolean => { return cgv != null; } /** * Triggered when the user accepts or declines the Terms of Sales */ const toggleTos = (): void => { setTos(!tos); } /** * Check if we are currently creating a payment schedule */ const isPaymentSchedule = (): boolean => { return schedule !== undefined; } /** * Return the formatted localized amount for the given price (eg. 20.5 => "20,50 €") */ const formatPrice = (amount: number): string => { return new Intl.NumberFormat(Fablab.intl_locale, {style: 'currency', currency: Fablab.intl_currency}).format(amount); } /** * Set the component as 'currently submitting' */ const handleSubmit = (): void => { setSubmitState(true); } /** * After sending the form with success, process the resulting payment method */ const handleFormSuccess = async (result: Invoice|PaymentSchedule): Promise => { setSubmitState(false); afterSuccess(result); } /** * 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); } /** * Check the form can be submitted. * => We're not currently already submitting the form, and, if the terms of service are enabled, the user must agree with them. */ const canSubmit = (): boolean => { let terms = true; if (hasCgv()) { terms = tos; } return !submitState && terms; } return ( {ready &&
{hasErrors() &&
{errors}
} {isPaymentSchedule() &&
} {hasCgv() &&
}
{!submitState && } {submitState &&
}
}
); }