Merge branch 'develop' of https://github.com/chatwoot/chatwoot into spike/multiple-conv-widget

This commit is contained in:
Nithin David 2021-08-18 11:24:30 +05:30
commit 430c0b1569
31 changed files with 252 additions and 94 deletions

View file

@ -161,3 +161,7 @@ USE_INBOX_AVATAR_FOR_BOT=true
# LETTER_OPENER=true # LETTER_OPENER=true
# meant to be used in github codespaces # meant to be used in github codespaces
# WEBPACKER_DEV_SERVER_PUBLIC= # WEBPACKER_DEV_SERVER_PUBLIC=
# If you want to use official mobile app,
# the notifications would be relayed via a Chatwoot server
ENABLE_PUSH_RELAY_SERVER=true

View file

@ -28,6 +28,10 @@
"FRONTEND_URL": { "FRONTEND_URL": {
"description": "Public root URL of the Chatwoot installation. This will be used in the emails.", "description": "Public root URL of the Chatwoot installation. This will be used in the emails.",
"value": "https://CHANGE.herokuapp.com" "value": "https://CHANGE.herokuapp.com"
},
"INSTALLATION_ENV": {
"description": "Installation method used for Chatwoot.",
"value": "heroku"
} }
}, },
"formation": { "formation": {

View file

@ -3,8 +3,9 @@ class ContactIdentifyAction
def perform def perform
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
@contact = merge_contact(existing_identified_contact, @contact) if merge_contacts?(existing_identified_contact, @contact) merge_if_existing_identified_contact
@contact = merge_contact(existing_email_contact, @contact) if merge_contacts?(existing_email_contact, @contact) merge_if_existing_email_contact
merge_if_existing_phone_number_contact
update_contact update_contact
end end
@contact @contact
@ -16,6 +17,18 @@ class ContactIdentifyAction
@account ||= @contact.account @account ||= @contact.account
end end
def merge_if_existing_identified_contact
@contact = merge_contact(existing_identified_contact, @contact) if merge_contacts?(existing_identified_contact, @contact)
end
def merge_if_existing_email_contact
@contact = merge_contact(existing_email_contact, @contact) if merge_contacts?(existing_email_contact, @contact)
end
def merge_if_existing_phone_number_contact
@contact = merge_contact(existing_phone_number_contact, @contact) if merge_contacts?(existing_phone_number_contact, @contact)
end
def existing_identified_contact def existing_identified_contact
return if params[:identifier].blank? return if params[:identifier].blank?
@ -28,6 +41,12 @@ class ContactIdentifyAction
@existing_email_contact ||= Contact.where(account_id: account.id).find_by(email: params[:email]) @existing_email_contact ||= Contact.where(account_id: account.id).find_by(email: params[:email])
end end
def existing_phone_number_contact
return if params[:phone_number].blank?
@existing_phone_number_contact ||= Contact.where(account_id: account.id).find_by(phone_number: params[:phone_number])
end
def merge_contacts?(existing_contact, _contact) def merge_contacts?(existing_contact, _contact)
existing_contact && existing_contact.id != @contact.id existing_contact && existing_contact.id != @contact.id
end end
@ -36,7 +55,9 @@ class ContactIdentifyAction
custom_attributes = params[:custom_attributes] ? @contact.custom_attributes.merge(params[:custom_attributes]) : @contact.custom_attributes custom_attributes = params[:custom_attributes] ? @contact.custom_attributes.merge(params[:custom_attributes]) : @contact.custom_attributes
# blank identifier or email will throw unique index error # blank identifier or email will throw unique index error
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded # TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
@contact.update!(params.slice(:name, :email, :identifier).reject { |_k, v| v.blank? }.merge({ custom_attributes: custom_attributes })) @contact.update!(params.slice(:name, :email, :identifier, :phone_number).reject do |_k, v|
v.blank?
end.merge({ custom_attributes: custom_attributes }))
ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present? ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
end end

View file

@ -22,7 +22,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return
contacts = resolved_contacts.where( contacts = resolved_contacts.where(
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search', 'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search OR contacts.identifier LIKE :search',
search: "%#{params[:q]}%" search: "%#{params[:q]}%"
) )
@contacts_count = contacts.count @contacts_count = contacts.count
@ -108,7 +108,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
end end
def contact_params def contact_params
params.require(:contact).permit(:name, :email, :phone_number, additional_attributes: {}, custom_attributes: {}) params.require(:contact).permit(:name, :identifier, :email, :phone_number, additional_attributes: {}, custom_attributes: {})
end end
def contact_custom_attributes def contact_custom_attributes

View file

@ -29,6 +29,6 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
end end
def permitted_params def permitted_params
params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, custom_attributes: {}) params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {})
end end
end end

