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
parent 462ab5241c
commit d2583d32e9
17 changed files with 890 additions and 206 deletions

View File

@@ -1,8 +1,10 @@
class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts::BaseController
before_action :validate_feature_enabled!
before_action :fetch_and_validate_inbox, if: -> { params[:inbox_id].present? }
# POST /api/v1/accounts/:account_id/whatsapp/authorization
# Handles the embedded signup callback data from the Facebook SDK
# Handles both initial authorization and reauthorization
# If inbox_id is present in params, it performs reauthorization
def create
validate_embedded_signup_params!
channel = process_embedded_signup
@@ -16,21 +18,42 @@ class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts:
def process_embedded_signup
service = Whatsapp::EmbeddedSignupService.new(
account: Current.account,
code: params[:code],
business_id: params[:business_id],
waba_id: params[:waba_id],
phone_number_id: params[:phone_number_id]
params: params.permit(:code, :business_id, :waba_id, :phone_number_id).to_h.symbolize_keys,
inbox_id: params[:inbox_id]
)
service.perform
end
def render_success_response(inbox)
def fetch_and_validate_inbox
@inbox = Current.account.inboxes.find(params[:inbox_id])
validate_reauthorization_required
end
def validate_reauthorization_required
return if @inbox.channel.reauthorization_required? || can_upgrade_to_embedded_signup?
render json: {
success: false,
message: I18n.t('inbox.reauthorization.not_required')
}, status: :unprocessable_entity
end
def can_upgrade_to_embedded_signup?
channel = @inbox.channel
return false unless channel.provider == 'whatsapp_cloud'
true
end
def render_success_response(inbox)
response = {
success: true,
id: inbox.id,
name: inbox.name,
channel_type: 'whatsapp'
}
response[:message] = I18n.t('inbox.reauthorization.success') if params[:inbox_id].present?
render json: response
end
def render_error_response(error)

View File

@@ -9,6 +9,13 @@ class WhatsappChannel extends ApiClient {
createEmbeddedSignup(params) {
return axios.post(`${this.baseUrl()}/whatsapp/authorization`, params);
}
reauthorizeWhatsApp({ inboxId, ...params }) {
return axios.post(`${this.baseUrl()}/whatsapp/authorization`, {
...params,
inbox_id: inboxId,
});
}
}
export default new WhatsappChannel();

View File

@@ -72,6 +72,24 @@
"MARK_ALL_READ": "All notifications marked as read",
"DELETE_ALL": "All notifications deleted",
"DELETE_ALL_READ": "All read notifications deleted"
},
"REAUTHORIZE": {
"TITLE": "Reauthorization Required",
"DESCRIPTION": "Your WhatsApp connection has expired. Please reconnect to continue receiving and sending messages.",
"BUTTON_TEXT": "Reconnect WhatsApp",
"LOADING_FACEBOOK": "Loading Facebook SDK...",
"SUCCESS": "WhatsApp reconnected successfully",
"ERROR": "Failed to reconnect WhatsApp. Please try again.",
"WHATSAPP_APP_ID_MISSING": "WhatsApp App ID is not configured. Please contact your administrator.",
"WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.",
"CONFIGURATION_ERROR": "Configuration error occurred during reauthorization.",
"FACEBOOK_LOAD_ERROR": "Failed to load Facebook SDK. Please try again.",
"TROUBLESHOOTING": {
"TITLE": "Troubleshooting",
"POPUP_BLOCKED": "Ensure pop-ups are allowed for this site",
"COOKIES": "Third-party cookies must be enabled",
"ADMIN_ACCESS": "You need admin access to the WhatsApp Business Account"
}
}
}
}

View File

@@ -598,6 +598,21 @@
"WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key",
"WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Enter the new API Key here",
"WHATSAPP_SECTION_UPDATE_BUTTON": "Update",
"WHATSAPP_EMBEDDED_SIGNUP_TITLE": "WhatsApp Embedded Signup",
"WHATSAPP_EMBEDDED_SIGNUP_SUBHEADER": "This inbox is connected through WhatsApp embedded signup.",
"WHATSAPP_EMBEDDED_SIGNUP_DESCRIPTION": "You can reconfigure this inbox to update your WhatsApp Business settings.",
"WHATSAPP_RECONFIGURE_BUTTON": "Reconfigure",
"WHATSAPP_CONNECT_TITLE": "Connect to WhatsApp Business",
"WHATSAPP_CONNECT_SUBHEADER": "Upgrade to WhatsApp embedded signup for easier management.",
"WHATSAPP_CONNECT_DESCRIPTION": "Connect this inbox to WhatsApp Business for enhanced features and easier management.",
"WHATSAPP_CONNECT_BUTTON": "Connect",
"WHATSAPP_CONNECT_SUCCESS": "Successfully connected to WhatsApp Business!",
"WHATSAPP_CONNECT_ERROR": "Failed to connect to WhatsApp Business. Please try again.",
"WHATSAPP_RECONFIGURE_SUCCESS": "Successfully reconfigured WhatsApp Business!",
"WHATSAPP_RECONFIGURE_ERROR": "Failed to reconfigure WhatsApp Business. Please try again.",
"WHATSAPP_APP_ID_MISSING": "WhatsApp App ID is not configured. Please contact your administrator.",
"WHATSAPP_CONFIG_ID_MISSING": "WhatsApp Configuration ID is not configured. Please contact your administrator.",
"WHATSAPP_LOGIN_CANCELLED": "WhatsApp login was cancelled. Please try again.",
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verification Token",
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
"WHATSAPP_TEMPLATES_SYNC_TITLE": "Sync Templates",

