import React, { FC, memo, useCallback, useEffect, useMemo, useState, } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../modules'; import { GlobalState } from '../../global/types'; import { PaymentStep, ShippingOption, Price } from '../../types'; import { formatCurrency } from '../../util/formatCurrency'; import { detectCardTypeText } from '../common/helpers/detectCardType'; import usePaymentReducer, { FormState } from '../../hooks/reducers/usePaymentReducer'; import useLang from '../../hooks/useLang'; import ShippingInfo from './ShippingInfo'; import Shipping from './Shipping'; import Checkout from './Checkout'; import PaymentInfo from './PaymentInfo'; import Button from '../ui/Button'; import Modal from '../ui/Modal'; import Transition from '../ui/Transition'; import Spinner from '../ui/Spinner'; import ConfirmPayment from './ConfirmPayment'; import './PaymentModal.scss'; const DEFAULT_PROVIDER = 'stripe'; const DONATE_PROVIDER = 'smartglocal'; const SUPPORTED_PROVIDERS = new Set([DEFAULT_PROVIDER, DONATE_PROVIDER]); export type OwnProps = { isOpen: boolean; onClose: () => void; }; type StateProps = { nameRequested?: boolean; shippingAddressRequested?: boolean; phoneRequested?: boolean; emailRequested?: boolean; flexible?: boolean; phoneToProvider?: boolean; emailToProvider?: boolean; currency?: string; prices?: Price[]; isProviderError: boolean; needCardholderName?: boolean; needCountry?: boolean; needZip?: boolean; confirmPaymentUrl?: string; }; type GlobalStateProps = Pick; const Invoice: FC = ({ isOpen, onClose, step, shippingOptions, savedInfo, canSaveCredentials, nameRequested, shippingAddressRequested, phoneRequested, emailRequested, phoneToProvider, emailToProvider, currency, passwordMissing, isProviderError, invoiceContent, nativeProvider, prices, needCardholderName, needCountry, needZip, confirmPaymentUrl, error, }) => { const { validateRequestedInfo, sendPaymentForm, setPaymentStep, sendCredentialsInfo, clearPaymentError, } = getActions(); const [paymentState, paymentDispatch] = usePaymentReducer(); const [isLoading, setIsLoading] = useState(false); const lang = useLang(); const canRenderFooter = step !== PaymentStep.ConfirmPayment; useEffect(() => { if (step || error) { setIsLoading(false); } }, [step, error]); useEffect(() => { if (error?.field) { paymentDispatch({ type: 'setFormErrors', payload: { [error.field]: error.message, }, }); } }, [error, paymentDispatch]); useEffect(() => { if (savedInfo) { const { name: fullName, phone, email, shippingAddress, } = savedInfo; paymentDispatch({ type: 'updateUserInfo', payload: { fullName, phone: phone && phone.charAt(0) !== '+' ? `+${phone}` : phone, email, ...(shippingAddress || {}), }, }); } }, [savedInfo, paymentDispatch]); const handleErrorModalClose = useCallback(() => { clearPaymentError(); }, [clearPaymentError]); const totalPrice = useMemo(() => { if (step !== PaymentStep.Checkout) { return 0; } return getTotalPrice(prices, shippingOptions, paymentState.shipping); }, [step, paymentState.shipping, prices, shippingOptions]); const checkoutInfo = useMemo(() => { if (step !== PaymentStep.Checkout) { return undefined; } return getCheckoutInfo(paymentState, shippingOptions, nativeProvider || ''); }, [step, paymentState, shippingOptions, nativeProvider]); function renderError() { if (!error) { return undefined; } return (

{error.description || 'Error'}

