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 { Input, Table } from "antd";
import { Input, Space, Table, Typography } from "antd";
import { Link } from "react-router-dom";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
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 PhoneNumberFormatter from "../../utils/PhoneFormatter";
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({
bodyshop: selectBodyshop,
@@ -44,18 +47,6 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) {
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
const getAssociatedOwners = (phoneNumber) => {
if (!ownersData?.owners) return [];
@@ -102,15 +93,20 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) {
}
return owners.map((owner) => (
<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>
));
},
sorter: (a, b) => {
const aOwners = getAssociatedOwners(a.phone_number);
const bOwners = getAssociatedOwners(b.phone_number);
const aName = aOwners[0] ? formatOwnerName(aOwners[0]) : "";
const bName = bOwners[0] ? formatOwnerName(bOwners[0]) : "";
const aName = aOwners[0] ? `${aOwners[0].ownr_fn} ${aOwners[0].ownr_ln}` : "";
const bName = bOwners[0] ? `${bOwners[0].ownr_fn} ${bOwners[0].ownr_ln}` : "";
return aName.localeCompare(bName);
}
},
@@ -124,6 +120,7 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) {
return (
<div>
<Paragraph>{t("consent.text_body")}</Paragraph>
<Input.Search
placeholder={t("general.labels.search")}
onSearch={(value) => setSearch(value)}

View File

@@ -92,13 +92,15 @@ export function ShopPage({ bodyshop, setSelectedHeader, setBreadcrumbs }) {
});
}
// Add Consent Settings tab
items.push({
key: "consent",
label: t("bodyshop.labels.consent_settings"),
children: <ShopInfoConsentComponent bodyshop={bodyshop} />
});
if (bodyshop.messagingservicesid) {
// Add Consent Settings tab
items.push({
key: "consent",
label: t("bodyshop.labels.consent_settings"),
children: <ShopInfoConsentComponent bodyshop={bodyshop} />
});
}
return (
<RbacWrapper action="shop:config">
<Tabs activeKey={search.tab} onChange={(key) => history({ search: `?tab=${key}` })} items={items} />

View File

@@ -3875,7 +3875,8 @@
"created_at": "Opt-Out Date",
"no_owners": "No Associated Owners",
"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": {
"title": "Phone Number Opt-Out List"

View File

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

View File

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

View File

@@ -12,6 +12,11 @@ const { phone } = require("phone");
const { admin } = require("../firebase/firebase-handler");
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
* @param req
@@ -58,9 +63,6 @@ const receive = async (req, res) => {
const messageText = (req.body.Body || "").trim().toUpperCase();
// 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)) {
// Check if the phone number is in phone_number_opt_out
const optOutCheck = await client.request(CHECK_PHONE_NUMBER_OPT_OUT, {