chore: front end improvments

This commit is contained in:
Muhsin Keloth
2025-06-10 16:06:22 +05:30
parent 20f63cf957
commit a5321376e7
2 changed files with 337 additions and 335 deletions
@@ -287,14 +287,18 @@
"AUTH_PROCESSING": "Authenticating with Meta...",
"WAITING_FOR_BUSINESS_INFO": "Please complete business setup in the Meta window...",
"PROCESSING": "Setting up your WhatsApp Business Account...",
"PROCESSING_DESC": "Please wait while we configure your WhatsApp Business Account. This may take a few moments.",
"LOADING_SDK": "Loading Facebook SDK...",
"CANCELLED": "WhatsApp signup was cancelled",
"STEP_AUTH": "Step 1: Authenticating with Meta",
"STEP_BUSINESS": "Step 2: Selecting business account",
"STEP_CREATING": "Step 3: Creating WhatsApp channel",
"SUCCESS_TITLE": "WhatsApp Business Account Connected!",
"SUCCESS_DESC": "Your WhatsApp Business Account has been successfully connected. Please provide a name for your inbox to complete the setup."
"SUCCESS_DESC": "Your WhatsApp Business Account has been successfully connected. Please provide a name for your inbox to complete the setup.",
"WAITING_FOR_AUTH": "Waiting for authentication...",
"INVALID_BUSINESS_DATA": "Invalid business data received from Facebook. Please try again.",
"SIGNUP_ERROR": "Signup error occurred",
"AUTH_NOT_COMPLETED": "Authentication not completed. Please restart the process.",
"SUCCESS_FALLBACK": "WhatsApp Business Account has been successfully configured"
},
"API": {
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
@@ -1,340 +1,349 @@
<script>
import { mapGetters } from 'vuex';
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { useStore } from 'vuex';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useAlert } from 'dashboard/composables';
import router from '../../../../index';
import Auth from 'dashboard/api/auth';
import Icon from 'next/icon/Icon.vue';
import NextButton from 'next/button/Button.vue';
import LoadingState from 'dashboard/components/widgets/LoadingState.vue';
import whatsappIcon from 'dashboard/assets/images/whatsapp.png';
export default {
components: { Icon, NextButton },
data() {
const store = useStore();
const router = useRouter();
const { t } = useI18n();
const fbSdkLoaded = ref(false);
const isProcessing = ref(false);
const processingMessage = ref('');
const authCodeReceived = ref(false);
const currentStep = ref('initial');
const authCode = ref(null);
const businessData = ref(null);
const isAuthenticating = ref(false);
const hasSignupStarted = ref(false);
const authHeaders = computed(() => {
if (Auth.hasAuthCookie()) {
const {
'access-token': accessToken,
'token-type': tokenType,
client,
expiry,
uid,
} = Auth.getAuthData();
return {
fbSdkLoaded: false,
isProcessing: false,
processingMessage: '',
authCodeReceived: false,
currentStep: 'initial', // 'initial', 'auth_received', 'processing', 'completed'
authCode: null,
businessData: null, // Store business data when received
'access-token': accessToken,
'token-type': tokenType,
client,
expiry,
uid,
};
}
return {};
});
const benefits = computed(() => [
{
key: 'EASY_SETUP',
text: t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.BENEFITS.EASY_SETUP'),
},
computed: {
...mapGetters({ uiFlags: 'inboxes/getUIFlags' }),
isLoading() {
return this.isProcessing || this.uiFlags.isCreating;
},
authHeaders() {
if (Auth.hasAuthCookie()) {
const {
'access-token': accessToken,
'token-type': tokenType,
client,
expiry,
uid,
} = Auth.getAuthData();
return {
'access-token': accessToken,
'token-type': tokenType,
client,
expiry,
uid,
};
}
return {};
},
{
key: 'SECURE_AUTH',
text: t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.BENEFITS.SECURE_AUTH'),
},
mounted() {
this.loadFacebookSdk();
window.addEventListener('message', this.handleSignupMessage);
{
key: 'AUTO_CONFIG',
text: t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.BENEFITS.AUTO_CONFIG'),
},
beforeUnmount() {
window.removeEventListener('message', this.handleSignupMessage);
},
methods: {
loadFacebookSdk() {
if (window.FB) {
this.fbSdkLoaded = true;
return;
}
const script = document.createElement('script');
script.src = 'https://connect.facebook.net/en_US/sdk.js';
script.async = true;
script.defer = true;
script.onload = () => {
window.FB.init({
appId: window.chatwootConfig?.whatsappAppId,
status: true,
xfbml: true,
version: window.chatwootConfig?.fbApiVersion || 'v21.0',
});
this.fbSdkLoaded = true;
};
document.body.appendChild(script);
},
]);
launchEmbeddedSignup() {
if (!window.FB) {
this.loadFacebookSdk();
setTimeout(() => this.launchEmbeddedSignup(), 1000);
return;
}
const showLoader = computed(() => hasSignupStarted.value || isProcessing.value);
this.currentStep = 'auth_processing';
this.processingMessage = this.$t(
'INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.AUTH_PROCESSING'
);
const handleSignupError = data => {
currentStep.value = 'initial';
isProcessing.value = false;
authCodeReceived.value = false;
isAuthenticating.value = false;
hasSignupStarted.value = false;
// Following Facebook's embedded signup documentation
window.FB.login(this.fbLoginCallback, {
config_id: window.chatwootConfig?.whatsappConfigurationId,
response_type: 'code',
override_default_response_type: true,
extras: {
setup: {},
featureType: '', // Leave empty for default flow
sessionInfoVersion: '3',
},
});
},
fbLoginCallback(response) {
if (response.authResponse && response.authResponse.code) {
// Authorization code received from Facebook
this.authCode = response.authResponse.code;
this.authCodeReceived = true;
this.currentStep = 'auth_received';
this.processingMessage = this.$t(
'INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.WAITING_FOR_BUSINESS_INFO'
);
// Check if we already have business data and process immediately
if (this.businessData) {
this.completeSignupFlow(this.businessData);
}
} else if (response.error) {
this.handleSignupError({ error: response.error });
} else {
this.currentStep = 'initial';
this.isProcessing = false;
useAlert(this.$t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.CANCELLED'));
}
},
handleSignupMessage(event) {
// Handle Facebook embedded signup message events
try {
const originUrl = new URL(event.origin);
const allowedHosts = ['facebook.com', 'www.facebook.com'];
if (!allowedHosts.includes(originUrl.hostname)) return;
} catch (error) {
// Invalid origin URL, reject the event
return;
}
try {
const data = JSON.parse(event.data);
if (data.type === 'WA_EMBEDDED_SIGNUP') {
this.handleEmbeddedSignupData(data);
}
} catch (error) {
// Handle non-JSON messages silently
}
},
async handleEmbeddedSignupData(data) {
// Handle different embedded signup events per Facebook documentation
if (data.event === 'FINISH') {
// Facebook might send business data in different structures
let businessData = data.data;
// If data.data doesn't exist, try other possible structures
if (!businessData) {
businessData = data.business_data || data.details || data;
}
// Validate we have the required business information
if (
businessData &&
(businessData.business_id || businessData.businessId) &&
(businessData.waba_id || businessData.wabaId)
) {
// Normalize the data structure to match our backend expectations
const normalizedData = {
business_id: businessData.business_id || businessData.businessId,
waba_id: businessData.waba_id || businessData.wabaId,
phone_number_id:
businessData.phone_number_id ||
businessData.phoneNumberId ||
businessData.phone_id,
};
// Store business data
this.businessData = normalizedData;
// Check if we already have auth code and process immediately
if (this.authCodeReceived && this.authCode) {
await this.completeSignupFlow(normalizedData);
} else {
this.currentStep = 'waiting_for_auth';
this.processingMessage = 'Waiting for authentication...';
}
} else {
this.handleSignupError({
error:
'Invalid business data received from Facebook. Please try again.',
});
}
} else if (data.event === 'CANCEL') {
this.handleSignupCancellation(data);
} else if (data.event === 'error') {
this.handleSignupError({
error: data.error_message || 'Signup error occurred',
error_id: data.error_id,
session_id: data.session_id,
});
}
},
async completeSignupFlow(businessData) {
if (!this.authCodeReceived || !this.authCode) {
this.handleSignupError({
error: 'Authentication not completed. Please restart the process.',
});
return;
}
this.currentStep = 'processing';
this.isProcessing = true;
this.processingMessage = this.$t(
'INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.PROCESSING'
);
try {
// Send both auth code and business info together (synchronous flow)
const accountId = this.$store.getters.getCurrentAccountId;
const response = await fetch('/whatsapp/embedded_signup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document
.querySelector('meta[name="csrf-token"]')
?.getAttribute('content'),
...this.authHeaders,
},
body: JSON.stringify({
account_id: accountId,
code: this.authCode,
business_id: businessData.business_id,
waba_id: businessData.waba_id,
phone_number_id: businessData.phone_number_id,
}),
});
const responseData = await response.json();
if (response.ok) {
// Clear the stored auth code for security
this.authCode = null;
// Handle synchronous success response
this.handleSignupSuccess(responseData);
} else {
throw new Error(responseData.message || responseData.error);
}
} catch (error) {
this.handleSignupError({ error: error.message });
}
},
handleSignupCancellation(data) {
this.currentStep = 'initial';
this.isProcessing = false;
this.authCodeReceived = false;
let message = this.$t(
'INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.CANCELLED'
);
if (data.data?.current_step) {
message += ` (Step: ${data.data.current_step})`;
}
useAlert(message);
},
handleSignupSuccess(inboxData) {
this.currentStep = 'completed';
this.isProcessing = false;
// Update the store with the new inbox data
if (inboxData && inboxData.id) {
// Add the new inbox to the store
this.$store.commit('inboxes/ADD_INBOXES', inboxData);
useAlert(this.$t('INBOX_MGMT.FINISH.MESSAGE'));
router.replace({
name: 'settings_inboxes_add_agents',
params: {
page: 'new',
inbox_id: inboxData.id,
},
});
} else {
// Fallback if inbox data is not properly formatted
useAlert('WhatsApp Business Account has been successfully configured');
router.replace({
name: 'settings_inbox_list',
});
}
},
handleSignupError(data) {
this.currentStep = 'initial';
this.isProcessing = false;
this.authCodeReceived = false;
const errorMessage =
data.error ||
data.message ||
this.$t('INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE');
useAlert(errorMessage);
},
resetSignupFlow() {
this.currentStep = 'initial';
this.isProcessing = false;
this.authCodeReceived = false;
this.processingMessage = '';
this.authCode = null;
this.businessData = null;
},
},
const errorMessage =
data.error ||
data.message ||
t('INBOX_MGMT.ADD.WHATSAPP.API.ERROR_MESSAGE');
useAlert(errorMessage);
};
const handleSignupCancellation = data => {
currentStep.value = 'initial';
isProcessing.value = false;
authCodeReceived.value = false;
isAuthenticating.value = false;
hasSignupStarted.value = false;
let message = t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.CANCELLED');
if (data.data?.current_step) {
message += ` (Step: ${data.data.current_step})`;
}
useAlert(message);
};
const handleSignupSuccess = inboxData => {
currentStep.value = 'completed';
isProcessing.value = false;
isAuthenticating.value = false;
// Update the store with the new inbox data
if (inboxData && inboxData.id) {
// Add the new inbox to the store
store.commit('inboxes/ADD_INBOXES', inboxData);
useAlert(t('INBOX_MGMT.FINISH.MESSAGE'));
router.replace({
name: 'settings_inboxes_add_agents',
params: {
page: 'new',
inbox_id: inboxData.id,
},
});
} else {
// Fallback if inbox data is not properly formatted
useAlert(t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.SUCCESS_FALLBACK'));
router.replace({
name: 'settings_inbox_list',
});
}
};
const completeSignupFlow = async businessDataParam => {
if (!authCodeReceived.value || !authCode.value) {
handleSignupError({
error: t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.AUTH_NOT_COMPLETED'),
});
return;
}
currentStep.value = 'processing';
isProcessing.value = true;
processingMessage.value = t(
'INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.PROCESSING'
);
try {
// Send both auth code and business info together (synchronous flow)
const accountId = store.getters.getCurrentAccountId;
const response = await fetch('/whatsapp/embedded_signup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document
.querySelector('meta[name="csrf-token"]')
?.getAttribute('content'),
...authHeaders.value,
},
body: JSON.stringify({
account_id: accountId,
code: authCode.value,
business_id: businessDataParam.business_id,
waba_id: businessDataParam.waba_id,
phone_number_id: businessDataParam.phone_number_id,
}),
});
const responseData = await response.json();
if (response.ok) {
// Clear the stored auth code for security
authCode.value = null;
// Handle synchronous success response
handleSignupSuccess(responseData);
} else {
throw new Error(responseData.message || responseData.error);
}
} catch (error) {
handleSignupError({ error: error.message });
}
};
const handleEmbeddedSignupData = async data => {
// Handle different embedded signup events per Facebook documentation
if (data.event === 'FINISH') {
// Facebook might send business data in different structures
let businessDataLocal = data.data;
// If data.data doesn't exist, try other possible structures
if (!businessDataLocal) {
businessDataLocal = data.business_data || data.details || data;
}
// Validate we have the required business information
if (
businessDataLocal &&
(businessDataLocal.business_id || businessDataLocal.businessId) &&
(businessDataLocal.waba_id || businessDataLocal.wabaId)
) {
// Normalize the data structure to match our backend expectations
const normalizedData = {
business_id:
businessDataLocal.business_id || businessDataLocal.businessId,
waba_id: businessDataLocal.waba_id || businessDataLocal.wabaId,
phone_number_id:
businessDataLocal.phone_number_id ||
businessDataLocal.phoneNumberId ||
businessDataLocal.phone_id,
};
// Store business data
businessData.value = normalizedData;
// Check if we already have auth code and process immediately
if (authCodeReceived.value && authCode.value) {
await completeSignupFlow(normalizedData);
} else {
currentStep.value = 'waiting_for_auth';
processingMessage.value = t(
'INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.WAITING_FOR_AUTH'
);
}
} else {
handleSignupError({
error: t(
'INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.INVALID_BUSINESS_DATA'
),
});
}
} else if (data.event === 'CANCEL') {
handleSignupCancellation(data);
} else if (data.event === 'error') {
handleSignupError({
error:
data.error_message ||
t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.SIGNUP_ERROR'),
error_id: data.error_id,
session_id: data.session_id,
});
}
};
const fbLoginCallback = response => {
if (response.authResponse && response.authResponse.code) {
// Authorization code received from Facebook
authCode.value = response.authResponse.code;
authCodeReceived.value = true;
currentStep.value = 'auth_received';
processingMessage.value = t(
'INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.WAITING_FOR_BUSINESS_INFO'
);
// Check if we already have business data and process immediately
if (businessData.value) {
completeSignupFlow(businessData.value);
}
} else if (response.error) {
handleSignupError({ error: response.error });
} else {
currentStep.value = 'initial';
isProcessing.value = false;
isAuthenticating.value = false;
hasSignupStarted.value = false;
useAlert(t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.CANCELLED'));
}
};
const handleSignupMessage = event => {
// Handle Facebook embedded signup message events
try {
const originUrl = new URL(event.origin);
const allowedHosts = ['facebook.com', 'www.facebook.com'];
if (!allowedHosts.includes(originUrl.hostname)) return;
} catch (error) {
// Invalid origin URL, reject the event
return;
}
try {
const data = JSON.parse(event.data);
if (data.type === 'WA_EMBEDDED_SIGNUP') {
handleEmbeddedSignupData(data);
}
} catch (error) {
// Handle non-JSON messages silently
}
};
const loadFacebookSdk = () => {
if (window.FB) {
fbSdkLoaded.value = true;
return;
}
const script = document.createElement('script');
script.src = 'https://connect.facebook.net/en_US/sdk.js';
script.async = true;
script.defer = true;
script.onload = () => {
window.FB.init({
appId: window.chatwootConfig?.whatsappAppId,
status: true,
xfbml: true,
version: window.chatwootConfig?.fbApiVersion || 'v21.0',
});
fbSdkLoaded.value = true;
};
document.body.appendChild(script);
};
const launchEmbeddedSignup = () => {
hasSignupStarted.value = true;
processingMessage.value = t(
'INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.AUTH_PROCESSING'
);
if (!window.FB) {
loadFacebookSdk();
setTimeout(() => launchEmbeddedSignup(), 1000);
return;
}
isAuthenticating.value = true;
currentStep.value = 'auth_processing';
// Following Facebook's embedded signup documentation
window.FB.login(fbLoginCallback, {
config_id: window.chatwootConfig?.whatsappConfigurationId,
response_type: 'code',
override_default_response_type: true,
extras: {
setup: {},
featureType: '', // Leave empty for default flow
sessionInfoVersion: '3',
},
});
};
onMounted(() => {
loadFacebookSdk();
window.addEventListener('message', handleSignupMessage);
});
onBeforeUnmount(() => {
window.removeEventListener('message', handleSignupMessage);
});
</script>
<template>
<div class="h-full">
<div v-if="isProcessing" class="py-8 text-center">
<div class="mb-4">
<div
class="w-12 h-12 mx-auto border-b-2 border-blue-600 rounded-full animate-spin"
/>
</div>
<h3 class="mb-2 text-lg font-medium text-slate-800 dark:text-slate-100">
{{ processingMessage }}
</h3>
<p class="text-slate-600 dark:text-slate-300">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.PROCESSING_DESC') }}
</p>
</div>
<LoadingState v-if="showLoader" :message="processingMessage" />
<div>
<div v-else>
<div class="flex flex-col items-start mb-4 text-left">
<div class="flex justify-start mb-5">
<div class="flex items-center justify-center w-12 h-12 rounded-lg">
<img
src="dashboard/assets/images/whatsapp.png"
:src="whatsappIcon"
:alt="$t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.WHATSAPP_CLOUD')"
class="object-contain w-10 h-10"
/>
@@ -350,30 +359,19 @@ export default {
</div>
<div class="flex flex-col gap-2 mb-8">
<div class="flex items-center gap-2 text-sm text-slate-11">
<div
v-for="benefit in benefits"
:key="benefit.key"
class="flex items-center gap-2 text-sm text-slate-11"
>
<Icon icon="i-lucide-check" class="text-slate-11" />
{{
$t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.BENEFITS.EASY_SETUP')
}}
</div>
<div class="flex items-center gap-2 text-sm text-slate-11">
<Icon icon="i-lucide-check" class="text-slate-11" />
<span>
{{
$t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.BENEFITS.SECURE_AUTH')
}}
</span>
</div>
<div class="flex items-center gap-2 text-sm text-slate-11">
<Icon icon="i-lucide-check" class="text-slate-11" />
{{
$t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.BENEFITS.AUTO_CONFIG')
}}
{{ benefit.text }}
</div>
</div>
<NextButton
:disabled="!fbSdkLoaded"
:disabled="!fbSdkLoaded || isAuthenticating"
:loading="isAuthenticating"
faded
slate
@click="launchEmbeddedSignup"