feature/IO-3182-Phone-Number-Consent - Checkpoint
This commit is contained in:
@@ -8,6 +8,7 @@ import ChatPopupComponent from "../chat-popup/chat-popup.component";
|
|||||||
import "./chat-affix.styles.scss";
|
import "./chat-affix.styles.scss";
|
||||||
import { registerMessagingHandlers, unregisterMessagingHandlers } from "./registerMessagingSocketHandlers";
|
import { registerMessagingHandlers, unregisterMessagingHandlers } from "./registerMessagingSocketHandlers";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
import { GET_PHONE_NUMBER_CONSENT } from "../../graphql/consent.queries.js";
|
||||||
|
|
||||||
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -34,16 +35,59 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
|||||||
|
|
||||||
SubscribeToTopicForFCMNotification();
|
SubscribeToTopicForFCMNotification();
|
||||||
|
|
||||||
//Register WS handlers
|
// Register WebSocket handlers
|
||||||
if (socket && socket.connected) {
|
if (socket && socket.connected) {
|
||||||
registerMessagingHandlers({ socket, client });
|
registerMessagingHandlers({ socket, client });
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
// Handle consent-changed events
|
||||||
if (socket && socket.connected) {
|
const handleConsentChanged = ({ bodyshopId, phone_number, consent_status }) => {
|
||||||
|
try {
|
||||||
|
client.cache.writeQuery(
|
||||||
|
{
|
||||||
|
query: GET_PHONE_NUMBER_CONSENT,
|
||||||
|
variables: { bodyshopid: bodyshopId, phone_number }
|
||||||
|
},
|
||||||
|
(data) => {
|
||||||
|
if (!data?.phone_number_consent?.[0]) {
|
||||||
|
return {
|
||||||
|
phone_number_consent: [
|
||||||
|
{
|
||||||
|
__typename: "phone_number_consent",
|
||||||
|
id: null,
|
||||||
|
bodyshopid: bodyshopId,
|
||||||
|
phone_number,
|
||||||
|
consent_status,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
consent_updated_at: new Date().toISOString(),
|
||||||
|
history: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
phone_number_consent: [
|
||||||
|
{
|
||||||
|
...data.phone_number_consent[0],
|
||||||
|
consent_status,
|
||||||
|
consent_updated_at: new Date().toISOString()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating consent cache:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on("consent-changed", handleConsentChanged);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.off("consent-changed", handleConsentChanged);
|
||||||
unregisterMessagingHandlers({ socket });
|
unregisterMessagingHandlers({ socket });
|
||||||
}
|
};
|
||||||
};
|
}
|
||||||
}, [bodyshop, socket, t, client]);
|
}, [bodyshop, socket, t, client]);
|
||||||
|
|
||||||
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import PhoneFormatter from "../../utils/PhoneFormatter";
|
|||||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import "./chat-conversation-list.styles.scss";
|
import "./chat-conversation-list.styles.scss";
|
||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import { GET_PHONE_NUMBER_CONSENTS } from "../../graphql/consent.queries.js";
|
||||||
|
import { phone } from "phone";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
selectedConversation: selectSelectedConversation
|
selectedConversation: selectSelectedConversation
|
||||||
@@ -20,25 +24,45 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation }) {
|
function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation }) {
|
||||||
// That comma is there for a reason, do not remove it
|
const { t } = useTranslation();
|
||||||
const [, forceUpdate] = useState(false);
|
const [, forceUpdate] = useState(false);
|
||||||
|
|
||||||
// Re-render every minute
|
// Normalize phone numbers and fetch consent statuses
|
||||||
|
const phoneNumbers = conversationList.map((item) => phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, ""));
|
||||||
|
const { data: consentData, loading: consentLoading } = useQuery(GET_PHONE_NUMBER_CONSENTS, {
|
||||||
|
variables: {
|
||||||
|
bodyshopid: conversationList[0]?.bodyshopid,
|
||||||
|
phone_numbers: phoneNumbers
|
||||||
|
},
|
||||||
|
skip: !conversationList.length || !conversationList[0]?.bodyshopid,
|
||||||
|
fetchPolicy: "cache-and-network"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a map of phone number to consent status
|
||||||
|
const consentMap = React.useMemo(() => {
|
||||||
|
const map = new Map();
|
||||||
|
consentData?.phone_number_consent?.forEach((consent) => {
|
||||||
|
map.set(consent.phone_number, consent.consent_status);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [consentData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
forceUpdate((prev) => !prev); // Toggle state to trigger re-render
|
forceUpdate((prev) => !prev);
|
||||||
}, 60000); // 1 minute in milliseconds
|
}, 60000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
return () => clearInterval(interval); // Cleanup on unmount
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Memoize the sorted conversation list
|
|
||||||
const sortedConversationList = React.useMemo(() => {
|
const sortedConversationList = React.useMemo(() => {
|
||||||
return _.orderBy(conversationList, ["updated_at"], ["desc"]);
|
return _.orderBy(conversationList, ["updated_at"], ["desc"]);
|
||||||
}, [conversationList]);
|
}, [conversationList]);
|
||||||
|
|
||||||
const renderConversation = (index) => {
|
const renderConversation = (index, t) => {
|
||||||
const item = sortedConversationList[index];
|
const item = sortedConversationList[index];
|
||||||
|
const normalizedPhone = phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, "");
|
||||||
|
const isConsented = consentMap.get(normalizedPhone) ?? false;
|
||||||
|
|
||||||
const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
||||||
const cardContentLeft =
|
const cardContentLeft =
|
||||||
item.job_conversations.length > 0
|
item.job_conversations.length > 0
|
||||||
@@ -60,7 +84,12 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count} />;
|
const cardExtra = (
|
||||||
|
<>
|
||||||
|
<Badge count={item.messages_aggregate.aggregate.count} />
|
||||||
|
{!isConsented && <Tag color="red">{t("messaging.labels.no_consent")}</Tag>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
const getCardStyle = () =>
|
const getCardStyle = () =>
|
||||||
item.id === selectedConversation
|
item.id === selectedConversation
|
||||||
@@ -73,7 +102,7 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
|||||||
onClick={() => setSelectedConversation(item.id)}
|
onClick={() => setSelectedConversation(item.id)}
|
||||||
className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`}
|
className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`}
|
||||||
>
|
>
|
||||||
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
|
<Card style={getCardStyle()} variant={true} size="small" extra={cardExtra} title={cardTitle}>
|
||||||
<div style={{ display: "inline-block", width: "70%", textAlign: "left" }}>{cardContentLeft}</div>
|
<div style={{ display: "inline-block", width: "70%", textAlign: "left" }}>{cardContentLeft}</div>
|
||||||
<div style={{ display: "inline-block", width: "30%", textAlign: "right" }}>{cardContentRight}</div>
|
<div style={{ display: "inline-block", width: "30%", textAlign: "right" }}>{cardContentRight}</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -85,7 +114,7 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
|||||||
<div className="chat-list-container">
|
<div className="chat-list-container">
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
data={sortedConversationList}
|
data={sortedConversationList}
|
||||||
itemContent={(index) => renderConversation(index)}
|
itemContent={(index) => renderConversation(index, t)}
|
||||||
style={{ height: "100%", width: "100%" }}
|
style={{ height: "100%", width: "100%" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
/* Add spacing and better alignment for items */
|
/* Add spacing and better alignment for items */
|
||||||
.chat-list-item {
|
.chat-list-item {
|
||||||
padding: 0.5rem 0; /* Add spacing between list items */
|
padding: 0.2rem 0; /* Add spacing between list items */
|
||||||
|
|
||||||
.ant-card {
|
.ant-card {
|
||||||
border-radius: 8px; /* Slight rounding for card edges */
|
border-radius: 8px; /* Slight rounding for card edges */
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
|||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
variables: {
|
variables: {
|
||||||
jobId: conversation.job_conversations[0] && conversation.job_conversations[0].jobid
|
jobId: conversation.job_conversations[0] && conversation.job_conversations[0]?.jobid
|
||||||
},
|
},
|
||||||
|
|
||||||
skip: !open || !conversation.job_conversations || conversation.job_conversations.length === 0
|
skip: !open || !conversation.job_conversations || conversation.job_conversations.length === 0
|
||||||
@@ -67,14 +67,14 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
|||||||
<>
|
<>
|
||||||
{!bodyshop.uselocalmediaserver && (
|
{!bodyshop.uselocalmediaserver && (
|
||||||
<JobsDocumentImgproxyGalleryExternal
|
<JobsDocumentImgproxyGalleryExternal
|
||||||
jobId={conversation.job_conversations[0].jobid}
|
jobId={conversation.job_conversations[0]?.jobid}
|
||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{bodyshop.uselocalmediaserver && open && (
|
{bodyshop.uselocalmediaserver && open && (
|
||||||
<JobDocumentsLocalGalleryExternal
|
<JobDocumentsLocalGalleryExternal
|
||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
jobId={conversation.job_conversations[0] && conversation.job_conversations[0].jobid}
|
jobId={conversation.job_conversations[0] && conversation.job_conversations[0]?.jobid}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -89,7 +89,7 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
|||||||
{bodyshop.uselocalmediaserver && open && (
|
{bodyshop.uselocalmediaserver && open && (
|
||||||
<JobDocumentsLocalGalleryExternal
|
<JobDocumentsLocalGalleryExternal
|
||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
jobId={conversation.job_conversations[0] && conversation.job_conversations[0].jobid}
|
jobId={conversation.job_conversations[0] && conversation.job_conversations[0]?.jobid}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import { selectIsSending, selectMessage } from "../../redux/messaging/messaging.
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component";
|
import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component";
|
||||||
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
|
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
|
||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import { GET_PHONE_NUMBER_CONSENT } from "../../graphql/consent.queries";
|
||||||
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
import { phone } from "phone";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -25,16 +29,23 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) {
|
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) {
|
||||||
const inputArea = useRef(null);
|
const inputArea = useRef(null);
|
||||||
const [selectedMedia, setSelectedMedia] = useState([]);
|
const [selectedMedia, setSelectedMedia] = useState([]);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const normalizedPhone = phone(conversation.phone_num, "CA").phoneNumber.replace(/^\+1/, "");
|
||||||
|
const { data: consentData } = useQuery(GET_PHONE_NUMBER_CONSENT, {
|
||||||
|
variables: { bodyshopid: bodyshop.id, phone_number: normalizedPhone },
|
||||||
|
fetchPolicy: "cache-and-network"
|
||||||
|
});
|
||||||
|
const isConsented = consentData?.phone_number_consent?.[0]?.consent_status ?? false;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
inputArea.current.focus();
|
inputArea.current.focus();
|
||||||
}, [isSending, setMessage]);
|
}, [isSending, setMessage]);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const handleEnter = () => {
|
const handleEnter = () => {
|
||||||
const selectedImages = selectedMedia.filter((i) => i.isSelected);
|
const selectedImages = selectedMedia.filter((i) => i.isSelected);
|
||||||
if ((message === "" || !message) && selectedImages.length === 0) return;
|
if ((message === "" || !message) && selectedImages.length === 0) return;
|
||||||
|
if (!isConsented) return;
|
||||||
logImEXEvent("messaging_send_message");
|
logImEXEvent("messaging_send_message");
|
||||||
|
|
||||||
if (selectedImages.length < 11) {
|
if (selectedImages.length < 11) {
|
||||||
@@ -44,7 +55,8 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
|||||||
messagingServiceSid: bodyshop.messagingservicesid,
|
messagingServiceSid: bodyshop.messagingservicesid,
|
||||||
conversationid: conversation.id,
|
conversationid: conversation.id,
|
||||||
selectedMedia: selectedImages,
|
selectedMedia: selectedImages,
|
||||||
imexshopid: bodyshop.imexshopid
|
imexshopid: bodyshop.imexshopid,
|
||||||
|
bodyshopid: bodyshop.id
|
||||||
};
|
};
|
||||||
sendMessage(newMessage);
|
sendMessage(newMessage);
|
||||||
setSelectedMedia(
|
setSelectedMedia(
|
||||||
@@ -57,6 +69,9 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="imex-flex-row" style={{ width: "100%" }}>
|
<div className="imex-flex-row" style={{ width: "100%" }}>
|
||||||
|
{!isConsented && (
|
||||||
|
<AlertComponent message={t("messaging.errors.no_consent")} type="warning" style={{ marginBottom: 8 }} />
|
||||||
|
)}
|
||||||
<ChatPresetsComponent className="imex-flex-row__margin" />
|
<ChatPresetsComponent className="imex-flex-row__margin" />
|
||||||
<ChatMediaSelector
|
<ChatMediaSelector
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
@@ -71,18 +86,18 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
|||||||
ref={inputArea}
|
ref={inputArea}
|
||||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||||
value={message}
|
value={message}
|
||||||
disabled={isSending}
|
disabled={isSending || !isConsented}
|
||||||
placeholder={t("messaging.labels.typeamessage")}
|
placeholder={t("messaging.labels.typeamessage")}
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
onPressEnter={(event) => {
|
onPressEnter={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!!!event.shiftKey) handleEnter();
|
if (!event.shiftKey && isConsented) handleEnter();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<SendOutlined
|
<SendOutlined
|
||||||
className="chat-send-message-button"
|
className="chat-send-message-button"
|
||||||
// disabled={message === "" || !message}
|
disabled={!isConsented || message === "" || !message}
|
||||||
onClick={handleEnter}
|
onClick={handleEnter}
|
||||||
/>
|
/>
|
||||||
<Spin
|
<Spin
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
|
import { Table, Switch, Input, Tooltip, Upload, Button } from "antd";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import {
|
||||||
|
GET_PHONE_NUMBER_CONSENTS,
|
||||||
|
SET_PHONE_NUMBER_CONSENT,
|
||||||
|
BULK_SET_PHONE_NUMBER_CONSENT
|
||||||
|
} from "../../graphql/consent.queries.js";
|
||||||
|
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||||
|
import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
||||||
|
import { UploadOutlined } from "@ant-design/icons";
|
||||||
|
import { phone } from "phone";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
|
function PhoneNumberConsentList({ bodyshop }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const { loading, data } = useQuery(GET_PHONE_NUMBER_CONSENTS, {
|
||||||
|
variables: { bodyshopid: bodyshop.id, search },
|
||||||
|
fetchPolicy: "network-only"
|
||||||
|
});
|
||||||
|
const [setConsent] = useMutation(SET_PHONE_NUMBER_CONSENT);
|
||||||
|
const [bulkSetConsent] = useMutation(BULK_SET_PHONE_NUMBER_CONSENT);
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("consent.phone_number"),
|
||||||
|
dataIndex: "phone_number",
|
||||||
|
render: (text) => <PhoneNumberFormatter>{text}</PhoneNumberFormatter>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("consent.status"),
|
||||||
|
dataIndex: "consent_status",
|
||||||
|
render: (status, record) => (
|
||||||
|
<Tooltip title={record.history?.[0]?.reason || "No audit history"}>
|
||||||
|
<Switch
|
||||||
|
checked={status}
|
||||||
|
onChange={(checked) =>
|
||||||
|
setConsent({
|
||||||
|
variables: {
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
phone_number: record.phone_number,
|
||||||
|
consent_status: checked,
|
||||||
|
reason: "Manual override in app",
|
||||||
|
changed_by: "user" // Replace with actual user email from context
|
||||||
|
},
|
||||||
|
optimisticResponse: {
|
||||||
|
insert_phone_number_consent_one: {
|
||||||
|
__typename: "phone_number_consent",
|
||||||
|
id: record.id,
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
phone_number: record.phone_number,
|
||||||
|
consent_status: checked,
|
||||||
|
created_at: record.created_at,
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
consent_updated_at: new Date().toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("consent.updated_at"),
|
||||||
|
dataIndex: "consent_updated_at",
|
||||||
|
render: (text) => <TimeAgoFormatter>{text}</TimeAgoFormatter>
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleBulkUpload = async (file) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (e) => {
|
||||||
|
const text = e.target.result;
|
||||||
|
const lines = text.split("\n").slice(1); // Skip header
|
||||||
|
const objects = lines
|
||||||
|
.filter((line) => line.trim())
|
||||||
|
.map((line) => {
|
||||||
|
const [phone_number, consent_status] = line.split(",");
|
||||||
|
return {
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
phone_number: phone(phone_number, "CA").phoneNumber.replace(/^\+1/, ""),
|
||||||
|
consent_status: consent_status.trim().toLowerCase() === "true"
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await bulkSetConsent({
|
||||||
|
variables: { objects },
|
||||||
|
context: { headers: { "x-reason": "System update via bulk upload", "x-changed-by": "system" } }
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Bulk upload failed:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Input.Search
|
||||||
|
placeholder={t("general.labels.search")}
|
||||||
|
onSearch={(value) => setSearch(value)}
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
<Upload beforeUpload={handleBulkUpload} accept=".csv" showUploadList={false}>
|
||||||
|
<Button icon={<UploadOutlined />}>{t("consent.bulk_upload")}</Button>
|
||||||
|
</Upload>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={data?.phone_number_consent}
|
||||||
|
loading={loading}
|
||||||
|
rowKey="id"
|
||||||
|
style={{ marginTop: 16 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(PhoneNumberConsentList);
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { useMutation } from "@apollo/client";
|
||||||
|
import { Switch, Typography } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import { UPDATE_BODYSHOP_ENFORCE_CONSENT } from "../../graphql/bodyshop.queries";
|
||||||
|
import PhoneNumberConsentList from "../phone-number-consent/phone-number-consent.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
|
function ShopInfoConsentComponent({ bodyshop }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [updateEnforceConsent] = useMutation(UPDATE_BODYSHOP_ENFORCE_CONSENT);
|
||||||
|
|
||||||
|
console.dir(bodyshop);
|
||||||
|
|
||||||
|
const enforceConsent = bodyshop?.enforce_sms_consent ?? false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Typography.Title level={4}>{t("settings.title")}</Typography.Title>
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<Typography.Text>{t("settings.enforce_sms_consent")}</Typography.Text>
|
||||||
|
<Switch
|
||||||
|
checked={enforceConsent}
|
||||||
|
onChange={(checked) =>
|
||||||
|
updateEnforceConsent({
|
||||||
|
variables: { id: bodyshop.id, enforce_sms_consent: checked },
|
||||||
|
optimisticResponse: {
|
||||||
|
update_bodyshops_by_pk: {
|
||||||
|
__typename: "bodyshops",
|
||||||
|
id: bodyshop.id,
|
||||||
|
enforce_sms_consent: checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<PhoneNumberConsentList bodyshop={bodyshop} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoConsentComponent);
|
||||||
@@ -142,6 +142,7 @@ export const QUERY_BODYSHOP = gql`
|
|||||||
intellipay_config
|
intellipay_config
|
||||||
md_ro_guard
|
md_ro_guard
|
||||||
notification_followers
|
notification_followers
|
||||||
|
enforce_sms_consent
|
||||||
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
|
employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
@@ -363,3 +364,12 @@ export const GET_ACTIVE_EMPLOYEES_IN_SHOP = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const UPDATE_BODYSHOP_ENFORCE_CONSENT = gql`
|
||||||
|
mutation UPDATE_BODYSHOP_ENFORCE_CONSENT($id: uuid!, $enforce_sms_consent: Boolean!) {
|
||||||
|
update_bodyshops_by_pk(pk_columns: { id: $id }, _set: { enforce_sms_consent: $enforce_sms_consent }) {
|
||||||
|
id
|
||||||
|
enforce_sms_consent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
90
client/src/graphql/consent.queries.js
Normal file
90
client/src/graphql/consent.queries.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { gql } from "@apollo/client";
|
||||||
|
|
||||||
|
export const GET_PHONE_NUMBER_CONSENT = gql`
|
||||||
|
query GET_PHONE_NUMBER_CONSENT($bodyshopid: uuid!, $phone_number: String!) {
|
||||||
|
phone_number_consent(where: { bodyshopid: { _eq: $bodyshopid }, phone_number: { _eq: $phone_number } }) {
|
||||||
|
id
|
||||||
|
bodyshopid
|
||||||
|
phone_number
|
||||||
|
consent_status
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
consent_updated_at
|
||||||
|
history(order_by: { changed_at: desc }, limit: 1) {
|
||||||
|
reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET_PHONE_NUMBER_CONSENTS = gql`
|
||||||
|
query GET_PHONE_NUMBER_CONSENTS($bodyshopid: uuid!, $phone_numbers: [String!]) {
|
||||||
|
phone_number_consent(
|
||||||
|
where: { bodyshopid: { _eq: $bodyshopid }, phone_number: { _in: $phone_numbers } }
|
||||||
|
order_by: { consent_updated_at: desc }
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
bodyshopid
|
||||||
|
phone_number
|
||||||
|
consent_status
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
consent_updated_at
|
||||||
|
history(order_by: { changed_at: desc }, limit: 1) {
|
||||||
|
reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SET_PHONE_NUMBER_CONSENT = gql`
|
||||||
|
mutation SET_PHONE_NUMBER_CONSENT(
|
||||||
|
$bodyshopid: uuid!
|
||||||
|
$phone_number: String!
|
||||||
|
$consent_status: Boolean!
|
||||||
|
$reason: String!
|
||||||
|
$changed_by: String!
|
||||||
|
) {
|
||||||
|
insert_phone_number_consent_one(
|
||||||
|
object: {
|
||||||
|
bodyshopid: $bodyshopid
|
||||||
|
phone_number: $phone_number
|
||||||
|
consent_status: $consent_status
|
||||||
|
consent_updated_at: "now()"
|
||||||
|
}
|
||||||
|
on_conflict: {
|
||||||
|
constraint: phone_number_consent_bodyshopid_phone_number_key
|
||||||
|
update_columns: [consent_status, consent_updated_at]
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
bodyshopid
|
||||||
|
phone_number
|
||||||
|
consent_status
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
consent_updated_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const BULK_SET_PHONE_NUMBER_CONSENT = gql`
|
||||||
|
mutation BULK_SET_PHONE_NUMBER_CONSENT($objects: [phone_number_consent_insert_input!]!) {
|
||||||
|
insert_phone_number_consent(
|
||||||
|
objects: $objects
|
||||||
|
on_conflict: {
|
||||||
|
constraint: phone_number_consent_bodyshopid_phone_number_key
|
||||||
|
update_columns: [consent_status, consent_updated_at]
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
affected_rows
|
||||||
|
returning {
|
||||||
|
id
|
||||||
|
bodyshopid
|
||||||
|
phone_number
|
||||||
|
consent_status
|
||||||
|
consent_updated_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -10,10 +10,10 @@ import ShopCsiConfig from "../../components/shop-csi-config/shop-csi-config.comp
|
|||||||
import ShopEmployeesContainer from "../../components/shop-employees/shop-employees.container";
|
import ShopEmployeesContainer from "../../components/shop-employees/shop-employees.container";
|
||||||
import ShopInfoContainer from "../../components/shop-info/shop-info.container";
|
import ShopInfoContainer from "../../components/shop-info/shop-info.container";
|
||||||
import ShopInfoUsersComponent from "../../components/shop-users/shop-users.component";
|
import ShopInfoUsersComponent from "../../components/shop-users/shop-users.component";
|
||||||
|
import ShopInfoConsentComponent from "../../components/shop-info/shop-info.consent.component";
|
||||||
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
|
|
||||||
import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component";
|
||||||
import ShopTeamsContainer from "../../components/shop-teams/shop-teams.container";
|
import ShopTeamsContainer from "../../components/shop-teams/shop-teams.container";
|
||||||
|
|
||||||
@@ -91,6 +91,14 @@ export function ShopPage({ bodyshop, setSelectedHeader, setBreadcrumbs }) {
|
|||||||
children: <ShopCsiConfig />
|
children: <ShopCsiConfig />
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add Consent Settings tab
|
||||||
|
items.push({
|
||||||
|
key: "consent",
|
||||||
|
label: t("bodyshop.labels.consent_settings"),
|
||||||
|
children: <ShopInfoConsentComponent bodyshop={bodyshop} />
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RbacWrapper action="shop:config">
|
<RbacWrapper action="shop:config">
|
||||||
<Tabs activeKey={search.tab} onChange={(key) => history({ search: `?tab=${key}` })} items={items} />
|
<Tabs activeKey={search.tab} onChange={(key) => history({ search: `?tab=${key}` })} items={items} />
|
||||||
|
|||||||
@@ -2805,6 +2805,7 @@ exports.GET_BODYSHOP_BY_ID = `
|
|||||||
intellipay_config
|
intellipay_config
|
||||||
state
|
state
|
||||||
notification_followers
|
notification_followers
|
||||||
|
enforce_sms_consent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -2968,3 +2969,103 @@ exports.GET_JOB_WATCHERS_MINIMAL = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Query to get consent status for a phone number
|
||||||
|
exports.GET_PHONE_NUMBER_CONSENT = `
|
||||||
|
query GET_PHONE_NUMBER_CONSENT($bodyshopid: uuid!, $phone_number: String!) {
|
||||||
|
phone_number_consent(where: { bodyshopid: { _eq: $bodyshopid }, phone_number: { _eq: $phone_number } }) {
|
||||||
|
id
|
||||||
|
bodyshopid
|
||||||
|
phone_number
|
||||||
|
consent_status
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
consent_updated_at
|
||||||
|
history(order_by: { changed_at: desc }) {
|
||||||
|
id
|
||||||
|
old_value
|
||||||
|
new_value
|
||||||
|
reason
|
||||||
|
changed_at
|
||||||
|
changed_by
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Query to get consent history
|
||||||
|
exports.GET_PHONE_NUMBER_CONSENT_HISTORY = `
|
||||||
|
query GET_PHONE_NUMBER_CONSENT_HISTORY($phone_number_consent_id: uuid!) {
|
||||||
|
phone_number_consent_history(where: { phone_number_consent_id: { _eq: $phone_number_consent_id } }, order_by: { changed_at: desc }) {
|
||||||
|
id
|
||||||
|
phone_number_consent_id
|
||||||
|
old_value
|
||||||
|
new_value
|
||||||
|
reason
|
||||||
|
changed_at
|
||||||
|
changed_by
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Mutation to set consent status
|
||||||
|
exports.SET_PHONE_NUMBER_CONSENT = `
|
||||||
|
mutation SET_PHONE_NUMBER_CONSENT($bodyshopid: uuid!, $phone_number: String!, $consent_status: Boolean!, $reason: String!) {
|
||||||
|
insert_phone_number_consent_one(
|
||||||
|
object: {
|
||||||
|
bodyshopid: $bodyshopid
|
||||||
|
phone_number: $phone_number
|
||||||
|
consent_status: $consent_status
|
||||||
|
consent_updated_at: "now()"
|
||||||
|
}
|
||||||
|
on_conflict: {
|
||||||
|
constraint: phone_number_consent_bodyshopid_phone_number_key
|
||||||
|
update_columns: [consent_status, consent_updated_at]
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
bodyshopid
|
||||||
|
phone_number
|
||||||
|
consent_status
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
consent_updated_at
|
||||||
|
}
|
||||||
|
insert_phone_number_consent_history_one(
|
||||||
|
object: {
|
||||||
|
phone_number_consent_id: $id
|
||||||
|
old_value: $old_value
|
||||||
|
new_value: $consent_status
|
||||||
|
reason: $reason
|
||||||
|
changed_by: $changed_by
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
reason
|
||||||
|
changed_at
|
||||||
|
changed_by
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Mutation for bulk consent updates
|
||||||
|
exports.BULK_SET_PHONE_NUMBER_CONSENT = `
|
||||||
|
mutation BULK_SET_PHONE_NUMBER_CONSENT($objects: [phone_number_consent_insert_input!]!) {
|
||||||
|
insert_phone_number_consent(
|
||||||
|
objects: $objects
|
||||||
|
on_conflict: {
|
||||||
|
constraint: phone_number_consent_bodyshopid_phone_number_key
|
||||||
|
update_columns: [consent_status, consent_updated_at]
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
affected_rows
|
||||||
|
returning {
|
||||||
|
id
|
||||||
|
bodyshopid
|
||||||
|
phone_number
|
||||||
|
consent_status
|
||||||
|
consent_updated_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ const {
|
|||||||
FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID,
|
FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID,
|
||||||
UNARCHIVE_CONVERSATION,
|
UNARCHIVE_CONVERSATION,
|
||||||
CREATE_CONVERSATION,
|
CREATE_CONVERSATION,
|
||||||
INSERT_MESSAGE
|
INSERT_MESSAGE,
|
||||||
|
SET_PHONE_NUMBER_CONSENT
|
||||||
} = require("../graphql-client/queries");
|
} = require("../graphql-client/queries");
|
||||||
const { phone } = require("phone");
|
const { phone } = require("phone");
|
||||||
const { admin } = require("../firebase/firebase-handler");
|
const { admin } = require("../firebase/firebase-handler");
|
||||||
@@ -91,7 +92,30 @@ const receive = async (req, res) => {
|
|||||||
|
|
||||||
const bodyshop = response.bodyshops[0];
|
const bodyshop = response.bodyshops[0];
|
||||||
|
|
||||||
// Sort conversations by `updated_at` (or `created_at`) and pick the last one
|
// Step 2: Handle consent
|
||||||
|
const normalizedPhone = phone(req.body.From, "CA").phoneNumber.replace(/^\+1/, "");
|
||||||
|
const isStop = req.body.Body.toUpperCase().includes("STOP");
|
||||||
|
const consentStatus = isStop ? false : true;
|
||||||
|
const reason = isStop ? "Customer texted STOP" : "Inbound message received";
|
||||||
|
|
||||||
|
const consentResponse = await client.request(SET_PHONE_NUMBER_CONSENT, {
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
phone_number: normalizedPhone,
|
||||||
|
consent_status: consentStatus,
|
||||||
|
reason,
|
||||||
|
changed_by: "system"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emit WebSocket event for consent change
|
||||||
|
const broadcastRoom = getBodyshopRoom(bodyshop.id);
|
||||||
|
ioRedis.to(broadcastRoom).emit("consent-changed", {
|
||||||
|
bodyshopId: bodyshop.id,
|
||||||
|
phone_number: normalizedPhone,
|
||||||
|
consent_status: consentStatus,
|
||||||
|
reason
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 3: Process conversation
|
||||||
const sortedConversations = bodyshop.conversations.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
const sortedConversations = bodyshop.conversations.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
||||||
const existingConversation = sortedConversations.length
|
const existingConversation = sortedConversations.length
|
||||||
? sortedConversations[sortedConversations.length - 1]
|
? sortedConversations[sortedConversations.length - 1]
|
||||||
@@ -104,14 +128,11 @@ const receive = async (req, res) => {
|
|||||||
image: !!req.body.MediaUrl0,
|
image: !!req.body.MediaUrl0,
|
||||||
image_path: generateMediaArray(req.body),
|
image_path: generateMediaArray(req.body),
|
||||||
isoutbound: false,
|
isoutbound: false,
|
||||||
userid: null // Add additional fields as necessary
|
userid: null
|
||||||
};
|
};
|
||||||
|
|
||||||
if (existingConversation) {
|
if (existingConversation) {
|
||||||
// Use the existing conversation
|
|
||||||
conversationid = existingConversation.id;
|
conversationid = existingConversation.id;
|
||||||
|
|
||||||
// Unarchive the conversation if necessary
|
|
||||||
if (existingConversation.archived) {
|
if (existingConversation.archived) {
|
||||||
await client.request(UNARCHIVE_CONVERSATION, {
|
await client.request(UNARCHIVE_CONVERSATION, {
|
||||||
id: conversationid,
|
id: conversationid,
|
||||||
@@ -119,11 +140,10 @@ const receive = async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Create a new conversation
|
|
||||||
const newConversationResponse = await client.request(CREATE_CONVERSATION, {
|
const newConversationResponse = await client.request(CREATE_CONVERSATION, {
|
||||||
conversation: {
|
conversation: {
|
||||||
bodyshopid: bodyshop.id,
|
bodyshopid: bodyshop.id,
|
||||||
phone_num: phone(req.body.From).phoneNumber,
|
phone_num: normalizedPhone,
|
||||||
archived: false
|
archived: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -131,13 +151,12 @@ const receive = async (req, res) => {
|
|||||||
conversationid = createdConversation.id;
|
conversationid = createdConversation.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure `conversationid` is added to the message
|
|
||||||
newMessage.conversationid = conversationid;
|
newMessage.conversationid = conversationid;
|
||||||
|
|
||||||
// Step 3: Insert the message into the conversation
|
// Step 4: Insert the message
|
||||||
const insertresp = await client.request(INSERT_MESSAGE, {
|
const insertresp = await client.request(INSERT_MESSAGE, {
|
||||||
msg: newMessage,
|
msg: newMessage,
|
||||||
conversationid: conversationid
|
conversationid
|
||||||
});
|
});
|
||||||
|
|
||||||
const message = insertresp?.insert_messages?.returning?.[0];
|
const message = insertresp?.insert_messages?.returning?.[0];
|
||||||
@@ -147,8 +166,7 @@ const receive = async (req, res) => {
|
|||||||
throw new Error("Conversation data is missing from the response.");
|
throw new Error("Conversation data is missing from the response.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: Notify clients through Redis
|
// Step 5: Notify clients
|
||||||
const broadcastRoom = getBodyshopRoom(conversation.bodyshop.id);
|
|
||||||
const conversationRoom = getBodyshopConversationRoom({
|
const conversationRoom = getBodyshopConversationRoom({
|
||||||
bodyshopId: conversation.bodyshop.id,
|
bodyshopId: conversation.bodyshop.id,
|
||||||
conversationId: conversation.id
|
conversationId: conversation.id
|
||||||
@@ -176,7 +194,7 @@ const receive = async (req, res) => {
|
|||||||
summary: false
|
summary: false
|
||||||
});
|
});
|
||||||
|
|
||||||
// Step 5: Send FCM notification
|
// Step 6: Send FCM notification
|
||||||
const fcmresp = await admin.messaging().send({
|
const fcmresp = await admin.messaging().send({
|
||||||
topic: `${message.conversation.bodyshop.imexshopid}-messaging`,
|
topic: `${message.conversation.bodyshop.imexshopid}-messaging`,
|
||||||
notification: {
|
notification: {
|
||||||
|
|||||||
Reference in New Issue
Block a user