View file

@ -8,6 +8,22 @@
font-weight: var(--font-weight-light); font-weight: var(--font-weight-light);
margin-top: var(--space-large); margin-top: var(--space-large);
} }
.update-subscription--checkbox {
display: flex;
input {
line-height: 1.5;
margin-right: var(--space-one);
margin-top: var(--space-smaller);
}
label {
font-size: var(--font-size-small);
line-height: 1.5;
margin-bottom: var(--space-normal);
}
}
} }
.alert-box { .alert-box {
@ -20,17 +36,4 @@
text-align: center; text-align: center;
} }
.update-subscription--checkbox {
display: flex;
input {
line-height: 1.5;
margin-right: var(--space-one);
}
div {
font-size: var(--font-size-small);
line-height: 1.5;
margin-bottom: var(--space-normal);
}
}

View file

@ -36,7 +36,7 @@ export default {
methods: { methods: {
openLink() { openLink() {
const win = window.open(this.url, '_blank', 'noopener'); const win = window.open(this.url, '_blank', 'noopener');
win.focus(); if (win) win.focus();
}, },
}, },
}; };

View file

@ -45,9 +45,20 @@ export default {
}, },
methods: { methods: {
valueWithLink(attribute) { valueWithLink(attribute) {
const messageFormatter = new MessageFormatter(attribute); const parsedAttribute = this.parseAttributeToString(attribute);
const messageFormatter = new MessageFormatter(parsedAttribute);
return messageFormatter.formattedMessage; return messageFormatter.formattedMessage;
}, },
parseAttributeToString(attribute) {
switch (typeof attribute) {
case 'string':
return attribute;
case 'object':
return JSON.stringify(attribute);
default:
return `${attribute}`;
}
},
}, },
}; };
</script> </script>

View file

