Merge pull request #2032 from OneUptime/whatsapp-webhook

Whatsapp webhook
This commit is contained in:
Simon Larsen 2025-10-13 13:32:25 +01:00 committed by GitHub
commit 51c3fcd3ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 561 additions and 20 deletions

View file

@ -68,6 +68,9 @@ const buildWhatsAppSetupMarkdown: BuildWhatsAppSetupMarkdown = (): string => {
WhatsAppTemplateIds,
) as Array<keyof typeof WhatsAppTemplateIds>;
const appApiBaseUrl: string = APP_API_URL.toString().replace(/\/$/, "");
const primaryWebhookUrl: string = `${appApiBaseUrl}/notification/whatsapp/webhook`;
const description: string =
"Follow these steps to connect Meta WhatsApp with OneUptime so notifications can be delivered via WhatsApp.";
@ -75,6 +78,7 @@ const buildWhatsAppSetupMarkdown: BuildWhatsAppSetupMarkdown = (): string => {
"Meta Business Manager admin access for the WhatsApp Business Account.",
"A WhatsApp Business phone number approved for API messaging.",
"Admin access to OneUptime with permission to edit global notification settings.",
"A webhook verify token string that you'll configure identically in Meta and OneUptime.",
];
const setupStepsList: Array<string> = [
@ -82,7 +86,7 @@ const buildWhatsAppSetupMarkdown: BuildWhatsAppSetupMarkdown = (): string => {
"From **Business Settings → Accounts → WhatsApp Accounts**, create or select the account that owns your sender phone number.",
"In Buisness Portfolio, create a system user and assign it to the WhatsApp Business Account with the role of **Admin**.",
"Generate a token for this system user and this will be your long-lived access token. Make sure to select the **whatsapp_business_management** and **whatsapp_business_messaging** permissions when generating the token.",
"Paste the access token and phone number ID into the **Meta WhatsApp Settings** card above, then save.",
"Paste the access token, phone number ID, and webhook verify token into the **Meta WhatsApp Settings** card above, then save.",
"For the **Business Account ID**, go to **Business Settings → Business Info** (or **Business Settings → WhatsApp Accounts → Settings**) and copy the **WhatsApp Business Account ID** value.",
"To locate the **App ID** and **App Secret**, open [Meta for Developers](https://developers.facebook.com/apps/), select your WhatsApp app, then navigate to **Settings → Basic**. The App ID is shown at the top; click **Show** next to **App Secret** to reveal and copy it.",
"Create each template listed below in the Meta WhatsApp Manager. Make sure the template name, language, and variables match exactly. You can however change the content to your preference. Please make sure it's approved by Meta.",
@ -169,12 +173,25 @@ const buildWhatsAppSetupMarkdown: BuildWhatsAppSetupMarkdown = (): string => {
.filter(Boolean)
.join("\n");
const webhookSection: string = [
"### Configure Meta Webhook Subscription",
"1. In the OneUptime Admin Dashboard, open **Settings → WhatsApp → Meta WhatsApp Settings** and enter a strong value in **Webhook Verify Token**. Save the form so the encrypted token is stored in Global Config.",
"2. Keep that verify token handy—Meta does not generate one for you. You'll paste the exact same value when configuring the callback.",
"3. In [Meta for Developers](https://developers.facebook.com/apps/), select your WhatsApp app and navigate to **WhatsApp → Configuration → Webhooks**.",
`4. Click **Configure**, then supply one of the following callback URLs when Meta asks for your endpoint:\n - \`${primaryWebhookUrl}\`\n `,
"5. Paste the verify token from step 1 into Meta's **Verify Token** field and submit. Meta will call the callback URL and expect that value to match before it approves the subscription.",
"6. After verification succeeds, subscribe to the **messages** field (and any other WhatsApp webhook categories you need) so delivery status updates are forwarded to OneUptime.",
]
.filter(Boolean)
.join("\n\n");
return [
description,
"### Prerequisites",
prerequisitesMarkdown,
"### Setup Steps",
setupStepsMarkdown,
webhookSection,
"### Required WhatsApp Templates",
templateSummaryTable,
"### Template Bodies",
@ -271,6 +288,18 @@ const SettingsWhatsApp: FunctionComponent = (): ReactElement => {
"Optional Business Account ID that owns the WhatsApp templates.",
placeholder: "123456789012345",
},
{
field: {
metaWhatsAppWebhookVerifyToken: true,
},
title: "Webhook Verify Token",
stepId: "meta-credentials",
fieldType: FormFieldSchemaType.EncryptedText,
required: false,
description:
"Secret token configured in Meta to validate webhook subscription requests.",
placeholder: "Webhook verify token",
},
{
field: {
metaWhatsAppAppId: true,
@ -324,6 +353,14 @@ const SettingsWhatsApp: FunctionComponent = (): ReactElement => {
fieldType: FieldType.Text,
placeholder: "Not Configured",
},
{
field: {
metaWhatsAppWebhookVerifyToken: true,
},
title: "Webhook Verify Token",
fieldType: FieldType.HiddenText,
placeholder: "Not Configured",
},
{
field: {
metaWhatsAppAppId: true,

View file

@ -1,6 +1,7 @@
import WhatsAppService from "../Services/WhatsAppService";
import BadDataException from "Common/Types/Exception/BadDataException";
import { JSONObject } from "Common/Types/JSON";
import GlobalConfig from "Common/Models/DatabaseModels/GlobalConfig";
import { JSONArray, JSONObject } from "Common/Types/JSON";
import ObjectID from "Common/Types/ObjectID";
import Phone from "Common/Types/Phone";
import WhatsAppMessage from "Common/Types/WhatsApp/WhatsAppMessage";
@ -9,17 +10,214 @@ import {
WhatsAppTemplateIds,
WhatsAppTemplateLanguage,
} from "Common/Types/WhatsApp/WhatsAppTemplates";
import WhatsAppStatus from "Common/Types/WhatsAppStatus";
import ClusterKeyAuthorization from "Common/Server/Middleware/ClusterKeyAuthorization";
import WhatsAppLogService from "Common/Server/Services/WhatsAppLogService";
import GlobalConfigService from "Common/Server/Services/GlobalConfigService";
import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
} from "Common/Server/Utils/Express";
import logger from "Common/Server/Utils/Logger";
import Response from "Common/Server/Utils/Response";
const router: ExpressRouter = Express.getRouter();
const MAX_STATUS_MESSAGE_LENGTH: number = 500;
export const mapWebhookStatusToWhatsAppStatus: (
status?: string,
) => WhatsAppStatus = (status?: string): WhatsAppStatus => {
switch ((status || "").toLowerCase()) {
case "sent":
return WhatsAppStatus.Sent;
case "delivered":
return WhatsAppStatus.Delivered;
case "read":
return WhatsAppStatus.Read;
case "failed":
return WhatsAppStatus.Failed;
case "deleted":
case "removed":
return WhatsAppStatus.Deleted;
case "warning":
return WhatsAppStatus.Warning;
case "queued":
case "pending":
return WhatsAppStatus.Queued;
case "error":
return WhatsAppStatus.Error;
case "success":
return WhatsAppStatus.Success;
default:
return WhatsAppStatus.Unknown;
}
};
export const buildStatusMessage: (payload: JSONObject) => string | undefined = (
payload: JSONObject,
): string | undefined => {
const messageParts: Array<string> = [];
const rawStatus: string | undefined = payload["status"]
? String(payload["status"])
: undefined;
if (rawStatus) {
messageParts.push(`Status: ${rawStatus}`);
}
const timestamp: string | undefined = payload["timestamp"]
? String(payload["timestamp"])
: undefined;
if (timestamp) {
const numericTimestamp: number = Number(timestamp);
if (!isNaN(numericTimestamp)) {
messageParts.push(
`Timestamp: ${new Date(numericTimestamp * 1000).toISOString()}`,
);
} else {
messageParts.push(`Timestamp: ${timestamp}`);
}
}
const conversation: JSONObject | undefined =
(payload["conversation"] as JSONObject | undefined) || undefined;
if (conversation) {
if (conversation["id"]) {
messageParts.push(`Conversation: ${conversation["id"]}`);
}
const origin: JSONObject | undefined =
(conversation["origin"] as JSONObject | undefined) || undefined;
if (origin?.["type"]) {
messageParts.push(`Origin: ${origin["type"]}`);
}
if (conversation["expiration_timestamp"]) {
const expirationTimestamp: number = Number(
conversation["expiration_timestamp"],
);
if (!isNaN(expirationTimestamp)) {
messageParts.push(
`Conversation expires: ${new Date(expirationTimestamp * 1000).toISOString()}`,
);
}
}
}
const pricing: JSONObject | undefined =
(payload["pricing"] as JSONObject | undefined) || undefined;
if (pricing) {
const pricingParts: Array<string> = [];
if (pricing["billable"] !== undefined) {
pricingParts.push(`billable=${pricing["billable"]}`);
}
if (pricing["category"]) {
pricingParts.push(`category=${pricing["category"]}`);
}
if (pricing["pricing_model"]) {
pricingParts.push(`model=${pricing["pricing_model"]}`);
}
if (pricingParts.length > 0) {
messageParts.push(`Pricing: ${pricingParts.join(", ")}`);
}
}
const errors: JSONArray | undefined =
(payload["errors"] as JSONArray | undefined) || undefined;
if (Array.isArray(errors) && errors.length > 0) {
const firstError: JSONObject = errors[0] as JSONObject;
const errorParts: Array<string> = [];
if (firstError["title"]) {
errorParts.push(String(firstError["title"]));
}
if (firstError["code"]) {
errorParts.push(`code=${firstError["code"]}`);
}
if (firstError["detail"]) {
errorParts.push(String(firstError["detail"]));
}
if (errorParts.length > 0) {
messageParts.push(`Error: ${errorParts.join(" - ")}`);
}
}
if (messageParts.length === 0) {
return undefined;
}
const statusMessage: string = messageParts.join(" | ");
if (statusMessage.length <= MAX_STATUS_MESSAGE_LENGTH) {
return statusMessage;
}
return `${statusMessage.substring(0, MAX_STATUS_MESSAGE_LENGTH - 3)}...`;
};
const updateWhatsAppLogStatus: (
statusPayload: JSONObject,
) => Promise<void> = async (statusPayload: JSONObject): Promise<void> => {
const messageId: string | undefined = statusPayload["id"]
? String(statusPayload["id"])
: undefined;
if (!messageId) {
logger.warn(
`[Meta WhatsApp Webhook] Received status payload without message id. Payload: ${JSON.stringify(statusPayload)}`,
);
return;
}
const rawStatus: string | undefined = statusPayload["status"]
? String(statusPayload["status"])
: undefined;
const derivedStatus: WhatsAppStatus =
mapWebhookStatusToWhatsAppStatus(rawStatus);
const statusMessage: string | undefined = buildStatusMessage(statusPayload);
const updateResult: number = await WhatsAppLogService.updateOneBy({
query: {
whatsAppMessageId: messageId,
},
data: {
status: derivedStatus,
...(statusMessage ? { statusMessage } : {}),
},
props: {
isRoot: true,
},
});
if (updateResult === 0) {
logger.warn(
`[Meta WhatsApp Webhook] No WhatsApp log found for message id ${messageId}. Payload: ${JSON.stringify(statusPayload)}`,
);
} else {
logger.debug(
`[Meta WhatsApp Webhook] Updated WhatsApp log for message id ${messageId} with status ${derivedStatus}.`,
);
}
};
const toTemplateVariables: (
rawVariables: JSONObject | undefined,
) => Record<string, string> | undefined = (
@ -119,6 +317,128 @@ router.post(
},
);
router.get("/webhook", async (req: ExpressRequest, res: ExpressResponse) => {
const mode: string | undefined = req.query["hub.mode"]
? String(req.query["hub.mode"])
: undefined;
const verifyToken: string | undefined = req.query["hub.verify_token"]
? String(req.query["hub.verify_token"])
: undefined;
const challenge: string | undefined = req.query["hub.challenge"]
? String(req.query["hub.challenge"])
: undefined;
if (mode === "subscribe" && challenge) {
const globalConfigTokenResponse: GlobalConfig | null =
await GlobalConfigService.findOneBy({
query: {
_id: ObjectID.getZeroObjectID().toString(),
},
props: {
isRoot: true,
},
select: {
metaWhatsAppWebhookVerifyToken: true,
},
});
const configuredVerifyToken: string | undefined =
globalConfigTokenResponse?.metaWhatsAppWebhookVerifyToken?.trim() ||
undefined;
if (!configuredVerifyToken) {
logger.error(
"Meta WhatsApp webhook verify token is not configured. Rejecting verification request.",
);
res.sendStatus(403);
return;
}
if (verifyToken === configuredVerifyToken) {
res.status(200).send(challenge);
return;
}
logger.warn(
"Meta WhatsApp webhook verification failed due to token mismatch.",
);
res.sendStatus(403);
return;
}
res.sendStatus(400);
});
router.post(
"/webhook",
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {
try {
const body: JSONObject = req.body as JSONObject;
if (
(body["object"] as string | undefined) !== "whatsapp_business_account"
) {
logger.debug(
`[Meta WhatsApp Webhook] Received event for unsupported object: ${JSON.stringify(body)}`,
);
return Response.sendEmptySuccessResponse(req, res);
}
const entries: JSONArray | undefined = body["entry"] as
| JSONArray
| undefined;
if (!Array.isArray(entries)) {
logger.warn(
`[Meta WhatsApp Webhook] Payload did not include entries array. Payload: ${JSON.stringify(body)}`,
);
return Response.sendEmptySuccessResponse(req, res);
}
const statusUpdatePromises: Array<Promise<void>> = [];
for (const entry of entries) {
const entryObject: JSONObject = entry as JSONObject;
const changes: JSONArray | undefined =
(entryObject["changes"] as JSONArray | undefined) || undefined;
if (!Array.isArray(changes)) {
continue;
}
for (const change of changes) {
const changeObject: JSONObject = change as JSONObject;
const value: JSONObject | undefined =
(changeObject["value"] as JSONObject | undefined) || undefined;
if (!value) {
continue;
}
const statuses: JSONArray | undefined =
(value["statuses"] as JSONArray | undefined) || undefined;
if (Array.isArray(statuses)) {
for (const statusItem of statuses) {
statusUpdatePromises.push(
updateWhatsAppLogStatus(statusItem as JSONObject),
);
}
}
}
}
if (statusUpdatePromises.length > 0) {
await Promise.allSettled(statusUpdatePromises);
}
return Response.sendEmptySuccessResponse(req, res);
} catch (err) {
return next(err);
}
},
);
router.post(
"/test",
async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => {

View file

@ -421,7 +421,7 @@ export default class WhatsAppService {
whatsAppLog.whatsAppMessageId = messageId;
}
whatsAppLog.status = WhatsAppStatus.Success;
whatsAppLog.status = WhatsAppStatus.Sent;
whatsAppLog.statusMessage = messageId
? `Message ID: ${messageId}`
: "WhatsApp message sent successfully";
@ -474,10 +474,14 @@ export default class WhatsAppService {
await UserOnCallLogTimelineService.updateOneById({
id: options.userOnCallLogTimelineId,
data: {
status:
whatsAppLog.status === WhatsAppStatus.Success
? UserNotificationStatus.Sent
: UserNotificationStatus.Error,
status: [
WhatsAppStatus.Success,
WhatsAppStatus.Sent,
WhatsAppStatus.Delivered,
WhatsAppStatus.Read,
].includes(whatsAppLog.status || WhatsAppStatus.Error)
? UserNotificationStatus.Sent
: UserNotificationStatus.Error,
statusMessage: whatsAppLog.statusMessage,
},
props: {

86
App/package-lock.json generated
View file

@ -27,7 +27,8 @@
"@types/xml2js": "^0.4.14",
"@types/xmldom": "^0.1.34",
"jest": "^28.1.0",
"nodemon": "^2.0.20"
"nodemon": "^2.0.20",
"ts-jest": "^28.0.8"
}
},
"../Common": {
@ -59,7 +60,9 @@
"@opentelemetry/sdk-trace-web": "^1.25.1",
"@opentelemetry/semantic-conventions": "^1.26.0",
"@remixicon/react": "^4.2.0",
"@simplewebauthn/server": "^13.2.2",
"@tippyjs/react": "^4.2.6",
"@types/archiver": "^6.0.3",
"@types/crypto-js": "^4.2.2",
"@types/qrcode": "^1.5.5",
"@types/react-highlight": "^0.12.8",
@ -68,7 +71,9 @@
"@types/web-push": "^3.6.4",
"acme-client": "^5.3.0",
"airtable": "^0.12.2",
"axios": "^1.7.2",
"archiver": "^7.0.1",
"axios": "^1.12.0",
"botbuilder": "^4.23.3",
"bullmq": "^5.3.3",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
@ -1915,6 +1920,19 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/bs-logger": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz",
"integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-json-stable-stringify": "2.x"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/bser": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
@ -3704,6 +3722,13 @@
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
@ -4660,6 +4685,63 @@
"nodetouch": "bin/nodetouch.js"
}
},
"node_modules/ts-jest": {
"version": "28.0.8",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-28.0.8.tgz",
"integrity": "sha512-5FaG0lXmRPzApix8oFG8RKjAz4ehtm8yMKOTy5HX3fY6W8kmvOrmcY0hKDElW52FJov+clhUbrKAqofnj4mXTg==",
"dev": true,
"license": "MIT",
"dependencies": {
"bs-logger": "0.x",
"fast-json-stable-stringify": "2.x",
"jest-util": "^28.0.0",
"json5": "^2.2.1",
"lodash.memoize": "4.x",
"make-error": "1.x",
"semver": "7.x",
"yargs-parser": "^21.0.1"
},
"bin": {
"ts-jest": "cli.js"
},
"engines": {
"node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
},
"peerDependencies": {
"@babel/core": ">=7.0.0-beta.0 <8",
"@jest/types": "^28.0.0",
"babel-jest": "^28.0.0",
"jest": "^28.0.0",
"typescript": ">=4.3"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"@jest/types": {
"optional": true
},
"babel-jest": {
"optional": true
},
"esbuild": {
"optional": true
}
}
},
"node_modules/ts-jest/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/ts-node": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",

View file

@ -35,7 +35,8 @@
"@types/nodemailer": "^6.4.14",
"@types/xml2js": "^0.4.14",
"@types/xmldom": "^0.1.34",
"jest": "^28.1.0",
"jest": "^28.1.0",
"ts-jest": "^28.0.8",
"nodemon": "^2.0.20"
}
}

View file

@ -352,6 +352,25 @@ export default class GlobalConfig extends GlobalConfigModel {
})
public metaWhatsAppAppSecret?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],
update: [],
})
@TableColumn({
type: TableColumnType.ShortText,
title: "Meta WhatsApp Webhook Verify Token",
description:
"Verify token configured in Meta to validate webhook subscriptions.",
})
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
unique: true,
})
public metaWhatsAppWebhookVerifyToken?: string = undefined;
@ColumnAccessControl({
create: [],
read: [],

View file

@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class MigrationName1760357680881 implements MigrationInterface {
public name = "MigrationName1760357680881";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "GlobalConfig" ADD "metaWhatsAppWebhookVerifyToken" character varying(100)`,
);
await queryRunner.query(
`ALTER TABLE "GlobalConfig" ADD CONSTRAINT "UQ_afe98d53b718f485d3d64b383b8" UNIQUE ("metaWhatsAppWebhookVerifyToken")`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "GlobalConfig" DROP CONSTRAINT "UQ_afe98d53b718f485d3d64b383b8"`,
);
await queryRunner.query(
`ALTER TABLE "GlobalConfig" DROP COLUMN "metaWhatsAppWebhookVerifyToken"`,
);
}
}

View file

@ -176,6 +176,7 @@ import { MigrationName1759232954703 } from "./1759232954703-MigrationName";
import { RenameUserTwoFactorAuthToUserTotpAuth1759234532998 } from "./1759234532998-MigrationName";
import { MigrationName1759943124812 } from "./1759943124812-MigrationName";
import { MigrationName1760345757975 } from "./1760345757975-MigrationName";
import { MigrationName1760357680881 } from "./1760357680881-MigrationName";
export default [
InitialMigration,
@ -356,4 +357,5 @@ export default [
RenameUserTwoFactorAuthToUserTotpAuth1759234532998,
MigrationName1759943124812,
MigrationName1760345757975,
MigrationName1760357680881,
];

View file

@ -1,8 +1,16 @@
enum WhatsAppStatus {
Success = "Success",
Sent = "Sent",
Delivered = "Delivered",
Read = "Read",
Failed = "Failed",
Deleted = "Deleted",
Warning = "Warning",
Queued = "Queued",
Error = "Error",
LowBalance = "Low Balance",
NotVerified = "Not Verified",
Unknown = "Unknown",
}
export default WhatsAppStatus;

View file

@ -4,7 +4,14 @@ import WhatsAppLog from "Common/Models/DatabaseModels/WhatsAppLog";
import FieldType from "Common/UI/Components/Types/FieldType";
import Columns from "Common/UI/Components/ModelTable/Columns";
import Pill from "Common/UI/Components/Pill/Pill";
import { Green, Red } from "Common/Types/BrandColors";
import {
Blue,
Green,
Grey,
Orange,
Red,
Yellow,
} from "Common/Types/BrandColors";
import WhatsAppStatus from "Common/Types/WhatsAppStatus";
import ProjectUtil from "Common/UI/Utils/Project";
import Filter from "Common/UI/Components/ModelFilter/Filter";
@ -15,6 +22,7 @@ import Query from "Common/Types/BaseDatabase/Query";
import BaseModel from "Common/Types/Workflow/Components/BaseModel";
import UserElement from "../User/User";
import User from "Common/Models/DatabaseModels/User";
import Color from "Common/Types/Color";
export interface WhatsAppLogsTableProps {
query?: Query<BaseModel>;
@ -28,6 +36,41 @@ const WhatsAppLogsTable: FunctionComponent<WhatsAppLogsTableProps> = (
const [modalText, setModalText] = useState<string>("");
const [modalTitle, setModalTitle] = useState<string>("");
const getStatusColor: (status?: WhatsAppStatus) => Color = (
status?: WhatsAppStatus,
): Color => {
switch (status) {
case WhatsAppStatus.Success:
case WhatsAppStatus.Delivered:
case WhatsAppStatus.Read:
return Green;
case WhatsAppStatus.Sent:
case WhatsAppStatus.Queued:
return Blue;
case WhatsAppStatus.Warning:
return Yellow;
case WhatsAppStatus.LowBalance:
return Orange;
case WhatsAppStatus.NotVerified:
case WhatsAppStatus.Unknown:
return Grey;
default:
return Red;
}
};
const parseStatus: (status?: string) => WhatsAppStatus | undefined = (
status?: string,
): WhatsAppStatus | undefined => {
if (!status) {
return undefined;
}
return (Object.values(WhatsAppStatus) as Array<string>).includes(status)
? (status as WhatsAppStatus)
: undefined;
};
const defaultColumns: Columns<WhatsAppLog> = [
{
field: { toNumber: true },
@ -61,17 +104,19 @@ const WhatsAppLogsTable: FunctionComponent<WhatsAppLogsTableProps> = (
title: "Status",
type: FieldType.Text,
getElement: (item: WhatsAppLog): ReactElement => {
if (item["status"]) {
return (
<Pill
isMinimal={false}
color={item["status"] === WhatsAppStatus.Success ? Green : Red}
text={item["status"] as string}
/>
);
const statusValue: string | undefined =
(item["status"] as string | undefined) || undefined;
if (!statusValue) {
return <></>;
}
return <></>;
const normalizedStatus: WhatsAppStatus | undefined =
parseStatus(statusValue);
const pillColor: Color = getStatusColor(normalizedStatus);
return <Pill isMinimal={false} color={pillColor} text={statusValue} />;
},
},
];