feat: add Whatsapp embedded signup API

This commit is contained in:
Tanmay Deep Sharma
2025-05-28 20:05:57 +05:30
parent 454686ab5d
commit c8a66b9ebb
15 changed files with 832 additions and 80 deletions
@@ -222,6 +222,7 @@
"DESC": "Start supporting your customers via WhatsApp.",
"PROVIDERS": {
"LABEL": "API Provider",
"WHATSAPP_EMBEDDED": "WhatsApp Business (Embedded Signup)",
"TWILIO": "Twilio",
"WHATSAPP_CLOUD": "WhatsApp Cloud",
"360_DIALOG": "360Dialog"
@@ -264,6 +265,28 @@
"WEBHOOK_VERIFICATION_TOKEN": "Webhook Verification Token"
},
"SUBMIT_BUTTON": "Create WhatsApp Channel",
"EMBEDDED_SIGNUP": {
"TITLE": "Quick Setup with Meta",
"DESC": "Connect your WhatsApp Business Account directly through Meta's secure signup flow.",
"BENEFITS": {
"TITLE": "Benefits of Embedded Signup:",
"EASY_SETUP": "One-click setup with no manual configuration required",
"SECURE_AUTH": "Secure OAuth-based authentication with Meta",
"AUTO_CONFIG": "Automatic webhook and phone number configuration"
},
"SUBMIT_BUTTON": "Connect with WhatsApp Business",
"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."
},
"API": {
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
}
@@ -1,12 +1,10 @@
<script>
/* eslint-env browser */
/* global FB */
import { mapGetters } from 'vuex';
import { useVuelidate } from '@vuelidate/core';
import { useAlert } from 'dashboard/composables';
import { required } from '@vuelidate/validators';
import router from '../../../../index';
import { loadScript } from 'dashboard/helper/DOMHelpers';
import { isPhoneE164OrEmpty, isNumber } from 'shared/helpers/Validators';
import NextButton from 'dashboard/components-next/button/Button.vue';
@@ -37,73 +35,8 @@ export default {
phoneNumberId: { required, isNumber },
businessAccountId: { required, isNumber },
},
mounted() {
this.initializeFacebookSDK();
},
methods: {
async initializeFacebookSDK() {
try {
await this.loadFBsdk();
this.runFBInit();
} catch (error) {
useAlert(this.$t('INBOX_MGMT.DETAILS.ERROR_FB_LOADING'));
}
},
runFBInit() {
FB.init({
appId: window.chatwootConfig.fbAppId,
xfbml: true,
version: window.chatwootConfig.fbApiVersion,
status: true,
});
window.fbSDKLoaded = true;
FB.AppEvents.logPageView();
},
async loadFBsdk() {
return loadScript('https://connect.facebook.net/en_US/sdk.js', {
id: 'facebook-jssdk',
});
},
fbLoginCallback(response) {
console.log('fbLoginCallback', response);
if (response.authResponse) {
const code = response.authResponse.code;
// Handle the code here
this.handleWhatsAppCode(code);
} else {
useAlert(this.$t('INBOX_MGMT.DETAILS.ERROR_FB_AUTH'));
}
},
handleWhatsAppCode(code) {
// TODO: Implement code handling logic
console.log('Received WhatsApp code:', code);
},
async launchWhatsAppSignup() {
if (!window.fbSDKLoaded) {
await this.initializeFacebookSDK();
}
try {
FB.login(this.fbLoginCallback, {
config_id: window.chatwootConfig.fbAppId,
response_type: 'code',
override_default_response_type: true,
extras: {
setup: {},
featureType: '',
sessionInfoVersion: '3',
},
});
} catch (error) {
useAlert(this.$t('INBOX_MGMT.DETAILS.ERROR_FB_AUTH'));
}
},
async createChannel() {
this.v$.$touch();
if (this.v$.$invalid) {
@@ -146,14 +79,7 @@ export default {
</script>
<template>
<NextButton
type="submit"
solid
blue
:label="$t('INBOX_MGMT.ADD.WHATSAPP.SUBMIT_BUTTON')"
@click="launchWhatsAppSignup"
/>
<!-- <form class="flex flex-col flex-wrap mx-0" @submit.prevent="createChannel()">
<form class="flex flex-col flex-wrap mx-0" @submit.prevent="createChannel()">
<div class="flex-grow-0 flex-shrink-0">
<label :class="{ error: v$.inboxName.$error }">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.INBOX_NAME.LABEL') }}
@@ -248,5 +174,5 @@ export default {
:label="$t('INBOX_MGMT.ADD.WHATSAPP.SUBMIT_BUTTON')"
/>
</div>
</form> -->
</form>
</template>
@@ -3,6 +3,7 @@ import PageHeader from '../../SettingsSubPageHeader.vue';
import Twilio from './Twilio.vue';
import ThreeSixtyDialogWhatsapp from './360DialogWhatsapp.vue';
import CloudWhatsapp from './CloudWhatsapp.vue';
import WhatsappEmbeddedSignup from './WhatsappEmbeddedSignup.vue';
export default {
components: {
@@ -10,10 +11,11 @@ export default {
Twilio,
ThreeSixtyDialogWhatsapp,
CloudWhatsapp,
WhatsappEmbeddedSignup,
},
data() {
return {
provider: 'whatsapp_cloud',
provider: 'whatsapp_embedded',
};
},
};
@@ -31,6 +33,9 @@ export default {
<label>
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.LABEL') }}
<select v-model="provider">
<option value="whatsapp_embedded">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.WHATSAPP_EMBEDDED') }}
</option>
<option value="whatsapp_cloud">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.PROVIDERS.WHATSAPP_CLOUD') }}
</option>
@@ -41,7 +46,8 @@ export default {
</label>
</div>
<Twilio v-if="provider === 'twilio'" type="whatsapp" />
<WhatsappEmbeddedSignup v-if="provider === 'whatsapp_embedded'" />
<Twilio v-else-if="provider === 'twilio'" type="whatsapp" />
<ThreeSixtyDialogWhatsapp v-else-if="provider === '360dialog'" />
<CloudWhatsapp v-else />
</div>
@@ -0,0 +1,396 @@
<script>
import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import router from '../../../../index';
import NextButton from 'dashboard/components-next/button/Button.vue';
import Auth from 'dashboard/api/auth';
export default {
components: {
NextButton,
},
data() {
return {
fbSdkLoaded: false,
isProcessing: false,
processingMessage: '',
authCodeReceived: false,
currentStep: 'initial', // 'initial', 'auth_received', 'processing', 'completed'
authCode: null,
businessData: null, // Store business data when received
};
},
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 {};
},
},
mounted() {
this.loadFacebookSdk();
window.addEventListener('message', this.handleSignupMessage);
},
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;
}
this.currentStep = 'auth_processing';
this.processingMessage = this.$t(
'INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.AUTH_PROCESSING'
);
// 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
if (!event.origin.endsWith('facebook.com')) 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;
},
},
};
</script>
<template>
<div class="flex flex-col">
<!-- Processing State -->
<div v-if="isProcessing" class="text-center py-8">
<div class="mb-4">
<div
class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"
/>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">
{{ processingMessage }}
</h3>
<p class="text-gray-600">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.PROCESSING_DESC') }}
</p>
<!-- Show current step for better UX -->
<div class="mt-4 text-sm text-gray-500">
<span v-if="currentStep === 'auth_processing'">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.STEP_AUTH') }}
</span>
<span v-else-if="currentStep === 'auth_received'">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.STEP_BUSINESS') }}
</span>
<span v-else-if="currentStep === 'processing'">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.STEP_CREATING') }}
</span>
</div>
</div>
<!-- Initial Setup State -->
<div v-else class="space-y-6">
<div class="bg-blue-50 border border-blue-200 rounded-lg p-6">
<h3 class="text-lg font-medium text-blue-900 mb-3">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.TITLE') }}
</h3>
<p class="text-blue-800 mb-4">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.DESC') }}
</p>
<div class="space-y-2 mb-6">
<h4 class="font-medium text-blue-900">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.BENEFITS.TITLE') }}
</h4>
<ul class="space-y-1 text-blue-800">
<li class="flex items-center">
<span class="w-2 h-2 bg-blue-600 rounded-full mr-3" />
{{
$t(
'INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.BENEFITS.EASY_SETUP'
)
}}
</li>
<li class="flex items-center">
<span class="w-2 h-2 bg-blue-600 rounded-full mr-3" />
{{
$t(
'INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.BENEFITS.SECURE_AUTH'
)
}}
</li>
<li class="flex items-center">
<span class="w-2 h-2 bg-blue-600 rounded-full mr-3" />
{{
$t(
'INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.BENEFITS.AUTO_CONFIG'
)
}}
</li>
</ul>
</div>
<NextButton
:disabled="!fbSdkLoaded"
solid
blue
:label="$t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.SUBMIT_BUTTON')"
@click="launchEmbeddedSignup"
/>
<p v-if="!fbSdkLoaded" class="text-sm text-gray-500 mt-2">
{{ $t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.LOADING_SDK') }}
</p>
</div>
</div>
</div>
</template>