@ -97,6 +97,15 @@ export const IFrameHelper = {
} }
}); });
}, },
setFrameHeightToFitContent: (extraHeight, isFixedHeight) => {
const iframe = IFrameHelper.getAppFrame();
const updatedIframeHeight = isFixedHeight ? `${extraHeight}px` : '100%';
if (iframe)
iframe.setAttribute('style', `height: ${updatedIframeHeight} !important`);
},
events: { events: {
loaded: message => { loaded: message => {
Cookies.set('cw_conversation', message.config.authToken, { Cookies.set('cw_conversation', message.config.authToken, {
@ -169,6 +178,13 @@ export const IFrameHelper = {
} }
}, },
updateIframeHeight: message => {
const { extraHeight = 0, isFixedHeight } = message;
if (!extraHeight) return;
IFrameHelper.setFrameHeightToFitContent(extraHeight, isFixedHeight);
},
resetUnreadMode: () => { resetUnreadMode: () => {
IFrameHelper.sendMessage('unset-unread-view'); IFrameHelper.sendMessage('unset-unread-view');
IFrameHelper.events.removeUnreadClass(); IFrameHelper.events.removeUnreadClass();
@ -178,22 +194,6 @@ export const IFrameHelper = {
const holderEl = document.querySelector('.woot-widget-holder'); const holderEl = document.querySelector('.woot-widget-holder');
removeClass(holderEl, 'has-unread-view'); removeClass(holderEl, 'has-unread-view');
}, },
updateIframeHeight: message => {
setTimeout(() => {
const iframe = IFrameHelper.getAppFrame();
const scrollableMessageHeight =
iframe.contentWindow.document.querySelector('.unread-messages')
.scrollHeight + 40;
const updatedIframeHeight = message.isFixedHeight
? `${scrollableMessageHeight}px`
: '100%';
iframe.setAttribute(
'style',
`height: ${updatedIframeHeight} !important`
);
}, 100);
},
}, },
pushEvent: eventName => { pushEvent: eventName => {
IFrameHelper.sendMessage('push-event', { eventName }); IFrameHelper.sendMessage('push-event', { eventName });

View file

@ -1,10 +1,11 @@
export const SDK_CSS = `.woot-widget-holder { export const SDK_CSS = `.woot-widget-holder {
box-shadow: 0 5px 40px rgba(0, 0, 0, .16) !important; box-shadow: 0 5px 40px rgba(0, 0, 0, .16) !important;
opacity: 1; opacity: 1;
will-change: transform, opacity;
transform: translateY(0);
overflow: hidden !important; overflow: hidden !important;
position: fixed !important; position: fixed !important;
transition-duration: 0.5s, 0.5s; transition: opacity 0.2s linear, transform 0.25s linear;
transition-property: opacity, bottom;
z-index: 2147483000 !important; z-index: 2147483000 !important;
} }
@ -73,6 +74,7 @@ export const SDK_CSS = `.woot-widget-holder {
} }
.woot-widget-bubble img { .woot-widget-bubble img {
all: revert;
height: 24px; height: 24px;
margin: 20px; margin: 20px;
width: 24px; width: 24px;
@ -110,7 +112,8 @@ export const SDK_CSS = `.woot-widget-holder {
} }
.woot--hide { .woot--hide {
bottom: -20000px !important; bottom: -100vh;
transform: translateY(40px);
top: unset !important; top: unset !important;
opacity: 0; opacity: 0;
visibility: hidden !important; visibility: hidden !important;

View file

@ -25,47 +25,42 @@ export default {
default: 'top', default: 'top',
}, },
}, },
mounted() {
this.focusItem();
},
methods: { methods: {
focusItem() { dropdownMenuButtons() {
this.$refs.dropdownMenu return this.$refs.dropdownMenu.querySelectorAll(
.querySelector('ul.dropdown li.dropdown-menu__item .button') 'ul.dropdown li.dropdown-menu__item .button'
.focus(); );
},
activeElementIndex() {
const menuButtons = this.dropdownMenuButtons();
const focusedButton = this.$refs.dropdownMenu.querySelector(
'ul.dropdown li.dropdown-menu__item .button:focus'
);
const activeIndex = [...menuButtons].indexOf(focusedButton);
return activeIndex;
}, },
handleKeyEvents(e) { handleKeyEvents(e) {
if (hasPressedArrowUpKey(e)) { const menuButtons = this.dropdownMenuButtons();
const items = this.$refs.dropdownMenu.querySelectorAll( const lastElementIndex = menuButtons.length - 1;
'ul.dropdown li.dropdown-menu__item .button'
);
const focusItems = this.$refs.dropdownMenu.querySelector(
'ul.dropdown li.dropdown-menu__item .button:focus'
);
const activeElementIndex = [...items].indexOf(focusItems);
const lastElementIndex = items.length - 1;
if (activeElementIndex >= 1) { if (menuButtons.length === 0) return;
items[activeElementIndex - 1].focus();
if (hasPressedArrowUpKey(e)) {
const activeIndex = this.activeElementIndex();
if (activeIndex >= 1) {
menuButtons[activeIndex - 1].focus();
} else { } else {
items[lastElementIndex].focus(); menuButtons[lastElementIndex].focus();
} }
} }
if (hasPressedArrowDownKey(e)) { if (hasPressedArrowDownKey(e)) {
const items = this.$refs.dropdownMenu.querySelectorAll( const activeIndex = this.activeElementIndex();
'li.dropdown-menu__item .button'
);
const focusItems = this.$refs.dropdownMenu.querySelector(
'li.dropdown-menu__item .button:focus'
);
const activeElementIndex = [...items].indexOf(focusItems);
const lastElementIndex = items.length - 1;
if (activeElementIndex === lastElementIndex) { if (activeIndex === lastElementIndex) {
items[0].focus(); menuButtons[0].focus();
} else { } else {
items[activeElementIndex + 1].focus(); menuButtons[activeIndex + 1].focus();
} }
} }
}, },

View file

@ -59,6 +59,16 @@ export default {
activeCampaign() { activeCampaign() {
this.setCampaignView(); this.setCampaignView();
}, },
showUnreadView(newVal) {
if (newVal) {
this.setIframeHeight(this.isMobile);
}
},
showCampaignView(newVal) {
if (newVal) {
this.setIframeHeight(this.isMobile);
}
},
}, },
mounted() { mounted() {
const { websiteToken, locale } = window.chatwootWebChannel; const { websiteToken, locale } = window.chatwootWebChannel;
@ -98,9 +108,13 @@ export default {
}); });
}, },
setIframeHeight(isFixedHeight) { setIframeHeight(isFixedHeight) {
IFrameHelper.sendMessage({ this.$nextTick(() => {
event: 'updateIframeHeight', const extraHeight = this.getExtraSpaceToscroll();
isFixedHeight, IFrameHelper.sendMessage({
event: 'updateIframeHeight',
isFixedHeight,
extraHeight,
});
}); });
}, },
setLocale(locale) { setLocale(locale) {
@ -256,6 +270,23 @@ export default {
}, },
}); });
}, },
getExtraSpaceToscroll: () => {
// This function calculates the extra space needed for the view to
// accomodate the height of close button + height of
// read messages button. So that scrollbar won't appear
const unreadMessageWrap = document.querySelector('.unread-messages');
const unreadCloseWrap = document.querySelector('.close-unread-wrap');
const readViewWrap = document.querySelector('.open-read-view-wrap');
if (!unreadMessageWrap) return 0;
// 24px to compensate the paddings
let extraHeight = 24 + unreadMessageWrap.scrollHeight;
if (unreadCloseWrap) extraHeight += unreadCloseWrap.scrollHeight;
if (readViewWrap) extraHeight += readViewWrap.scrollHeight;
return extraHeight;
},
}, },
}; };
</script> </script>

