feat: add reauth flow for wa embedded signup (#11940)

# Pull Request Template

## Description

Please include a summary of the change and issue(s) fixed. Also, mention
relevant motivation, context, and any dependencies that this change
requires.
Fixes # (issue)

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide
instructions so we can reproduce. Please also list any relevant details
for your test configuration.


## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
Tanmay Deep Sharma
2025-08-08 01:48:45 +05:30
committed by GitHub
co-authored by Muhsin Keloth
parent 462ab5241c
commit d2583d32e9
17 changed files with 890 additions and 206 deletions
@@ -11,6 +11,7 @@ import InstagramReauthorize from './channels/instagram/Reauthorize.vue';
import DuplicateInboxBanner from './channels/instagram/DuplicateInboxBanner.vue';
import MicrosoftReauthorize from './channels/microsoft/Reauthorize.vue';
import GoogleReauthorize from './channels/google/Reauthorize.vue';
import WhatsappReauthorize from './channels/whatsapp/Reauthorize.vue';
import PreChatFormSettings from './PreChatForm/Settings.vue';
import WeeklyAvailability from './components/WeeklyAvailability.vue';
import GreetingsEditor from 'shared/components/GreetingsEditor.vue';
@@ -44,6 +45,7 @@ export default {
GoogleReauthorize,
NextButton,
InstagramReauthorize,
WhatsappReauthorize,
DuplicateInboxBanner,
Editor,
},
@@ -87,10 +89,7 @@ export default {
return this.tabs[this.selectedTabIndex]?.key;
},
shouldShowWhatsAppConfiguration() {
return !!(
this.isAWhatsAppCloudChannel &&
this.inbox.provider_config?.source !== 'embedded_signup'
);
return this.isAWhatsAppCloudChannel;
},
whatsAppAPIProviderName() {
if (this.isAWhatsAppCloudChannel) {
@@ -247,6 +246,14 @@ export default {
this.inbox.reauthorization_required
);
},
whatsappUnauthorized() {
return (
this.isAWhatsAppChannel &&
this.inbox.provider === 'whatsapp_cloud' &&
this.inbox.provider_config?.source === 'embedded_signup' &&
this.inbox.reauthorization_required
);
},
},
watch: {
$route(to) {
@@ -416,6 +423,7 @@ export default {
<FacebookReauthorize v-if="facebookUnauthorized" :inbox="inbox" />
<GoogleReauthorize v-if="googleUnauthorized" :inbox="inbox" />
<InstagramReauthorize v-if="instagramUnauthorized" :inbox="inbox" />
<WhatsappReauthorize v-if="whatsappUnauthorized" :inbox="inbox" />
<DuplicateInboxBanner
v-if="hasDuplicateInstagramInbox"
:content="$t('INBOX_MGMT.ADD.INSTAGRAM.DUPLICATE_INBOX_BANNER')"
@@ -7,8 +7,13 @@ import { useAlert } from 'dashboard/composables';
import Icon from 'next/icon/Icon.vue';
import NextButton from 'next/button/Button.vue';
import LoadingState from 'dashboard/components/widgets/LoadingState.vue';
import { loadScript } from 'dashboard/helper/DOMHelpers';
import { parseAPIErrorResponse } from 'dashboard/store/utils/api';
import {
setupFacebookSdk,
initWhatsAppEmbeddedSignup,
createMessageHandler,
isValidBusinessData,
} from './whatsapp/utils';
const store = useStore();
const router = useRouter();
@@ -120,14 +125,6 @@ const completeSignupFlow = async businessDataParam => {
}
};
const isValidBusinessData = businessDataLocal => {
return (
businessDataLocal &&
businessDataLocal.business_id &&
businessDataLocal.waba_id
);
};
// Message handling
const handleEmbeddedSignupData = async data => {
if (data.event === 'FINISH') {
@@ -162,9 +159,26 @@ const handleEmbeddedSignupData = async data => {
}
};
const fbLoginCallback = response => {
if (response.authResponse && response.authResponse.code) {
authCode.value = response.authResponse.code;
const handleSignupMessage = createMessageHandler(handleEmbeddedSignupData);
const launchEmbeddedSignup = async () => {
try {
isAuthenticating.value = true;
processingMessage.value = t(
'INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.AUTH_PROCESSING'
);
await setupFacebookSdk(
window.chatwootConfig?.whatsappAppId,
window.chatwootConfig?.whatsappApiVersion
);
fbSdkLoaded.value = true;
const code = await initWhatsAppEmbeddedSignup(
window.chatwootConfig?.whatsappConfigurationId
);
authCode.value = code;
authCodeReceived.value = true;
processingMessage.value = t(
'INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.WAITING_FOR_BUSINESS_INFO'
@@ -173,79 +187,18 @@ const fbLoginCallback = response => {
if (businessData.value) {
completeSignupFlow(businessData.value);
}
} else if (response.error) {
handleSignupError({ error: response.error });
} else {
isProcessing.value = false;
isAuthenticating.value = false;
useAlert(t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.CANCELLED'));
}
};
const handleSignupMessage = event => {
// Validate origin for security - following Facebook documentation
// https://developers.facebook.com/docs/whatsapp/embedded-signup/implementation#step-3--add-embedded-signup-to-your-website
if (!event.origin.endsWith('facebook.com')) return;
// Parse and handle WhatsApp embedded signup events
try {
const data = JSON.parse(event.data);
if (data.type === 'WA_EMBEDDED_SIGNUP') {
handleEmbeddedSignupData(data);
}
} catch {
// Ignore non-JSON or irrelevant messages
}
};
const runFBInit = () => {
window.FB.init({
appId: window.chatwootConfig?.whatsappAppId,
autoLogAppEvents: true,
xfbml: true,
version: window.chatwootConfig?.whatsappApiVersion || 'v22.0',
});
fbSdkLoaded.value = true;
};
const loadFacebookSdk = async () => {
return loadScript('https://connect.facebook.net/en_US/sdk.js', {
async: true,
defer: true,
crossOrigin: 'anonymous',
});
};
const tryWhatsAppLogin = () => {
isAuthenticating.value = true;
processingMessage.value = t(
'INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.AUTH_PROCESSING'
);
window.FB.login(fbLoginCallback, {
config_id: window.chatwootConfig?.whatsappConfigurationId,
response_type: 'code',
override_default_response_type: true,
extras: {
setup: {},
featureType: '',
sessionInfoVersion: '3',
},
});
};
const launchEmbeddedSignup = async () => {
try {
// Load SDK first if not loaded, following Facebook.vue pattern exactly
await loadFacebookSdk();
runFBInit(); // Initialize FB after loading
// Now proceed with login
tryWhatsAppLogin();
} catch (error) {
handleSignupError({
error: t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.SDK_LOAD_ERROR'),
});
if (error.message === 'Login cancelled') {
isProcessing.value = false;
isAuthenticating.value = false;
useAlert(t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.CANCELLED'));
} else {
handleSignupError({
error:
error.message ||
t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.SDK_LOAD_ERROR'),
});
}
}
};
@@ -259,7 +212,6 @@ const cleanupMessageListener = () => {
};
const initialize = () => {
window.fbAsyncInit = runFBInit;
setupMessageListener();
};
@@ -0,0 +1,190 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useAlert } from 'dashboard/composables';
import InboxReconnectionRequired from '../../components/InboxReconnectionRequired.vue';
import whatsappChannel from 'dashboard/api/channel/whatsappChannel';
import {
setupFacebookSdk,
initWhatsAppEmbeddedSignup,
createMessageHandler,
isValidBusinessData,
} from './utils';
const props = defineProps({
inbox: {
type: Object,
required: true,
},
});
const { t } = useI18n();
const isRequestingAuthorization = ref(false);
const isLoadingFacebook = ref(true);
const whatsappAppId = computed(() => window.chatwootConfig.whatsappAppId);
const whatsappConfigurationId = computed(
() => window.chatwootConfig.whatsappConfigurationId
);
const reauthorizeWhatsApp = async params => {
isRequestingAuthorization.value = true;
try {
const response = await whatsappChannel.reauthorizeWhatsApp({
inboxId: props.inbox.id,
...params,
});
if (response.data.success) {
useAlert(t('INBOX.REAUTHORIZE.SUCCESS'));
} else {
useAlert(response.data.message || t('INBOX.REAUTHORIZE.ERROR'));
}
} catch (error) {
useAlert(error.message || t('INBOX.REAUTHORIZE.ERROR'));
} finally {
isRequestingAuthorization.value = false;
}
};
const handleEmbeddedSignupEvents = async (data, authCode) => {
if (!data || typeof data !== 'object') {
return;
}
// Handle different event types
if (data.event === 'FINISH') {
const businessData = data.data;
if (isValidBusinessData(businessData) && businessData.phone_number_id) {
await reauthorizeWhatsApp({
code: authCode,
business_id: businessData.business_id,
waba_id: businessData.waba_id,
phone_number_id: businessData.phone_number_id,
});
} else {
useAlert(
t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.INVALID_BUSINESS_DATA')
);
}
} else if (data.event === 'CANCEL') {
isRequestingAuthorization.value = false;
useAlert(t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.CANCELLED'));
} else if (data.event === 'error') {
isRequestingAuthorization.value = false;
useAlert(
data.error_message ||
t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.SIGNUP_ERROR')
);
}
};
const startEmbeddedSignup = authCode => {
const messageHandler = createMessageHandler(data =>
handleEmbeddedSignupEvents(data, authCode)
);
window.addEventListener('message', messageHandler);
};
const handleLoginAndReauthorize = async () => {
// Validate required configuration
if (!whatsappAppId.value) {
throw new Error('WhatsApp App ID is required');
}
if (!whatsappConfigurationId.value) {
throw new Error('WhatsApp Configuration ID is required');
}
try {
const authCode = await initWhatsAppEmbeddedSignup(
whatsappConfigurationId.value
);
// Check if this is a reauthorization scenario where we already have the business data
const existingConfig = props.inbox.provider_config;
if (
existingConfig &&
existingConfig.business_account_id &&
existingConfig.phone_number_id
) {
await reauthorizeWhatsApp({
code: authCode,
business_id: existingConfig.business_account_id,
waba_id: existingConfig.business_account_id,
phone_number_id: existingConfig.phone_number_id,
});
} else {
startEmbeddedSignup(authCode);
}
} catch (error) {
if (error.message === 'Login cancelled') {
useAlert(t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.CANCELLED'));
} else {
useAlert(
error.message ||
t('INBOX_MGMT.ADD.WHATSAPP.EMBEDDED_SIGNUP.AUTH_NOT_COMPLETED')
);
}
throw error;
}
};
const requestAuthorization = async () => {
if (isLoadingFacebook.value) {
useAlert(t('INBOX.REAUTHORIZE.LOADING_FACEBOOK'));
return;
}
isRequestingAuthorization.value = true;
try {
await handleLoginAndReauthorize();
} catch (error) {
useAlert(error.message || t('INBOX.REAUTHORIZE.CONFIGURATION_ERROR'));
} finally {
// Reset only if not already processing through embedded signup
if (!window.FB || !window.FB.getLoginStatus) {
isRequestingAuthorization.value = false;
}
}
};
onMounted(async () => {
try {
// Validate required configuration
if (!whatsappAppId.value) {
useAlert(t('INBOX.REAUTHORIZE.WHATSAPP_APP_ID_MISSING'));
return;
}
if (!whatsappConfigurationId.value) {
useAlert(t('INBOX.REAUTHORIZE.WHATSAPP_CONFIG_ID_MISSING'));
return;
}
// Load Facebook SDK and initialize
await setupFacebookSdk(
whatsappAppId.value,
window.chatwootConfig?.whatsappApiVersion
);
} catch (error) {
useAlert(t('INBOX.REAUTHORIZE.FACEBOOK_LOAD_ERROR'));
} finally {
isLoadingFacebook.value = false;
}
});
// Expose requestAuthorization function for parent components
defineExpose({
requestAuthorization,
});
</script>
<template>
<InboxReconnectionRequired
class="mx-8 mt-5"
:is-loading="isRequestingAuthorization"
@reauthorize="requestAuthorization"
/>
</template>
@@ -0,0 +1,89 @@
import { loadScript } from 'dashboard/helper/DOMHelpers';
export const loadFacebookSdk = async () => {
return loadScript('https://connect.facebook.net/en_US/sdk.js', {
async: true,
defer: true,
crossOrigin: 'anonymous',
});
};
export const initializeFacebook = (appId, apiVersion) => {
const version = apiVersion || 'v22.0';
return new Promise(resolve => {
const init = () => {
window.FB.init({
appId,
autoLogAppEvents: true,
xfbml: true,
version,
});
resolve();
};
if (window.FB) {
init();
} else {
window.fbAsyncInit = init;
}
});
};
export const isValidBusinessData = businessData => {
return businessData && businessData.business_id && businessData.waba_id;
};
export const createMessageHandler = onEmbeddedSignupData => {
return event => {
if (!event.origin.endsWith('facebook.com')) return;
try {
let data;
if (typeof event.data === 'string') {
data = JSON.parse(event.data);
} else if (typeof event.data === 'object' && event.data !== null) {
data = event.data;
} else {
return;
}
if (data.type === 'WA_EMBEDDED_SIGNUP') {
onEmbeddedSignupData(data);
}
} catch {
// Ignore non-JSON or irrelevant messages
}
};
};
export const initWhatsAppEmbeddedSignup = configId => {
return new Promise((resolve, reject) => {
window.FB.login(
response => {
if (response.authResponse && response.authResponse.code) {
resolve(response.authResponse.code);
} else if (response.error) {
reject(new Error(response.error));
} else {
reject(new Error('Login cancelled'));
}
},
{
config_id: configId,
response_type: 'code',
override_default_response_type: true,
extras: {
setup: {},
featureType: '',
sessionInfoVersion: '3',
},
}
);
});
};
export const setupFacebookSdk = async (appId, apiVersion) => {
const version = apiVersion || 'v22.0';
await loadFacebookSdk();
await initializeFacebook(appId, version);
};
@@ -7,6 +7,7 @@ import SmtpSettings from '../SmtpSettings.vue';
import { useVuelidate } from '@vuelidate/core';
import { required } from '@vuelidate/validators';
import NextButton from 'dashboard/components-next/button/Button.vue';
import WhatsappReauthorize from '../channels/whatsapp/Reauthorize.vue';
export default {
components: {
@@ -14,6 +15,7 @@ export default {
ImapSettings,
SmtpSettings,
NextButton,
WhatsappReauthorize,
},
mixins: [inboxMixin],
props: {
@@ -29,12 +31,21 @@ export default {
return {
hmacMandatory: false,
whatsAppInboxAPIKey: '',
isRequestingReauthorization: false,
isSyncingTemplates: false,
};
},
validations: {
whatsAppInboxAPIKey: { required },
},
computed: {
isEmbeddedSignupWhatsApp() {
return this.inbox.provider_config?.source === 'embedded_signup';
},
whatsappAppId() {
return window.chatwootConfig?.whatsappAppId;
},
},
watch: {
inbox() {
this.setDefaults();
@@ -84,6 +95,11 @@ export default {
useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE'));
}
},
async handleReconfigure() {
if (this.$refs.whatsappReauth) {
await this.$refs.whatsappReauth.requestAuthorization();
}
},
async syncTemplates() {
this.isSyncingTemplates = true;
try {
@@ -210,45 +226,80 @@ export default {
</div>
<div v-else-if="isAWhatsAppChannel && !isATwilioChannel">
<div v-if="inbox.provider_config" class="mx-8">
<SettingsSection
:title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_WEBHOOK_TITLE')"
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_WEBHOOK_SUBHEADER')"
>
<woot-code :script="inbox.provider_config.webhook_verify_token" />
</SettingsSection>
<SettingsSection
:title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_TITLE')"
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_SUBHEADER')"
>
<woot-code :script="inbox.provider_config.api_key" />
</SettingsSection>
<SettingsSection
:title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_TITLE')"
:sub-title="
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_SUBHEADER')
"
>
<div
class="flex flex-1 justify-between items-center mt-2 whatsapp-settings--content"
<!-- Embedded Signup Section -->
<template v-if="isEmbeddedSignupWhatsApp">
<SettingsSection
v-if="whatsappAppId"
:title="
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_EMBEDDED_SIGNUP_TITLE')
"
:sub-title="
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_EMBEDDED_SIGNUP_SUBHEADER')
"
>
<woot-input
v-model="whatsAppInboxAPIKey"
type="text"
class="flex-1 mr-2 [&>input]:!mb-0"
:placeholder="
$t(
'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_PLACEHOLDER'
)
"
/>
<NextButton
:disabled="v$.whatsAppInboxAPIKey.$invalid"
@click="updateWhatsAppInboxAPIKey"
<div class="flex gap-4 items-center">
<p class="text-sm text-slate-600">
{{
$t(
'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_EMBEDDED_SIGNUP_DESCRIPTION'
)
}}
</p>
<NextButton @click="handleReconfigure">
{{ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_RECONFIGURE_BUTTON') }}
</NextButton>
</div>
</SettingsSection>
</template>
<!-- Manual Setup Section -->
<template v-else>
<SettingsSection
:title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_WEBHOOK_TITLE')"
:sub-title="
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_WEBHOOK_SUBHEADER')
"
>
<woot-code :script="inbox.provider_config.webhook_verify_token" />
</SettingsSection>
<SettingsSection
:title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_TITLE')"
:sub-title="
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_SUBHEADER')
"
>
<woot-code :script="inbox.provider_config.api_key" />
</SettingsSection>
<SettingsSection
:title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_TITLE')"
:sub-title="
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_SUBHEADER')
"
>
<div
class="flex flex-1 justify-between items-center mt-2 whatsapp-settings--content"
>
{{ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_BUTTON') }}
</NextButton>
</div>
</SettingsSection>
<woot-input
v-model="whatsAppInboxAPIKey"
type="text"
class="flex-1 mr-2 [&>input]:!mb-0"
:placeholder="
$t(
'INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_PLACEHOLDER'
)
"
/>
<NextButton
:disabled="v$.whatsAppInboxAPIKey.$invalid"
@click="updateWhatsAppInboxAPIKey"
>
{{
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_BUTTON')
}}
</NextButton>
</div>
</SettingsSection>
</template>
<SettingsSection
:title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_TEMPLATES_SYNC_TITLE')"
:sub-title="
@@ -262,6 +313,12 @@ export default {
</div>
</SettingsSection>
</div>
<WhatsappReauthorize
v-if="isEmbeddedSignupWhatsApp"
ref="whatsappReauth"
:inbox="inbox"
class="hidden"
/>
</div>
</template>