feat: Whatsapp es reauth flow fe (#11915)

# 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-07-10 11:52:25 +05:30
committed by GitHub
co-authored by Muhsin Keloth
parent 50d023caed
commit 66ce599e4e
5 changed files with 428 additions and 1 deletions
@@ -0,0 +1,317 @@
import { ref, computed } from 'vue';
import { useStore } from 'vuex';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useAlert } from 'dashboard/composables';
import Auth from 'dashboard/api/auth';
export function useWhatsappReauthorization(inboxId) {
const store = useStore();
const router = useRouter();
const { t } = useI18n();
// State
const fbSdkLoaded = ref(false);
const isProcessing = ref(false);
const processingMessage = ref('');
const authCodeReceived = ref(false);
const authCode = ref(null);
const businessData = ref(null);
const isAuthenticating = ref(false);
// Computed
const authHeaders = computed(() => {
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 {};
});
const showLoader = computed(
() => isAuthenticating.value || isProcessing.value
);
// Error handling
const handleReauthorizationError = data => {
isProcessing.value = false;
authCodeReceived.value = false;
isAuthenticating.value = false;
const errorMessage =
data.error ||
data.message ||
t('INBOX_MGMT.SETTINGS.WHATSAPP_REAUTHORIZE.ERROR');
useAlert(errorMessage);
};
const handleReauthorizationCancellation = () => {
isProcessing.value = false;
authCodeReceived.value = false;
isAuthenticating.value = false;
useAlert(t('INBOX_MGMT.SETTINGS.WHATSAPP_REAUTHORIZE.CANCELLED'));
};
const handleReauthorizationSuccess = async () => {
isProcessing.value = false;
isAuthenticating.value = false;
// Refresh the inbox to update reauthorization status
await store.dispatch('inboxes/get');
useAlert(t('INBOX_MGMT.SETTINGS.WHATSAPP_REAUTHORIZE.SUCCESS'));
// Reload the page to reflect the changes
router.go(0);
};
// Reauthorization flow
const completeReauthorizationFlow = async businessDataParam => {
if (!authCodeReceived.value || !authCode.value) {
handleReauthorizationError({
error: t('INBOX_MGMT.SETTINGS.WHATSAPP_REAUTHORIZE.AUTH_NOT_COMPLETED'),
});
return;
}
isProcessing.value = true;
processingMessage.value = t(
'INBOX_MGMT.SETTINGS.WHATSAPP_REAUTHORIZE.PROCESSING'
);
try {
// Send reauthorization request
const accountId = store.getters.getCurrentAccountId;
const response = await fetch('/whatsapp/reauthorize', {
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,
inbox_id: inboxId,
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
handleReauthorizationSuccess();
} else {
throw new Error(responseData.message || responseData.error);
}
} catch (error) {
handleReauthorizationError({ error: error.message });
}
};
const isValidBusinessData = businessDataLocal => {
return (
businessDataLocal &&
(businessDataLocal.business_id || businessDataLocal.businessId) &&
(businessDataLocal.waba_id || businessDataLocal.wabaId)
);
};
// Message handling
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 (isValidBusinessData(businessDataLocal)) {
// 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 completeReauthorizationFlow(normalizedData);
} else {
processingMessage.value = t(
'INBOX_MGMT.SETTINGS.WHATSAPP_REAUTHORIZE.WAITING_FOR_AUTH'
);
}
} else {
handleReauthorizationError({
error: t(
'INBOX_MGMT.SETTINGS.WHATSAPP_REAUTHORIZE.INVALID_BUSINESS_DATA'
),
});
}
} else if (data.event === 'CANCEL') {
handleReauthorizationCancellation();
} else if (data.event === 'error') {
handleReauthorizationError({
error:
data.error_message ||
t('INBOX_MGMT.SETTINGS.WHATSAPP_REAUTHORIZE.ERROR'),
});
}
};
const fbLoginCallback = response => {
if (response.authResponse && response.authResponse.code) {
// Authorization code received from Facebook
authCode.value = response.authResponse.code;
authCodeReceived.value = true;
processingMessage.value = t(
'INBOX_MGMT.SETTINGS.WHATSAPP_REAUTHORIZE.WAITING_FOR_BUSINESS_INFO'
);
// Check if we already have business data and process immediately
if (businessData.value) {
completeReauthorizationFlow(businessData.value);
}
} else if (response.error) {
handleReauthorizationError({ error: response.error });
} else {
isProcessing.value = false;
isAuthenticating.value = false;
useAlert(t('INBOX_MGMT.SETTINGS.WHATSAPP_REAUTHORIZE.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
}
};
// Facebook SDK
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?.whatsappApiVersion || 'v22.0',
});
fbSdkLoaded.value = true;
};
document.body.appendChild(script);
};
const launchReauthorization = () => {
processingMessage.value = t(
'INBOX_MGMT.SETTINGS.WHATSAPP_REAUTHORIZE.AUTH_PROCESSING'
);
if (!window.FB) {
loadFacebookSdk();
setTimeout(() => launchReauthorization(), 1000);
return;
}
isAuthenticating.value = true;
// Following Facebook's embedded signup documentation
// Using auth_type: 'reauthorize' to force reauthorization
window.FB.login(fbLoginCallback, {
config_id: window.chatwootConfig?.whatsappConfigurationId,
response_type: 'code',
override_default_response_type: true,
auth_type: 'reauthorize',
extras: {
setup: {},
featureType: '', // Leave empty for default flow
sessionInfoVersion: '3',
},
});
};
// Lifecycle
const setupMessageListener = () => {
window.addEventListener('message', handleSignupMessage);
};
const cleanupMessageListener = () => {
window.removeEventListener('message', handleSignupMessage);
};
// Initialize
const initialize = () => {
loadFacebookSdk();
setupMessageListener();
};
return {
// State
fbSdkLoaded,
isProcessing,
processingMessage,
isAuthenticating,
// Computed
showLoader,
// Methods
launchReauthorization,
initialize,
cleanupMessageListener,
};
}
@@ -600,7 +600,24 @@
"WHATSAPP_SECTION_UPDATE_BUTTON": "Update",
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verification Token",
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings"
"WHATSAPP_RECONFIGURE_TITLE": "Reconfigure WhatsApp",
"WHATSAPP_RECONFIGURE_SUBHEADER": "Update your WhatsApp Business account configuration using the embedded signup flow.",
"WHATSAPP_RECONFIGURE_BUTTON": "Reconfigure with WhatsApp",
"WHATSAPP_RECONFIGURE_ERROR": "Failed to initiate WhatsApp reconfiguration. Please try again.",
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings",
"WHATSAPP_REAUTHORIZE": {
"TITLE": "Reauthorize WhatsApp Connection",
"DESCRIPTION": "Your WhatsApp Business account needs to be reauthorized to continue receiving messages. Click the button below to reconnect your account.",
"AUTH_PROCESSING": "Processing WhatsApp reauthorization...",
"WAITING_FOR_BUSINESS_INFO": "Waiting for business information...",
"WAITING_FOR_AUTH": "Waiting for authentication...",
"PROCESSING": "Updating your WhatsApp configuration...",
"SUCCESS": "WhatsApp successfully reauthorized!",
"ERROR": "Failed to reauthorize WhatsApp. Please try again.",
"CANCELLED": "WhatsApp reauthorization was cancelled.",
"AUTH_NOT_COMPLETED": "Authentication not completed. Please try again.",
"INVALID_BUSINESS_DATA": "Invalid business data received. Please try again."
}
},
"HELP_CENTER": {
"LABEL": "Help Center",
@@ -620,6 +637,12 @@
"MESSAGE_SUCCESS": "Reconnection successful",
"MESSAGE_ERROR": "There was an error, please try again"
},
"WHATSAPP_REAUTHORIZE": {
"TITLE": "Reauthorize WhatsApp",
"SUBTITLE": "Your WhatsApp connection has expired, please reconnect your WhatsApp Business account to continue services",
"MESSAGE_SUCCESS": "WhatsApp reconnection successful",
"MESSAGE_ERROR": "There was an error reconnecting WhatsApp, please try again"
},
"PRE_CHAT_FORM": {
"DESCRIPTION": "Pre chat forms enable you to capture user information before they start conversation with you.",
"SET_FIELDS": "Pre chat form fields",
@@ -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';
@@ -45,6 +46,7 @@ export default {
NextButton,
InstagramReauthorize,
DuplicateInboxBanner,
WhatsappReauthorize,
Editor,
},
mixins: [inboxMixin],
@@ -231,6 +233,11 @@ export default {
facebookUnauthorized() {
return this.isAFacebookInbox && this.inbox.reauthorization_required;
},
whatsappUnauthorized() {
return (
this.isAWhatsAppCloudChannel && this.inbox.reauthorization_required
);
},
googleUnauthorized() {
const isLegacyInbox = ['imap.gmail.com', 'imap.google.com'].includes(
this.inbox.imap_address
@@ -410,6 +417,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')"
@@ -0,0 +1,38 @@
<script setup>
import { onMounted, onBeforeUnmount } from 'vue';
import InboxReconnectionRequired from '../../components/InboxReconnectionRequired.vue';
import LoadingState from 'dashboard/components/widgets/LoadingState.vue';
import { useWhatsappReauthorization } from 'dashboard/composables/useWhatsappReauthorization';
const props = defineProps({
inbox: {
type: Object,
required: true,
},
});
const {
processingMessage,
showLoader,
launchReauthorization,
initialize,
cleanupMessageListener,
} = useWhatsappReauthorization(props.inbox.id);
onMounted(() => {
initialize();
});
onBeforeUnmount(() => {
cleanupMessageListener();
});
</script>
<template>
<div>
<InboxReconnectionRequired
class="mx-8 mt-5"
@reauthorize="launchReauthorization"
/>
<LoadingState v-if="showLoader" :message="processingMessage" />
</div>
</template>
@@ -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 { useWhatsappReauthorization } from 'dashboard/composables/useWhatsappReauthorization';
export default {
components: {
@@ -34,6 +35,20 @@ export default {
validations: {
whatsAppInboxAPIKey: { required },
},
computed: {
isWhatsAppCloudChannel() {
return this.inbox.provider === 'whatsapp_cloud';
},
hasWhatsappAppId() {
return (
window.chatwootConfig?.whatsappAppId &&
window.chatwootConfig.whatsappAppId !== 'none'
);
},
shouldShowReconfigureButton() {
return this.isWhatsAppCloudChannel && this.hasWhatsappAppId;
},
},
watch: {
inbox() {
this.setDefaults();
@@ -83,6 +98,18 @@ export default {
useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE'));
}
},
reconfigureWhatsApp() {
// Initialize the WhatsApp reauthorization flow
const { launchReauthorization, initialize } = useWhatsappReauthorization(
this.inbox.id
);
// Initialize and launch
initialize();
launchReauthorization();
// Note: cleanup will happen when component unmounts or user navigates away
},
},
};
</script>
@@ -208,7 +235,21 @@ export default {
>
<woot-code :script="inbox.provider_config.api_key" />
</SettingsSection>
<!-- Show reconfigure button for WhatsApp Cloud with embedded signup and app ID configured -->
<SettingsSection
v-if="shouldShowReconfigureButton"
:title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_RECONFIGURE_TITLE')"
:sub-title="
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_RECONFIGURE_SUBHEADER')
"
>
<NextButton class="mt-2" @click="reconfigureWhatsApp">
{{ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_RECONFIGURE_BUTTON') }}
</NextButton>
</SettingsSection>
<!-- Show API key update for non-embedded signup WhatsApp or when app ID is missing -->
<SettingsSection
v-else
:title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_TITLE')"
:sub-title="
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_SECTION_UPDATE_SUBHEADER')