From 7f43ba33f6628739d53d0f8710f28a5582454d54 Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 23 Jan 2026 15:50:38 -0500 Subject: [PATCH] feature/IO-3499-React-19 - Phone Number Formatter / Chat Open Button / Chat Affix container --- .../chat-affix/chat-affix.container.jsx | 37 +++++---- .../chat-open-button.component.jsx | 81 +++++++++++++------ client/src/translations/en_us/common.json | 3 +- client/src/translations/es/common.json | 3 +- client/src/translations/fr/common.json | 4 +- client/src/utils/PhoneFormatter.jsx | 9 +-- 6 files changed, 82 insertions(+), 55 deletions(-) diff --git a/client/src/components/chat-affix/chat-affix.container.jsx b/client/src/components/chat-affix/chat-affix.container.jsx index af99d4719..21a4ede7e 100644 --- a/client/src/components/chat-affix/chat-affix.container.jsx +++ b/client/src/components/chat-affix/chat-affix.container.jsx @@ -12,11 +12,16 @@ export function ChatAffixContainer({ bodyshop, chatVisible, currentUser }) { const client = useApolloClient(); const { socket } = useSocket(); - // 1) FCM subscription (independent of socket handler registration) - useEffect(() => { - if (!bodyshop?.messagingservicesid) return; + const messagingServicesId = bodyshop?.messagingservicesid; + const bodyshopId = bodyshop?.id; + const imexshopid = bodyshop?.imexshopid; - async function subscribeToTopicForFCMNotification() { + const messagingEnabled = Boolean(messagingServicesId); + + useEffect(() => { + if (!messagingEnabled) return; + + (async () => { try { await requestForToken(); await axios.post("/notifications/subscribe", { @@ -24,23 +29,19 @@ export function ChatAffixContainer({ bodyshop, chatVisible, currentUser }) { vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY }), type: "messaging", - imexshopid: bodyshop.imexshopid + imexshopid }); } catch (error) { console.log("Error attempting to subscribe to messaging topic: ", error); } - } + })(); + }, [messagingEnabled, imexshopid]); - subscribeToTopicForFCMNotification(); - }, [bodyshop?.messagingservicesid, bodyshop?.imexshopid]); - - // 2) Register socket handlers as soon as socket is connected (regardless of chatVisible) useEffect(() => { if (!socket) return; - if (!bodyshop?.messagingservicesid) return; - if (!bodyshop?.id) return; + if (!messagingEnabled) return; + if (!bodyshopId) return; - // If socket isn't connected yet, ensure no stale handlers remain. if (!socket.connected) { unregisterMessagingHandlers({ socket }); return; @@ -56,16 +57,14 @@ export function ChatAffixContainer({ bodyshop, chatVisible, currentUser }) { bodyshop }); - return () => { - unregisterMessagingHandlers({ socket }); - }; - }, [socket, socket?.connected, bodyshop?.id, bodyshop?.messagingservicesid, client, currentUser?.email]); + return () => unregisterMessagingHandlers({ socket }); + }, [socket, messagingEnabled, bodyshopId, client, currentUser?.email, bodyshop]); - if (!bodyshop?.messagingservicesid) return <>; + if (!messagingEnabled) return null; return (
- {bodyshop?.messagingservicesid ? : null} + {messagingEnabled ? : null}
); } diff --git a/client/src/components/chat-open-button/chat-open-button.component.jsx b/client/src/components/chat-open-button/chat-open-button.component.jsx index 5c6781edd..4d58f7a58 100644 --- a/client/src/components/chat-open-button/chat-open-button.component.jsx +++ b/client/src/components/chat-open-button/chat-open-button.component.jsx @@ -1,22 +1,23 @@ +import { Button } from "antd"; import parsePhoneNumber from "libphonenumber-js"; +import { useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; -import { openChatByPhone } from "../../redux/messaging/messaging.actions"; -import PhoneNumberFormatter from "../../utils/PhoneFormatter"; - import { createStructuredSelector } from "reselect"; +import { openChatByPhone } from "../../redux/messaging/messaging.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; import { searchingForConversation } from "../../redux/messaging/messaging.selectors"; import { useSocket } from "../../contexts/SocketIO/useSocket.js"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; +import PhoneNumberFormatter from "../../utils/PhoneFormatter"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, - searchingForConversation: searchingForConversation + searchingForConversation }); const mapDispatchToProps = (dispatch) => ({ - openChatByPhone: (phone) => dispatch(openChatByPhone(phone)) + openChatByPhone: (payload) => dispatch(openChatByPhone(payload)) }); export function ChatOpenButton({ bodyshop, searchingForConversation, phone, type, jobid, openChatByPhone }) { @@ -24,31 +25,59 @@ export function ChatOpenButton({ bodyshop, searchingForConversation, phone, type const { socket } = useSocket(); const notification = useNotification(); - if (!phone) return <>; + if (!phone) return null; - if (!bodyshop.messagingservicesid) { - return {phone}; - } + const messagingEnabled = Boolean(bodyshop?.messagingservicesid); + const parsed = useMemo(() => { + if (!messagingEnabled) return null; + try { + return parsePhoneNumber(phone, "CA") || null; + } catch { + return null; + } + }, [messagingEnabled, phone]); + + const isValid = Boolean(parsed?.isValid?.() && parsed.isValid()); + const clickable = messagingEnabled && !searchingForConversation && isValid; + + const onClick = useCallback( + (e) => { + e.preventDefault(); + e.stopPropagation(); + + if (!messagingEnabled) return; + if (searchingForConversation) return; + + if (!isValid) { + notification.error({ title: t("messaging.error.invalidphone") }); + return; + } + + openChatByPhone({ + phone_num: parsed.formatInternational(), + jobid, + socket + }); + }, + [messagingEnabled, searchingForConversation, isValid, parsed, jobid, socket, openChatByPhone, notification, t] + ); + + const content = {phone}; + + // If not clickable, render plain formatted text (no link styling) + if (!clickable) return content; + + // Clickable: render as a link-styled button (best for a “command”) return ( - { - e.preventDefault(); - e.stopPropagation(); - - if (searchingForConversation) return; // Prevent finding the same thing twice. - - const p = parsePhoneNumber(phone, "CA"); - if (p && p.isValid()) { - openChatByPhone({ phone_num: p.formatInternational(), jobid, socket }); - } else { - notification.error({ title: t("messaging.error.invalidphone") }); - } - }} + ); } diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index f08c77f21..363c53308 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -2431,7 +2431,8 @@ "messaging": { "actions": { "link": "Link to Job", - "new": "New Conversation" + "new": "New Conversation", + "openchat": "Open Chat" }, "errors": { "invalidphone": "The phone number is invalid. Unable to open conversation. ", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index a8441a160..287f76346 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -2428,7 +2428,8 @@ "messaging": { "actions": { "link": "", - "new": "" + "new": "", + "openchat": "" }, "errors": { "invalidphone": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 97a04bf3b..0a56716bf 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -2428,7 +2428,9 @@ "messaging": { "actions": { "link": "", - "new": "" + "new": "", + + "openchat": "" }, "errors": { "invalidphone": "", diff --git a/client/src/utils/PhoneFormatter.jsx b/client/src/utils/PhoneFormatter.jsx index 90fc49e50..cfd63b786 100644 --- a/client/src/utils/PhoneFormatter.jsx +++ b/client/src/utils/PhoneFormatter.jsx @@ -11,13 +11,8 @@ export default function PhoneNumberFormatter({ children, type }) { return ( - {phone} - {type ? ( - <> - {" "} - ({type}) - - ) : null} + {phone} + {type ? ({type}) : null} ); }