Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a679da329 | ||
|
|
5f5634ced3 | ||
|
|
bddb561546 | ||
|
|
20227403ce | ||
|
|
0a13dbedfb | ||
|
|
8d554f2b74 | ||
|
|
4854fb7b4b |
+1
-1
@@ -1030,7 +1030,7 @@ GEM
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
websocket-driver (0.7.7)
|
||||
websocket-driver (0.8.2)
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
|
||||
@@ -6,7 +6,10 @@ class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts
|
||||
{
|
||||
redirect_uri: "#{base_url}/microsoft/callback",
|
||||
scope: scope,
|
||||
state: state
|
||||
state: state,
|
||||
# Force the Microsoft account picker so an already-signed-in account does not
|
||||
# silently authorize and re-bind to an existing inbox in the new-inbox flow.
|
||||
prompt: 'select_account'
|
||||
}
|
||||
)
|
||||
if redirect_url
|
||||
|
||||
@@ -18,7 +18,8 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
|
||||
contact: @contact_inbox.contact,
|
||||
params: permitted_params.to_h.deep_symbolize_keys.except(:identifier)
|
||||
)
|
||||
render json: contact_identify_action.perform
|
||||
contact_identify_action.perform
|
||||
@contact_inbox.reload
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -44,6 +44,12 @@ const showChatSupport = computed(() => {
|
||||
);
|
||||
});
|
||||
|
||||
const toggleChatSupport = () => {
|
||||
if (window.$chatwoot) {
|
||||
window.$chatwoot.toggle();
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems = computed(() => {
|
||||
return [
|
||||
{
|
||||
@@ -51,9 +57,7 @@ const menuItems = computed(() => {
|
||||
showOnCustomBrandedInstance: false,
|
||||
label: t('SIDEBAR_ITEMS.CONTACT_SUPPORT'),
|
||||
icon: 'i-lucide-life-buoy',
|
||||
click: () => {
|
||||
window.$chatwoot.toggle();
|
||||
},
|
||||
click: toggleChatSupport,
|
||||
},
|
||||
{
|
||||
show: true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import ChannelSelector from '../ChannelSelector.vue';
|
||||
import { IS_WHATSAPP_INBOX_CREATION_DISABLED } from 'dashboard/constants/globals';
|
||||
import { IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED } from 'dashboard/constants/globals';
|
||||
|
||||
const props = defineProps({
|
||||
channel: {
|
||||
@@ -21,7 +21,10 @@ const hasFbConfigured = computed(() => {
|
||||
});
|
||||
|
||||
const hasInstagramConfigured = computed(() => {
|
||||
return window.chatwootConfig?.instagramAppId;
|
||||
return (
|
||||
!IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED &&
|
||||
window.chatwootConfig?.instagramAppId
|
||||
);
|
||||
});
|
||||
|
||||
const hasTiktokConfigured = computed(() => {
|
||||
@@ -59,7 +62,7 @@ const isActive = computed(() => {
|
||||
|
||||
if (key === 'whatsapp_call') {
|
||||
return (
|
||||
!IS_WHATSAPP_INBOX_CREATION_DISABLED &&
|
||||
!IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED &&
|
||||
props.enabledFeatures.channel_voice &&
|
||||
!!window.chatwootConfig?.whatsappAppId &&
|
||||
window.chatwootConfig.whatsappAppId !== 'none'
|
||||
|
||||
@@ -79,6 +79,7 @@ export default {
|
||||
};
|
||||
export const DEFAULT_REDIRECT_URL = '/app/';
|
||||
|
||||
// Temporarily disables WhatsApp embedded signup and WhatsApp Call inbox
|
||||
// creation. Flip to false when the channel is brought back.
|
||||
export const IS_WHATSAPP_INBOX_CREATION_DISABLED = true;
|
||||
// Temporarily disables Instagram and WhatsApp inbox creation
|
||||
// (WhatsApp embedded signup popup, Instagram OAuth, WhatsApp Call).
|
||||
// Flip to false when the channels are brought back.
|
||||
export const IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED = true;
|
||||
|
||||
@@ -263,7 +263,7 @@
|
||||
"EMAIL_VERIFICATION_SENT": "Verification email has been sent. Please check your inbox.",
|
||||
"ACCOUNT_SUSPENDED": {
|
||||
"TITLE": "Account Suspended",
|
||||
"MESSAGE": "Your account is suspended. Please reach out to the support team for more information."
|
||||
"MESSAGE": "Your account has been suspended due to activity that may violate our policies. If you believe this is a mistake, please contact our support team."
|
||||
},
|
||||
"NO_ACCOUNTS": {
|
||||
"TITLE": "No account found",
|
||||
|
||||
+5
-3
@@ -1,5 +1,5 @@
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { IS_WHATSAPP_INBOX_CREATION_DISABLED } from 'dashboard/constants/globals';
|
||||
import { IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED } from 'dashboard/constants/globals';
|
||||
|
||||
// OAuth/SDK channels need installation-level app credentials to be usable. When
|
||||
// the credential is missing the channel is "not configured" and is hidden from
|
||||
@@ -14,12 +14,14 @@ export function useChannelConfig() {
|
||||
// WhatsApp is onboarded only via Meta embedded signup, which needs both the
|
||||
// app id (not the 'none' sentinel) and the signup configuration id.
|
||||
whatsapp: () =>
|
||||
!IS_WHATSAPP_INBOX_CREATION_DISABLED &&
|
||||
!IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED &&
|
||||
Boolean(installationConfig.whatsappAppId) &&
|
||||
installationConfig.whatsappAppId !== 'none' &&
|
||||
Boolean(installationConfig.whatsappConfigurationId),
|
||||
facebook: () => Boolean(installationConfig.fbAppId),
|
||||
instagram: () => Boolean(installationConfig.instagramAppId),
|
||||
instagram: () =>
|
||||
!IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED &&
|
||||
Boolean(installationConfig.instagramAppId),
|
||||
tiktok: () => Boolean(installationConfig.tiktokAppId),
|
||||
gmail: () => Boolean(installationConfig.googleOAuthClientId),
|
||||
outlook: () => Boolean(globalConfig.value.azureAppId),
|
||||
|
||||
+3
-3
@@ -6,11 +6,11 @@ import { useDetectedChannels } from '../../inbox-setup/useDetectedChannels';
|
||||
|
||||
vi.mock('vue-router');
|
||||
|
||||
// Neutralize the temporary WhatsApp kill switch so these specs keep covering
|
||||
// the credential-based gating it short-circuits.
|
||||
// Neutralize the temporary Instagram/WhatsApp kill switch so these specs keep
|
||||
// covering the credential-based gating it currently short-circuits.
|
||||
vi.mock('dashboard/constants/globals', async importOriginal => ({
|
||||
...(await importOriginal()),
|
||||
IS_WHATSAPP_INBOX_CREATION_DISABLED: false,
|
||||
IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED: false,
|
||||
}));
|
||||
|
||||
// Mounts the composable against a real store and the real useAccount (only
|
||||
|
||||
@@ -7,7 +7,7 @@ import ThreeSixtyDialogWhatsapp from './360DialogWhatsapp.vue';
|
||||
import CloudWhatsapp from './CloudWhatsapp.vue';
|
||||
import WhatsappEmbeddedSignup from './WhatsappEmbeddedSignup.vue';
|
||||
import ChannelSelector from 'dashboard/components/ChannelSelector.vue';
|
||||
import { IS_WHATSAPP_INBOX_CREATION_DISABLED } from 'dashboard/constants/globals';
|
||||
import { IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED } from 'dashboard/constants/globals';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -24,7 +24,7 @@ const PROVIDER_TYPES = {
|
||||
|
||||
const hasWhatsappAppId = computed(() => {
|
||||
return (
|
||||
!IS_WHATSAPP_INBOX_CREATION_DISABLED &&
|
||||
!IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED &&
|
||||
window.chatwootConfig?.whatsappAppId &&
|
||||
window.chatwootConfig.whatsappAppId !== 'none'
|
||||
);
|
||||
|
||||
@@ -1,22 +1,51 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import { onMounted } from 'vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
|
||||
const toggleSupportWidgetVisibility = () => {
|
||||
if (window.$chatwoot) {
|
||||
window.$chatwoot.toggleBubbleVisibility('show');
|
||||
}
|
||||
const globalConfig = useMapGetter('globalConfig/get');
|
||||
|
||||
// Hide the CTA entirely when the installation has no support inbox — the widget
|
||||
// SDK never loads there, so window.$chatwoot stays undefined and the button
|
||||
// would be a no-op.
|
||||
const canContactSupport = computed(() =>
|
||||
Boolean(globalConfig.value.chatwootInboxToken)
|
||||
);
|
||||
|
||||
// The widget SDK loads asynchronously. Until it signals `chatwoot:ready` it
|
||||
// can't be opened, and poking it early throws (its bubble DOM isn't mounted yet),
|
||||
// so keep the button disabled with a loader till then.
|
||||
const isWidgetReady = ref(Boolean(window.$chatwoot?.hasLoaded));
|
||||
|
||||
const showSupportBubble = () => {
|
||||
window.$chatwoot?.toggleBubbleVisibility('show');
|
||||
};
|
||||
|
||||
const setupListenerForWidgetEvent = () => {
|
||||
window.addEventListener('chatwoot:on-message', () => {
|
||||
toggleSupportWidgetVisibility();
|
||||
});
|
||||
const toggleSupportWidget = () => {
|
||||
window.$chatwoot?.toggle();
|
||||
};
|
||||
|
||||
const onWidgetReady = () => {
|
||||
isWidgetReady.value = true;
|
||||
showSupportBubble();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
toggleSupportWidgetVisibility();
|
||||
setupListenerForWidgetEvent();
|
||||
// Reveal the support bubble once the widget is ready (immediately if it
|
||||
// already loaded before this view mounted), and keep it revealed whenever a
|
||||
// new support message comes in.
|
||||
if (isWidgetReady.value) {
|
||||
showSupportBubble();
|
||||
} else {
|
||||
window.addEventListener('chatwoot:ready', onWidgetReady);
|
||||
}
|
||||
window.addEventListener('chatwoot:on-message', showSupportBubble);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('chatwoot:ready', onWidgetReady);
|
||||
window.removeEventListener('chatwoot:on-message', showSupportBubble);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -25,6 +54,16 @@ onMounted(() => {
|
||||
<EmptyState
|
||||
:title="$t('APP_GLOBAL.ACCOUNT_SUSPENDED.TITLE')"
|
||||
:message="$t('APP_GLOBAL.ACCOUNT_SUSPENDED.MESSAGE')"
|
||||
/>
|
||||
>
|
||||
<div v-if="canContactSupport" class="flex justify-center">
|
||||
<NextButton
|
||||
icon="i-lucide-life-buoy"
|
||||
:label="$t('SIDEBAR_ITEMS.CONTACT_SUPPORT')"
|
||||
:is-loading="!isWidgetReady"
|
||||
:disabled="!isWidgetReady"
|
||||
@click="toggleSupportWidget"
|
||||
/>
|
||||
</div>
|
||||
</EmptyState>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -43,7 +43,7 @@ RSpec.describe 'Microsoft Authorization API', type: :request do
|
||||
]
|
||||
expect(params['scope']).to eq(expected_scope)
|
||||
expect(params['redirect_uri']).to eq(["#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback"])
|
||||
expect(url).not_to match(/(?:\?|&)prompt=/)
|
||||
expect(params['prompt']).to eq(['select_account'])
|
||||
|
||||
# Validate state parameter exists and can be decoded back to the account
|
||||
expect(params['state']).to be_present
|
||||
|
||||
@@ -47,5 +47,17 @@ RSpec.describe 'Public Inbox Contacts API', type: :request do
|
||||
data = response.parsed_body
|
||||
expect(data['name']).to eq 'John Smith'
|
||||
end
|
||||
|
||||
it 'does not expose internal contact columns' do
|
||||
contact.update!(identifier: 'contact-identifier', custom_attributes: { tier: 'vip' }, additional_attributes: { company_name: 'Acme' })
|
||||
|
||||
patch "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}",
|
||||
params: { name: 'John Smith' }
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data.keys).to include('email', 'id', 'name', 'phone_number', 'pubsub_token', 'source_id')
|
||||
expect(data.keys).not_to include('account_id', 'identifier', 'custom_attributes', 'additional_attributes', 'company_id')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user