feat(whatsapp): persist phone number health

This commit is contained in:
Muhsin
2026-07-21 10:27:14 +04:00
parent a61a8bfbc6
commit 9c7418a40a
16 changed files with 1045 additions and 157 deletions
@@ -686,36 +686,93 @@
"WIDGET_FEATURES": "Widget features",
"ACCOUNT_HEALTH": {
"TITLE": "Manage your WhatsApp account",
"DESCRIPTION": "Review your WhatsApp account status, messaging limits, and quality. Update settings or resolve issues if needed",
"DESCRIPTION": "Review your WhatsApp phone number health, messaging capacity, and linked Meta business details.",
"GO_TO_SETTINGS": "Go to Meta Business Manager",
"NO_DATA": "Health data is not available",
"ERRORS": {
"AUTHORIZATION": {
"EMBEDDED_TITLE": "WhatsApp connection needs to be refreshed",
"EMBEDDED_DESCRIPTION": "Meta could not validate the connection for this inbox. Go to Configuration to reconfigure it using Embedded Signup.",
"MANUAL_TITLE": "WhatsApp access token needs attention",
"MANUAL_DESCRIPTION": "Meta could not validate the access token for this inbox. Go to Configuration to verify or replace it."
},
"GENERIC_TITLE": "WhatsApp health data is unavailable",
"GENERIC_DESCRIPTION": "Meta could not return the current health details. Review the error below and try again later.",
"GO_TO_CONFIGURATION": "Go to Configuration"
},
"SECTIONS": {
"PHONE_NUMBER": "Phone number",
"HEALTH_AND_CAPACITY": "Health and capacity",
"BUSINESS_ACCOUNT": "Business account"
},
"FIELDS": {
"DISPLAY_PHONE_NUMBER": {
"LABEL": "Display phone number",
"TOOLTIP": "Phone number displayed to customers"
},
"VERIFIED_NAME": {
"LABEL": "Business name",
"TOOLTIP": "Business name verified by WhatsApp"
"LABEL": "WhatsApp display name",
"TOOLTIP": "Display name associated with this phone number on WhatsApp"
},
"PHONE_NUMBER_ID": {
"LABEL": "Phone number ID",
"TOOLTIP": "Meta identifier for this WhatsApp phone number"
},
"COEXISTENCE": {
"LABEL": "Coexistence",
"TOOLTIP": "This number is connected to both the WhatsApp Business app and Cloud API"
},
"DISPLAY_NAME_STATUS": {
"LABEL": "Display name status",
"TOOLTIP": "Status of your business name verification"
"TOOLTIP": "Review status of the WhatsApp display name"
},
"LAST_ONBOARDED_TIME": {
"LABEL": "Last onboarded",
"TOOLTIP": "Most recent time Meta onboarded this phone number"
},
"PHONE_NUMBER_STATUS": {
"LABEL": "Phone number status",
"TOOLTIP": "Current connection status of this phone number on WhatsApp"
},
"QUALITY_RATING": {
"LABEL": "Quality rating",
"TOOLTIP": "WhatsApp quality rating for your account"
"TOOLTIP": "Quality rating Meta assigns to this phone number based on recent messaging signals"
},
"MESSAGING_LIMIT_TIER": {
"LABEL": "Messaging limit tier",
"TOOLTIP": "Daily messaging limit for your account"
"TOOLTIP": "Number of customers this phone number can initiate conversations with in 24 hours"
},
"ACCOUNT_MODE": {
"LABEL": "Account mode",
"TOOLTIP": "Current operating mode of your WhatsApp account"
"TOOLTIP": "Current operating mode of this WhatsApp phone number"
},
"CODE_VERIFICATION_STATUS": {
"LABEL": "Code verification",
"TOOLTIP": "Verification-code status reported by Meta for this phone number"
},
"THROUGHPUT_LEVEL": {
"LABEL": "Throughput",
"TOOLTIP": "Message throughput level assigned to this phone number"
},
"BUSINESS_ACCOUNT_NAME": {
"LABEL": "WhatsApp Business Account name",
"TOOLTIP": "Name of the WhatsApp Business Account linked to this phone number"
},
"BUSINESS_ACCOUNT_ID": {
"LABEL": "WhatsApp Business Account ID",
"TOOLTIP": "Meta identifier for the linked WhatsApp Business Account"
},
"BUSINESS_PORTFOLIO_NAME": {
"LABEL": "Business portfolio name",
"TOOLTIP": "Name of the Meta business portfolio that owns the WhatsApp Business Account"
},
"BUSINESS_PORTFOLIO_ID": {
"LABEL": "Business portfolio ID",
"TOOLTIP": "Meta identifier for the business portfolio that owns the WhatsApp Business Account"
}
},
"VALUES": {
"NOT_AVAILABLE": "Not available",
"TIERS": {
"TIER_250": "250 customers per 24h",
"TIER_1000": "1K customers per 24h",
@@ -727,6 +784,16 @@
},
"STATUSES": {
"APPROVED": "Approved",
"CONNECTED": "Connected",
"VERIFIED": "Verified",
"STANDARD": "Standard",
"ACTIVE": "Active",
"EXPIRED": "Expired",
"FLAGGED": "Flagged",
"RESTRICTED": "Restricted",
"BANNED": "Banned",
"DISCONNECTED": "Disconnected",
"UNKNOWN": "Not available",
"PENDING_REVIEW": "Pending Review",
"AVAILABLE_WITHOUT_REVIEW": "Available Without Review",
"REJECTED": "Rejected",
@@ -746,7 +813,11 @@
"REGISTER_SUCCESS": "Webhook registered successfully",
"REGISTER_ERROR": "Failed to register webhook. Please try again.",
"CONFIGURED_SUCCESS": "Webhook configured successfully",
"URL_MISMATCH": "Webhook URL mismatch"
"URL_MISMATCH": "Webhook URL mismatch",
"CONFIGURED_URL": "Configured URL",
"EXPECTED_URL": "Expected URL",
"COPY_URL": "Copy webhook URL",
"COPY_SUCCESS": "Webhook URL copied to clipboard"
}
},
"SETTINGS": "Settings",
@@ -411,6 +411,7 @@ export default {
return (
this.isAWhatsAppCloudChannel &&
this.isEmbeddedSignupWhatsApp &&
this.healthError?.type !== 'authorization' &&
this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.WHATSAPP_MANUAL_TRANSFER
@@ -580,11 +581,27 @@ export default {
const response = await InboxHealthAPI.getHealthStatus(this.inbox.id);
this.healthData = response.data;
} catch (error) {
this.healthError = error.message || 'Failed to fetch health data';
const apiError = error.response?.data?.error;
this.healthData = null;
this.healthError =
typeof apiError === 'object'
? apiError
: {
type: 'generic',
message: apiError || error.message,
};
} finally {
this.isLoadingHealth = false;
}
},
goToWhatsAppConfiguration() {
const configurationTabIndex = this.tabs.findIndex(
tab => tab.key === 'configuration'
);
if (configurationTabIndex !== -1) {
this.onTabChange(configurationTabIndex);
}
},
async registerWebhook() {
if (!this.inbox) return;
@@ -1402,8 +1419,11 @@ export default {
<div v-if="selectedTabKey === 'whatsapp-health'">
<AccountHealth
:health-data="healthData"
:health-error="healthError"
:is-embedded-signup="isEmbeddedSignupWhatsApp"
:is-registering-webhook="isRegisteringWebhook"
@register-webhook="registerWebhook"
@go-to-configuration="goToWhatsAppConfiguration"
/>
</div>
<WhatsappManualMigrationDialog
@@ -1,24 +1,34 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
import ButtonV4 from 'next/button/Button.vue';
import Icon from 'dashboard/components-next/icon/Icon.vue';
import { useAlert } from 'dashboard/composables';
const props = defineProps({
healthData: {
type: Object,
default: null,
},
healthError: {
type: Object,
default: null,
},
isEmbeddedSignup: {
type: Boolean,
default: false,
},
isRegisteringWebhook: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['registerWebhook']);
const emit = defineEmits(['registerWebhook', 'goToConfiguration']);
const { t } = useI18n();
const { t, te, locale } = useI18n();
const QUALITY_COLORS = {
GREEN: 'text-n-teal-11',
@@ -29,8 +39,17 @@ const QUALITY_COLORS = {
const STATUS_COLORS = {
APPROVED: 'text-n-teal-11',
CONNECTED: 'text-n-teal-11',
VERIFIED: 'text-n-teal-11',
STANDARD: 'text-n-teal-11',
ACTIVE: 'text-n-teal-11',
PENDING_REVIEW: 'text-n-amber-11',
AVAILABLE_WITHOUT_REVIEW: 'text-n-teal-11',
EXPIRED: 'text-n-amber-11',
FLAGGED: 'text-n-amber-11',
RESTRICTED: 'text-n-ruby-9',
BANNED: 'text-n-ruby-9',
DISCONNECTED: 'text-n-ruby-9',
REJECTED: 'text-n-ruby-9',
DECLINED: 'text-n-ruby-9',
};
@@ -40,85 +59,267 @@ const MODE_COLORS = {
SANDBOX: 'text-n-slate-11',
};
const healthItems = computed(() => {
const formatStatusDisplay = status => {
const translationKey = `INBOX_MGMT.ACCOUNT_HEALTH.VALUES.STATUSES.${status}`;
if (te(translationKey)) {
return t(translationKey);
}
return status
.toLowerCase()
.split('_')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};
const formatDateTime = value => {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return new Intl.DateTimeFormat(locale.value, {
dateStyle: 'medium',
timeStyle: 'short',
}).format(date);
};
const healthSections = computed(() => {
if (!props.healthData) {
return [];
}
const {
id: phoneNumberId,
display_phone_number: displayPhoneNumber,
verified_name: verifiedName,
name_status: nameStatus,
quality_rating: qualityRating,
messaging_limit_tier: messagingLimitTier,
account_mode: accountMode,
status: phoneNumberStatus,
code_verification_status: codeVerificationStatus,
throughput_level: throughputLevel,
last_onboarded_time: lastOnboardedTime,
is_on_biz_app: isOnBizApp,
platform_type: platformType,
business_account_id: businessAccountId,
business_account_name: businessAccountName,
business_portfolio_id: businessPortfolioId,
business_portfolio_name: businessPortfolioName,
} = props.healthData;
const notAvailable = t('INBOX_MGMT.ACCOUNT_HEALTH.VALUES.NOT_AVAILABLE');
return [
{
key: 'displayPhoneNumber',
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_PHONE_NUMBER.LABEL'),
value: displayPhoneNumber || 'N/A',
tooltip: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_PHONE_NUMBER.TOOLTIP'
),
show: true,
key: 'phoneNumber',
title: t('INBOX_MGMT.ACCOUNT_HEALTH.SECTIONS.PHONE_NUMBER'),
items: [
{
key: 'displayPhoneNumber',
label: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_PHONE_NUMBER.LABEL'
),
value: displayPhoneNumber || notAvailable,
tooltip: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_PHONE_NUMBER.TOOLTIP'
),
},
{
key: 'verifiedName',
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.VERIFIED_NAME.LABEL'),
value: verifiedName || notAvailable,
tooltip: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.VERIFIED_NAME.TOOLTIP'),
},
{
key: 'phoneNumberId',
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.PHONE_NUMBER_ID.LABEL'),
value: phoneNumberId || notAvailable,
tooltip: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.PHONE_NUMBER_ID.TOOLTIP'
),
},
{
key: 'coexistence',
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.COEXISTENCE.LABEL'),
value: 'ACTIVE',
tooltip: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.COEXISTENCE.TOOLTIP'),
type: 'status',
show: isOnBizApp === true && platformType === 'CLOUD_API',
},
{
key: 'displayNameStatus',
label: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_NAME_STATUS.LABEL'
),
value: nameStatus || 'UNKNOWN',
tooltip: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_NAME_STATUS.TOOLTIP'
),
type: 'status',
},
{
key: 'lastOnboardedTime',
label: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.LAST_ONBOARDED_TIME.LABEL'
),
value: lastOnboardedTime
? formatDateTime(lastOnboardedTime)
: notAvailable,
tooltip: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.LAST_ONBOARDED_TIME.TOOLTIP'
),
show: Boolean(lastOnboardedTime),
},
],
},
{
key: 'verifiedName',
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.VERIFIED_NAME.LABEL'),
value: verifiedName || 'N/A',
tooltip: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.VERIFIED_NAME.TOOLTIP'),
show: true,
key: 'healthAndCapacity',
title: t('INBOX_MGMT.ACCOUNT_HEALTH.SECTIONS.HEALTH_AND_CAPACITY'),
items: [
{
key: 'phoneNumberStatus',
label: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.PHONE_NUMBER_STATUS.LABEL'
),
value: phoneNumberStatus || 'UNKNOWN',
tooltip: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.PHONE_NUMBER_STATUS.TOOLTIP'
),
type: 'status',
},
{
key: 'qualityRating',
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.QUALITY_RATING.LABEL'),
value: qualityRating || 'UNKNOWN',
tooltip: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.QUALITY_RATING.TOOLTIP'),
type: 'quality',
},
{
key: 'messagingLimitTier',
label: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.MESSAGING_LIMIT_TIER.LABEL'
),
value: messagingLimitTier || 'UNKNOWN',
tooltip: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.MESSAGING_LIMIT_TIER.TOOLTIP'
),
type: 'tier',
},
{
key: 'accountMode',
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.ACCOUNT_MODE.LABEL'),
value: accountMode || 'UNKNOWN',
tooltip: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.ACCOUNT_MODE.TOOLTIP'),
type: 'mode',
},
{
key: 'codeVerificationStatus',
label: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.CODE_VERIFICATION_STATUS.LABEL'
),
value: codeVerificationStatus || 'UNKNOWN',
tooltip: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.CODE_VERIFICATION_STATUS.TOOLTIP'
),
type: 'status',
},
{
key: 'throughputLevel',
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.THROUGHPUT_LEVEL.LABEL'),
value: throughputLevel || 'UNKNOWN',
tooltip: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.THROUGHPUT_LEVEL.TOOLTIP'
),
type: 'status',
},
],
},
{
key: 'displayNameStatus',
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_NAME_STATUS.LABEL'),
value: nameStatus || 'UNKNOWN',
tooltip: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.DISPLAY_NAME_STATUS.TOOLTIP'
),
show: true,
type: 'status',
key: 'businessAccount',
title: t('INBOX_MGMT.ACCOUNT_HEALTH.SECTIONS.BUSINESS_ACCOUNT'),
items: [
{
key: 'businessAccountName',
label: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_ACCOUNT_NAME.LABEL'
),
value: businessAccountName || notAvailable,
tooltip: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_ACCOUNT_NAME.TOOLTIP'
),
},
{
key: 'businessAccountId',
label: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_ACCOUNT_ID.LABEL'
),
value: businessAccountId || notAvailable,
tooltip: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_ACCOUNT_ID.TOOLTIP'
),
},
{
key: 'businessPortfolioName',
label: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_PORTFOLIO_NAME.LABEL'
),
value: businessPortfolioName || notAvailable,
tooltip: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_PORTFOLIO_NAME.TOOLTIP'
),
},
{
key: 'businessPortfolioId',
label: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_PORTFOLIO_ID.LABEL'
),
value: businessPortfolioId || notAvailable,
tooltip: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.BUSINESS_PORTFOLIO_ID.TOOLTIP'
),
},
],
},
{
key: 'qualityRating',
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.QUALITY_RATING.LABEL'),
value: qualityRating || 'UNKNOWN',
tooltip: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.QUALITY_RATING.TOOLTIP'),
show: true,
type: 'quality',
},
{
key: 'messagingLimitTier',
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.MESSAGING_LIMIT_TIER.LABEL'),
value: messagingLimitTier || 'UNKNOWN',
tooltip: t(
'INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.MESSAGING_LIMIT_TIER.TOOLTIP'
),
show: true,
type: 'tier',
},
{
key: 'accountMode',
label: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.ACCOUNT_MODE.LABEL'),
value: accountMode || 'UNKNOWN',
tooltip: t('INBOX_MGMT.ACCOUNT_HEALTH.FIELDS.ACCOUNT_MODE.TOOLTIP'),
show: true,
type: 'mode',
},
];
].map(section => ({
...section,
items: section.items.filter(item => item.show !== false),
}));
});
const errorState = computed(() => {
if (!props.healthError) return null;
if (props.healthError.type === 'authorization') {
return {
title: props.isEmbeddedSignup
? t('INBOX_MGMT.ACCOUNT_HEALTH.ERRORS.AUTHORIZATION.EMBEDDED_TITLE')
: t('INBOX_MGMT.ACCOUNT_HEALTH.ERRORS.AUTHORIZATION.MANUAL_TITLE'),
description: props.isEmbeddedSignup
? t(
'INBOX_MGMT.ACCOUNT_HEALTH.ERRORS.AUTHORIZATION.EMBEDDED_DESCRIPTION'
)
: t(
'INBOX_MGMT.ACCOUNT_HEALTH.ERRORS.AUTHORIZATION.MANUAL_DESCRIPTION'
),
showConfigurationAction: true,
};
}
return {
title: t('INBOX_MGMT.ACCOUNT_HEALTH.ERRORS.GENERIC_TITLE'),
description: t('INBOX_MGMT.ACCOUNT_HEALTH.ERRORS.GENERIC_DESCRIPTION'),
showConfigurationAction: false,
};
});
const handleGoToSettings = () => {
const { business_id: businessId } = props.healthData || {};
const { business_portfolio_id: businessPortfolioId } = props.healthData || {};
if (businessId) {
// WhatsApp Business Manager URL with specific business ID and phone numbers tab
const whatsappBusinessUrl = `https://business.facebook.com/latest/whatsapp_manager/phone_numbers/?business_id=${businessId}&tab=phone-numbers`;
if (businessPortfolioId) {
const whatsappBusinessUrl = `https://business.facebook.com/latest/whatsapp_manager/phone_numbers/?business_id=${businessPortfolioId}&tab=phone-numbers`;
window.open(whatsappBusinessUrl, '_blank');
} else {
// Fallback to general WhatsApp Business Manager if business_id is not available
const fallbackUrl = 'https://business.facebook.com/';
window.open(fallbackUrl, '_blank');
}
@@ -130,9 +331,6 @@ const getQualityRatingTextColor = rating =>
const formatTierDisplay = tier =>
t(`INBOX_MGMT.ACCOUNT_HEALTH.VALUES.TIERS.${tier}`) || tier;
const formatStatusDisplay = status =>
t(`INBOX_MGMT.ACCOUNT_HEALTH.VALUES.STATUSES.${status}`) || status;
const formatModeDisplay = mode =>
t(`INBOX_MGMT.ACCOUNT_HEALTH.VALUES.MODES.${mode}`) || mode;
@@ -163,6 +361,15 @@ const webhookUrlMismatch = computed(
const handleRegisterWebhook = () => {
emit('registerWebhook');
};
const handleGoToConfiguration = () => {
emit('goToConfiguration');
};
const handleCopyWebhookUrl = async url => {
await copyTextToClipboard(url);
useAlert(t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.COPY_SUCCESS'));
};
</script>
<template>
@@ -192,55 +399,62 @@ const handleRegisterWebhook = () => {
</ButtonV4>
</div>
<div v-if="healthData" class="grid grid-cols-1 gap-4 xs:grid-cols-2">
<div
v-for="item in healthItems"
:key="item.key"
class="flex flex-col gap-2 p-4 rounded-lg border border-n-weak bg-n-solid-1"
>
<div class="flex gap-2 items-center">
<span class="text-body-main font-medium text-n-slate-11">
{{ item.label }}
</span>
<Icon
v-tooltip.top="item.tooltip"
icon="i-lucide-info"
class="flex-shrink-0 w-4 h-4 cursor-help text-n-slate-9"
/>
<div v-if="healthData" class="space-y-6">
<section v-for="section in healthSections" :key="section.key">
<h3 class="mb-3 text-heading-4 text-n-slate-12">
{{ section.title }}
</h3>
<div class="grid grid-cols-1 gap-4 xs:grid-cols-2">
<div
v-for="item in section.items"
:key="item.key"
class="flex flex-col gap-2 p-4 rounded-lg border border-n-weak bg-n-solid-1"
>
<div class="flex gap-2 items-center">
<span class="text-body-main font-medium text-n-slate-11">
{{ item.label }}
</span>
<Icon
v-tooltip.top="item.tooltip"
icon="i-lucide-info"
class="flex-shrink-0 w-4 h-4 cursor-help text-n-slate-9"
/>
</div>
<div class="flex items-center">
<span
v-if="item.type === 'quality'"
class="inline-flex items-center px-2 py-0.5 min-h-6 text-label-small rounded-md bg-n-alpha-2"
:class="getQualityRatingTextColor(item.value)"
>
{{ item.value }}
</span>
<span
v-else-if="item.type === 'status'"
class="inline-flex items-center px-2 py-0.5 min-h-6 text-label-small rounded-md bg-n-alpha-2"
:class="getStatusTextColor(item.value)"
>
{{ formatStatusDisplay(item.value) }}
</span>
<span
v-else-if="item.type === 'mode'"
class="inline-flex items-center px-2 py-0.5 min-h-6 text-label-small rounded-md bg-n-alpha-2"
:class="getModeStatusTextColor(item.value)"
>
{{ formatModeDisplay(item.value) }}
</span>
<span
v-else-if="item.type === 'tier'"
class="text-label text-n-slate-12"
>
{{ formatTierDisplay(item.value) }}
</span>
<span v-else class="text-label text-n-slate-12">
{{ item.value }}
</span>
</div>
</div>
</div>
<div class="flex items-center">
<span
v-if="item.type === 'quality'"
class="inline-flex items-center px-2 py-0.5 min-h-6 text-label-small rounded-md bg-n-alpha-2"
:class="getQualityRatingTextColor(item.value)"
>
{{ item.value }}
</span>
<span
v-else-if="item.type === 'status'"
class="inline-flex items-center px-2 py-0.5 min-h-6 text-label-small rounded-md bg-n-alpha-2"
:class="getStatusTextColor(item.value)"
>
{{ formatStatusDisplay(item.value) }}
</span>
<span
v-else-if="item.type === 'mode'"
class="inline-flex items-center px-2 py-0.5 min-h-6 text-label-small rounded-md bg-n-alpha-2"
:class="getModeStatusTextColor(item.value)"
>
{{ formatModeDisplay(item.value) }}
</span>
<span
v-else-if="item.type === 'tier'"
class="text-label text-n-slate-12"
>
{{ formatTierDisplay(item.value) }}
</span>
<span v-else class="text-label text-n-slate-12">{{
item.value
}}</span>
</div>
</div>
</section>
<!-- Webhook configuration card -->
<div
@@ -289,6 +503,87 @@ const handleRegisterWebhook = () => {
{{ t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.REGISTER_BUTTON') }}
</ButtonV4>
</div>
<div v-if="webhookConfigured" class="pt-1 space-y-2">
<div class="flex items-center gap-3 min-w-0">
<span class="flex-shrink-0 text-label-small text-n-slate-10">
{{ t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.CONFIGURED_URL') }}
</span>
<span
v-tooltip.top="webhookUrl"
class="flex-1 min-w-0 font-mono text-label-small truncate text-n-slate-12"
>
{{ webhookUrl }}
</span>
<ButtonV4
v-tooltip.top="t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.COPY_URL')"
type="button"
ghost
sm
slate
icon="i-lucide-copy"
class="flex-shrink-0"
:aria-label="t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.COPY_URL')"
@click="handleCopyWebhookUrl(webhookUrl)"
/>
</div>
<div
v-if="webhookUrlMismatch"
class="flex items-center gap-3 min-w-0"
>
<span class="flex-shrink-0 text-label-small text-n-slate-10">
{{ t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.EXPECTED_URL') }}
</span>
<span
v-tooltip.top="healthData.expected_webhook_url"
class="flex-1 min-w-0 font-mono text-label-small truncate text-n-slate-12"
>
{{ healthData.expected_webhook_url }}
</span>
<ButtonV4
v-tooltip.top="t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.COPY_URL')"
type="button"
ghost
sm
slate
icon="i-lucide-copy"
class="flex-shrink-0"
:aria-label="t('INBOX_MGMT.ACCOUNT_HEALTH.WEBHOOK.COPY_URL')"
@click="handleCopyWebhookUrl(healthData.expected_webhook_url)"
/>
</div>
</div>
</div>
</div>
<div v-else-if="errorState" class="pt-2">
<div
class="flex flex-col gap-4 p-5 rounded-lg border border-n-weak bg-n-solid-1"
>
<div class="flex gap-3 items-start">
<Icon
icon="i-lucide-alert-triangle"
class="flex-shrink-0 mt-0.5 w-5 h-5 text-n-amber-11"
/>
<div class="min-w-0">
<h3 class="text-heading-4 text-n-slate-12">
{{ errorState.title }}
</h3>
<p class="mt-1 text-body-main text-n-slate-11">
{{ errorState.description }}
</p>
</div>
</div>
<div
v-if="healthError.message"
class="p-3 rounded-lg bg-n-alpha-1 text-label-small text-n-slate-11"
>
<p class="break-words">{{ healthError.message }}</p>
</div>
<div v-if="errorState.showConfigurationAction">
<ButtonV4 sm solid blue @click="handleGoToConfiguration">
{{ t('INBOX_MGMT.ACCOUNT_HEALTH.ERRORS.GO_TO_CONFIGURATION') }}
</ButtonV4>
</div>
</div>
</div>