View file

@ -29,6 +29,7 @@ export const actions = {
name: userObject.name, name: userObject.name,
avatar_url: userObject.avatar_url, avatar_url: userObject.avatar_url,
identifier_hash: userObject.identifier_hash, identifier_hash: userObject.identifier_hash,
phone_number: userObject.phone_number,
}; };
const { const {
data: { pubsub_token: pubsubToken }, data: { pubsub_token: pubsubToken },

View file

@ -23,7 +23,7 @@
/> />
</div> </div>
<div> <div class="open-read-view-wrap">
<button <button
v-if="unreadMessageCount" v-if="unreadMessageCount"
class="button clear-button" class="button clear-button"

View file

@ -9,6 +9,7 @@ class Notification::PushNotificationService
notification_subscriptions.each do |subscription| notification_subscriptions.each do |subscription|
send_browser_push(subscription) send_browser_push(subscription)
send_fcm_push(subscription) send_fcm_push(subscription)
send_push_via_chatwoot_hub(subscription)
end end
end end
@ -74,6 +75,18 @@ class Notification::PushNotificationService
fcm = FCM.new(ENV['FCM_SERVER_KEY']) fcm = FCM.new(ENV['FCM_SERVER_KEY'])
response = fcm.send([subscription.subscription_attributes['push_token']], fcm_options) response = fcm.send([subscription.subscription_attributes['push_token']], fcm_options)
remove_subscription_if_error(subscription, response)
end
def send_push_via_chatwoot_hub(subscription)
return if ENV['FCM_SERVER_KEY']
return unless ActiveModel::Type::Boolean.new.cast(ENV.fetch('ENABLE_PUSH_RELAY_SERVER', true))
return unless subscription.fcm?
ChatwootHub.send_browser_push([subscription.subscription_attributes['push_token']], fcm_options)
end
def remove_subscription_if_error(subscription, response)
subscription.destroy! if JSON.parse(response[:body])['results']&.first&.keys&.include?('error') subscription.destroy! if JSON.parse(response[:body])['results']&.first&.keys&.include?('error')
end end

View file

@ -43,9 +43,9 @@
</label> </label>
<div class="update-subscription--checkbox"> <div class="update-subscription--checkbox">
<%= check_box_tag "subscribe_to_updates", 'true', true %> <%= check_box_tag "subscribe_to_updates", 'true', true %>
<div for="subscribe_to_updates"> <label for="subscribe_to_updates">
Subscribe to release notes, newsletters & product feedback surveys. Subscribe to release notes, newsletters & product feedback surveys.
</div> </label>
</div> </div>
<button type="submit" class="button nice large expanded"> <button type="submit" class="button nice large expanded">
Finish Setup Finish Setup

View file

@ -39,7 +39,8 @@ window.addEventListener('chatwoot:ready', function() {
window.$chatwoot.setUser('<%= user_id %>', { window.$chatwoot.setUser('<%= user_id %>', {
identifier_hash: '<%= user_hash %>', identifier_hash: '<%= user_hash %>',
email: 'jane@example.com', email: 'jane@example.com',
name: 'Jane Doe' name: 'Jane Doe',
phone_number: ''
}); });
} }
}) })

