-
+ class="bg-n-alpha-3 rounded-full size-12 p-2 ring ring-n-solid-1 border border-n-strong shadow-sm"
+ >
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue
index 420d7b7b9..609fd3fd4 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue
@@ -3,6 +3,7 @@ import { mapGetters } from 'vuex';
import { shouldBeUrl } from 'shared/helpers/Validators';
import { useAlert } from 'dashboard/composables';
import { useVuelidate } from '@vuelidate/core';
+import Avatar from 'next/avatar/Avatar.vue';
import SettingIntroBanner from 'dashboard/components/widgets/SettingIntroBanner.vue';
import SettingsSection from '../../../../components/SettingsSection.vue';
import inboxMixin from 'shared/mixins/inboxMixin';
@@ -11,6 +12,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';
@@ -24,6 +26,7 @@ import SenderNameExamplePreview from './components/SenderNameExamplePreview.vue'
import NextButton from 'dashboard/components-next/button/Button.vue';
import { INBOX_TYPES } from 'dashboard/helper/inbox';
import { WIDGET_BUILDER_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
+import { getInboxIconByType } from 'dashboard/helper/inbox';
import Editor from 'dashboard/components-next/Editor/Editor.vue';
export default {
@@ -44,8 +47,10 @@ export default {
GoogleReauthorize,
NextButton,
InstagramReauthorize,
+ WhatsappReauthorize,
DuplicateInboxBanner,
Editor,
+ Avatar,
},
mixins: [inboxMixin],
setup() {
@@ -87,10 +92,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) {
@@ -114,16 +116,22 @@ export default {
key: 'collaborators',
name: this.$t('INBOX_MGMT.TABS.COLLABORATORS'),
},
- {
- key: 'businesshours',
- name: this.$t('INBOX_MGMT.TABS.BUSINESS_HOURS'),
- },
- {
- key: 'csat',
- name: this.$t('INBOX_MGMT.TABS.CSAT'),
- },
];
+ if (!this.isAVoiceChannel) {
+ visibleToAllChannelTabs = [
+ ...visibleToAllChannelTabs,
+ {
+ key: 'businesshours',
+ name: this.$t('INBOX_MGMT.TABS.BUSINESS_HOURS'),
+ },
+ {
+ key: 'csat',
+ name: this.$t('INBOX_MGMT.TABS.CSAT'),
+ },
+ ];
+ }
+
if (this.isAWebWidgetInbox) {
visibleToAllChannelTabs = [
...visibleToAllChannelTabs,
@@ -142,6 +150,7 @@ export default {
this.isATwilioChannel ||
this.isALineChannel ||
this.isAPIInbox ||
+ this.isAVoiceChannel ||
(this.isAnEmailChannel && !this.inbox.provider) ||
this.shouldShowWhatsAppConfiguration ||
this.isAWebWidgetInbox
@@ -174,7 +183,10 @@ export default {
inbox() {
return this.$store.getters['inboxes/getInbox'](this.currentInboxId);
},
-
+ inboxIcon() {
+ const { medium, channel_type: type } = this.inbox;
+ return getInboxIconByType(type, medium);
+ },
inboxName() {
if (this.isATwilioSMSChannel || this.isATwilioWhatsAppChannel) {
return `${this.inbox.name} (${
@@ -247,6 +259,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) {
@@ -276,8 +296,17 @@ export default {
}
return [...selected, current];
},
+ refreshAvatarUrlOnTabChange(index) {
+ // Refresh avatar URL on tab change from inbox_settings and widgetBuilder tabs, to ensure real-time updates
+ if (
+ this.inbox &&
+ ['inbox_settings', 'widgetBuilder'].includes(this.tabs[index].key)
+ )
+ this.avatarUrl = this.inbox.avatar_url;
+ },
onTabChange(selectedTabIndex) {
this.selectedTabIndex = selectedTabIndex;
+ this.refreshAvatarUrlOnTabChange(selectedTabIndex);
},
fetchInboxSettings() {
this.selectedTabIndex = 0;
@@ -416,6 +445,7 @@ export default {
+
-
+
+
+
+
-
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/WidgetBuilder.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/WidgetBuilder.vue
index 08ad90a92..08f536185 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/WidgetBuilder.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/WidgetBuilder.vue
@@ -9,6 +9,7 @@ import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
import { WIDGET_BUILDER_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
import NextButton from 'dashboard/components-next/button/Button.vue';
+import Avatar from 'next/avatar/Avatar.vue';
import Editor from 'dashboard/components-next/Editor/Editor.vue';
export default {
@@ -17,6 +18,7 @@ export default {
InputRadioGroup,
NextButton,
Editor,
+ Avatar,
},
props: {
inbox: {
@@ -276,15 +278,23 @@ export default {
@@ -249,10 +242,7 @@ export default {
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Instagram.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Instagram.vue
index bcdeafcc9..4577265a5 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Instagram.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Instagram.vue
@@ -1,11 +1,9 @@
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/whatsapp/utils.js b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/whatsapp/utils.js
new file mode 100644
index 000000000..9a0eef9b0
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/whatsapp/utils.js
@@ -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: 'whatsapp_business_app_onboarding',
+ sessionInfoVersion: '3',
+ },
+ }
+ );
+ });
+};
+
+export const setupFacebookSdk = async (appId, apiVersion) => {
+ const version = apiVersion || 'v22.0';
+ await loadFacebookSdk();
+ await initializeFacebook(appId, version);
+};
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/SenderNameExamplePreview.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/SenderNameExamplePreview.vue
index 5c08b143b..6e5d2c251 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/SenderNameExamplePreview.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/SenderNameExamplePreview.vue
@@ -1,11 +1,11 @@
diff --git a/app/javascript/dashboard/store/modules/conversations/helpers/filterHelpers.js b/app/javascript/dashboard/store/modules/conversations/helpers/filterHelpers.js
index d64d3c56b..3f1c32059 100644
--- a/app/javascript/dashboard/store/modules/conversations/helpers/filterHelpers.js
+++ b/app/javascript/dashboard/store/modules/conversations/helpers/filterHelpers.js
@@ -167,7 +167,14 @@ const contains = (filterValue, conversationValue) => {
*/
const compareDates = (conversationValue, filterValue, compareFn) => {
const conversationDate = coerceToDate(conversationValue);
- const filterDate = coerceToDate(filterValue);
+
+ // In saved views, the filterValue might be returned as an Array
+ // In conversation list, when filtering, the filterValue will be returned as a string
+ const valueToCompare = Array.isArray(filterValue)
+ ? filterValue[0]
+ : filterValue;
+ const filterDate = coerceToDate(valueToCompare);
+
if (conversationDate === null || filterDate === null) return false;
return compareFn(conversationDate, filterDate);
};
diff --git a/app/javascript/dashboard/store/modules/conversations/helpers/specs/filterHelpers.spec.js b/app/javascript/dashboard/store/modules/conversations/helpers/specs/filterHelpers.spec.js
index 0c9e6a5c0..b36128819 100644
--- a/app/javascript/dashboard/store/modules/conversations/helpers/specs/filterHelpers.spec.js
+++ b/app/javascript/dashboard/store/modules/conversations/helpers/specs/filterHelpers.spec.js
@@ -602,6 +602,59 @@ describe('filterHelpers', () => {
expect(matchesFilters(conversation, filters)).toBe(false);
});
+ // Test for array filter values (saved views)
+ it('should handle array filter values for date comparison in saved views', () => {
+ const conversation = { created_at: 1647777600000 }; // March 20, 2022
+ const filters = [
+ {
+ attribute_key: 'created_at',
+ filter_operator: 'is_greater_than',
+ values: ['2022-03-19'], // Array format from saved views
+ query_operator: 'and',
+ },
+ ];
+ expect(matchesFilters(conversation, filters)).toBe(true);
+ });
+
+ it('should handle array filter values with is_less_than operator', () => {
+ const conversation = { created_at: 1647777600000 }; // March 20, 2022
+ const filters = [
+ {
+ attribute_key: 'created_at',
+ filter_operator: 'is_less_than',
+ values: ['2022-03-21'], // Array format from saved views
+ query_operator: 'and',
+ },
+ ];
+ expect(matchesFilters(conversation, filters)).toBe(true);
+ });
+
+ it('should handle array filter values with timestamp', () => {
+ const conversation = { created_at: 1647777600000 }; // March 20, 2022
+ const filters = [
+ {
+ attribute_key: 'created_at',
+ filter_operator: 'is_greater_than',
+ values: [1647691200], // March 19, 2022 as array (in seconds)
+ query_operator: 'and',
+ },
+ ];
+ expect(matchesFilters(conversation, filters)).toBe(true);
+ });
+
+ it('should handle empty array filter values', () => {
+ const conversation = { created_at: 1647777600000 }; // March 20, 2022
+ const filters = [
+ {
+ attribute_key: 'created_at',
+ filter_operator: 'is_greater_than',
+ values: [], // Empty array
+ query_operator: 'and',
+ },
+ ];
+ expect(matchesFilters(conversation, filters)).toBe(false);
+ });
+
it('should handle non-date string values in date comparison', () => {
const conversation = { created_at: 1647777600000 }; // March 20, 2022
const filters = [
@@ -721,6 +774,19 @@ describe('filterHelpers', () => {
];
expect(matchesFilters(conversation, filters)).toBe(true);
});
+
+ it('should handle array values for days_before operator', () => {
+ const conversation = { created_at: 1647777600000 }; // March 20, 2022
+ const filters = [
+ {
+ attribute_key: 'created_at',
+ filter_operator: 'days_before',
+ values: ['3'], // Array format from saved views
+ query_operator: 'and',
+ },
+ ];
+ expect(matchesFilters(conversation, filters)).toBe(true);
+ });
});
// Multiple filters tests
diff --git a/app/javascript/dashboard/store/modules/inboxes.js b/app/javascript/dashboard/store/modules/inboxes.js
index a6b93c923..c4789a7a9 100644
--- a/app/javascript/dashboard/store/modules/inboxes.js
+++ b/app/javascript/dashboard/store/modules/inboxes.js
@@ -44,15 +44,52 @@ export const getters = {
const messagesTemplates =
whatsAppMessageTemplates || apiInboxMessageTemplates;
- // filtering out the whatsapp templates with media
- if (messagesTemplates instanceof Array) {
- return messagesTemplates.filter(template => {
- return !template.components.some(
- i => i.format === 'IMAGE' || i.format === 'VIDEO'
- );
- });
+ return messagesTemplates;
+ },
+ getFilteredWhatsAppTemplates: $state => inboxId => {
+ const [inbox] = $state.records.filter(
+ record => record.id === Number(inboxId)
+ );
+
+ const {
+ message_templates: whatsAppMessageTemplates,
+ additional_attributes: additionalAttributes,
+ } = inbox || {};
+
+ const { message_templates: apiInboxMessageTemplates } =
+ additionalAttributes || {};
+ const templates = whatsAppMessageTemplates || apiInboxMessageTemplates;
+
+ if (!templates || !Array.isArray(templates)) {
+ return [];
}
- return [];
+
+ return templates.filter(template => {
+ // Ensure template has required properties
+ if (!template || !template.status || !template.components) {
+ return false;
+ }
+
+ // Only show approved templates
+ if (template.status.toLowerCase() !== 'approved') {
+ return false;
+ }
+
+ // Filter out interactive templates (LIST, PRODUCT, CATALOG), location templates, and call permission templates
+ const hasUnsupportedComponents = template.components.some(
+ component =>
+ ['LIST', 'PRODUCT', 'CATALOG', 'CALL_PERMISSION_REQUEST'].includes(
+ component.type
+ ) ||
+ (component.type === 'HEADER' && component.format === 'LOCATION')
+ );
+
+ if (hasUnsupportedComponents) {
+ return false;
+ }
+
+ return true;
+ });
},
getNewConversationInboxes($state) {
return $state.records.filter(inbox => {
diff --git a/app/javascript/dashboard/store/modules/specs/inboxes/getters.spec.js b/app/javascript/dashboard/store/modules/specs/inboxes/getters.spec.js
index f9ed57d63..eeb52b1dc 100644
--- a/app/javascript/dashboard/store/modules/specs/inboxes/getters.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/inboxes/getters.spec.js
@@ -1,5 +1,6 @@
import { getters } from '../../inboxes';
import inboxList from './fixtures';
+import { templates } from './templateFixtures';
describe('#getters', () => {
it('getInboxes', () => {
@@ -93,4 +94,269 @@ describe('#getters', () => {
provider: 'default',
});
});
+
+ describe('getFilteredWhatsAppTemplates', () => {
+ it('returns empty array when inbox not found', () => {
+ const state = { records: [] };
+ expect(getters.getFilteredWhatsAppTemplates(state)(999)).toEqual([]);
+ });
+
+ it('returns empty array when templates is null or undefined', () => {
+ const state = {
+ records: [
+ {
+ id: 1,
+ channel_type: 'Channel::Whatsapp',
+ message_templates: null,
+ additional_attributes: { message_templates: undefined },
+ },
+ ],
+ };
+ expect(getters.getFilteredWhatsAppTemplates(state)(1)).toEqual([]);
+ });
+
+ it('returns empty array when templates is not an array', () => {
+ const state = {
+ records: [
+ {
+ id: 1,
+ channel_type: 'Channel::Whatsapp',
+ message_templates: 'invalid',
+ additional_attributes: {},
+ },
+ ],
+ };
+ expect(getters.getFilteredWhatsAppTemplates(state)(1)).toEqual([]);
+ });
+
+ it('filters out templates without required properties', () => {
+ const invalidTemplates = [
+ { name: 'incomplete_template' }, // missing status and components
+ { status: 'approved' }, // missing name and components
+ { name: 'another_incomplete', status: 'approved' }, // missing components
+ ];
+
+ const state = {
+ records: [
+ {
+ id: 1,
+ channel_type: 'Channel::Whatsapp',
+ message_templates: invalidTemplates,
+ },
+ ],
+ };
+ expect(getters.getFilteredWhatsAppTemplates(state)(1)).toEqual([]);
+ });
+
+ it('filters out non-approved templates', () => {
+ const mixedStatusTemplates = [
+ {
+ name: 'pending_template',
+ status: 'pending',
+ components: [{ type: 'BODY', text: 'Test' }],
+ },
+ {
+ name: 'rejected_template',
+ status: 'rejected',
+ components: [{ type: 'BODY', text: 'Test' }],
+ },
+ {
+ name: 'approved_template',
+ status: 'approved',
+ components: [{ type: 'BODY', text: 'Test' }],
+ },
+ ];
+
+ const state = {
+ records: [
+ {
+ id: 1,
+ channel_type: 'Channel::Whatsapp',
+ message_templates: mixedStatusTemplates,
+ },
+ ],
+ };
+
+ const result = getters.getFilteredWhatsAppTemplates(state)(1);
+ expect(result).toHaveLength(1);
+ expect(result[0].name).toBe('approved_template');
+ });
+
+ it('filters out interactive templates (LIST, PRODUCT, CATALOG)', () => {
+ const interactiveTemplates = [
+ {
+ name: 'list_template',
+ status: 'approved',
+ components: [
+ { type: 'BODY', text: 'Choose an option' },
+ { type: 'LIST', sections: [] },
+ ],
+ },
+ {
+ name: 'product_template',
+ status: 'approved',
+ components: [
+ { type: 'BODY', text: 'Product info' },
+ { type: 'PRODUCT', catalog_id: '123' },
+ ],
+ },
+ {
+ name: 'catalog_template',
+ status: 'approved',
+ components: [
+ { type: 'BODY', text: 'Catalog' },
+ { type: 'CATALOG', thumbnail_product_retailer_id: '123' },
+ ],
+ },
+ {
+ name: 'regular_template',
+ status: 'approved',
+ components: [{ type: 'BODY', text: 'Regular message' }],
+ },
+ ];
+
+ const state = {
+ records: [
+ {
+ id: 1,
+ channel_type: 'Channel::Whatsapp',
+ message_templates: interactiveTemplates,
+ },
+ ],
+ };
+
+ const result = getters.getFilteredWhatsAppTemplates(state)(1);
+ expect(result).toHaveLength(1);
+ expect(result[0].name).toBe('regular_template');
+ });
+
+ it('filters out location templates', () => {
+ const locationTemplates = [
+ {
+ name: 'location_template',
+ status: 'approved',
+ components: [
+ { type: 'HEADER', format: 'LOCATION' },
+ { type: 'BODY', text: 'Location message' },
+ ],
+ },
+ {
+ name: 'regular_template',
+ status: 'approved',
+ components: [
+ { type: 'HEADER', format: 'TEXT', text: 'Header' },
+ { type: 'BODY', text: 'Regular message' },
+ ],
+ },
+ ];
+
+ const state = {
+ records: [
+ {
+ id: 1,
+ channel_type: 'Channel::Whatsapp',
+ message_templates: locationTemplates,
+ },
+ ],
+ };
+
+ const result = getters.getFilteredWhatsAppTemplates(state)(1);
+ expect(result).toHaveLength(1);
+ expect(result[0].name).toBe('regular_template');
+ });
+
+ it('returns valid templates from fixture data', () => {
+ const state = {
+ records: [
+ {
+ id: 1,
+ channel_type: 'Channel::Whatsapp',
+ message_templates: templates,
+ },
+ ],
+ };
+
+ const result = getters.getFilteredWhatsAppTemplates(state)(1);
+
+ // All templates in fixtures should be approved and valid
+ expect(result.length).toBeGreaterThan(0);
+
+ // Verify all returned templates are approved
+ result.forEach(template => {
+ expect(template.status).toBe('approved');
+ expect(template.components).toBeDefined();
+ expect(Array.isArray(template.components)).toBe(true);
+ });
+
+ // Verify specific templates from fixtures are included
+ const templateNames = result.map(t => t.name);
+ expect(templateNames).toContain('sample_flight_confirmation');
+ expect(templateNames).toContain('sample_issue_resolution');
+ expect(templateNames).toContain('sample_shipping_confirmation');
+ expect(templateNames).toContain('no_variable_template');
+ expect(templateNames).toContain('order_confirmation');
+ });
+
+ it('prioritizes message_templates over additional_attributes.message_templates', () => {
+ const primaryTemplates = [
+ {
+ name: 'primary_template',
+ status: 'approved',
+ components: [{ type: 'BODY', text: 'Primary' }],
+ },
+ ];
+
+ const fallbackTemplates = [
+ {
+ name: 'fallback_template',
+ status: 'approved',
+ components: [{ type: 'BODY', text: 'Fallback' }],
+ },
+ ];
+
+ const state = {
+ records: [
+ {
+ id: 1,
+ channel_type: 'Channel::Whatsapp',
+ message_templates: primaryTemplates,
+ additional_attributes: {
+ message_templates: fallbackTemplates,
+ },
+ },
+ ],
+ };
+
+ const result = getters.getFilteredWhatsAppTemplates(state)(1);
+ expect(result).toHaveLength(1);
+ expect(result[0].name).toBe('primary_template');
+ });
+
+ it('falls back to additional_attributes.message_templates when message_templates is null', () => {
+ const fallbackTemplates = [
+ {
+ name: 'fallback_template',
+ status: 'approved',
+ components: [{ type: 'BODY', text: 'Fallback' }],
+ },
+ ];
+
+ const state = {
+ records: [
+ {
+ id: 1,
+ channel_type: 'Channel::Whatsapp',
+ message_templates: null,
+ additional_attributes: {
+ message_templates: fallbackTemplates,
+ },
+ },
+ ],
+ };
+
+ const result = getters.getFilteredWhatsAppTemplates(state)(1);
+ expect(result).toHaveLength(1);
+ expect(result[0].name).toBe('fallback_template');
+ });
+ });
});
diff --git a/app/javascript/shared/mixins/specs/whatsappTemplates/fixtures.js b/app/javascript/dashboard/store/modules/specs/inboxes/templateFixtures.js
similarity index 50%
rename from app/javascript/shared/mixins/specs/whatsappTemplates/fixtures.js
rename to app/javascript/dashboard/store/modules/specs/inboxes/templateFixtures.js
index 02c24b2bb..c4c6a0b40 100644
--- a/app/javascript/shared/mixins/specs/whatsappTemplates/fixtures.js
+++ b/app/javascript/dashboard/store/modules/specs/inboxes/templateFixtures.js
@@ -260,4 +260,285 @@ export const templates = [
],
rejected_reason: 'NONE',
},
+ {
+ name: 'order_confirmation',
+ status: 'approved',
+ category: 'TICKET_UPDATE',
+ language: 'en_US',
+ namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
+ components: [
+ {
+ type: 'HEADER',
+ format: 'IMAGE',
+ example: {
+ header_handle: ['https://example.com/shoes.jpg'],
+ },
+ },
+ {
+ text: 'Hi your order {{1}} is confirmed. Please wait for further updates',
+ type: 'BODY',
+ },
+ ],
+ rejected_reason: 'NONE',
+ },
+ {
+ name: 'technician_visit',
+ status: 'approved',
+ category: 'UTILITY',
+ language: 'en_US',
+ namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
+ components: [
+ {
+ text: 'Technician visit',
+ type: 'HEADER',
+ format: 'TEXT',
+ },
+ {
+ text: "Hi {{1}}, we're scheduling a technician visit to {{2}} on {{3}} between {{4}} and {{5}}. Please confirm if this time slot works for you.",
+ type: 'BODY',
+ },
+ {
+ type: 'BUTTONS',
+ buttons: [
+ {
+ text: 'Confirm',
+ type: 'QUICK_REPLY',
+ },
+ {
+ text: 'Reschedule',
+ type: 'QUICK_REPLY',
+ },
+ ],
+ },
+ ],
+ rejected_reason: 'NONE',
+ },
+ {
+ name: 'event_invitation_static',
+ status: 'approved',
+ category: 'MARKETING',
+ language: 'en',
+ namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
+ components: [
+ {
+ text: "You're invited to {{event_name}} at {{location}}, Join us for an amazing experience!",
+ type: 'BODY',
+ },
+ {
+ type: 'BUTTONS',
+ buttons: [
+ {
+ url: 'https://events.example.com/register',
+ text: 'Visit website',
+ type: 'URL',
+ },
+ {
+ url: 'https://maps.app.goo.gl/YoWAzRj1GDuxs6qz8',
+ text: 'Get Directions',
+ type: 'URL',
+ },
+ ],
+ },
+ ],
+ rejected_reason: 'NONE',
+ },
+ {
+ name: 'purchase_receipt',
+ status: 'approved',
+ category: 'UTILITY',
+ language: 'en_US',
+ namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
+ components: [
+ {
+ type: 'HEADER',
+ format: 'DOCUMENT',
+ },
+ {
+ text: 'Thank you for using your {{1}} card at {{2}}. Your {{3}} is attached as a PDF.',
+ type: 'BODY',
+ },
+ ],
+ rejected_reason: 'NONE',
+ },
+ {
+ name: 'discount_coupon',
+ status: 'approved',
+ category: 'MARKETING',
+ language: 'en',
+ namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
+ components: [
+ {
+ text: '🎉 Special offer for you! Get {{discount_percentage}}% off your next purchase. Use the code below at checkout',
+ type: 'BODY',
+ },
+ {
+ type: 'BUTTONS',
+ buttons: [
+ {
+ text: 'Copy offer code',
+ type: 'COPY_CODE',
+ },
+ ],
+ },
+ ],
+ rejected_reason: 'NONE',
+ },
+ {
+ name: 'support_callback',
+ status: 'approved',
+ category: 'UTILITY',
+ language: 'en',
+ namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
+ components: [
+ {
+ text: 'Hello {{name}}, our support team will call you regarding ticket # {{ticket_id}}.',
+ type: 'BODY',
+ },
+ {
+ type: 'BUTTONS',
+ buttons: [
+ {
+ text: 'Call Support',
+ type: 'PHONE_NUMBER',
+ phone_number: '+16506677566',
+ },
+ ],
+ },
+ ],
+ rejected_reason: 'NONE',
+ },
+ {
+ name: 'training_video',
+ status: 'approved',
+ category: 'MARKETING',
+ language: 'en',
+ namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
+ components: [
+ {
+ type: 'HEADER',
+ format: 'VIDEO',
+ },
+ {
+ text: "Hi {{name}}, here's your training video. Please watch by{{date}}.",
+ type: 'BODY',
+ },
+ ],
+ rejected_reason: 'NONE',
+ },
+ {
+ name: 'product_launch',
+ status: 'approved',
+ category: 'MARKETING',
+ language: 'en',
+ namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
+ components: [
+ {
+ type: 'HEADER',
+ format: 'IMAGE',
+ },
+ {
+ text: 'New arrival! Our stunning coat now available in {{color}} color.',
+ type: 'BODY',
+ },
+ {
+ text: 'Free shipping on orders over $100. Limited time offer.',
+ type: 'FOOTER',
+ },
+ ],
+ rejected_reason: 'NONE',
+ },
+ {
+ name: 'greet',
+ status: 'approved',
+ category: 'MARKETING',
+ language: 'en',
+ namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
+ components: [
+ {
+ text: 'Hey {{customer_name}} how may I help you?',
+ type: 'BODY',
+ },
+ ],
+ rejected_reason: 'NONE',
+ },
+ {
+ name: 'hello_world',
+ status: 'approved',
+ category: 'UTILITY',
+ language: 'en_US',
+ namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
+ components: [
+ {
+ text: 'Hello World',
+ type: 'HEADER',
+ format: 'TEXT',
+ },
+ {
+ text: 'Welcome and congratulations!! This message demonstrates your ability to send a WhatsApp message notification from the Cloud API, hosted by Meta. Thank you for taking the time to test with us.',
+ type: 'BODY',
+ },
+ {
+ text: 'WhatsApp Business Platform sample message',
+ type: 'FOOTER',
+ },
+ ],
+ rejected_reason: 'NONE',
+ },
+ {
+ name: 'feedback_request',
+ status: 'approved',
+ category: 'MARKETING',
+ language: 'en',
+ namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
+ components: [
+ {
+ text: "Hey {{name}}, how was your experience with Puma? We'd love your feedback!",
+ type: 'BODY',
+ },
+ {
+ type: 'BUTTONS',
+ buttons: [
+ {
+ url: 'https://feedback.example.com/survey',
+ text: 'Leave Feedback',
+ type: 'URL',
+ },
+ ],
+ },
+ ],
+ rejected_reason: 'NONE',
+ },
+ {
+ name: 'address_update',
+ status: 'approved',
+ category: 'UTILITY',
+ language: 'en_US',
+ namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
+ components: [
+ {
+ text: 'Address update',
+ type: 'HEADER',
+ format: 'TEXT',
+ },
+ {
+ text: 'Hi {{1}}, your delivery address has been successfully updated to {{2}}. Contact {{3}} for any inquiries.',
+ type: 'BODY',
+ },
+ ],
+ rejected_reason: 'NONE',
+ },
+ {
+ name: 'delivery_confirmation',
+ status: 'approved',
+ category: 'UTILITY',
+ language: 'en_US',
+ namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
+ components: [
+ {
+ text: '{{1}}, your order was successfully delivered on {{2}}.\n\nThank you for your purchase.\n',
+ type: 'BODY',
+ },
+ ],
+ rejected_reason: 'NONE',
+ },
];
diff --git a/app/javascript/histoire.setup.ts b/app/javascript/histoire.setup.ts
index 7642da78d..1c80a9f85 100644
--- a/app/javascript/histoire.setup.ts
+++ b/app/javascript/histoire.setup.ts
@@ -1,6 +1,7 @@
import './design-system/histoire.scss';
import { defineSetupVue3 } from '@histoire/plugin-vue';
-import i18nMessages from 'dashboard/i18n';
+import dashboardI18n from 'dashboard/i18n';
+import widgetI18n from 'widget/i18n';
import { createI18n } from 'vue-i18n';
import { vResizeObserver } from '@vueuse/components';
import store from 'dashboard/store';
@@ -9,10 +10,30 @@ import VueDOMPurifyHTML from 'vue-dompurify-html';
import { domPurifyConfig } from 'shared/helpers/HTMLSanitizer.js';
import { directive as onClickaway } from 'vue3-click-away';
+function mergeMessages(...sources) {
+ return sources.reduce((acc, src) => {
+ Object.keys(src).forEach(key => {
+ if (
+ acc[key] &&
+ typeof acc[key] === 'object' &&
+ typeof src[key] === 'object'
+ ) {
+ acc[key] = mergeMessages(acc[key], src[key]);
+ } else {
+ acc[key] = src[key];
+ }
+ });
+ return acc;
+ }, {});
+}
+
const i18n = createI18n({
legacy: false, // https://github.com/intlify/vue-i18n/issues/1902
locale: 'en',
- messages: i18nMessages,
+ messages: mergeMessages(
+ structuredClone(dashboardI18n),
+ structuredClone(widgetI18n)
+ ),
});
export const setupVue3 = defineSetupVue3(({ app }) => {
diff --git a/app/javascript/shared/components/Branding.vue b/app/javascript/shared/components/Branding.vue
index cc1ae385d..9ca976d80 100644
--- a/app/javascript/shared/components/Branding.vue
+++ b/app/javascript/shared/components/Branding.vue
@@ -1,5 +1,5 @@
+
+
+
+
+
+ {{ headerText }}
+
+
+
+
+
+
+
+
diff --git a/app/javascript/widget/components/Availability/AvailabilityText.story.vue b/app/javascript/widget/components/Availability/AvailabilityText.story.vue
new file mode 100644
index 000000000..aafcb3556
--- /dev/null
+++ b/app/javascript/widget/components/Availability/AvailabilityText.story.vue
@@ -0,0 +1,217 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/widget/components/Availability/AvailabilityText.vue b/app/javascript/widget/components/Availability/AvailabilityText.vue
new file mode 100644
index 000000000..649224d20
--- /dev/null
+++ b/app/javascript/widget/components/Availability/AvailabilityText.vue
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+ {{ replyTimeMessage }}
+
+
+
+
+ {{
+ isOnline
+ ? replyTimeMessage
+ : t('TEAM_AVAILABILITY.BACK_AS_SOON_AS_POSSIBLE')
+ }}
+
+
+
+
+
+ {{ t('TEAM_AVAILABILITY.BACK_AS_SOON_AS_POSSIBLE') }}
+
+
+
+
+ {{ t('REPLY_TIME.BACK_IN_SOME_TIME') }}
+
+
+
+
+ {{ t('REPLY_TIME.BACK_TOMORROW') }}
+
+
+
+
+ {{
+ t('REPLY_TIME.BACK_ON_DAY', {
+ day: dayNames[nextSlot.config.dayOfWeek],
+ })
+ }}
+
+
+
+
+ {{
+ t('REPLY_TIME.BACK_IN_MINUTES', {
+ time: `${roundedMinutesUntilOpen}`,
+ })
+ }}
+
+
+
+
+ {{ t('REPLY_TIME.BACK_IN_HOURS', adjustedHoursUntilOpen) }}
+
+
+
+
+ {{
+ t('REPLY_TIME.BACK_AT_TIME', {
+ time: formattedOpeningTime,
+ })
+ }}
+
+
+
diff --git a/app/javascript/widget/components/ChatFooter.vue b/app/javascript/widget/components/ChatFooter.vue
index 841b37216..c85727a2b 100755
--- a/app/javascript/widget/components/ChatFooter.vue
+++ b/app/javascript/widget/components/ChatFooter.vue
@@ -6,7 +6,7 @@ import FooterReplyTo from 'widget/components/FooterReplyTo.vue';
import ChatInputWrap from 'widget/components/ChatInputWrap.vue';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { sendEmailTranscript } from 'widget/api/conversation';
-import routerMixin from 'widget/mixins/routerMixin';
+import { useRouter } from 'vue-router';
import { IFrameHelper } from '../helpers/utils';
import { CHATWOOT_ON_START_CONVERSATION } from '../constants/sdkEvents';
import { emitter } from 'shared/helpers/mitt';
@@ -17,7 +17,10 @@ export default {
CustomButton,
FooterReplyTo,
},
- mixins: [routerMixin],
+ setup() {
+ const router = useRouter();
+ return { router };
+ },
data() {
return {
inReplyTo: null,
@@ -55,15 +58,8 @@ export default {
emitter.on(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.toggleReplyTo);
},
methods: {
- ...mapActions('conversation', [
- 'sendMessage',
- 'sendAttachment',
- 'clearConversations',
- ]),
- ...mapActions('conversationAttributes', [
- 'getAttributes',
- 'clearConversationAttributes',
- ]),
+ ...mapActions('conversation', ['sendMessage', 'sendAttachment']),
+ ...mapActions('conversationAttributes', ['getAttributes']),
async handleSendMessage(content) {
await this.sendMessage({
content,
@@ -84,9 +80,7 @@ export default {
this.inReplyTo = null;
},
startNewConversation() {
- this.clearConversations();
- this.clearConversationAttributes();
- this.replaceRoute('prechat-form');
+ this.router.replace({ name: 'prechat-form' });
IFrameHelper.sendMessage({
event: 'onEvent',
eventIdentifier: CHATWOOT_ON_START_CONVERSATION,
diff --git a/app/javascript/widget/components/ChatHeader.vue b/app/javascript/widget/components/ChatHeader.vue
index 7c497b70f..578fb984e 100644
--- a/app/javascript/widget/components/ChatHeader.vue
+++ b/app/javascript/widget/components/ChatHeader.vue
@@ -1,55 +1,26 @@
-
@@ -79,9 +50,12 @@ export default {
${isOnline ? 'bg-n-teal-10' : 'hidden'}`"
/>
-
- {{ replyWaitMessage }}
-
+
diff --git a/app/javascript/widget/components/GroupedAvatars.vue b/app/javascript/widget/components/GroupedAvatars.vue
index 42e133478..14d375a03 100644
--- a/app/javascript/widget/components/GroupedAvatars.vue
+++ b/app/javascript/widget/components/GroupedAvatars.vue
@@ -1,5 +1,5 @@
@@ -76,17 +29,8 @@ export default {
-
-
-
- {{ headerMessage }}
-
-
- {{ replyWaitMessage }}
-
-
-
-
+
+
|