feat(widget): Allow widget loading in mobile app WebViews when domain restrictions are set (#13763)

When `allowed_domains` is configured on a web widget inbox, the server
responds with Content-Security-Policy: frame-ancestors <domains>, which
blocks the widget iframe in mobile app WebViews. This happens because
WebViews load content from file:// or null origins, which cannot match
any domain in the frame-ancestors directive.

This adds a per-inbox toggle — "Enable widget in mobile apps" — that
skips the frame-ancestors header when the request has no valid Origin
(i.e., it comes from a mobile WebView). Web browsers with a real origin
still get domain restrictions enforced as usual.

<img width="2330" height="1490" alt="CleanShot 2026-03-11 at 10 13
01@2x"
src="https://github.com/user-attachments/assets/d9326fac-020d-4ce7-9ced-0c185468c8fc"
/>


Fixes
https://linear.app/chatwoot/issue/CW-6560/widget-is-not-loading-from-iosandroid-widgets

How to test

1. Go to Settings → Inboxes → (Web Widget) → Configuration
2. Set allowed_domains to a specific domain (e.g., *.example.com)
3. Try loading the widget in a mobile app WebView — it should be blocked
4. Enable "Enable widget in mobile apps" checkbox
5. Reload the widget in the WebView — it should now load successfully
6. Verify the widget on a website not in the allowed domains list is
still blocked

---------

Co-authored-by: iamsivin <iamsivin@gmail.com>
This commit is contained in:
Muhsin Keloth
2026-03-17 14:29:41 +04:00
committed by GitHub
parent 688218de0a
commit a4c3d3d8c0
6 changed files with 159 additions and 77 deletions

View File

@@ -218,13 +218,10 @@ export default {
return getInboxIconByType(type, medium, 'line');
},
bannerMaxWidth() {
const narrowTabs = [
'collaborators',
'configuration',
'bot-configuration',
];
const narrowTabs = ['collaborators', 'bot-configuration'];
const wideIfWebWidget = ['configuration', 'inbox-settings'];
if (narrowTabs.includes(this.selectedTabKey)) return 'max-w-4xl';
if (this.selectedTabKey === 'inbox-settings') {
if (wideIfWebWidget.includes(this.selectedTabKey)) {
return this.isAWebWidgetInbox ? 'max-w-7xl' : 'max-w-4xl';
}
return 'max-w-7xl';
@@ -354,6 +351,8 @@ export default {
this.$nextTick(() => {
this.setTabFromRouteParam();
});
} else {
this.selectedFeatureFlags = newInbox?.selected_feature_flags || [];
}
},
immediate: true,
@@ -1164,7 +1163,11 @@ export default {
<div v-if="selectedTabKey === 'collaborators'" class="mx-6 max-w-4xl">
<CollaboratorsPage :inbox="inbox" />
</div>
<div v-if="selectedTabKey === 'configuration'" class="mx-6 max-w-4xl">
<div
v-if="selectedTabKey === 'configuration'"
class="mx-6"
:class="isAWebWidgetInbox ? 'max-w-7xl' : 'max-w-4xl'"
>
<ConfigurationPage :inbox="inbox" />
</div>
<div v-if="selectedTabKey === 'csat'">

View File

@@ -2,6 +2,8 @@
import { useAlert } from 'dashboard/composables';
import inboxMixin from 'shared/mixins/inboxMixin';
import SettingsFieldSection from 'dashboard/components-next/Settings/SettingsFieldSection.vue';
import SettingsToggleSection from 'dashboard/components-next/Settings/SettingsToggleSection.vue';
import SettingsAccordion from 'dashboard/components-next/Settings/SettingsAccordion.vue';
import ImapSettings from '../ImapSettings.vue';
import SmtpSettings from '../SmtpSettings.vue';
import { useVuelidate } from '@vuelidate/core';
@@ -14,6 +16,8 @@ import { sanitizeAllowedDomains } from 'dashboard/helper/URLHelper';
export default {
components: {
SettingsFieldSection,
SettingsToggleSection,
SettingsAccordion,
ImapSettings,
SmtpSettings,
NextButton,
@@ -33,11 +37,13 @@ export default {
data() {
return {
hmacMandatory: false,
allowMobileWebview: false,
whatsAppInboxAPIKey: '',
isRequestingReauthorization: false,
isSyncingTemplates: false,
allowedDomains: '',
isUpdatingAllowedDomains: false,
isSettingDefaults: false,
};
},
validations: {
@@ -58,14 +64,28 @@ export default {
inbox() {
this.setDefaults();
},
allowMobileWebview() {
if (!this.isSettingDefaults) this.handleMobileWebviewFlag();
},
hmacMandatory() {
if (!this.isSettingDefaults && this.isAWebWidgetInbox)
this.handleHmacFlag();
},
},
mounted() {
this.setDefaults();
},
methods: {
setDefaults() {
this.isSettingDefaults = true;
this.hmacMandatory = this.inbox.hmac_mandatory || false;
this.allowMobileWebview = (
this.inbox.selected_feature_flags || []
).includes('allow_mobile_webview');
this.allowedDomains = this.inbox.allowed_domains || '';
this.$nextTick(() => {
this.isSettingDefaults = false;
});
},
handleHmacFlag() {
this.updateInbox();
@@ -85,6 +105,26 @@ export default {
useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE'));
}
},
async handleMobileWebviewFlag() {
try {
const currentFlags = this.inbox.selected_feature_flags || [];
const selectedFlags = this.allowMobileWebview
? [...currentFlags, 'allow_mobile_webview']
: currentFlags.filter(f => f !== 'allow_mobile_webview');
const payload = {
id: this.inbox.id,
formData: false,
channel: {
selected_feature_flags: selectedFlags,
},
};
await this.$store.dispatch('inboxes/updateInbox', payload);
useAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE'));
} catch (error) {
useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE'));
}
},
async updateAllowedDomains() {
this.isUpdatingAllowedDomains = true;
const sanitizedAllowedDomains = sanitizeAllowedDomains(
@@ -196,75 +236,86 @@ export default {
</SettingsFieldSection>
</div>
<div v-else-if="isAWebWidgetInbox">
<div>
<SettingsFieldSection
:label="$t('INBOX_MGMT.SETTINGS_POPUP.ALLOWED_DOMAINS.TITLE')"
:help-text="$t('INBOX_MGMT.SETTINGS_POPUP.ALLOWED_DOMAINS.SUBTITLE')"
class="[&>div]:!items-start"
<div class="space-y-4">
<SettingsToggleSection
:header="$t('INBOX_MGMT.SETTINGS_POPUP.ALLOWED_DOMAINS.TITLE')"
:description="
$t('INBOX_MGMT.SETTINGS_POPUP.ALLOWED_DOMAINS.DESCRIPTION')
"
hide-toggle
>
<TextArea
v-model="allowedDomains"
:placeholder="
$t('INBOX_MGMT.SETTINGS_POPUP.ALLOWED_DOMAINS.PLACEHOLDER')
"
auto-height
min-height="8rem"
class="w-full"
/>
<template #extra>
<div class="grid grid-cols-1 lg:grid-cols-8">
<div class="col-span-1 lg:col-span-2 invisible" />
<div class="col-span-1 lg:col-span-6 mt-4 justify-self-end">
<NextButton
:label="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
:is-loading="isUpdatingAllowedDomains"
@click="updateAllowedDomains"
/>
</div>
</div>
</template>
</SettingsFieldSection>
<SettingsFieldSection
:label="$t('INBOX_MGMT.SETTINGS_POPUP.HMAC_VERIFICATION')"
>
<woot-code :script="inbox.hmac_token" />
<template #extra>
<div class="grid grid-cols-1 lg:grid-cols-8">
<div class="col-span-1 lg:col-span-2 invisible" />
<p
class="col-span-1 lg:col-span-6 mt-1.5 text-label-small text-n-slate-11 ltr:ml-1 rtl:mr-1"
>
{{ $t('INBOX_MGMT.SETTINGS_POPUP.HMAC_DESCRIPTION') }}
<a
target="_blank"
rel="noopener noreferrer"
href="https://www.chatwoot.com/docs/product/channels/live-chat/sdk/identity-validation/"
class="text-n-blue-11 hover:underline text-label-small"
>
{{ $t('INBOX_MGMT.SETTINGS_POPUP.HMAC_LINK_TO_DOCS') }}
</a>
</p>
</div>
</template>
</SettingsFieldSection>
<SettingsFieldSection
:label="$t('INBOX_MGMT.SETTINGS_POPUP.HMAC_MANDATORY_VERIFICATION')"
:help-text="$t('INBOX_MGMT.SETTINGS_POPUP.HMAC_MANDATORY_DESCRIPTION')"
>
<div class="flex gap-2 items-center">
<input
id="hmacMandatory"
v-model="hmacMandatory"
type="checkbox"
@change="handleHmacFlag"
<template #editor>
<TextArea
v-model="allowedDomains"
:placeholder="
$t('INBOX_MGMT.SETTINGS_POPUP.ALLOWED_DOMAINS.PLACEHOLDER')
"
auto-height
resize
class="w-full [&>div]:!bg-transparent [&>div]:!border-none [&>div]:!border-0 [&>div]:px-0 [&>div]:pb-0 [&>div]:pt-0"
/>
<label for="hmacMandatory" class="text-body-main text-n-slate-12">
{{ $t('INBOX_MGMT.EDIT.ENABLE_HMAC.LABEL') }}
</label>
</div>
</SettingsFieldSection>
<div class="mt-3 flex justify-end">
<NextButton
:label="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
:is-loading="isUpdatingAllowedDomains"
@click="updateAllowedDomains"
/>
</div>
</template>
</SettingsToggleSection>
<SettingsToggleSection
v-model="allowMobileWebview"
:header="$t('INBOX_MGMT.SETTINGS_POPUP.ALLOW_MOBILE_WEBVIEW.LABEL')"
:description="
$t('INBOX_MGMT.SETTINGS_POPUP.ALLOW_MOBILE_WEBVIEW.SUBTITLE')
"
/>
</div>
<SettingsAccordion
:title="$t('INBOX_MGMT.SETTINGS_POPUP.IDENTITY_VALIDATION.TITLE')"
class="mt-6"
>
<SettingsToggleSection
:header="$t('INBOX_MGMT.SETTINGS_POPUP.IDENTITY_VALIDATION.TITLE')"
:description="
$t('INBOX_MGMT.SETTINGS_POPUP.IDENTITY_VALIDATION.DESCRIPTION')
"
hide-toggle
>
<template #editor>
<p class="mb-1 text-sm font-medium text-n-slate-12">
{{ $t('INBOX_MGMT.SETTINGS_POPUP.IDENTITY_VALIDATION.SECRET_KEY') }}
</p>
<woot-code :script="inbox.hmac_token" />
<p class="mt-1.5 text-label-small text-n-slate-11">
{{ $t('INBOX_MGMT.SETTINGS_POPUP.HMAC_DESCRIPTION') }}
<a
target="_blank"
rel="noopener noreferrer"
href="https://www.chatwoot.com/docs/product/channels/live-chat/sdk/identity-validation/"
class="text-n-blue-11 hover:underline text-label-small"
>
{{
$t('INBOX_MGMT.SETTINGS_POPUP.IDENTITY_VALIDATION.VIEW_DOCS')
}}
</a>
</p>
</template>
</SettingsToggleSection>
<SettingsToggleSection
v-model="hmacMandatory"
:header="
$t('INBOX_MGMT.SETTINGS_POPUP.IDENTITY_VALIDATION.REQUIRE_LABEL')
"
:description="
$t(
'INBOX_MGMT.SETTINGS_POPUP.IDENTITY_VALIDATION.REQUIRE_DESCRIPTION'
)
"
/>
</SettingsAccordion>
</div>
<div v-else-if="isAPIInbox">
<SettingsFieldSection