View file

@ -1,5 +1,5 @@
shared: &shared shared: &shared
version: '1.18.1' version: '1.19.0'
development: development:
<<: *shared <<: *shared

View file

@ -63,6 +63,7 @@ sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env
sed -i -e '/POSTGRES_USERNAME/ s/=.*/=chatwoot/' .env sed -i -e '/POSTGRES_USERNAME/ s/=.*/=chatwoot/' .env
sed -i -e "/POSTGRES_PASSWORD/ s/=.*/=$pg_pass/" .env sed -i -e "/POSTGRES_PASSWORD/ s/=.*/=$pg_pass/" .env
sed -i -e '/RAILS_ENV/ s/=.*/=$RAILS_ENV/' .env sed -i -e '/RAILS_ENV/ s/=.*/=$RAILS_ENV/' .env
echo -en "\nINSTALLATION_ENV=LINUX_SCRIPT" >> ".env"
RAILS_ENV=production bundle exec rake db:create RAILS_ENV=production bundle exec rake db:create
RAILS_ENV=production bundle exec rake db:reset RAILS_ENV=production bundle exec rake db:reset

View file

@ -70,6 +70,7 @@ sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env
sed -i -e '/POSTGRES_USERNAME/ s/=.*/=chatwoot/' .env sed -i -e '/POSTGRES_USERNAME/ s/=.*/=chatwoot/' .env
sed -i -e "/POSTGRES_PASSWORD/ s/=.*/=$pg_pass/" .env sed -i -e "/POSTGRES_PASSWORD/ s/=.*/=$pg_pass/" .env
sed -i -e '/RAILS_ENV/ s/=.*/=$RAILS_ENV/' .env sed -i -e '/RAILS_ENV/ s/=.*/=$RAILS_ENV/' .env
echo -en "\nINSTALLATION_ENV=LINUX_SCRIPT" >> ".env"
RAILS_ENV=production bundle exec rake db:create RAILS_ENV=production bundle exec rake db:create
RAILS_ENV=production bundle exec rake db:reset RAILS_ENV=production bundle exec rake db:reset

View file

@ -15,6 +15,7 @@ services:
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- RAILS_ENV=production - RAILS_ENV=production
- INSTALLATION_ENV=docker
entrypoint: docker/entrypoints/rails.sh entrypoint: docker/entrypoints/rails.sh
command: ['bundle', 'exec', 'rails', 's', '-p', '3000', '-b', '0.0.0.0'] command: ['bundle', 'exec', 'rails', 's', '-p', '3000', '-b', '0.0.0.0']
@ -26,6 +27,7 @@ services:
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- RAILS_ENV=production - RAILS_ENV=production
- INSTALLATION_ENV=docker
command: ['bundle', 'exec', 'sidekiq', '-C', 'config/sidekiq.yml'] command: ['bundle', 'exec', 'sidekiq', '-C', 'config/sidekiq.yml']
postgres: postgres:

View file

