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:
co-authored by
Muhsin Keloth
parent
50d023caed
commit
66ce599e4e
@@ -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')"
|
||||
|
||||
+38
@@ -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>
|
||||
+41
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user