From a040aee96bfde336499b4ba05d770f091e353f05 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 4 Apr 2023 08:57:55 +0530 Subject: [PATCH] feat: allow adding custom attributes to conversations from the SDK (#6782) * feat: add conversation attributes method to sdk and widget app * feat: add endpoints to update custom attributes * refactor: update SDK api * feat: add api and actions for conversation updates * fix: error message * test: custom attributes on conversations controller --------- Co-authored-by: Muhsin Keloth --- .../api/v1/widget/conversations_controller.rb | 10 ++++ app/javascript/packs/sdk.js | 20 +++++++ app/javascript/widget/App.vue | 10 ++++ app/javascript/widget/api/conversation.js | 20 +++++++ .../store/modules/conversation/actions.js | 18 ++++++ config/routes.rb | 2 + .../widget/conversations_controller_spec.rb | 55 +++++++++++++++++++ 7 files changed, 135 insertions(+) diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb index 9b8c4da81..2c72a0361 100644 --- a/app/controllers/api/v1/widget/conversations_controller.rb +++ b/app/controllers/api/v1/widget/conversations_controller.rb @@ -65,6 +65,16 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController head :ok end + def set_custom_attributes + conversation.update!(custom_attributes: permitted_params[:custom_attributes]) + end + + def destroy_custom_attributes + conversation.custom_attributes = conversation.custom_attributes.excluding(params[:custom_attribute]) + conversation.save! + render json: conversation + end + private def trigger_typing_event(event) diff --git a/app/javascript/packs/sdk.js b/app/javascript/packs/sdk.js index c5d98f65d..80cbb6de0 100755 --- a/app/javascript/packs/sdk.js +++ b/app/javascript/packs/sdk.js @@ -114,6 +114,26 @@ const runSDK = ({ baseUrl, websiteToken }) => { } }, + setConversationCustomAttributes(customAttributes = {}) { + if (!customAttributes || !Object.keys(customAttributes).length) { + throw new Error('Custom attributes should have atleast one key'); + } else { + IFrameHelper.sendMessage('set-conversation-custom-attributes', { + customAttributes, + }); + } + }, + + deleteConversationCustomAttribute(customAttribute = '') { + if (!customAttribute) { + throw new Error('Custom attribute is required'); + } else { + IFrameHelper.sendMessage('delete-conversation-custom-attribute', { + customAttribute, + }); + } + }, + setLabel(label = '') { IFrameHelper.sendMessage('set-label', { label }); }, diff --git a/app/javascript/widget/App.vue b/app/javascript/widget/App.vue index 6b402c499..a6d627fca 100755 --- a/app/javascript/widget/App.vue +++ b/app/javascript/widget/App.vue @@ -274,6 +274,16 @@ export default { 'contacts/deleteCustomAttribute', message.customAttribute ); + } else if (message.event === 'set-conversation-custom-attributes') { + this.$store.dispatch( + 'conversation/setCustomAttributes', + message.customAttributes + ); + } else if (message.event === 'delete-conversation-custom-attribute') { + this.$store.dispatch( + 'conversation/deleteCustomAttribute', + message.customAttribute + ); } else if (message.event === 'set-locale') { this.setLocale(message.locale); this.setBubbleLabel(); diff --git a/app/javascript/widget/api/conversation.js b/app/javascript/widget/api/conversation.js index 4cf4de25e..78e4fe382 100755 --- a/app/javascript/widget/api/conversation.js +++ b/app/javascript/widget/api/conversation.js @@ -50,6 +50,24 @@ const toggleStatus = async () => { ); }; +const setCustomAttributes = async customAttributes => { + return API.post( + `/api/v1/widget/conversations/set_custom_attributes${window.location.search}`, + { + custom_attributes: customAttributes, + } + ); +}; + +const deleteCustomAttribute = async customAttribute => { + return API.post( + `/api/v1/widget/conversations/destroy_custom_attributes${window.location.search}`, + { + custom_attribute: [customAttribute], + } + ); +}; + export { createConversationAPI, sendMessageAPI, @@ -60,4 +78,6 @@ export { setUserLastSeenAt, sendEmailTranscript, toggleStatus, + setCustomAttributes, + deleteCustomAttribute, }; diff --git a/app/javascript/widget/store/modules/conversation/actions.js b/app/javascript/widget/store/modules/conversation/actions.js index ae06afa9c..ec29ea13c 100644 --- a/app/javascript/widget/store/modules/conversation/actions.js +++ b/app/javascript/widget/store/modules/conversation/actions.js @@ -6,6 +6,8 @@ import { toggleTyping, setUserLastSeenAt, toggleStatus, + setCustomAttributes, + deleteCustomAttribute, } from 'widget/api/conversation'; import { createTemporaryMessage, getNonDeletedMessages } from './helpers'; @@ -139,4 +141,20 @@ export const actions = { resolveConversation: async () => { await toggleStatus(); }, + + setCustomAttributes: async (_, customAttributes = {}) => { + try { + await setCustomAttributes(customAttributes); + } catch (error) { + // IgnoreError + } + }, + + deleteCustomAttribute: async (_, customAttribute) => { + try { + await deleteCustomAttribute(customAttribute); + } catch (error) { + // IgnoreError + } + }, }; diff --git a/config/routes.rb b/config/routes.rb index 218b859be..9fcc09081 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -226,6 +226,8 @@ Rails.application.routes.draw do resources :messages, only: [:index, :create, :update] resources :conversations, only: [:index, :create] do collection do + post :destroy_custom_attributes + post :set_custom_attributes post :update_last_seen post :toggle_typing post :transcript diff --git a/spec/controllers/api/v1/widget/conversations_controller_spec.rb b/spec/controllers/api/v1/widget/conversations_controller_spec.rb index 4cd594a12..d76707bbc 100644 --- a/spec/controllers/api/v1/widget/conversations_controller_spec.rb +++ b/spec/controllers/api/v1/widget/conversations_controller_spec.rb @@ -255,4 +255,59 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do end end end + + describe 'POST /api/v1/widget/conversations/set_custom_attributes' do + let(:params) { { website_token: web_widget.website_token, custom_attributes: { 'product_name': 'Chatwoot' } } } + + context 'with invalid website token' do + it 'returns unauthorized' do + post '/api/v1/widget/conversations/set_custom_attributes', params: { website_token: '' } + expect(response).to have_http_status(:not_found) + end + end + + context 'with correct website token' do + it 'sets the values when provided' do + post '/api/v1/widget/conversations/set_custom_attributes', + headers: { 'X-Auth-Token' => token }, + params: params, + as: :json + + expect(response).to have_http_status(:success) + conversation.reload + # conversation custom attributes should have "product_name" key with value "Chatwoot" + expect(conversation.custom_attributes).to include('product_name' => 'Chatwoot') + end + end + end + + describe 'POST /api/v1/widget/conversations/destroy_custom_attributes' do + let(:params) { { website_token: web_widget.website_token, custom_attribute: ['product_name'] } } + + context 'with invalid website token' do + it 'returns unauthorized' do + post '/api/v1/widget/conversations/destroy_custom_attributes', params: { website_token: '' } + expect(response).to have_http_status(:not_found) + end + end + + context 'with correct website token' do + it 'sets the values when provided' do + # ensure conversation has the attribute + conversation.custom_attributes = { 'product_name': 'Chatwoot' } + conversation.save! + expect(conversation.custom_attributes).to include('product_name' => 'Chatwoot') + + post '/api/v1/widget/conversations/destroy_custom_attributes', + headers: { 'X-Auth-Token' => token }, + params: params, + as: :json + + expect(response).to have_http_status(:success) + conversation.reload + # conversation custom attributes should not have "product_name" key with value "Chatwoot" + expect(conversation.custom_attributes).not_to include('product_name' => 'Chatwoot') + end + end + end end