feat(whatsapp): add guided manual setup flow

This commit is contained in:
Muhsin
2026-07-11 16:35:18 +04:00
parent 98154bbeab
commit 86582569ee
24 changed files with 1649 additions and 11 deletions
@@ -16,6 +16,26 @@ class WhatsappChannel extends ApiClient {
inbox_id: inboxId,
});
}
previewManualSetup(params) {
return axios.post(`${this.baseUrl()}/whatsapp/manual/preview`, params);
}
connectManualSetup(params) {
return axios.post(`${this.baseUrl()}/whatsapp/manual/connect`, params);
}
getManualWebhookStatus(inboxId) {
return axios.get(
`${this.baseUrl()}/whatsapp/manual/${inboxId}/webhook_status`
);
}
setupManualWebhook(inboxId) {
return axios.post(
`${this.baseUrl()}/whatsapp/manual/${inboxId}/setup_webhook`
);
}
}
export default new WhatsappChannel();
@@ -294,6 +294,111 @@
"WEBHOOK_URL": "Webhook URL",
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
},
"MANUAL_SETUP": {
"HEADER": {
"TITLE": "Connect WhatsApp manually",
"DESCRIPTION": "Follow these steps to prepare your Meta account and connect your number to Chatwoot."
},
"PROGRESS": "Step {current} of {total}",
"STEPS": {
"1": { "LABEL": "Create Meta app" },
"2": { "LABEL": "Add phone number and get IDs" },
"3": { "LABEL": "Generate access token" },
"4": { "LABEL": "Review and connect" },
"5": { "LABEL": "Verify connection" }
},
"APP": {
"TITLE": "Create or select a Meta app",
"DESCRIPTION": "Your WhatsApp number must belong to a Meta app with the WhatsApp use case enabled.",
"ITEM_1": "Open {metaDevelopers} and sign in with an administrator account.",
"META_DEVELOPERS": "Meta Developers",
"ITEM_2": "Create a new app, or select the app you already use for this WhatsApp number.",
"ITEM_3": "Choose the option to connect with customers through WhatsApp.",
"ITEM_4": "Select the business portfolio that owns, or will own, the WhatsApp number.",
"VIDEO_TITLE": "Watch: Create a Meta app",
"VIDEO_DESCRIPTION": "This short walkthrough shows where to start a new app in Meta Developers."
},
"NUMBER": {
"TITLE": "Add your phone number and get its IDs",
"DESCRIPTION": "Add and verify the production number in your Meta app, then copy the two identifiers shown in API Setup.",
"ITEM_1": "Open the WhatsApp use case in your Meta app and choose API Setup.",
"ITEM_2": "In the Send and receive messages section, open the From selector.",
"ITEM_3": "Select an existing production number, or choose Add phone number.",
"ITEM_4": "Complete the WhatsApp business profile requested by Meta.",
"ITEM_5": "Verify the phone number using the OTP sent by SMS or voice call.",
"ITEM_6": "Copy the Phone Number ID and WhatsApp Business Account ID shown in API Setup.",
"VIDEO_TITLE": "Watch: Add a phone number and find its IDs",
"VIDEO_DESCRIPTION": "This walkthrough shows how to add or select a production number and copy the identifiers from Meta."
},
"TOKEN": {
"TITLE": "Generate a permanent access token",
"DESCRIPTION": "Create a Meta system user with access to your app and WhatsApp Business Account.",
"ITEM_1": "Open Meta Business Settings and go to Users → System users.",
"ITEM_2": "Create an admin system user, or select an existing one.",
"ITEM_3": "Assign your Meta app and WhatsApp Business Account to the system user.",
"ITEM_4": "Grant full control for the assigned WhatsApp assets.",
"ITEM_5": "Generate a token for your Meta app and set its expiration to Never.",
"ITEM_6": "Select whatsapp_business_management and whatsapp_business_messaging, then copy the token.",
"VIDEO_TITLE": "Watch: Generate a permanent access token",
"VIDEO_DESCRIPTION": "This walkthrough shows how to select your Meta app, choose a non-expiring token, and grant the required WhatsApp permissions.",
"WARNING": "Meta shows the token only once. Copy it before closing the dialog."
},
"DETAILS": {
"WABA_LABEL": "WhatsApp Business Account ID",
"WABA_PLACEHOLDER": "Enter WABA ID",
"WABA_HELP": "Copy this from the API Setup page in your Meta app.",
"PHONE_ID_LABEL": "Phone Number ID",
"PHONE_ID_PLACEHOLDER": "Enter Phone Number ID",
"PHONE_ID_HELP": "Copy the ID shown beside your production phone number in Meta.",
"TOKEN_LABEL": "Permanent access token",
"TOKEN_PLACEHOLDER": "Paste access token",
"TOKEN_HELP": "Use a permanent system-user token with WhatsApp messaging and management permissions."
},
"REVIEW": {
"TITLE": "Review and connect your number",
"DESCRIPTION": "These details were retrieved directly from Meta.",
"VERIFIED": "The number and token were verified with Meta.",
"BUSINESS_NAME": "Business name",
"PHONE_NUMBER": "Phone number",
"PHONE_ID": "Phone Number ID",
"WABA_ID": "WhatsApp Business Account ID",
"INBOX_NAME": "Inbox name",
"INBOX_NAME_HELP": "We generated this name from your verified Meta business name. You can change it."
},
"VERIFY": {
"TITLE": "Verify your connection",
"DESCRIPTION": "Chatwoot is checking number access and configuring your Meta webhook.",
"NUMBER_ACCESS": "Number access",
"TEMPLATE_ACCESS": "Template access",
"CALLBACK": "Webhook callback",
"SUBSCRIPTION": "Webhook subscription",
"WEBHOOK_URL": "Webhook URL",
"COPY": "Copy",
"COPY_SUCCESS": "Webhook URL copied to clipboard",
"COMPLETE": "Verified",
"PENDING": "Pending",
"SUCCESS": "Your WhatsApp number is connected and ready for agent assignment."
},
"ACTIONS": {
"EXIT": "Change provider",
"BACK": "Back",
"OPEN_META_APPS": "Open Meta Apps",
"APP_READY": "My Meta app is ready",
"NEXT": "Next",
"OPEN_BUSINESS_SETTINGS": "Open Meta Business Settings",
"SHOW_TOKEN": "Show token",
"HIDE_TOKEN": "Hide token",
"VERIFY_DETAILS": "Verify details",
"CONNECT": "Connect number",
"RETRY_WEBHOOK": "Retry webhook setup",
"CONTINUE": "Continue to add agents"
},
"ERRORS": {
"IDS_REQUIRED": "Enter the Phone Number ID and WhatsApp Business Account ID.",
"REQUIRED": "Enter the WABA ID, Phone Number ID, and permanent access token.",
"GENERIC": "We could not complete the WhatsApp setup. Check your details and try again."
}
},
"SUBMIT_BUTTON": "Create WhatsApp Channel",
"EMBEDDED_SIGNUP": {
"TITLE": "Quick setup with Meta",
@@ -49,9 +49,10 @@ const hasDuplicateInstagramInbox = computed(() => {
});
const shouldShowWhatsAppWebhookDetails = computed(() => {
const source = currentInbox.value.provider_config?.source;
return (
isAWhatsAppCloudChannel.value &&
currentInbox.value.provider_config?.source !== 'embedded_signup'
!['embedded_signup', 'manual_setup_v2'].includes(source)
);
});
@@ -69,7 +69,7 @@ const items = computed(() => {
:global-config="globalConfig"
:items="items"
/>
<div class="col-span-6 flex flex-col overflow-y-auto">
<div class="col-span-6 flex min-h-0 flex-col overflow-y-auto">
<router-view />
</div>
</div>
@@ -5,6 +5,7 @@ import { useI18n, I18nT } from 'vue-i18n';
import Twilio from './Twilio.vue';
import ThreeSixtyDialogWhatsapp from './360DialogWhatsapp.vue';
import CloudWhatsapp from './CloudWhatsapp.vue';
import WhatsappManualSetup from './WhatsappManualSetup.vue';
import WhatsappEmbeddedSignup from './WhatsappEmbeddedSignup.vue';
import ChannelSelector from 'dashboard/components/ChannelSelector.vue';
import { useAccount } from 'dashboard/composables/useAccount';
@@ -71,14 +72,21 @@ const shouldShowCloudWhatsapp = provider => {
);
};
const isManualSetup = computed(
() =>
showConfiguration.value && shouldShowCloudWhatsapp(selectedProvider.value)
);
const handleManualLinkClick = () => {
selectProvider(PROVIDER_TYPES.WHATSAPP_MANUAL);
};
</script>
<template>
<div class="overflow-auto col-span-6 p-6 w-full h-full">
<div v-if="showProviderSelection">
<div class="col-span-6 w-full h-full min-h-0 overflow-y-auto p-6">
<WhatsappManualSetup v-if="isManualSetup" />
<div v-else-if="showProviderSelection">
<div class="mb-10 text-left">
<h1 class="mb-2 text-lg font-medium text-n-slate-12">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.SELECT_PROVIDER.TITLE') }}
@@ -138,9 +146,6 @@ const handleManualLinkClick = () => {
</div>
</div>
<!-- Show manual setup -->
<CloudWhatsapp v-else-if="shouldShowCloudWhatsapp(selectedProvider)" />
<!-- Other providers -->
<Twilio
v-else-if="selectedProvider === PROVIDER_TYPES.TWILIO"
@@ -0,0 +1,852 @@
<script setup>
import { computed, onBeforeUnmount, reactive, ref } from 'vue';
import { I18nT, useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { useStore } from 'vuex';
import WhatsappChannelAPI from 'dashboard/api/channel/whatsappChannel';
import Button from 'dashboard/components-next/button/Button.vue';
import Icon from 'dashboard/components-next/icon/Icon.vue';
import Input from 'dashboard/components-next/input/Input.vue';
import { useAlert } from 'dashboard/composables';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const store = useStore();
const TOTAL_STEPS = 5;
const POLL_INTERVAL = 2000;
const MAX_POLL_ATTEMPTS = 5;
const META_APPS_URL = 'https://developers.facebook.com/apps/';
const META_BUSINESS_SETTINGS_URL =
'https://business.facebook.com/settings/system-users/';
const CREATE_APP_VIDEO_URL =
'/videos/whatsapp/manual-setup/create-meta-app.mp4';
const CREATE_APP_VIDEO_POSTER_URL =
'/videos/whatsapp/manual-setup/create-meta-app-poster.jpg';
const ADD_NUMBER_VIDEO_URL =
'/videos/whatsapp/manual-setup/add-phone-number.mp4';
const ADD_NUMBER_VIDEO_POSTER_URL =
'/videos/whatsapp/manual-setup/add-phone-number-poster.jpg';
const GENERATE_TOKEN_VIDEO_URL =
'/videos/whatsapp/manual-setup/generate-access-token.mp4';
const GENERATE_TOKEN_VIDEO_POSTER_URL =
'/videos/whatsapp/manual-setup/generate-access-token-poster.jpg';
const currentStep = ref(1);
const isLoading = ref(false);
const errorMessage = ref('');
const inboxId = ref(null);
const pollTimer = ref(null);
const showAccessToken = ref(false);
const form = reactive({
wabaId: '',
phoneNumberId: '',
accessToken: '',
inboxName: '',
});
const preview = ref(null);
const connection = reactive({
numberAccess: false,
templateAccess: false,
webhookSetup: false,
callbackVerified: false,
callbackConfigured: false,
callbackUrl: '',
subscriptionVerified: false,
});
const idsComplete = computed(
() => form.wabaId.trim() && form.phoneNumberId.trim()
);
const detailsComplete = computed(
() => idsComplete.value && form.accessToken.trim()
);
const connectionReady = computed(
() =>
connection.numberAccess &&
connection.templateAccess &&
connection.webhookSetup &&
connection.callbackVerified &&
connection.callbackConfigured &&
connection.subscriptionVerified
);
const progressSteps = computed(() =>
Array.from({ length: TOTAL_STEPS }, (_, index) => index + 1)
);
const stepLabels = computed(() => [
t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.STEPS.1.LABEL'),
t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.STEPS.2.LABEL'),
t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.STEPS.3.LABEL'),
t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.STEPS.4.LABEL'),
t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.STEPS.5.LABEL'),
]);
const appInstructions = computed(() => [
t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.APP.ITEM_2'),
t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.APP.ITEM_3'),
t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.APP.ITEM_4'),
]);
const numberInstructions = computed(() => [
t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.NUMBER.ITEM_1'),
t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.NUMBER.ITEM_2'),
t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.NUMBER.ITEM_3'),
t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.NUMBER.ITEM_4'),
t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.NUMBER.ITEM_5'),
t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.NUMBER.ITEM_6'),
]);
const tokenInstructions = computed(() => [
t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.TOKEN.ITEM_1'),
t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.TOKEN.ITEM_2'),
t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.TOKEN.ITEM_3'),
t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.TOKEN.ITEM_4'),
t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.TOKEN.ITEM_5'),
t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.TOKEN.ITEM_6'),
]);
const statusRows = computed(() => [
{
key: 'number',
label: t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.VERIFY.NUMBER_ACCESS'),
complete: connection.numberAccess,
},
{
key: 'templates',
label: t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.VERIFY.TEMPLATE_ACCESS'),
complete: connection.templateAccess,
},
{
key: 'callback',
label: t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.VERIFY.CALLBACK'),
complete:
connection.webhookSetup &&
connection.callbackConfigured &&
connection.callbackVerified,
},
{
key: 'subscription',
label: t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.VERIFY.SUBSCRIPTION'),
complete: connection.subscriptionVerified,
},
]);
const apiErrorMessage = error =>
error.response?.data?.message ||
t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.ERRORS.GENERIC');
const setStep = step => {
errorMessage.value = '';
currentStep.value = step;
};
const returnToProviders = () => {
router.push({
name: route.name,
params: route.params,
query: {},
});
};
const goBack = () => {
if (currentStep.value > 1) {
setStep(currentStep.value - 1);
return;
}
returnToProviders();
};
const continueFromIds = () => {
if (!idsComplete.value) {
errorMessage.value = t(
'INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.ERRORS.IDS_REQUIRED'
);
return;
}
setStep(3);
};
const verifyDetails = async () => {
if (!detailsComplete.value) {
errorMessage.value = t(
'INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.ERRORS.REQUIRED'
);
return;
}
isLoading.value = true;
errorMessage.value = '';
try {
const { data } = await WhatsappChannelAPI.previewManualSetup({
waba_id: form.wabaId.trim(),
phone_number_id: form.phoneNumberId.trim(),
access_token: form.accessToken.trim(),
});
preview.value = data;
form.inboxName = data.suggested_inbox_name;
setStep(4);
} catch (error) {
errorMessage.value = apiErrorMessage(error);
} finally {
isLoading.value = false;
}
};
const applyWebhookStatus = status => {
connection.callbackVerified = Boolean(status.callback_verified);
connection.callbackConfigured = Boolean(status.callback_configured);
connection.callbackUrl = status.callback_url || '';
connection.subscriptionVerified = Boolean(status.subscription_verified);
};
const copyWebhookUrl = async () => {
await copyTextToClipboard(connection.callbackUrl);
useAlert(t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.VERIFY.COPY_SUCCESS'));
};
const refreshWebhookStatus = async ({ showError = true } = {}) => {
if (!inboxId.value) return;
try {
const { data } = await WhatsappChannelAPI.getManualWebhookStatus(
inboxId.value
);
applyWebhookStatus(data);
} catch (error) {
if (showError) errorMessage.value = apiErrorMessage(error);
}
};
const pollWebhookStatus = async attempt => {
await refreshWebhookStatus({ showError: false });
if (connectionReady.value || attempt >= MAX_POLL_ATTEMPTS) return;
pollTimer.value = window.setTimeout(
() => pollWebhookStatus(attempt + 1),
POLL_INTERVAL
);
};
const connectNumber = async () => {
isLoading.value = true;
errorMessage.value = '';
try {
const { data } = await WhatsappChannelAPI.connectManualSetup({
waba_id: form.wabaId.trim(),
phone_number_id: form.phoneNumberId.trim(),
access_token: form.accessToken.trim(),
inbox_name: form.inboxName.trim(),
});
inboxId.value = data.id;
connection.numberAccess = Boolean(data.number_access);
connection.templateAccess = Boolean(data.template_access);
connection.webhookSetup = Boolean(data.webhook_setup);
if (data.webhook_error) errorMessage.value = data.webhook_error;
currentStep.value = 5;
await pollWebhookStatus(1);
} catch (error) {
errorMessage.value = apiErrorMessage(error);
} finally {
isLoading.value = false;
}
};
const retryWebhookSetup = async () => {
isLoading.value = true;
errorMessage.value = '';
try {
const { data } = await WhatsappChannelAPI.setupManualWebhook(inboxId.value);
connection.webhookSetup = true;
applyWebhookStatus(data);
if (!connectionReady.value) await pollWebhookStatus(1);
} catch (error) {
errorMessage.value = apiErrorMessage(error);
} finally {
isLoading.value = false;
}
};
const continueToAgents = async () => {
await store.dispatch('inboxes/get');
router.replace({
name: 'settings_inboxes_add_agents',
params: { page: 'new', inbox_id: inboxId.value },
});
};
onBeforeUnmount(() => {
if (pollTimer.value) window.clearTimeout(pollTimer.value);
});
</script>
<template>
<div class="mx-auto flex w-full max-w-6xl flex-col gap-6 py-2">
<div class="flex flex-wrap items-start justify-between gap-4 px-1">
<div class="max-w-3xl">
<h1 class="text-2xl font-semibold text-n-slate-12">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.HEADER.TITLE') }}
</h1>
<p class="mt-1 text-sm text-n-slate-11">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.HEADER.DESCRIPTION') }}
</p>
</div>
<Button
:label="$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.ACTIONS.EXIT')"
icon="i-lucide-arrow-left"
variant="ghost"
color="slate"
size="sm"
@click="returnToProviders"
/>
</div>
<div
class="rounded-2xl border border-n-weak bg-n-background p-6 shadow-sm sm:p-8"
>
<div class="flex flex-col gap-3">
<div class="flex items-center justify-between text-sm">
<span class="font-medium text-n-slate-12">
{{
$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.PROGRESS', {
current: currentStep,
total: TOTAL_STEPS,
})
}}
</span>
<span class="text-n-slate-11">
{{ stepLabels[currentStep - 1] }}
</span>
</div>
<div class="grid grid-cols-5 gap-2">
<div
v-for="step in progressSteps"
:key="step"
class="h-1.5 rounded-full"
:class="step <= currentStep ? 'bg-n-brand' : 'bg-n-alpha-3'"
/>
</div>
</div>
<div
v-if="errorMessage"
class="mt-6 rounded-lg border border-n-ruby-5 bg-n-ruby-3 px-4 py-3 text-sm text-n-ruby-11"
>
{{ errorMessage }}
</div>
<section v-if="currentStep === 1" class="mt-8 flex flex-col gap-7">
<div>
<h2 class="text-2xl font-semibold text-n-slate-12">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.APP.TITLE') }}
</h2>
<p class="mt-3 max-w-3xl text-base leading-7 text-n-slate-11">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.APP.DESCRIPTION') }}
</p>
</div>
<ol
class="ml-5 list-decimal space-y-3 text-n-slate-11 marker:font-medium marker:text-n-slate-12"
>
<li class="pl-2 text-sm leading-6">
<I18nT
keypath="INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.APP.ITEM_1"
tag="span"
>
<template #metaDevelopers>
<a
:href="META_APPS_URL"
target="_blank"
rel="noopener noreferrer"
class="font-medium text-n-brand hover:underline"
>
{{
$t(
'INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.APP.META_DEVELOPERS'
)
}}
</a>
</template>
</I18nT>
</li>
<li
v-for="instruction in appInstructions"
:key="instruction"
class="pl-2 text-sm leading-6"
>
{{ instruction }}
</li>
</ol>
<figure class="overflow-hidden rounded-xl border border-n-weak">
<video
class="block w-full bg-n-solid-1"
controls
muted
playsinline
preload="metadata"
:poster="CREATE_APP_VIDEO_POSTER_URL"
:aria-label="
$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.APP.VIDEO_TITLE')
"
>
<source :src="CREATE_APP_VIDEO_URL" type="video/mp4" />
</video>
<figcaption class="border-t border-n-weak bg-n-alpha-1 px-4 py-3">
<p class="text-sm font-medium text-n-slate-12">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.APP.VIDEO_TITLE') }}
</p>
<p class="mt-1 text-sm text-n-slate-11">
{{
$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.APP.VIDEO_DESCRIPTION')
}}
</p>
</figcaption>
</figure>
<div
class="flex items-center justify-between border-t border-n-weak pt-5"
>
<Button
:label="$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.ACTIONS.BACK')"
variant="outline"
color="slate"
@click="goBack"
/>
<Button
:label="
$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.ACTIONS.APP_READY')
"
trailing-icon
icon="i-lucide-arrow-right"
@click="setStep(2)"
/>
</div>
</section>
<section v-else-if="currentStep === 2" class="mt-8 flex flex-col gap-7">
<div>
<h2 class="text-2xl font-semibold text-n-slate-12">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.NUMBER.TITLE') }}
</h2>
<p class="mt-3 max-w-3xl text-base leading-7 text-n-slate-11">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.NUMBER.DESCRIPTION') }}
</p>
</div>
<ol
class="ml-5 list-decimal space-y-3 text-n-slate-11 marker:font-medium marker:text-n-slate-12"
>
<li
v-for="instruction in numberInstructions"
:key="instruction"
class="pl-2 text-sm leading-6"
>
{{ instruction }}
</li>
</ol>
<figure class="overflow-hidden rounded-xl border border-n-weak">
<video
class="block w-full bg-n-solid-1"
controls
muted
playsinline
preload="metadata"
:poster="ADD_NUMBER_VIDEO_POSTER_URL"
:aria-label="
$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.NUMBER.VIDEO_TITLE')
"
>
<source :src="ADD_NUMBER_VIDEO_URL" type="video/mp4" />
</video>
<figcaption class="border-t border-n-weak bg-n-alpha-1 px-4 py-3">
<p class="text-sm font-medium text-n-slate-12">
{{
$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.NUMBER.VIDEO_TITLE')
}}
</p>
<p class="mt-1 text-sm text-n-slate-11">
{{
$t(
'INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.NUMBER.VIDEO_DESCRIPTION'
)
}}
</p>
</figcaption>
</figure>
<a
:href="META_APPS_URL"
target="_blank"
rel="noopener noreferrer"
class="inline-flex w-fit items-center gap-2 text-sm font-medium text-n-brand hover:underline"
>
{{
$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.ACTIONS.OPEN_META_APPS')
}}
<Icon icon="i-lucide-external-link" />
</a>
<div
class="grid gap-5 rounded-xl border border-n-weak p-5 lg:grid-cols-2"
>
<Input
v-model="form.phoneNumberId"
:label="
$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.DETAILS.PHONE_ID_LABEL')
"
:placeholder="
$t(
'INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.DETAILS.PHONE_ID_PLACEHOLDER'
)
"
:message="
$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.DETAILS.PHONE_ID_HELP')
"
/>
<Input
v-model="form.wabaId"
:label="
$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.DETAILS.WABA_LABEL')
"
:placeholder="
$t(
'INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.DETAILS.WABA_PLACEHOLDER'
)
"
:message="
$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.DETAILS.WABA_HELP')
"
/>
</div>
<div
class="flex items-center justify-between border-t border-n-weak pt-5"
>
<Button
:label="$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.ACTIONS.BACK')"
variant="outline"
color="slate"
@click="goBack"
/>
<Button
:label="$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.ACTIONS.NEXT')"
trailing-icon
icon="i-lucide-arrow-right"
@click="continueFromIds"
/>
</div>
</section>
<section v-else-if="currentStep === 3" class="mt-8 flex flex-col gap-7">
<div>
<h2 class="text-2xl font-semibold text-n-slate-12">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.TOKEN.TITLE') }}
</h2>
<p class="mt-3 max-w-3xl text-base leading-7 text-n-slate-11">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.TOKEN.DESCRIPTION') }}
</p>
</div>
<ol
class="ml-5 list-decimal space-y-3 text-n-slate-11 marker:font-medium marker:text-n-slate-12"
>
<li
v-for="instruction in tokenInstructions"
:key="instruction"
class="pl-2 text-sm leading-6"
>
{{ instruction }}
</li>
</ol>
<figure class="overflow-hidden rounded-xl border border-n-weak">
<video
class="block w-full bg-n-solid-1"
controls
muted
playsinline
preload="metadata"
:poster="GENERATE_TOKEN_VIDEO_POSTER_URL"
:aria-label="
$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.TOKEN.VIDEO_TITLE')
"
>
<source :src="GENERATE_TOKEN_VIDEO_URL" type="video/mp4" />
</video>
<figcaption class="border-t border-n-weak bg-n-alpha-1 px-4 py-3">
<p class="text-sm font-medium text-n-slate-12">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.TOKEN.VIDEO_TITLE') }}
</p>
<p class="mt-1 text-sm text-n-slate-11">
{{
$t(
'INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.TOKEN.VIDEO_DESCRIPTION'
)
}}
</p>
</figcaption>
</figure>
<a
:href="META_BUSINESS_SETTINGS_URL"
target="_blank"
rel="noopener noreferrer"
class="inline-flex w-fit items-center gap-2 text-sm font-medium text-n-brand hover:underline"
>
{{
$t(
'INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.ACTIONS.OPEN_BUSINESS_SETTINGS'
)
}}
<Icon icon="i-lucide-external-link" />
</a>
<div class="rounded-xl border border-n-weak p-5">
<div class="grid items-end gap-3 sm:grid-cols-[1fr_auto]">
<Input
v-model="form.accessToken"
:type="showAccessToken ? 'text' : 'password'"
autocomplete="off"
:label="
$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.DETAILS.TOKEN_LABEL')
"
:placeholder="
$t(
'INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.DETAILS.TOKEN_PLACEHOLDER'
)
"
:message="
$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.DETAILS.TOKEN_HELP')
"
/>
<Button
:label="
showAccessToken
? $t(
'INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.ACTIONS.HIDE_TOKEN'
)
: $t(
'INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.ACTIONS.SHOW_TOKEN'
)
"
variant="outline"
color="slate"
@click="showAccessToken = !showAccessToken"
/>
</div>
<p class="mt-4 flex items-start gap-2 text-sm text-n-amber-11">
<Icon icon="i-lucide-triangle-alert" class="mt-0.5 shrink-0" />
{{ $t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.TOKEN.WARNING') }}
</p>
</div>
<div
class="flex items-center justify-between border-t border-n-weak pt-5"
>
<Button
:label="$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.ACTIONS.BACK')"
variant="outline"
color="slate"
@click="goBack"
/>
<Button
:label="
$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.ACTIONS.VERIFY_DETAILS')
"
:is-loading="isLoading"
:disabled="isLoading"
@click="verifyDetails"
/>
</div>
</section>
<section v-else-if="currentStep === 4" class="mt-8 flex flex-col gap-7">
<div>
<h2 class="text-2xl font-semibold text-n-slate-12">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.REVIEW.TITLE') }}
</h2>
<p class="mt-3 max-w-3xl text-base leading-7 text-n-slate-11">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.REVIEW.DESCRIPTION') }}
</p>
</div>
<div
class="rounded-xl border border-n-teal-5 bg-n-teal-3 px-4 py-3 text-sm text-n-teal-11"
>
<div class="flex items-center gap-2 font-medium">
<Icon icon="i-lucide-badge-check" />
{{ $t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.REVIEW.VERIFIED') }}
</div>
</div>
<dl class="divide-y divide-n-weak rounded-xl border border-n-weak px-5">
<div class="grid grid-cols-2 gap-4 py-4">
<dt class="text-sm text-n-slate-11">
{{
$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.REVIEW.BUSINESS_NAME')
}}
</dt>
<dd class="text-sm font-medium text-n-slate-12">
{{ preview.verified_name || preview.display_phone_number }}
</dd>
</div>
<div class="grid grid-cols-2 gap-4 py-4">
<dt class="text-sm text-n-slate-11">
{{
$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.REVIEW.PHONE_NUMBER')
}}
</dt>
<dd class="text-sm font-medium text-n-slate-12">
{{ preview.display_phone_number }}
</dd>
</div>
<div class="grid grid-cols-2 gap-4 py-4">
<dt class="text-sm text-n-slate-11">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.REVIEW.PHONE_ID') }}
</dt>
<dd class="text-sm font-medium text-n-slate-12">
{{ preview.phone_number_id }}
</dd>
</div>
<div class="grid grid-cols-2 gap-4 py-4">
<dt class="text-sm text-n-slate-11">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.REVIEW.WABA_ID') }}
</dt>
<dd class="text-sm font-medium text-n-slate-12">
{{ preview.waba_id }}
</dd>
</div>
</dl>
<Input
v-model="form.inboxName"
:label="$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.REVIEW.INBOX_NAME')"
:message="
$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.REVIEW.INBOX_NAME_HELP')
"
/>
<div
class="flex items-center justify-between border-t border-n-weak pt-5"
>
<Button
:label="$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.ACTIONS.BACK')"
variant="outline"
color="slate"
@click="goBack"
/>
<Button
:label="$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.ACTIONS.CONNECT')"
:is-loading="isLoading"
:disabled="isLoading || !form.inboxName.trim()"
@click="connectNumber"
/>
</div>
</section>
<section v-else class="mt-8 flex flex-col gap-7">
<div>
<h2 class="text-2xl font-semibold text-n-slate-12">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.VERIFY.TITLE') }}
</h2>
<p class="mt-3 max-w-3xl text-base leading-7 text-n-slate-11">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.VERIFY.DESCRIPTION') }}
</p>
</div>
<div
v-if="connection.callbackUrl"
class="rounded-xl border border-n-weak p-5"
>
<p class="text-sm font-medium text-n-slate-12">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.VERIFY.WEBHOOK_URL') }}
</p>
<div class="mt-3 flex items-center gap-3">
<code
class="min-w-0 flex-1 break-all rounded-lg bg-n-alpha-1 px-3 py-2.5 text-sm text-n-slate-12"
>
{{ connection.callbackUrl }}
</code>
<Button
:label="$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.VERIFY.COPY')"
icon="i-lucide-copy"
variant="outline"
color="slate"
@click="copyWebhookUrl"
/>
</div>
</div>
<div
class="flex flex-col divide-y divide-n-weak rounded-xl border border-n-weak px-5"
>
<div
v-for="status in statusRows"
:key="status.key"
class="flex items-center justify-between py-4"
>
<span class="text-sm font-medium text-n-slate-12">
{{ status.label }}
</span>
<span
class="flex items-center gap-2 text-sm"
:class="status.complete ? 'text-n-teal-11' : 'text-n-amber-11'"
>
<Icon
:icon="
status.complete ? 'i-lucide-check-circle' : 'i-lucide-clock-3'
"
/>
{{
status.complete
? $t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.VERIFY.COMPLETE')
: $t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.VERIFY.PENDING')
}}
</span>
</div>
</div>
<p
v-if="connectionReady"
class="rounded-lg bg-n-teal-3 px-4 py-3 text-sm text-n-teal-11"
>
{{ $t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.VERIFY.SUCCESS') }}
</p>
<div
class="flex items-center justify-between border-t border-n-weak pt-5"
>
<Button
:label="
$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.ACTIONS.RETRY_WEBHOOK')
"
variant="outline"
color="slate"
:is-loading="isLoading"
:disabled="isLoading"
@click="retryWebhookSetup"
/>
<Button
:label="$t('INBOX_MGMT.ADD.WHATSAPP.MANUAL_SETUP.ACTIONS.CONTINUE')"
trailing-icon
icon="i-lucide-arrow-right"
:disabled="isLoading"
@click="continueToAgents"
/>
</div>
</section>
</div>
</div>
</template>