diff --git a/app/javascript/dashboard/composables/useWhatsappReauthorization.js b/app/javascript/dashboard/composables/useWhatsappReauthorization.js new file mode 100644 index 000000000..47ca43b18 --- /dev/null +++ b/app/javascript/dashboard/composables/useWhatsappReauthorization.js @@ -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, + }; +} diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index eb999a0e5..5c6eb82cf 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -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", diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue index a3f60c6d5..783e0b881 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue @@ -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 { + +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(); +}); + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue index bc5355c3c..c91da7b26 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue @@ -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 + }, }, }; @@ -208,7 +229,21 @@ export default { > + + + {{ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_RECONFIGURE_BUTTON') }} + + + +