atlp-rwanda/hackers-ec-Fe

View on GitHub
src/components/payment/PaymentToggleModel.tsx

Summary

Maintainability
D
2 days
Test Coverage
F
45%
/* eslint-disable prefer-const */
import { useEffect, useState } from 'react';
import momoIcon from '../../assets/momo.png';
import PaymentButton from '../buttons/PaymentButton';
import { useAppSelector, useAppDispatch } from '../../redux/hooks/hooks';
import { stripePayment } from '../../redux/features/StripePaymentSlice';
import stripeIcon from '../../assets/stripe icon.png';
import cancel from '../../assets/cancel.png';
import { payModel } from '../../redux/features/toggleSlice';
import useToast from '../../hooks/useToast';
import FormInput from '../Forms/InputText';
import { ChevronLeft } from 'lucide-react';
import { motion } from 'framer-motion';
import { FadeLoader } from 'react-spinners';
import CopyCard from '../buttons/CopyCard';
import {
    momoValidation,
    momoValidationType,
} from '../../validations/momValidation';
import { SubmitHandler, useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { DynamicData } from '../../@types/DynamicData';
import { momoPay, resetMomo } from '../../redux/features/momoPaymentSlice';
import IconLoader from '../Loaders/IconLoader';
import { useNavigate } from 'react-router-dom';
import { getCarts } from '../../redux/features/cartSlice';

const initialCopyData = { token: '', referenceId: '' };
function PaymentToggleModel() {
    const navigate = useNavigate();
    const dispatch = useAppDispatch();
    const [current, setCurrent] = useState(0);
    const [previousStep, setPreviousStep] = useState(0);
    const [copyData, setCopyData] = useState(initialCopyData);
    const { showErrorMessage, showSuccessMessage } = useToast();
    const { data, isLoading, error } = useAppSelector((state) => state.stripe);
    let {
        data: momoData,
        isLoading: processing,
        error: momoError,
        paymentStatus,
        message,
    } = useAppSelector((state) => state.momo);
    const difference = current - previousStep;

    const paymentToggleModel = (): boolean => {
        dispatch(payModel());
        return true;
    };

    const {
        register,
        handleSubmit,
        reset,
        setValue,
        getValues,
        formState: { errors },
    } = useForm<momoValidationType>({
        resolver: zodResolver(momoValidation),
    });

    useEffect(() => {
        if (!data) {
            dispatch(stripePayment()).unwrap();
        }
    }, [data, dispatch]);

    useEffect(() => {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        let interval: any;
        const checkPayment = async () => {
            try {
                await handleSubmit(handleMomoPay)();
            } catch (e) {
                showErrorMessage('Error checking payment status');
            }
        };

        if (
            !processing &&
            (paymentStatus === 'PENDING' || paymentStatus === 'SUCCESSFUL')
        ) {
            interval = setInterval(async () => {
                await checkPayment();
            }, 10000);
        }

        return () => clearInterval(interval);
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [getValues, handleSubmit, paymentStatus, processing]);

    useEffect(() => {
        if (error || momoError) {
            showErrorMessage(error || (momoError as string));
            reset();
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [error, momoError]);

    const handleStripePayment = () => {
        const url: string = data?.data.sessionUrl || '';
        window.location.href = url;
    };

    const handleCancel = () => {
        reset();
        dispatch(resetMomo());
        setPreviousStep(1);
        setCurrent(0);
    };

    const handleMomoPay: SubmitHandler<momoValidationType> = async (
        data: momoValidationType,
    ) => {
        try {
            const res = await dispatch(momoPay(data)).unwrap();
            paymentStatus = res?.status;
            momoData = res?.data;
            message = res?.message;
            if (!processing && paymentStatus) {
                if (paymentStatus === 'SUCCESSFUL') {
                    if (momoData) {
                        showSuccessMessage(message!);
                        navigate(`/orders/${momoData?.order?.id}`);
                        dispatch(getCarts());
                        reset();
                        dispatch(resetMomo());
                        paymentToggleModel();
                    }
                }
                if (paymentStatus === 'PENDING') {
                    setCopyData({
                        token: momoData?.token,
                        referenceId: momoData?.XReferenceId,
                    });
                    setPreviousStep(1);
                    setCurrent(2);
                } else if (paymentStatus === 'FAILED') {
                    showErrorMessage(`${message}(${momoData as unknown as string})`);
                    reset();
                    dispatch(resetMomo());
                    setPreviousStep(1);
                    setCurrent(0);
                }
            }
        } catch (e) {
            const err = e as DynamicData;
            showErrorMessage(
                err?.data?.message ||
                    err?.message ||
                    'Unknown error occurred! Please try again!',
            );
            reset();
            setPreviousStep(1);
            setCurrent(0);
        }
    };

    return (
        <div className="flex fixed bg-neutral-grey justify-center items-center bg-fixed bg-opacity-[0.8] w-screen h-screen z-50">
            <div className="w-[90%] tablet:w-[60%] h-[50%] rounded-[2rem] shadow-bottom-left-right bg-neutral-white opacity-1 tablet:py-12 overflow-hidden">
                {current === 0 && (
                    <motion.div
                        initial={
                            previousStep === 0
                                ? false
                                : { x: difference >= 0 ? '50%' : '-50%', opacity: 1 }
                        }
                        animate={{ x: 0, opacity: 1 }}
                        transition={{ duration: 0.5, ease: 'easeInOut' }}
                        className="w-full h-full flex justify-around items-center flex-col"
                    >
                        <h1 className="text-xl font-semibold">Continue payment with:</h1>
                        <PaymentButton
                            bgColor="bg-action-warning text-neutral-white"
                            icon={momoIcon}
                            title="Mobile Money"
                            handlePayment={() => {
                                setPreviousStep(0);
                                setCurrent(1);
                            }}
                        />
                        <PaymentButton
                            handlePayment={handleStripePayment}
                            bgColor="bg-[#6A39BA] text-neutral-white"
                            icon={stripeIcon}
                            title="Stripe"
                            disable={isLoading}
                        />
                        <PaymentButton
                            handlePayment={paymentToggleModel}
                            bgColor="bg-neutral-white"
                            icon={cancel}
                            title="Cancel"
                            iconSize="w-[1rem]"
                        />
                    </motion.div>
                )}
                {current === 1 && (
                    <motion.div
                        initial={{ x: difference >= 0 ? '50%' : '-50%', opacity: 1 }}
                        animate={{ x: 0, opacity: 1 }}
                        transition={{ duration: 0.5, ease: 'easeInOut' }}
                        className="flex flex-col items-center justify-center gap-4 w-full h-full"
                    >
                        <h1 className="text-xl font-semibold">Enter Phone number</h1>
                        <p className="w-[70%] text-xs text-center my-5">
                            Sorry, this momo payment is not yet launched, you will need to
                            approve this payment using sandbox on momo API.
                        </p>
                        <form action="" onSubmit={handleSubmit(handleMomoPay)}>
                            <FormInput
                                type="text"
                                placeholder="Enter phone number"
                                otherStyles="text-center py-2 px-10 mt-2 border !bg-transparent mb-5"
                                {...register('phoneNumber')}
                                onChange={(e) => setValue('phoneNumber', e.target.value)}
                                error={errors.phoneNumber}
                            />
                            <button
                                type="submit"
                                className="w-full border bg-action-warning text-white py-2 rounded-full flex items-center justify-center gap-2"
                            >
                                {processing ? (
                                    <>
                                        <IconLoader className="animate-spin mr-1" />{' '}
                                        {'processing....'}
                                    </>
                                ) : (
                                    'Pay'
                                )}
                            </button>
                        </form>
                        <button
                            type="button"
                            className="flex items-center justify-center gap-2"
                            onClick={handleCancel}
                        >
                            <ChevronLeft size={18} /> Cancel
                        </button>
                    </motion.div>
                )}
                {current === 2 && (
                    <motion.div
                        initial={{ x: difference >= 0 ? '50%' : '-50%', opacity: 1 }}
                        animate={{ x: 0, opacity: 1 }}
                        transition={{ duration: 0.5, ease: 'easeInOut' }}
                        className="w-full h-full flex items-center justify-center flex-col gsp-4"
                    >
                        <FadeLoader color="green" />
                        <p className="w-[70%] text-xs text-center my-5">
                            Use this endpoint to approve:{' '}
                            <a
                                href="https://momodeveloper.mtn.com/API-collections#api=collection&operation=GetPaymentStatus"
                                target="_blank"
                                rel="noopener noreferrer"
                                className="text-yellow-600"
                            >
                                momo API
                            </a>
                            . Copy the needed credentials below!
                        </p>
                        <div className="grid grid-cols-2 gap-4 p-3">
                            <CopyCard
                                name="Authorization token"
                                text="Authorization token"
                                value={copyData.token}
                            />
                            <CopyCard
                                name="Target Environment"
                                text="Target Environment"
                                value="sandbox"
                            />
                            <CopyCard
                                name="XReferenceId"
                                text="XReferenceId"
                                value={copyData.referenceId}
                            />
                            <CopyCard
                                name="Ocp-Apim-Subscription-Key"
                                text="Ocp-Apim-Subscription-Key"
                                value={import.meta.env.VITE_OCP_SUBSCRIPTION_KEY}
                            />
                        </div>
                        <button
                            type="button"
                            className="flex items-center justify-center gap-2"
                            onClick={handleCancel}
                        >
                            <ChevronLeft size={18} /> Cancel
                        </button>
                    </motion.div>
                )}
            </div>
        </div>
    );
}

export default PaymentToggleModel;