@ -2,6 +2,7 @@ class ChatwootHub
BASE_URL = ENV['CHATWOOT_HUB_URL'] || 'https://hub.2.chatwoot.com' BASE_URL = ENV['CHATWOOT_HUB_URL'] || 'https://hub.2.chatwoot.com'
PING_URL = "#{BASE_URL}/ping".freeze PING_URL = "#{BASE_URL}/ping".freeze
REGISTRATION_URL = "#{BASE_URL}/instances".freeze REGISTRATION_URL = "#{BASE_URL}/instances".freeze
PUSH_NOTIFICATION_URL = "#{BASE_URL}/send_push".freeze
EVENTS_URL = "#{BASE_URL}/events".freeze EVENTS_URL = "#{BASE_URL}/events".freeze
def self.installation_identifier def self.installation_identifier
@ -14,7 +15,8 @@ class ChatwootHub
{ {
installation_identifier: installation_identifier, installation_identifier: installation_identifier,
installation_version: Chatwoot.config[:version], installation_version: Chatwoot.config[:version],
installation_host: URI.parse(ENV.fetch('FRONTEND_URL', '')).host installation_host: URI.parse(ENV.fetch('FRONTEND_URL', '')).host,
installation_env: ENV.fetch('INSTALLATION_ENV', '')
} }
end end
@ -50,7 +52,16 @@ class ChatwootHub
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
Rails.logger.info "Exception: #{e.message}" Rails.logger.info "Exception: #{e.message}"
rescue StandardError => e rescue StandardError => e
Raven.capture_exception(e) Sentry.capture_exception(e)
end
def self.send_browser_push(fcm_token_list, fcm_options)
info = { fcm_token_list: fcm_token_list, fcm_options: fcm_options }
RestClient.post(PUSH_NOTIFICATION_URL, info.merge(instance_config).to_json, { content_type: :json, accept: :json })
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
Rails.logger.info "Exception: #{e.message}"
rescue StandardError => e
Sentry.capture_exception(e)
end end
def self.emit_event(event_name, event_data) def self.emit_event(event_name, event_data)

View file

@ -1,7 +1,8 @@
module ExceptionList module ExceptionList
REST_CLIENT_EXCEPTIONS = [RestClient::NotFound, RestClient::GatewayTimeout, RestClient::BadRequest, REST_CLIENT_EXCEPTIONS = [RestClient::NotFound, RestClient::GatewayTimeout, RestClient::BadRequest,
RestClient::MethodNotAllowed, RestClient::Forbidden, RestClient::InternalServerError, RestClient::MethodNotAllowed, RestClient::Forbidden, RestClient::InternalServerError,
RestClient::Exceptions::OpenTimeout, RestClient::Exceptions::ReadTimeout, SocketError].freeze RestClient::Exceptions::OpenTimeout, RestClient::Exceptions::ReadTimeout,
RestClient::MovedPermanently, SocketError].freeze
SMTP_EXCEPTIONS = [ SMTP_EXCEPTIONS = [
Net::SMTPSyntaxError Net::SMTPSyntaxError
].freeze ].freeze

View file

