import { CopyFilled, DeleteFilled } from "@ant-design/icons"; import { useLazyQuery, useMutation } from "@apollo/client/react"; import { Button, Card, Col, Form, Input, message, Row, Space, Spin, Statistic } from "antd"; import axios from "axios"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS } from "../../graphql/payment_response.queries"; import { getCurrentUser, logImEXEvent } from "../../firebase/firebase.utils"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { insertAuditTrail } from "../../redux/application/application.actions"; import { toggleModalVisible } from "../../redux/modals/modals.actions"; import { selectCardPayment } from "../../redux/modals/modals.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component"; import JobSearchSelectComponent from "../job-search-select/job-search-select.component"; const mapStateToProps = createStructuredSelector({ cardPaymentModal: selectCardPayment, bodyshop: selectBodyshop, currentUser: getCurrentUser }); const mapDispatchToProps = (dispatch) => ({ insertAuditTrail: ({ jobid, operation, type }) => dispatch( insertAuditTrail({ jobid, operation, type }) ), toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")) }); const CardPaymentModalComponent = ({ bodyshop, currentUser, cardPaymentModal, toggleModalVisible, insertAuditTrail }) => { const { context, actions } = cardPaymentModal; const [form] = Form.useForm(); const [paymentLink, setPaymentLink] = useState(); const isMountedRef = useRef(true); const [loading, setLoading] = useState(false); const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE); const { t } = useTranslation(); const notification = useNotification(); const [loadRoAndOwnerByJobPks, { data, loading: queryLoading, error: queryError, refetch, called }] = useLazyQuery( QUERY_RO_AND_OWNER_BY_JOB_PKS, { fetchPolicy: "network-only", } ); const safeRefetchRoAndOwner = useCallback( (vars) => { // First run: execute the lazy query if (!called) return loadRoAndOwnerByJobPks({ variables: vars }); // Subsequent runs: refetch expects the variables object directly (not { variables: ... }) return refetch(vars); }, [called, loadRoAndOwnerByJobPks, refetch] ); useEffect(() => { return () => { isMountedRef.current = false; }; }, []); const setLoadingSafe = useCallback((value) => { if (isMountedRef.current) setLoading(value); }, []); // Watch form payments so we can query jobs when all jobids are filled (without side effects during render) const payments = Form.useWatch(["payments"], form); const jobids = useMemo(() => { if (!Array.isArray(payments) || payments.length === 0) return []; return payments.map((p) => p?.jobid).filter(Boolean); }, [payments]); const allJobIdsFilled = useMemo(() => { if (!Array.isArray(payments) || payments.length === 0) return false; return payments.every((p) => !!p?.jobid); }, [payments]); const lastJobidsKeyRef = useRef(""); useEffect(() => { if (!allJobIdsFilled) return; const nextKey = jobids.join("|"); if (!nextKey || nextKey === lastJobidsKeyRef.current) return; lastJobidsKeyRef.current = nextKey; safeRefetchRoAndOwner({ jobids }); }, [allJobIdsFilled, jobids, safeRefetchRoAndOwner]); const collectIPayFields = () => { const iPayFields = document.querySelectorAll(".ipayfield"); const iPayData = {}; iPayFields.forEach((field) => { iPayData[field.dataset.ipayname] = field.value; }); return iPayData; }; const SetIntellipayCallbackFunctions = () => { console.log("*** Set IntelliPay callback functions."); const isLikelyUserCancel = (response) => { const reason = String(response?.declinereason ?? "").toLowerCase(); // Heuristics: adjust if IntelliPay gives you a known cancel code/message return ( reason.includes("cancel") || reason.includes("canceled") || reason.includes("closed") || // many gateways won't have a paymentid if user cancels before submitting !response?.paymentid ); }; window.intellipay.runOnClose(() => { // This is the path for Cancel / X try { // If IntelliPay uses this flag, clear it so initialize() won't reopen unexpectedly window.intellipay.isAutoOpen = false; } catch { // ignore } // Optional: if IntelliPay needs re-init after close, uncomment: // try { window.intellipay.initialize?.(); } catch {} setLoadingSafe(false); }); window.intellipay.runOnApproval(() => { // keep your existing behavior setTimeout(() => { if (actions?.refetch) actions.refetch(); setLoadingSafe(false); toggleModalVisible(); }, 750); }); window.intellipay.runOnNonApproval(async (response) => { try { // If cancel is reported as "non-approval", don't record it as a failed payment if (isLikelyUserCancel(response)) return; const { payments } = form.getFieldsValue(); await insertPaymentResponse({ variables: { paymentResponse: payments.map((payment) => ({ amount: payment.amount, bodyshopid: bodyshop.id, jobid: payment.jobid, declinereason: response.declinereason, ext_paymentid: response.paymentid?.toString?.() ?? null, successful: false, response })) } }); payments.forEach((payment) => insertAuditTrail({ jobid: payment.jobid, operation: AuditTrailMapping.failedpayment(), type: "failedpayment" }) ); } finally { // IMPORTANT: always clear loading, even on errors setLoadingSafe(false); } }); }; const handleIntelliPayCharge = async () => { setLoading(true); // Validate try { await form.validateFields(); } catch { setLoadingSafe(false); return; } const iPayData = collectIPayFields(); const { payments } = form.getFieldsValue(); try { logImEXEvent("payment_cc_lightbox"); const response = await axios.post("/intellipay/lightbox_credentials", { bodyshop, refresh: !!window.intellipay, paymentSplitMeta: form.getFieldsValue(), iPayData: iPayData, comment: btoa(JSON.stringify({ payments, userEmail: currentUser.email })) }); if (window.intellipay) { // Use Function constructor instead of eval for security (still executes dynamic code but safer) // IntelliPay provides initialization code that must be executed Function(response.data)(); pollForIntelliPay(() => { SetIntellipayCallbackFunctions(); window.intellipay.autoOpen(); }); } else { const rg = document.createRange(); const node = rg.createContextualFragment(response.data); document.documentElement.appendChild(node); pollForIntelliPay(() => { SetIntellipayCallbackFunctions(); window.intellipay.isAutoOpen = true; window.intellipay.initialize(); }); } } catch { notification.error({ title: t("job_payments.notifications.error.openingip") }); setLoadingSafe(false); } }; const handleIntelliPayChargeShortLink = async () => { setLoading(true); // Validate try { await form.validateFields(); } catch { setLoadingSafe(false); return; } const iPayData = collectIPayFields(); try { const { payments } = form.getFieldsValue(); logImEXEvent("payment_cc_shortlink"); const response = await axios.post("/intellipay/generate_payment_url", { bodyshop, amount: payments.reduce((acc, val) => acc + (val?.amount || 0), 0), account: payments && data?.jobs?.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null, comment: btoa(JSON.stringify({ payments, userEmail: currentUser.email })), paymentSplitMeta: form.getFieldsValue(), iPayData: iPayData }); if (response?.data?.shorUrl) { setPaymentLink(response.data.shorUrl); await navigator.clipboard.writeText(response.data.shorUrl); message.success(t("general.actions.copied")); } setLoadingSafe(false); } catch { notification.error({ title: t("job_payments.notifications.error.openingip") }); setLoadingSafe(false); } }; return (
{(fields, { add, remove }) => (
{fields.map((field, index) => ( remove(field.name)} /> ))}
)}
{/* Hidden IntelliPay fields driven by watched payments + query result. IMPORTANT: no refetch() here (avoid side effects during render). */} 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null} /> 0 ? (data.jobs.find((j) => j.ownr_ea)?.ownr_ea ?? null) : null} /> prevValues.payments?.map((p) => p?.amount).join() !== curValues.payments?.map((p) => p?.amount).join() } > {() => { const { payments } = form.getFieldsValue(); const totalAmountToCharge = payments?.reduce((acc, val) => acc + (val?.amount || 0), 0); return ( ); }}
{paymentLink && ( { navigator.clipboard.writeText(paymentLink); message.success(t("general.actions.copied")); }} >
{paymentLink}
)} {queryError ? (
{queryError.message}
) : null}
); }; export default connect(mapStateToProps, mapDispatchToProps)(CardPaymentModalComponent); // Poll for window.IntelliPay.fixAmount for 5 seconds. If it doesn't come up, just try anyways to force the possible error. function pollForIntelliPay(callbackFunction) { const timeout = 5000; const interval = 150; const startTime = Date.now(); function checkFixAmount() { if (window.intellipay?.fixAmount) { callbackFunction(); return; } if (Date.now() - startTime >= timeout) { console.log("Stopped polling IntelliPay after 5 seconds. Attempting to set functions anyways."); callbackFunction(); return; } setTimeout(checkFixAmount, interval); } checkFixAmount(); }