feature/IO-3182-Phone-Number-Consent - Checkpoint

This commit is contained in:
Dave Richer
2025-05-27 11:03:09 -04:00
parent 7e884c42ea
commit fe2600029f
6 changed files with 34 additions and 30 deletions

View File

@@ -1,14 +1,17 @@
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import { Input, Table } from "antd"; import { Input, Space, Table, Typography } from "antd";
import { Link } from "react-router-dom";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import { GET_PHONE_NUMBER_OPT_OUTS, SEARCH_OWNERS_BY_PHONE_NUMBERS } from "../../graphql/phone-number-opt-out.queries"; import { GET_PHONE_NUMBER_OPT_OUTS, SEARCH_OWNERS_BY_PHONE_NUMBERS } from "../../graphql/phone-number-opt-out.queries";
import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import { TimeAgoFormatter } from "../../utils/DateFormatter"; import { TimeAgoFormatter } from "../../utils/DateFormatter";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
const { Paragraph } = Typography; // Destructure Paragraph from Typography
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -44,18 +47,6 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) {
fetchPolicy: "network-only" fetchPolicy: "network-only"
}); });
// Format owner names for display
const formatOwnerName = (owner) => {
const parts = [];
if (owner.ownr_fn || owner.ownr_ln) {
parts.push([owner.ownr_fn, owner.ownr_ln].filter(Boolean).join(" "));
}
if (owner.ownr_co_nm) {
parts.push(owner.ownr_co_nm);
}
return parts.join(", ") || "-";
};
// Map phone numbers to their associated owners and identify phone field // Map phone numbers to their associated owners and identify phone field
const getAssociatedOwners = (phoneNumber) => { const getAssociatedOwners = (phoneNumber) => {
if (!ownersData?.owners) return []; if (!ownersData?.owners) return [];
@@ -102,15 +93,20 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) {
} }
return owners.map((owner) => ( return owners.map((owner) => (
<div key={owner.id}> <div key={owner.id}>
{formatOwnerName(owner)} ({owner.phoneField}) <Space direction="horizontal">
<Link to={"/manage/owners/" + owner.id}>
<OwnerNameDisplay ownerObject={owner} />
</Link>
({owner.phoneField})
</Space>
</div> </div>
)); ));
}, },
sorter: (a, b) => { sorter: (a, b) => {
const aOwners = getAssociatedOwners(a.phone_number); const aOwners = getAssociatedOwners(a.phone_number);
const bOwners = getAssociatedOwners(b.phone_number); const bOwners = getAssociatedOwners(b.phone_number);
const aName = aOwners[0] ? formatOwnerName(aOwners[0]) : ""; const aName = aOwners[0] ? `${aOwners[0].ownr_fn} ${aOwners[0].ownr_ln}` : "";
const bName = bOwners[0] ? formatOwnerName(bOwners[0]) : ""; const bName = bOwners[0] ? `${bOwners[0].ownr_fn} ${bOwners[0].ownr_ln}` : "";
return aName.localeCompare(bName); return aName.localeCompare(bName);
} }
}, },
@@ -124,6 +120,7 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) {
return ( return (
<div> <div>
<Paragraph>{t("consent.text_body")}</Paragraph>
<Input.Search <Input.Search
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
onSearch={(value) => setSearch(value)} onSearch={(value) => setSearch(value)}

View File

@@ -92,13 +92,15 @@ export function ShopPage({ bodyshop, setSelectedHeader, setBreadcrumbs }) {
}); });
} }
// Add Consent Settings tab if (bodyshop.messagingservicesid) {
items.push({ // Add Consent Settings tab
key: "consent", items.push({
label: t("bodyshop.labels.consent_settings"), key: "consent",
children: <ShopInfoConsentComponent bodyshop={bodyshop} /> 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} />

View File

@@ -3875,7 +3875,8 @@
"created_at": "Opt-Out Date", "created_at": "Opt-Out Date",
"no_owners": "No Associated Owners", "no_owners": "No Associated Owners",
"phone_1": "Phone 1", "phone_1": "Phone 1",
"phone_2": "Phone 2" "phone_2": "Phone 2",
"text_body": "Users can opt out of receiving SMS messages by replying with keywords such as STOP, UNSUBSCRIBE, CANCEL, END, QUIT, STOPALL, REVOKE and OPTOUT. To opt back in, users can reply with START, YES, or UNSTOP. Even after opting out, users can still send messages to us, which will be received and processed as needed. Ensure customers are informed to reply with these keywords to manage their messaging preferences. After opting out, users receive a confirmation message and will not receive further messages until they opt back in."
}, },
"settings": { "settings": {
"title": "Phone Number Opt-Out List" "title": "Phone Number Opt-Out List"

View File

@@ -3877,7 +3877,8 @@
"created_at": "", "created_at": "",
"no_owners": "", "no_owners": "",
"phone_1": "", "phone_1": "",
"phone_2": "" "phone_2": "",
"text_body": ""
}, },
"settings": { "settings": {
"title": "" "title": ""

View File

@@ -3877,7 +3877,8 @@
"created_at": "Opt-Out Date", "created_at": "Opt-Out Date",
"no_owners": "No Associated Owners", "no_owners": "No Associated Owners",
"phone_1": "Phone 1", "phone_1": "Phone 1",
"phone_2": "Phone 2" "phone_2": "Phone 2",
"text_body": ""
}, },
"settings": { "settings": {
"title": "" "title": ""

View File

@@ -12,6 +12,11 @@ const { phone } = require("phone");
const { admin } = require("../firebase/firebase-handler"); const { admin } = require("../firebase/firebase-handler");
const InstanceManager = require("../utils/instanceMgr").default; const InstanceManager = require("../utils/instanceMgr").default;
/ TWILLIO KEYWORDS;
// Note: When we handle different languages, we might need to adjust these keywords accordingly.
const optInKeywords = ["START", "YES", "UNSTOP"];
const optOutKeywords = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT", "REVOKE", "OPTOUT"];
/** /**
* Receive SMS messages from Twilio and process them * Receive SMS messages from Twilio and process them
* @param req * @param req
@@ -58,9 +63,6 @@ const receive = async (req, res) => {
const messageText = (req.body.Body || "").trim().toUpperCase(); const messageText = (req.body.Body || "").trim().toUpperCase();
// Step 2: Check for opt-in or opt-out keywords // Step 2: Check for opt-in or opt-out keywords
const optInKeywords = ["START", "YES", "UNSTOP"];
const optOutKeywords = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT"];
if (optInKeywords.includes(messageText) || optOutKeywords.includes(messageText)) { if (optInKeywords.includes(messageText) || optOutKeywords.includes(messageText)) {
// Check if the phone number is in phone_number_opt_out // Check if the phone number is in phone_number_opt_out
const optOutCheck = await client.request(CHECK_PHONE_NUMBER_OPT_OUT, { const optOutCheck = await client.request(CHECK_PHONE_NUMBER_OPT_OUT, {