feat: Add WhatsApp reauthorization frontend flow
This commit is contained in:
@@ -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>
|
||||
+35
@@ -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,14 @@ export default {
|
||||
validations: {
|
||||
whatsAppInboxAPIKey: { required },
|
||||
},
|
||||
computed: {
|
||||
isWhatsAppCloudWithEmbeddedSignup() {
|
||||
return (
|
||||
this.inbox.provider === 'whatsapp_cloud' &&
|
||||
this.inbox.provider_config?.source === 'embedded_signup'
|
||||
);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
inbox() {
|
||||
this.setDefaults();
|
||||
@@ -83,6 +92,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 +229,21 @@ export default {
|
||||
>
|
||||
<woot-code :script="inbox.provider_config.api_key" />
|
||||
</SettingsSection>
|
||||
<!-- Show reconfigure button for WhatsApp Cloud with embedded signup -->
|
||||
<SettingsSection
|
||||
v-if="isWhatsAppCloudWithEmbeddedSignup"
|
||||
: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 -->
|
||||
<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