From 36fd077babf70b1e3ed2b24db8a43ffcb9107600 Mon Sep 17 00:00:00 2001 From: Dave Date: Wed, 14 Jan 2026 00:33:44 -0500 Subject: [PATCH] feature/IO-3499-React-19: Bug Fixes / Checkpoint --- client/.env.development.imex | 3 +- .../card-payment-modal.component.jsx | 105 ++-- client/vite.config.js | 465 +++++++++--------- 3 files changed, 317 insertions(+), 256 deletions(-) diff --git a/client/.env.development.imex b/client/.env.development.imex index 79d1b4e63..5223bde09 100644 --- a/client/.env.development.imex +++ b/client/.env.development.imex @@ -17,4 +17,5 @@ TEST_PASSWORD="test123" VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com -VITE_APP_AMP_KEY=6228a598e57cd66875cfd41604f1f891 \ No newline at end of file +VITE_APP_AMP_KEY=6228a598e57cd66875cfd41604f1f891 +#VITE_ENABLE_COMPILER_IN_DEV=1 diff --git a/client/src/components/card-payment-modal/card-payment-modal.component.jsx b/client/src/components/card-payment-modal/card-payment-modal.component.jsx index 2c5862c24..ab3384fce 100644 --- a/client/src/components/card-payment-modal/card-payment-modal.component.jsx +++ b/client/src/components/card-payment-modal/card-payment-modal.component.jsx @@ -46,6 +46,8 @@ const CardPaymentModalComponent = ({ 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(); @@ -69,6 +71,16 @@ const CardPaymentModalComponent = ({ [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); @@ -106,45 +118,74 @@ const CardPaymentModalComponent = ({ 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(() => { - //window.intellipay.initialize(); + // 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(() => { - // 2024-04-25: Nothing is going to happen here anymore. We'll completely rely on the callback. - // Add a slight delay to allow the refetch to properly get the data. + // keep your existing behavior setTimeout(() => { if (actions?.refetch) actions.refetch(); - setLoading(false); + setLoadingSafe(false); toggleModalVisible(); }, 750); }); window.intellipay.runOnNonApproval(async (response) => { - // Mutate unsuccessful payment + 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, + 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, - declinereason: response.declinereason, - ext_paymentid: response.paymentid.toString(), - successful: false, - response - })) - } - }); - - payments.forEach((payment) => - insertAuditTrail({ - jobid: payment.jobid, - operation: AuditTrailMapping.failedpayment(), - type: "failedpayment" - }) - ); + operation: AuditTrailMapping.failedpayment(), + type: "failedpayment" + }) + ); + } finally { + // IMPORTANT: always clear loading, even on errors + setLoadingSafe(false); + } }); }; @@ -154,7 +195,7 @@ const CardPaymentModalComponent = ({ try { await form.validateFields(); } catch { - setLoading(false); + setLoadingSafe(false); return; } @@ -185,7 +226,7 @@ const CardPaymentModalComponent = ({ document.documentElement.appendChild(node); pollForIntelliPay(() => { SetIntellipayCallbackFunctions(); - // eslint-disable-next-line react-compiler/react-compiler + window.intellipay.isAutoOpen = true; window.intellipay.initialize(); }); @@ -194,7 +235,7 @@ const CardPaymentModalComponent = ({ notification.error({ title: t("job_payments.notifications.error.openingip") }); - setLoading(false); + setLoadingSafe(false); } }; @@ -204,7 +245,7 @@ const CardPaymentModalComponent = ({ try { await form.validateFields(); } catch { - setLoading(false); + setLoadingSafe(false); return; } @@ -227,12 +268,12 @@ const CardPaymentModalComponent = ({ await navigator.clipboard.writeText(response.data.shorUrl); message.success(t("general.actions.copied")); } - setLoading(false); + setLoadingSafe(false); } catch { notification.error({ title: t("job_payments.notifications.error.openingip") }); - setLoading(false); + setLoadingSafe(false); } }; diff --git a/client/vite.config.js b/client/vite.config.js index 2119dd104..1d9eb75ad 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -37,239 +37,258 @@ const getFormattedTimestamp = () => export const logger = createLogger("info", { allowClearScreen: false }); -export default defineConfig({ - base: "/", - plugins: [ - ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })), +// Read HTTPS certs once (used by both server + preview) +const httpsCerts = { + key: await fsPromises.readFile("../certs/key.pem"), + cert: await fsPromises.readFile("../certs/cert.pem") +}; - // PWA only for production builds (faster dev) - VitePWA({ - apply: "build", - injectRegister: "auto", - registerType: "prompt", - workbox: { - navigateFallbackDenylist: [/^\/api\//] // prevent caching API routes - }, - manifest: { - short_name: InstanceRenderManager({ - instance: process.env.VITE_APP_INSTANCE, - imex: "ImEX Online", - rome: "Rome Online" - }), - name: InstanceRenderManager({ - instance: process.env.VITE_APP_INSTANCE, - imex: "ImEX Online", - rome: "Rome Online" - }), - description: "The ultimate bodyshop management system.", - icons: [ - { - src: InstanceRenderManager({ - instance: process.env.VITE_APP_INSTANCE, - imex: "favicon.png", - rome: "ro-favicon.png" - }), - sizes: "64x64 32x32 24x24 16x16", - type: "image/x-icon" - }, - { - src: InstanceRenderManager({ - instance: process.env.VITE_APP_INSTANCE, - imex: "logo192.png", - rome: "logo192.png" - }), - type: "image/png", - sizes: "192x192" - }, - { - src: InstanceRenderManager({ - instance: process.env.VITE_APP_INSTANCE, - imex: "logo512.png", - rome: "ro-favicon.png" - }), - type: "image/png", - sizes: "512x512" - } - ], - theme_color: InstanceRenderManager({ - instance: process.env.VITE_APP_INSTANCE, - imex: "#1890ff", - rome: "#fff" - }), - background_color: "#fff", - gcm_sender_id: "103953800507" - } - }), +export default defineConfig(({ command, mode }) => { + // Only enable React Compiler on build in production/test (keeps dev as fast as possible) + const isBuild = command === "build"; + const isTestBuild = + mode === "test" || process.env.VITE_APP_IS_TEST === "true" || process.env.VITE_APP_IS_TEST === "1"; + const enableReactCompiler = + process.env.VITE_ENABLE_COMPILER_IN_DEV || (isBuild && (mode === "production" || isTestBuild)); - react({ - babel: { - plugins: [ - ['babel-plugin-react-compiler', { - // Exclude third-party drag-and-drop library from compilation - sources: (filename) => { - return !filename.includes('trello-board/dnd'); + console.log(enableReactCompiler ? "React Compiler enabled" : "React Compiler disabled"); + + return { + base: "/", + plugins: [ + ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })), + + // PWA only for production builds (faster dev) + VitePWA({ + apply: "build", + injectRegister: "auto", + registerType: "prompt", + workbox: { + navigateFallbackDenylist: [/^\/api\//] // prevent caching API routes + }, + manifest: { + short_name: InstanceRenderManager({ + instance: process.env.VITE_APP_INSTANCE, + imex: "ImEX Online", + rome: "Rome Online" + }), + name: InstanceRenderManager({ + instance: process.env.VITE_APP_INSTANCE, + imex: "ImEX Online", + rome: "Rome Online" + }), + description: "The ultimate bodyshop management system.", + icons: [ + { + src: InstanceRenderManager({ + instance: process.env.VITE_APP_INSTANCE, + imex: "favicon.png", + rome: "ro-favicon.png" + }), + sizes: "64x64 32x32 24x24 16x16", + type: "image/x-icon" + }, + { + src: InstanceRenderManager({ + instance: process.env.VITE_APP_INSTANCE, + imex: "logo192.png", + rome: "logo192.png" + }), + type: "image/png", + sizes: "192x192" + }, + { + src: InstanceRenderManager({ + instance: process.env.VITE_APP_INSTANCE, + imex: "logo512.png", + rome: "ro-favicon.png" + }), + type: "image/png", + sizes: "512x512" } - }] - ] - } - }), - eslint(), - - // Sentry only for production builds (no dev overhead) - sentryVitePlugin({ - apply: "build", - org: "imex", - reactComponentAnnotation: { enabled: true }, - release: { - name: `${process.env.VITE_APP_IS_TEST ? "test" : "production"}-${currentDatePST}-${commitHash}`.trim() - }, - project: InstanceRenderManager({ - instance: process.env.VITE_APP_INSTANCE, - imex: "imexonline", - rome: "rome-online" - }) - }) - ], - - define: { - APP_VERSION: JSON.stringify(process.env.npm_package_version), - __COMMIT_HASH__: JSON.stringify(commitHash) - }, - - server: { - host: true, - port: 3000, - open: true, - proxy: { - "/ws": { - target: "ws://localhost:4000", - secure: false, - ws: true - }, - "/wss": { - target: "ws://localhost:4000", - secure: false, - ws: true - }, - "/api": { - target: "http://localhost:4000", - changeOrigin: true, - secure: false, - ws: false, - rewrite: (path) => { - const replacedValue = path.replace(/^\/api/, ""); - logger.info( - `${chalk.grey.bold(getFormattedTimestamp())} ${chalk.cyan.bold("[vite]")} ${chalk.green.bold("[API]")} ${chalk.blue(replacedValue)}` - ); - return replacedValue; - } - } - }, - https: { - key: await fsPromises.readFile("../certs/key.pem"), - cert: await fsPromises.readFile("../certs/cert.pem") - } - }, - - preview: { - port: 6000, - host: true, - open: true, - https: { - key: await fsPromises.readFile("../certs/key.pem"), - cert: await fsPromises.readFile("../certs/cert.pem") - }, - proxy: { - "/ws": { - target: "ws://localhost:4000", - rewriteWsOrigin: true, - secure: false, - ws: true - }, - "/wss": { - target: "ws://localhost:4000", - rewriteWsOrigin: true, - secure: false, - ws: true - }, - "/api": { - target: "http://localhost:4000", - changeOrigin: true, - secure: false, - ws: false - } - } - }, - - build: { - sourcemap: true, - - rollupOptions: { - output: { - manualChunks: { - antd: ["antd"], - "react-redux": ["react-redux"], - redux: ["redux"], - lodash: ["lodash"], - "@sentry/react": ["@sentry/react"], - "@splitsoftware/splitio-react": ["@splitsoftware/splitio-react"], - logrocket: ["logrocket"], - firebase: [ - "@firebase/analytics", - "@firebase/app", - "@firebase/firestore", - "@firebase/auth", - "@firebase/messaging" ], - markerjs2: ["markerjs2"], - "@apollo/client": ["@apollo/client"], - "libphonenumber-js": ["libphonenumber-js"], - recharts: ["recharts"] + theme_color: InstanceRenderManager({ + instance: process.env.VITE_APP_INSTANCE, + imex: "#1890ff", + rome: "#fff" + }), + background_color: "#fff", + gcm_sender_id: "103953800507" + } + }), + + react( + enableReactCompiler + ? { + babel: { + plugins: [ + [ + "babel-plugin-react-compiler", + { + // Exclude third-party drag-and-drop library from compilation + sources: (filename) => { + return !filename.includes("trello-board/dnd"); + } + } + ] + ] + } + } + : undefined + ), + + eslint(), + + // Sentry only for production builds (no dev overhead) + sentryVitePlugin({ + apply: "build", + org: "imex", + reactComponentAnnotation: { enabled: true }, + release: { + name: `${process.env.VITE_APP_IS_TEST ? "test" : "production"}-${currentDatePST}-${commitHash}`.trim() + }, + project: InstanceRenderManager({ + instance: process.env.VITE_APP_INSTANCE, + imex: "imexonline", + rome: "rome-online" + }) + }) + ], + + define: { + APP_VERSION: JSON.stringify(process.env.npm_package_version), + __COMMIT_HASH__: JSON.stringify(commitHash) + }, + + server: { + host: true, + port: 3000, + open: true, + proxy: { + "/ws": { + target: "ws://localhost:4000", + secure: false, + ws: true + }, + "/wss": { + target: "ws://localhost:4000", + secure: false, + ws: true + }, + "/api": { + target: "http://localhost:4000", + changeOrigin: true, + secure: false, + ws: false, + rewrite: (path) => { + const replacedValue = path.replace(/^\/api/, ""); + logger.info( + `${chalk.grey.bold(getFormattedTimestamp())} ${chalk.cyan.bold("[vite]")} ${chalk.green.bold("[API]")} ${chalk.blue(replacedValue)}` + ); + return replacedValue; + } + } + }, + https: httpsCerts + }, + + preview: { + port: 6000, + host: true, + open: true, + https: httpsCerts, + proxy: { + "/ws": { + target: "ws://localhost:4000", + rewriteWsOrigin: true, + secure: false, + ws: true + }, + "/wss": { + target: "ws://localhost:4000", + rewriteWsOrigin: true, + secure: false, + ws: true + }, + "/api": { + target: "http://localhost:4000", + changeOrigin: true, + secure: false, + ws: false } } }, - cssMinify: "lightningcss" - }, + build: { + sourcemap: true, - // Strip console/debugger in prod to shrink bundles - esbuild: { - //drop: ["console", "debugger"] - }, + rollupOptions: { + output: { + manualChunks: { + antd: ["antd"], + "react-redux": ["react-redux"], + redux: ["redux"], + lodash: ["lodash"], + "@sentry/react": ["@sentry/react"], + "@splitsoftware/splitio-react": ["@splitsoftware/splitio-react"], + logrocket: ["logrocket"], + firebase: [ + "@firebase/analytics", + "@firebase/app", + "@firebase/firestore", + "@firebase/auth", + "@firebase/messaging" + ], + markerjs2: ["markerjs2"], + "@apollo/client": ["@apollo/client"], + "libphonenumber-js": ["libphonenumber-js"], + recharts: ["recharts"] + } + } + }, - optimizeDeps: { - include: [ - "react", - "react-dom", - "antd", - "lodash", - "@sentry/react", - "@apollo/client", - "@reduxjs/toolkit", - "axios", - "react-router-dom", - "dayjs", - "redux", - "react-redux", - "@firebase/app", - "@firebase/analytics", - "@firebase/firestore", - "@firebase/auth", - "@firebase/messaging", - "@firebase/util" - ], - esbuildOptions: { - loader: { ".jsx": "jsx", ".tsx": "tsx" } - } - }, - - css: { - transformer: "lightningcss", - lightningcss: { - targets: lightningCssTargets + cssMinify: "lightningcss" }, - preprocessorOptions: { - scss: { quietDeps: true } + + // Strip console/debugger in prod to shrink bundles + esbuild: { + //drop: ["console", "debugger"] + }, + + optimizeDeps: { + include: [ + "react", + "react-dom", + "antd", + "lodash", + "@sentry/react", + "@apollo/client", + "@reduxjs/toolkit", + "axios", + "react-router-dom", + "dayjs", + "redux", + "react-redux", + "@firebase/app", + "@firebase/analytics", + "@firebase/firestore", + "@firebase/auth", + "@firebase/messaging", + "@firebase/util" + ], + esbuildOptions: { + loader: { ".jsx": "jsx", ".tsx": "tsx" } + } + }, + + css: { + transformer: "lightningcss", + lightningcss: { + targets: lightningCssTargets + }, + preprocessorOptions: { + scss: { quietDeps: true } + } } - } + }; });