@ -1,6 +1,6 @@
{ {
"name": "@chatwoot/chatwoot", "name": "@chatwoot/chatwoot",
"version": "1.18.1", "version": "1.19.0",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"eslint": "eslint app/javascript --fix", "eslint": "eslint app/javascript --fix",

View file

@ -45,6 +45,17 @@ describe ::ContactIdentifyAction do
end end
end end
context 'when contact with same phone_number exists' do
it 'merges the current contact to phone_number contact' do
existing_phone_number_contact = create(:contact, account: account, phone_number: '+919999888877')
params = { phone_number: '+919999888877' }
result = described_class.new(contact: contact, params: params).perform
expect(result.id).to eq existing_phone_number_contact.id
expect(result.name).to eq existing_phone_number_contact.name
expect { contact.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context 'when contacts with blank identifiers exist and identify action is called with blank identifier' do context 'when contacts with blank identifiers exist and identify action is called with blank identifier' do
it 'updates the attributes of contact passed in to identify action' do it 'updates the attributes of contact passed in to identify action' do
create(:contact, account: account, identifier: '') create(:contact, account: account, identifier: '')

View file

@ -188,6 +188,19 @@ RSpec.describe 'Contacts API', type: :request do
expect(response.body).to include(contact2.email) expect(response.body).to include(contact2.email)
expect(response.body).not_to include(contact1.email) expect(response.body).not_to include(contact1.email)
end end
it 'matches the contact respecting the identifier character casing' do
contact_normal = create(:contact, name: 'testcontact', account: account, identifier: 'testidentifer')
contact_special = create(:contact, name: 'testcontact', account: account, identifier: 'TestIdentifier')
get "/api/v1/accounts/#{account.id}/contacts/search",
params: { q: 'TestIdentifier' },
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(contact_special.identifier)
expect(response.body).not_to include(contact_normal.identifier)
end
end end
end end
@ -284,7 +297,7 @@ RSpec.describe 'Contacts API', type: :request do
expect(json_response['payload']['contact']['custom_attributes']).to eq({ 'test' => 'test', 'test1' => 'test1' }) expect(json_response['payload']['contact']['custom_attributes']).to eq({ 'test' => 'test', 'test1' => 'test1' })
end end
it 'creates the contact identifier when inbox id is passed' do it 'creates the contact inbox when inbox id is passed' do
expect do expect do
post "/api/v1/accounts/#{account.id}/contacts", headers: admin.create_new_auth_token, post "/api/v1/accounts/#{account.id}/contacts", headers: admin.create_new_auth_token,
params: valid_params.merge({ inbox_id: inbox.id }) params: valid_params.merge({ inbox_id: inbox.id })

View file

@ -20,17 +20,20 @@ describe Notification::PushNotificationService do
described_class.new(notification: notification).perform described_class.new(notification: notification).perform
expect(Webpush).to have_received(:payload_send) expect(Webpush).to have_received(:payload_send)
expect(FCM).not_to have_received(:new) expect(FCM).not_to have_received(:new)
ENV['ENABLE_ACCOUNT_SIGNUP'] = nil ENV['VAPID_PUBLIC_KEY'] = nil
end end
it 'sends a fcm notification for firebase subscription' do it 'sends a fcm notification for firebase subscription' do
ENV['FCM_SERVER_KEY'] = 'test' ENV['FCM_SERVER_KEY'] = 'test'
ENV['ENABLE_PUSH_RELAY_SERVER'] = 'false'
create(:notification_subscription, user: notification.user, subscription_type: 'fcm') create(:notification_subscription, user: notification.user, subscription_type: 'fcm')
described_class.new(notification: notification).perform described_class.new(notification: notification).perform
expect(FCM).to have_received(:new) expect(FCM).to have_received(:new)
expect(Webpush).not_to have_received(:payload_send) expect(Webpush).not_to have_received(:payload_send)
ENV['ENABLE_ACCOUNT_SIGNUP'] = nil
ENV['FCM_SERVER_KEY'] = nil
ENV['ENABLE_PUSH_RELAY_SERVER'] = nil
end end
end end
end end

View file

@ -5,7 +5,13 @@ properties:
required: true required: true
name: name:
type: string type: string
description: name of the contact
email: email:
type: string type: string
description: email of the contact
phone_number: phone_number:
type: string type: string
description: phone number of the contact
identifier:
type: string
description: A unique identifier for the contact in external system

View file

@ -2,7 +2,13 @@ type: object
properties: properties:
name: name:
type: string type: string
description: name of the contact
email: email:
type: string type: string
description: email of the contact
phone_number: phone_number:
type: string type: string
description: phone number of the contact
identifier:
type: string
description: A unique identifier for the contact in external system

View file

@ -8,6 +8,7 @@ get:
- name: q - name: q
in: query in: query
type: string type: string
description: Search using contact `name`, `identifier`, `email` or `phone number`
- $ref: '#/parameters/contact_sort_param' - $ref: '#/parameters/contact_sort_param'
- $ref: '#/parameters/page' - $ref: '#/parameters/page'
responses: responses:

View file

@ -1281,7 +1281,8 @@
{ {
"name": "q", "name": "q",
"in": "query", "in": "query",
"type": "string" "type": "string",
"description": "Search using contact `name`, `identifier`, `email` or `phone number`"
}, },
{ {
"$ref": "#/parameters/contact_sort_param" "$ref": "#/parameters/contact_sort_param"
@ -3376,13 +3377,20 @@
"required": true "required": true
}, },
"name": { "name": {
"type": "string" "type": "string",
"description": "name of the contact"
}, },
"email": { "email": {
"type": "string" "type": "string",
"description": "email of the contact"
}, },
"phone_number": { "phone_number": {
"type": "string" "type": "string",
"description": "phone number of the contact"
},
"identifier": {
"type": "string",
"description": "A unique identifier for the contact in external system"
} }
} }
}, },
@ -3390,13 +3398,20 @@
"type": "object", "type": "object",
"properties": { "properties": {
"name": { "name": {
"type": "string" "type": "string",
"description": "name of the contact"
}, },
"email": { "email": {
"type": "string" "type": "string",
"description": "email of the contact"
}, },
"phone_number": { "phone_number": {
"type": "string" "type": "string",
"description": "phone number of the contact"
},
"identifier": {
"type": "string",
"description": "A unique identifier for the contact in external system"
} }
} }
}, },