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)
|
addressable (>= 2.8.0)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
websocket-driver (0.7.7)
|
websocket-driver (0.8.2)
|
||||||
base64
|
base64
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ class Api::V1::Accounts::Microsoft::AuthorizationsController < Api::V1::Accounts
|
|||||||
{
|
{
|
||||||
redirect_uri: "#{base_url}/microsoft/callback",
|
redirect_uri: "#{base_url}/microsoft/callback",
|
||||||
scope: scope,
|
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
|
if redirect_url
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
|
|||||||
contact: @contact_inbox.contact,
|
contact: @contact_inbox.contact,
|
||||||
params: permitted_params.to_h.deep_symbolize_keys.except(:identifier)
|
params: permitted_params.to_h.deep_symbolize_keys.except(:identifier)
|
||||||
)
|
)
|
||||||
render json: contact_identify_action.perform
|
contact_identify_action.perform
|
||||||
|
@contact_inbox.reload
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ const showChatSupport = computed(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const toggleChatSupport = () => {
|
||||||
|
if (window.$chatwoot) {
|
||||||
|
window.$chatwoot.toggle();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const menuItems = computed(() => {
|
const menuItems = computed(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -51,9 +57,7 @@ const menuItems = computed(() => {
|
|||||||
showOnCustomBrandedInstance: false,
|
showOnCustomBrandedInstance: false,
|
||||||
label: t('SIDEBAR_ITEMS.CONTACT_SUPPORT'),
|
label: t('SIDEBAR_ITEMS.CONTACT_SUPPORT'),
|
||||||
icon: 'i-lucide-life-buoy',
|
icon: 'i-lucide-life-buoy',
|
||||||
click: () => {
|
click: toggleChatSupport,
|
||||||
window.$chatwoot.toggle();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
show: true,
|
show: true,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import ChannelSelector from '../ChannelSelector.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({
|
const props = defineProps({
|
||||||
channel: {
|
channel: {
|
||||||
@@ -21,7 +21,10 @@ const hasFbConfigured = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const hasInstagramConfigured = computed(() => {
|
const hasInstagramConfigured = computed(() => {
|
||||||
return window.chatwootConfig?.instagramAppId;
|
return (
|
||||||
|
!IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED &&
|
||||||
|
window.chatwootConfig?.instagramAppId
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasTiktokConfigured = computed(() => {
|
const hasTiktokConfigured = computed(() => {
|
||||||
@@ -59,7 +62,7 @@ const isActive = computed(() => {
|
|||||||
|
|
||||||
if (key === 'whatsapp_call') {
|
if (key === 'whatsapp_call') {
|
||||||
return (
|
return (
|
||||||
!IS_WHATSAPP_INBOX_CREATION_DISABLED &&
|
!IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED &&
|
||||||
props.enabledFeatures.channel_voice &&
|
props.enabledFeatures.channel_voice &&
|
||||||
!!window.chatwootConfig?.whatsappAppId &&
|
!!window.chatwootConfig?.whatsappAppId &&
|
||||||
window.chatwootConfig.whatsappAppId !== 'none'
|
window.chatwootConfig.whatsappAppId !== 'none'
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export default {
|
|||||||
};
|
};
|
||||||
export const DEFAULT_REDIRECT_URL = '/app/';
|
export const DEFAULT_REDIRECT_URL = '/app/';
|
||||||
|
|
||||||
// Temporarily disables WhatsApp embedded signup and WhatsApp Call inbox
|
// Temporarily disables Instagram and WhatsApp inbox creation
|
||||||
// creation. Flip to false when the channel is brought back.
|
// (WhatsApp embedded signup popup, Instagram OAuth, WhatsApp Call).
|
||||||
export const IS_WHATSAPP_INBOX_CREATION_DISABLED = true;
|
// 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.",
|
"EMAIL_VERIFICATION_SENT": "Verification email has been sent. Please check your inbox.",
|
||||||
"ACCOUNT_SUSPENDED": {
|
"ACCOUNT_SUSPENDED": {
|
||||||
"TITLE": "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": {
|
"NO_ACCOUNTS": {
|
||||||
"TITLE": "No account found",
|
"TITLE": "No account found",
|
||||||
|
|||||||
+5
-3
@@ -1,5 +1,5 @@
|
|||||||
import { useMapGetter } from 'dashboard/composables/store';
|
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
|
// 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
|
// 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
|
// WhatsApp is onboarded only via Meta embedded signup, which needs both the
|
||||||
// app id (not the 'none' sentinel) and the signup configuration id.
|
// app id (not the 'none' sentinel) and the signup configuration id.
|
||||||
whatsapp: () =>
|
whatsapp: () =>
|
||||||
!IS_WHATSAPP_INBOX_CREATION_DISABLED &&
|
!IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED &&
|
||||||
Boolean(installationConfig.whatsappAppId) &&
|
Boolean(installationConfig.whatsappAppId) &&
|
||||||
installationConfig.whatsappAppId !== 'none' &&
|
installationConfig.whatsappAppId !== 'none' &&
|
||||||
Boolean(installationConfig.whatsappConfigurationId),
|
Boolean(installationConfig.whatsappConfigurationId),
|
||||||
facebook: () => Boolean(installationConfig.fbAppId),
|
facebook: () => Boolean(installationConfig.fbAppId),
|
||||||
instagram: () => Boolean(installationConfig.instagramAppId),
|
instagram: () =>
|
||||||
|
!IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED &&
|
||||||
|
Boolean(installationConfig.instagramAppId),
|
||||||
tiktok: () => Boolean(installationConfig.tiktokAppId),
|
tiktok: () => Boolean(installationConfig.tiktokAppId),
|
||||||
gmail: () => Boolean(installationConfig.googleOAuthClientId),
|
gmail: () => Boolean(installationConfig.googleOAuthClientId),
|
||||||
outlook: () => Boolean(globalConfig.value.azureAppId),
|
outlook: () => Boolean(globalConfig.value.azureAppId),
|
||||||
|
|||||||
+3
-3
@@ -6,11 +6,11 @@ import { useDetectedChannels } from '../../inbox-setup/useDetectedChannels';
|
|||||||
|
|
||||||
vi.mock('vue-router');
|
vi.mock('vue-router');
|
||||||
|
|
||||||
// Neutralize the temporary WhatsApp kill switch so these specs keep covering
|
// Neutralize the temporary Instagram/WhatsApp kill switch so these specs keep
|
||||||
// the credential-based gating it short-circuits.
|
// covering the credential-based gating it currently short-circuits.
|
||||||
vi.mock('dashboard/constants/globals', async importOriginal => ({
|
vi.mock('dashboard/constants/globals', async importOriginal => ({
|
||||||
...(await 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
|
// 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 CloudWhatsapp from './CloudWhatsapp.vue';
|
||||||
import WhatsappEmbeddedSignup from './WhatsappEmbeddedSignup.vue';
|
import WhatsappEmbeddedSignup from './WhatsappEmbeddedSignup.vue';
|
||||||
import ChannelSelector from 'dashboard/components/ChannelSelector.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 route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -24,7 +24,7 @@ const PROVIDER_TYPES = {
|
|||||||
|
|
||||||
const hasWhatsappAppId = computed(() => {
|
const hasWhatsappAppId = computed(() => {
|
||||||
return (
|
return (
|
||||||
!IS_WHATSAPP_INBOX_CREATION_DISABLED &&
|
!IS_INSTAGRAM_WHATSAPP_INBOX_CREATION_DISABLED &&
|
||||||
window.chatwootConfig?.whatsappAppId &&
|
window.chatwootConfig?.whatsappAppId &&
|
||||||
window.chatwootConfig.whatsappAppId !== 'none'
|
window.chatwootConfig.whatsappAppId !== 'none'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,22 +1,51 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
||||||
import EmptyState from 'dashboard/components/widgets/EmptyState.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 = () => {
|
const globalConfig = useMapGetter('globalConfig/get');
|
||||||
if (window.$chatwoot) {
|
|
||||||
window.$chatwoot.toggleBubbleVisibility('show');
|
// 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 = () => {
|
const toggleSupportWidget = () => {
|
||||||
window.addEventListener('chatwoot:on-message', () => {
|
window.$chatwoot?.toggle();
|
||||||
toggleSupportWidgetVisibility();
|
};
|
||||||
});
|
|
||||||
|
const onWidgetReady = () => {
|
||||||
|
isWidgetReady.value = true;
|
||||||
|
showSupportBubble();
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
toggleSupportWidgetVisibility();
|
// Reveal the support bubble once the widget is ready (immediately if it
|
||||||
setupListenerForWidgetEvent();
|
// 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>
|
</script>
|
||||||
|
|
||||||
@@ -25,6 +54,16 @@ onMounted(() => {
|
|||||||
<EmptyState
|
<EmptyState
|
||||||
:title="$t('APP_GLOBAL.ACCOUNT_SUSPENDED.TITLE')"
|
:title="$t('APP_GLOBAL.ACCOUNT_SUSPENDED.TITLE')"
|
||||||
:message="$t('APP_GLOBAL.ACCOUNT_SUSPENDED.MESSAGE')"
|
: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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ RSpec.describe 'Microsoft Authorization API', type: :request do
|
|||||||
]
|
]
|
||||||
expect(params['scope']).to eq(expected_scope)
|
expect(params['scope']).to eq(expected_scope)
|
||||||
expect(params['redirect_uri']).to eq(["#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback"])
|
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
|
# Validate state parameter exists and can be decoded back to the account
|
||||||
expect(params['state']).to be_present
|
expect(params['state']).to be_present
|
||||||
|
|||||||
@@ -47,5 +47,17 @@ RSpec.describe 'Public Inbox Contacts API', type: :request do
|
|||||||
data = response.parsed_body
|
data = response.parsed_body
|
||||||
expect(data['name']).to eq 'John Smith'
|
expect(data['name']).to eq 'John Smith'
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user