View File

@@ -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')"

View File

@@ -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();
};

View File

@@ -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>

View File

@@ -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);
};

View File

@@ -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>

View File

@@ -32,8 +32,8 @@ class Channel::Whatsapp < ApplicationRecord
validates :phone_number, presence: true, uniqueness: true
validate :validate_provider_config
before_save :setup_webhooks
after_create :sync_templates
after_create_commit :setup_webhooks
before_destroy :teardown_webhooks
def name
@@ -78,8 +78,12 @@ class Channel::Whatsapp < ApplicationRecord
handle_webhook_setup_error(e)
end
def provider_config_changed?
will_save_change_to_provider_config?
end
def should_setup_webhooks?
whatsapp_cloud_provider? && embedded_signup_source? && webhook_config_present?
whatsapp_cloud_provider? && embedded_signup_source? && webhook_config_present? && provider_config_changed?
end
def whatsapp_cloud_provider?

View File

@@ -1,10 +1,11 @@
class Whatsapp::EmbeddedSignupService
def initialize(account:, code:, business_id:, waba_id:, phone_number_id:)
def initialize(account:, params:, inbox_id: nil)
@account = account
@code = code
@business_id = business_id
@waba_id = waba_id
@phone_number_id = phone_number_id
@code = params[:code]
@business_id = params[:business_id]
@waba_id = params[:waba_id]
@phone_number_id = params[:phone_number_id]
@inbox_id = inbox_id
end
def perform
@@ -19,11 +20,19 @@ class Whatsapp::EmbeddedSignupService
# Validate token has access to the WABA
Whatsapp::TokenValidationService.new(access_token, @waba_id).perform
# Create channel
waba_info = { waba_id: @waba_id, business_name: phone_info[:business_name] }
# Webhook setup is now handled in the channel after_create_commit callback
Whatsapp::ChannelCreationService.new(@account, waba_info, phone_info, access_token).perform
# Reauthorization flow if inbox_id is present
if @inbox_id.present?
Whatsapp::ReauthorizationService.new(
account: @account,
inbox_id: @inbox_id,
phone_number_id: @phone_number_id,
business_id: @business_id
).perform(access_token, phone_info)
else
# Create channel for new authorization
waba_info = { waba_id: @waba_id, business_name: phone_info[:business_name] }
Whatsapp::ChannelCreationService.new(@account, waba_info, phone_info, access_token).perform
end
rescue StandardError => e
Rails.logger.error("[WHATSAPP] Embedded signup failed: #{e.message}")
raise e

View File

@@ -0,0 +1,42 @@
class Whatsapp::ReauthorizationService
def initialize(account:, inbox_id:, phone_number_id:, business_id:)
@account = account
@inbox_id = inbox_id
@phone_number_id = phone_number_id
@business_id = business_id
end
def perform(access_token, phone_info)
inbox = @account.inboxes.find(@inbox_id)
channel = inbox.channel
# Validate phone number matches for reauthorization
if phone_info[:phone_number] != channel.phone_number
raise StandardError, "Phone number mismatch. Expected #{channel.phone_number}, got #{phone_info[:phone_number]}"
end
# Update channel configuration
update_channel_config(channel, access_token, phone_info)
# Mark as reauthorized
channel.reauthorized! if channel.respond_to?(:reauthorized!)
channel
end
private
def update_channel_config(channel, access_token, phone_info)
current_config = channel.provider_config || {}
channel.provider_config = current_config.merge(
'api_key' => access_token,
'phone_number_id' => @phone_number_id,
'business_account_id' => @business_id,
'source' => 'embedded_signup'
)
channel.save!
# Update inbox name if business name changed
business_name = phone_info[:business_name] || phone_info[:verified_name]
channel.inbox.update!(name: business_name) if business_name.present?
end
end

View File

@@ -116,4 +116,5 @@ json.provider resource.channel.try(:provider)
if resource.whatsapp?
json.message_templates resource.channel.try(:message_templates)
json.provider_config resource.channel.try(:provider_config) if Current.account_user&.administrator?
json.reauthorization_required resource.channel.try(:reauthorization_required?)
end