{error.description || 'Error'}

); } function renderModalContent(currentStep: PaymentStep) { switch (currentStep) { case PaymentStep.ShippingInfo: return ( ); case PaymentStep.Shipping: return ( ); case PaymentStep.PaymentInfo: return ( ); case PaymentStep.Checkout: return ( ); case PaymentStep.ConfirmPayment: return ( ); default: return undefined; } } const validateRequest = useCallback(() => { const { saveInfo } = paymentState; const requestInfo = getRequestInfo(paymentState); validateRequestedInfo({ requestInfo, saveInfo }); }, [validateRequestedInfo, paymentState]); const sendCredentials = useCallback(() => { const credentials = getCredentials(paymentState); sendCredentialsInfo({ credentials, }); }, [sendCredentialsInfo, paymentState]); const sendForm = useCallback(() => { sendPaymentForm({ shippingOptionId: paymentState.shipping, saveCredentials: paymentState.saveCredentials, }); }, [sendPaymentForm, paymentState]); const setStep = useCallback((nextStep) => { setPaymentStep({ step: nextStep }); }, [setPaymentStep]); const handleButtonClick = useCallback(() => { setIsLoading(true); switch (step) { case PaymentStep.ShippingInfo: return validateRequest(); case PaymentStep.Shipping: return setStep(PaymentStep.PaymentInfo); case PaymentStep.PaymentInfo: return sendCredentials(); case PaymentStep.Checkout: return sendForm(); default: return () => { }; } }, [step, validateRequest, setStep, sendCredentials, sendForm]); const modalHeader = useMemo(() => { switch (step) { case PaymentStep.ShippingInfo: return lang('PaymentShippingInfo'); case PaymentStep.Shipping: return lang('PaymentShippingMethod'); case PaymentStep.PaymentInfo: return lang('PaymentCardInfo'); case PaymentStep.Checkout: return lang('PaymentCheckout'); case PaymentStep.ConfirmPayment: return lang('Checkout.WebConfirmation.Title'); default: return ''; } }, [step, lang]); const buttonText = useMemo(() => { switch (step) { case PaymentStep.Checkout: return lang('Checkout.PayPrice', formatCurrency(totalPrice, currency, lang.code)); default: return lang('Next'); } }, [step, lang, currency, totalPrice]); if (isProviderError) { return (

Sorry, Telegram WebZ doesn't support payments with this provider yet.
Please use one of our mobile apps to do this.

); } return (

{modalHeader}

{step !== undefined ? (
{renderModalContent(step)}
) : (
)} {canRenderFooter && (
)} {error && !error.field && renderError()}
); }; export default memo(withGlobal( (global): StateProps & GlobalStateProps => { const { step, shippingOptions, savedInfo, canSaveCredentials, invoice, invoiceContent, nativeProvider, nativeParams, passwordMissing, error, confirmPaymentUrl, } = global.payment; const isProviderError = Boolean(invoice && (!nativeProvider || !SUPPORTED_PROVIDERS.has(nativeProvider))); const { needCardholderName, needCountry, needZip } = (nativeParams || {}); const { nameRequested, phoneRequested, emailRequested, shippingAddressRequested, flexible, phoneToProvider, emailToProvider, currency, prices, } = (invoice || {}); return { step, shippingOptions, savedInfo, canSaveCredentials, nativeProvider, passwordMissing, nameRequested, shippingAddressRequested, phoneRequested, emailRequested, flexible, phoneToProvider, emailToProvider, currency, prices, isProviderError, invoiceContent, needCardholderName, needCountry, needZip, error, confirmPaymentUrl, }; }, )(Invoice)); function findShippingOption(shippingOptions: ShippingOption[], optionId: string) { return shippingOptions.find(({ id }) => id === optionId); } function getShippingPrices(shippingOptions: ShippingOption[], shippingOption: string) { const option = findShippingOption(shippingOptions, shippingOption); return option?.prices; } function getTotalPrice(prices: Price[] = [], shippingOptions: ShippingOption[] | undefined, shippingOption: string) { const shippingPrices = shippingOptions ? getShippingPrices(shippingOptions, shippingOption) : []; let total = 0; const totalPrices = prices.concat(shippingPrices || []); total = totalPrices.reduce((acc, cur) => { return acc + cur.amount; }, total); return total; } function getCheckoutInfo(state: FormState, shippingOptions: ShippingOption[] | undefined, paymentProvider: string) { const cardTypeText = detectCardTypeText(state.cardNumber); const paymentMethod = `${cardTypeText} *${state.cardNumber.slice(-4)}`; const shippingAddress = state.streetLine1 ? `${state.streetLine1}, ${state.city}, ${state.countryIso2}` : undefined; const { phone, fullName: name } = state; const shippingOption = shippingOptions ? findShippingOption(shippingOptions, state.shipping) : undefined; const shippingMethod = shippingOption?.title; return { paymentMethod, paymentProvider, shippingAddress, name, phone, shippingMethod, }; } function getRequestInfo(paymentState: FormState) { const { streetLine1, streetLine2, city, state, countryIso2, postCode, fullName: name, phone, email, } = paymentState; const shippingAddress = { streetLine1, streetLine2, city, state, countryIso2, postCode, }; return { name, phone, email, shippingAddress, }; } function getCredentials(paymentState: FormState) { const { cardNumber, cardholder, expiry, cvv, billingCountry, billingZip, } = paymentState; const [expiryMonth, expiryYear] = expiry.split('/'); const data = { cardNumber, cardholder, expiryMonth, expiryYear, cvv, country: billingCountry, zip: billingZip, }; return { data, }; }