From d827e664539ab4abf1f67fee486c8717ccab5914 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Tue, 8 Apr 2025 10:47:41 +0530 Subject: [PATCH] feat: Instagram Inbox using Instagram Business Login (#11054) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR introduces basic minimum version of **Instagram Business Login**, making Instagram inbox setup more straightforward by removing the Facebook Page dependency. This update enhances user experience and aligns with Meta’s recommended best practices. Fixes https://linear.app/chatwoot/issue/CW-3728/instagram-login-how-to-implement-the-changes ## Why Introduce Instagram as a Separate Inbox? Currently, our Instagram integration requires linking an Instagram account to a Facebook Page, making setup complex. To simplify this process, Instagram now offers **Instagram Business Login**, which allows users to authenticate directly with their Instagram credentials. The **Instagram API with Instagram Login** enables businesses and creators to send and receive messages without needing a Facebook Page connection. While an Instagram Business or Creator account is still required, this approach provides a more straightforward integration process. | **Existing Approach (Facebook Login for Business)** | **New Approach (Instagram Business Login)** | | --- | --- | | Requires linking Instagram to a Facebook Page | No Facebook Page required | | Users log in via Facebook credentials | Users log in via Instagram credentials | | Configuration is more complex | Simpler setup | Meta recommends using **Instagram Business Login** as the preferred authentication method due to its easier configuration and improved developer experience. --- ## Implementation Plan The core messaging functionality is already in place, but the transition to **Instagram Business Login** requires adjustments. ### Changes & Considerations - **API Adjustments**: The Instagram API uses `graph.instagram`, whereas Koala (our existing library) interacts with `graph.facebook`. We may need to modify API calls accordingly. - **Three Main Modules**: 1. **Instagram Business Login** – Handle authentication flow. 2. **Permissions & Features** – Ensure necessary API scopes are granted. 3. **Webhooks** – Enable real-time message retrieval. ![CleanShot 2025-03-10 at 21 32 28@2x](https://github.com/user-attachments/assets/1b019001-8d16-4e59-aca2-ced81e98f538) --- ## Instagram Login Flow 1. User clicks **"Create Inbox"** for Instagram. 2. App redirects to the [Instagram Authorization URL](https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/business-login#embed-the-business-login-url). 3. After authentication, Instagram returns an authorization code. 5. The app exchanges the code for a **long-lived token** (valid for 60 days). 6. Tokens are refreshed periodically to maintain access. 7. Once completed, the app creates an inbox and redirects to the Chatwoot dashboard. --- ## How to Test the Instagram Inbox 1. Create a new app on [Meta's Developer Portal](https://developers.facebook.com/apps/). 2. Select **Business** as the app type and configure it. 3. Add the Instagram product and connect a business account. 4. Copy Instagram app ID and Instagram app secret 5. Add the Instagram app ID and Instagram app secret to your app config via `{Chatwoot installation url}/super_admin/app_config?config=instagram` 6. Configure Webhooks: - Callback URL: `{your_chatwoot_url}/webhooks/instagram` - Verify Token: `INSTAGRAM_VERIFY_TOKEN` - Subscribe to `messages`, `messaging_seen`, and `message_reactions` events. 7. Set up **Instagram Business Login**: - Redirect URL: `{your_chatwoot_url}/instagram/callback` 8. Test inbox creation via the Chatwoot dashboard. ## Troubleshooting & Common Errors ### Insufficient Developer Role Error - Ensure the Instagram user is added as a developer: - **Meta Dashboard → App Roles → Roles → Add People → Enter Instagram ID** ### API Access Deactivated - Ensure the **Privacy Policy URL** is valid and correctly set. ### Invalid request: Request parameters are invalid: Invalid redirect_uri - Please configure the Frontend URL. The Frontend URL does not match the authorization URL. --- ## To-Do List - [x] Basic integration setup completed. - [x] Enable sending messages via [Messaging API](https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/messaging-api). - [x] Implement automatic webhook subscriptions on inbox creation. - [x] Handle **canceled authorization errors**. - [x] Handle all the errors https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/error-codes - [x] Dynamically fetch **account IDs** instead of hardcoding them. - [x] Prevent duplicate Instagram channel creation for the same account. - [x] Use **Global Config** instead of environment variables. - [x] Explore **Human Agent feature** for message handling. - [x] Write and refine **test cases** for all scenarios. - [x] Implement **token refresh mechanism** (tokens expire after 60 days). Fixes https://github.com/chatwoot/chatwoot/issues/10440 --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: Shivam Mishra --- .../contact_inbox_with_contact_builder.rb | 24 ++ .../instagram/base_message_builder.rb | 179 +++++++++ .../messages/instagram/message_builder.rb | 214 ++-------- .../instagram/messenger/message_builder.rb | 33 ++ .../messages/messenger/message_builder.rb | 16 +- app/controllers/concerns/instagram_concern.rb | 1 - .../components-next/icon/provider.js | 1 + .../components-next/message/MessageMeta.vue | 8 +- .../components/widgets/ChannelItem.vue | 5 + .../widgets/conversation/ReplyBox.vue | 5 +- .../dashboard/composables/useInbox.js | 5 + app/javascript/dashboard/featureFlags.js | 1 + .../dashboard/i18n/locale/en/inboxMgmt.json | 1 + .../dashboard/settings/inbox/ChannelList.vue | 4 +- .../settings/inbox/channels/Instagram.vue | 26 +- app/jobs/send_reply_job.rb | 5 +- app/jobs/webhooks/instagram_events_job.rb | 147 ++++++- app/models/channel/instagram.rb | 8 + app/services/instagram/base_message_text.rb | 69 ++++ app/services/instagram/base_send_service.rb | 95 +++++ app/services/instagram/message_text.rb | 177 ++------ .../instagram/messenger/message_text.rb | 37 ++ .../messenger/send_on_instagram_service.rb | 40 ++ app/services/instagram/read_status_service.rb | 10 +- .../instagram/refresh_oauth_token_service.rb | 2 +- .../instagram/send_on_instagram_service.rb | 120 +----- .../instagram/webhooks_base_service.rb | 41 +- .../app/helpers/super_admin/features.yml | 12 +- .../images/dashboard/channels/instagram.png | Bin 0 -> 1335709 bytes ...contact_inbox_with_contact_builder_spec.rb | 28 ++ .../instagram/message_builder_spec.rb | 347 ++++++++-------- .../messenger/message_builder_spec.rb | 378 ++++++++++++++++++ .../v2/reports/inbox_summary_builder_spec.rb | 68 ++-- .../instagram/callbacks_controller_spec.rb | 16 + .../instagram_message_create_event.rb | 2 +- spec/jobs/send_reply_job_spec.rb | 24 ++ .../webhooks/instagram_events_job_spec.rb | 269 +++++++++---- .../send_on_instagram_service_spec.rb | 184 +++++++++ .../instagram/read_status_service_spec.rb | 4 +- .../send_on_instagram_service_spec.rb | 93 +++-- 40 files changed, 1868 insertions(+), 831 deletions(-) create mode 100644 app/builders/messages/instagram/base_message_builder.rb create mode 100644 app/builders/messages/instagram/messenger/message_builder.rb create mode 100644 app/services/instagram/base_message_text.rb create mode 100644 app/services/instagram/base_send_service.rb create mode 100644 app/services/instagram/messenger/message_text.rb create mode 100644 app/services/instagram/messenger/send_on_instagram_service.rb create mode 100644 public/assets/images/dashboard/channels/instagram.png create mode 100644 spec/builders/messages/instagram/messenger/message_builder_spec.rb create mode 100644 spec/services/instagram/messenger/send_on_instagram_service_spec.rb diff --git a/app/builders/contact_inbox_with_contact_builder.rb b/app/builders/contact_inbox_with_contact_builder.rb index 2f30093db..2c0e6087e 100644 --- a/app/builders/contact_inbox_with_contact_builder.rb +++ b/app/builders/contact_inbox_with_contact_builder.rb @@ -63,9 +63,33 @@ class ContactInboxWithContactBuilder contact = find_contact_by_identifier(contact_attributes[:identifier]) contact ||= find_contact_by_email(contact_attributes[:email]) contact ||= find_contact_by_phone_number(contact_attributes[:phone_number]) + contact ||= find_contact_by_instagram_source_id(source_id) if instagram_channel? + contact end + def instagram_channel? + inbox.channel_type == 'Channel::Instagram' + end + + # There might be existing contact_inboxes created through Channel::FacebookPage + # with the same Instagram source_id. New Instagram interactions should create fresh contact_inboxes + # while still reusing contacts if found in Facebook channels so that we can create + # new conversations with the same contact. + def find_contact_by_instagram_source_id(instagram_id) + return if instagram_id.blank? + + existing_contact_inbox = ContactInbox.joins(:inbox) + .where(source_id: instagram_id) + .where( + 'inboxes.channel_type = ? AND inboxes.account_id = ?', + 'Channel::FacebookPage', + account.id + ).first + + existing_contact_inbox&.contact + end + def find_contact_by_identifier(identifier) return if identifier.blank? diff --git a/app/builders/messages/instagram/base_message_builder.rb b/app/builders/messages/instagram/base_message_builder.rb new file mode 100644 index 000000000..b8f892d97 --- /dev/null +++ b/app/builders/messages/instagram/base_message_builder.rb @@ -0,0 +1,179 @@ +class Messages::Instagram::BaseMessageBuilder < Messages::Messenger::MessageBuilder + attr_reader :messaging + + def initialize(messaging, inbox, outgoing_echo: false) + super() + @messaging = messaging + @inbox = inbox + @outgoing_echo = outgoing_echo + end + + def perform + return if @inbox.channel.reauthorization_required? + + ActiveRecord::Base.transaction do + build_message + end + rescue StandardError => e + handle_error(e) + end + + private + + def attachments + @messaging[:message][:attachments] || {} + end + + def message_type + @outgoing_echo ? :outgoing : :incoming + end + + def message_identifier + message[:mid] + end + + def message_source_id + @outgoing_echo ? recipient_id : sender_id + end + + def message_is_unsupported? + message[:is_unsupported].present? && @messaging[:message][:is_unsupported] == true + end + + def sender_id + @messaging[:sender][:id] + end + + def recipient_id + @messaging[:recipient][:id] + end + + def message + @messaging[:message] + end + + def contact + @contact ||= @inbox.contact_inboxes.find_by(source_id: message_source_id)&.contact + end + + def conversation + @conversation ||= set_conversation_based_on_inbox_config + end + + def set_conversation_based_on_inbox_config + if @inbox.lock_to_single_conversation + find_conversation_scope.order(created_at: :desc).first || build_conversation + else + find_or_build_for_multiple_conversations + end + end + + def find_conversation_scope + Conversation.where(conversation_params) + end + + def find_or_build_for_multiple_conversations + last_conversation = find_conversation_scope.where.not(status: :resolved).order(created_at: :desc).first + return build_conversation if last_conversation.nil? + + last_conversation + end + + def message_content + @messaging[:message][:text] + end + + def story_reply_attributes + message[:reply_to][:story] if message[:reply_to].present? && message[:reply_to][:story].present? + end + + def message_reply_attributes + message[:reply_to][:mid] if message[:reply_to].present? && message[:reply_to][:mid].present? + end + + def build_message + # Duplicate webhook events may be sent for the same message + # when a user is connected to the Instagram account through both Messenger and Instagram login. + # Therefore, we need to check if the message already exists before creating it. + return if message_already_exists? + + return if @outgoing_echo + + return if message_content.blank? && all_unsupported_files? + + @message = conversation.messages.create!(message_params) + save_story_id + + attachments.each do |attachment| + process_attachment(attachment) + end + end + + def save_story_id + return if story_reply_attributes.blank? + + @message.save_story_info(story_reply_attributes) + end + + def build_conversation + @contact_inbox ||= contact.contact_inboxes.find_by!(source_id: message_source_id) + Conversation.create!(conversation_params.merge( + contact_inbox_id: @contact_inbox.id, + additional_attributes: additional_conversation_attributes + )) + end + + def additional_conversation_attributes + {} + end + + def conversation_params + { + account_id: @inbox.account_id, + inbox_id: @inbox.id, + contact_id: contact.id + } + end + + def message_params + params = { + account_id: conversation.account_id, + inbox_id: conversation.inbox_id, + message_type: message_type, + source_id: message_identifier, + content: message_content, + sender: @outgoing_echo ? nil : contact, + content_attributes: { + in_reply_to_external_id: message_reply_attributes + } + } + + params[:content_attributes][:is_unsupported] = true if message_is_unsupported? + params + end + + def message_already_exists? + cw_message = conversation.messages.where( + source_id: @messaging[:message][:mid] + ).first + + cw_message.present? + end + + def all_unsupported_files? + return if attachments.empty? + + attachments_type = attachments.pluck(:type).uniq.first + unsupported_file_type?(attachments_type) + end + + def handle_error(error) + ChatwootExceptionTracker.new(error, account: @inbox.account).capture_exception + true + end + + # Abstract methods to be implemented by subclasses + def get_story_object_from_source_id(source_id) + raise NotImplementedError + end +end diff --git a/app/builders/messages/instagram/message_builder.rb b/app/builders/messages/instagram/message_builder.rb index 7f1b9cab2..4e7150894 100644 --- a/app/builders/messages/instagram/message_builder.rb +++ b/app/builders/messages/instagram/message_builder.rb @@ -1,200 +1,42 @@ -# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo` -# Assumptions -# 1. Incase of an outgoing message which is echo, source_id will NOT be nil, -# based on this we are showing "not sent from chatwoot" message in frontend -# Hence there is no need to set user_id in message for outgoing echo messages. - -class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder - attr_reader :messaging - +class Messages::Instagram::MessageBuilder < Messages::Instagram::BaseMessageBuilder def initialize(messaging, inbox, outgoing_echo: false) - super() - @messaging = messaging - @inbox = inbox - @outgoing_echo = outgoing_echo - end - - def perform - return if @inbox.channel.reauthorization_required? - - ActiveRecord::Base.transaction do - build_message - end - rescue Koala::Facebook::AuthenticationError => e - Rails.logger.warn("Instagram authentication error for inbox: #{@inbox.id} with error: #{e.message}") - Rails.logger.error e - @inbox.channel.authorization_error! - raise - rescue StandardError => e - ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception - true + super(messaging, inbox, outgoing_echo: outgoing_echo) end private - def attachments - @messaging[:message][:attachments] || {} + def get_story_object_from_source_id(source_id) + url = "#{base_uri}/#{source_id}?fields=story,from&access_token=#{@inbox.channel.access_token}" + + response = HTTParty.get(url) + + return JSON.parse(response.body).with_indifferent_access if response.success? + + # Create message first if it doesn't exist + @message ||= conversation.messages.create!(message_params) + handle_error_response(response) + nil end - def message_type - @outgoing_echo ? :outgoing : :incoming - end + def handle_error_response(response) + parsed_response = JSON.parse(response.body) + error_code = parsed_response.dig('error', 'code') - def message_identifier - message[:mid] - end + # https://developers.facebook.com/docs/messenger-platform/error-codes + # Access token has expired or become invalid. + channel.authorization_error! if error_code == 190 - def message_source_id - @outgoing_echo ? recipient_id : sender_id - end - - def message_is_unsupported? - message[:is_unsupported].present? && @messaging[:message][:is_unsupported] == true - end - - def sender_id - @messaging[:sender][:id] - end - - def recipient_id - @messaging[:recipient][:id] - end - - def message - @messaging[:message] - end - - def contact - @contact ||= @inbox.contact_inboxes.find_by(source_id: message_source_id)&.contact - end - - def conversation - @conversation ||= set_conversation_based_on_inbox_config - end - - def instagram_direct_message_conversation - Conversation.where(conversation_params) - .where("additional_attributes ->> 'type' = 'instagram_direct_message'") - end - - def set_conversation_based_on_inbox_config - if @inbox.lock_to_single_conversation - instagram_direct_message_conversation.order(created_at: :desc).first || build_conversation - else - find_or_build_for_multiple_conversations + # There was a problem scraping data from the provided link. + # https://developers.facebook.com/docs/graph-api/guides/error-handling/ search for error code 1609005 + if error_code == 1_609_005 + @message.attachments.destroy_all + @message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content')) end + + Rails.logger.error("[InstagramStoryFetchError]: #{parsed_response.dig('error', 'message')} #{error_code}") end - def find_or_build_for_multiple_conversations - last_conversation = instagram_direct_message_conversation.where.not(status: :resolved).order(created_at: :desc).first - - return build_conversation if last_conversation.nil? - - last_conversation + def base_uri + "https://graph.instagram.com/#{GlobalConfigService.load('INSTAGRAM_API_VERSION', 'v22.0')}" end - - def message_content - @messaging[:message][:text] - end - - def story_reply_attributes - message[:reply_to][:story] if message[:reply_to].present? && message[:reply_to][:story].present? - end - - def message_reply_attributes - message[:reply_to][:mid] if message[:reply_to].present? && message[:reply_to][:mid].present? - end - - def build_message - return if @outgoing_echo && already_sent_from_chatwoot? - return if message_content.blank? && all_unsupported_files? - - @message = conversation.messages.create!(message_params) - save_story_id - - attachments.each do |attachment| - process_attachment(attachment) - end - end - - def save_story_id - return if story_reply_attributes.blank? - - @message.save_story_info(story_reply_attributes) - end - - def build_conversation - @contact_inbox ||= contact.contact_inboxes.find_by!(source_id: message_source_id) - - Conversation.create!(conversation_params.merge( - contact_inbox_id: @contact_inbox.id, - additional_attributes: { type: 'instagram_direct_message' } - )) - end - - def conversation_params - { - account_id: @inbox.account_id, - inbox_id: @inbox.id, - contact_id: contact.id - } - end - - def message_params - params = { - account_id: conversation.account_id, - inbox_id: conversation.inbox_id, - message_type: message_type, - source_id: message_identifier, - content: message_content, - sender: @outgoing_echo ? nil : contact, - content_attributes: { - in_reply_to_external_id: message_reply_attributes - } - } - - params[:content_attributes][:is_unsupported] = true if message_is_unsupported? - params - end - - def already_sent_from_chatwoot? - cw_message = conversation.messages.where( - source_id: @messaging[:message][:mid] - ).first - - cw_message.present? - end - - def all_unsupported_files? - return if attachments.empty? - - attachments_type = attachments.pluck(:type).uniq.first - unsupported_file_type?(attachments_type) - end - - ### Sample response - # { - # "object": "instagram", - # "entry": [ - # { - # "id": "",// ig id of the business - # "time": 1569262486134, - # "messaging": [ - # { - # "sender": { - # "id": "" - # }, - # "recipient": { - # "id": "" - # }, - # "timestamp": 1569262485349, - # "message": { - # "mid": "", - # "text": "" - # } - # } - # ] - # } - # ], - # } end diff --git a/app/builders/messages/instagram/messenger/message_builder.rb b/app/builders/messages/instagram/messenger/message_builder.rb new file mode 100644 index 000000000..1263dee90 --- /dev/null +++ b/app/builders/messages/instagram/messenger/message_builder.rb @@ -0,0 +1,33 @@ +class Messages::Instagram::Messenger::MessageBuilder < Messages::Instagram::BaseMessageBuilder + def initialize(messaging, inbox, outgoing_echo: false) + super(messaging, inbox, outgoing_echo: outgoing_echo) + end + + private + + def get_story_object_from_source_id(source_id) + k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook? + k.get_object(source_id, fields: %w[story from]) || {} + rescue Koala::Facebook::AuthenticationError + @inbox.channel.authorization_error! + raise + rescue Koala::Facebook::ClientError => e + # The exception occurs when we are trying fetch the deleted story or blocked story. + @message.attachments.destroy_all + @message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content')) + Rails.logger.error e + {} + rescue StandardError => e + ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception + {} + end + + def find_conversation_scope + Conversation.where(conversation_params) + .where("additional_attributes ->> 'type' = 'instagram_direct_message'") + end + + def additional_conversation_attributes + { type: 'instagram_direct_message' } + end +end diff --git a/app/builders/messages/messenger/message_builder.rb b/app/builders/messages/messenger/message_builder.rb index f01a236c9..4e7f2849d 100644 --- a/app/builders/messages/messenger/message_builder.rb +++ b/app/builders/messages/messenger/message_builder.rb @@ -68,20 +68,8 @@ class Messages::Messenger::MessageBuilder message.save! end - def get_story_object_from_source_id(source_id) - k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook? - k.get_object(source_id, fields: %w[story from]) || {} - rescue Koala::Facebook::AuthenticationError - @inbox.channel.authorization_error! - raise - rescue Koala::Facebook::ClientError => e - # The exception occurs when we are trying fetch the deleted story or blocked story. - @message.attachments.destroy_all - @message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content')) - Rails.logger.error e - {} - rescue StandardError => e - ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception + # This is a placeholder method to be overridden by child classes + def get_story_object_from_source_id(_source_id) {} end diff --git a/app/controllers/concerns/instagram_concern.rb b/app/controllers/concerns/instagram_concern.rb index f4dcfa010..04de9e63c 100644 --- a/app/controllers/concerns/instagram_concern.rb +++ b/app/controllers/concerns/instagram_concern.rb @@ -1,6 +1,5 @@ module InstagramConcern extend ActiveSupport::Concern - include HTTParty def instagram_client ::OAuth2::Client.new( diff --git a/app/javascript/dashboard/components-next/icon/provider.js b/app/javascript/dashboard/components-next/icon/provider.js index 5b00376d1..9c0a24925 100644 --- a/app/javascript/dashboard/components-next/icon/provider.js +++ b/app/javascript/dashboard/components-next/icon/provider.js @@ -12,6 +12,7 @@ export function useChannelIcon(inbox) { 'Channel::TwitterProfile': 'i-ri-twitter-x-fill', 'Channel::WebWidget': 'i-ri-global-fill', 'Channel::Whatsapp': 'i-ri-whatsapp-fill', + 'Channel::Instagram': 'i-ri-instagram-fill', }; const providerIconMap = { diff --git a/app/javascript/dashboard/components-next/message/MessageMeta.vue b/app/javascript/dashboard/components-next/message/MessageMeta.vue index 41a0a3d15..d2ddd2c77 100644 --- a/app/javascript/dashboard/components-next/message/MessageMeta.vue +++ b/app/javascript/dashboard/components-next/message/MessageMeta.vue @@ -19,6 +19,7 @@ const { isAWebWidgetInbox, isAWhatsAppChannel, isAnEmailChannel, + isAInstagramChannel, } = useInbox(); const { status, isPrivate, createdAt, sourceId, messageType } = @@ -47,7 +48,8 @@ const isSent = computed(() => { isATwilioChannel.value || isAFacebookInbox.value || isASmsInbox.value || - isATelegramChannel.value + isATelegramChannel.value || + isAInstagramChannel.value ) { return sourceId.value && status.value === MESSAGE_STATUS.SENT; } @@ -86,7 +88,8 @@ const isRead = computed(() => { if ( isAWhatsAppChannel.value || isATwilioChannel.value || - isAFacebookInbox.value + isAFacebookInbox.value || + isAInstagramChannel.value ) { return sourceId.value && status.value === MESSAGE_STATUS.READ; } @@ -102,7 +105,6 @@ const statusToShow = computed(() => { if (isRead.value) return MESSAGE_STATUS.READ; if (isDelivered.value) return MESSAGE_STATUS.DELIVERED; if (isSent.value) return MESSAGE_STATUS.SENT; - if (status.value === MESSAGE_STATUS.FAILED) return MESSAGE_STATUS.FAILED; return MESSAGE_STATUS.PROGRESS; }); diff --git a/app/javascript/dashboard/components/widgets/ChannelItem.vue b/app/javascript/dashboard/components/widgets/ChannelItem.vue index eb665cc53..80f328980 100644 --- a/app/javascript/dashboard/components/widgets/ChannelItem.vue +++ b/app/javascript/dashboard/components/widgets/ChannelItem.vue @@ -32,6 +32,10 @@ export default { return this.enabledFeatures.channel_email; } + if (key === 'instagram') { + return this.enabledFeatures.channel_instagram; + } + return [ 'website', 'twilio', @@ -40,6 +44,7 @@ export default { 'sms', 'telegram', 'line', + 'instagram', ].includes(key); }, }, diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index 992f61929..e8a9ba827 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -261,7 +261,8 @@ export default { this.isAnEmailChannel || this.isASmsInbox || this.isATelegramChannel || - this.isALineChannel + this.isALineChannel || + this.isAInstagramChannel ); }, replyButtonLabel() { @@ -1076,7 +1077,7 @@ export default { v-if="showSelfAssignBanner" action-button-variant="ghost" color-scheme="secondary" - class="banner--self-assign mx-2 mb-2 rounded-lg" + class="mx-2 mb-2 rounded-lg banner--self-assign" :banner-message="$t('CONVERSATION.NOT_ASSIGNED_TO_YOU')" has-action-button :action-button-label="$t('CONVERSATION.ASSIGN_TO_ME')" diff --git a/app/javascript/dashboard/composables/useInbox.js b/app/javascript/dashboard/composables/useInbox.js index b8e399665..2c59bc6e5 100644 --- a/app/javascript/dashboard/composables/useInbox.js +++ b/app/javascript/dashboard/composables/useInbox.js @@ -121,6 +121,10 @@ export const useInbox = () => { ); }); + const isAInstagramChannel = computed(() => { + return channelType.value === INBOX_TYPES.INSTAGRAM; + }); + return { inbox, isAFacebookInbox, @@ -137,5 +141,6 @@ export const useInbox = () => { isAWhatsAppCloudChannel, is360DialogWhatsAppChannel, isAnEmailChannel, + isAInstagramChannel, }; }; diff --git a/app/javascript/dashboard/featureFlags.js b/app/javascript/dashboard/featureFlags.js index f7430bdba..1a6ada9fa 100644 --- a/app/javascript/dashboard/featureFlags.js +++ b/app/javascript/dashboard/featureFlags.js @@ -34,6 +34,7 @@ export const FEATURE_FLAGS = { CUSTOM_ROLES: 'custom_roles', CHATWOOT_V4: 'chatwoot_v4', REPORT_V4: 'report_v4', + CHANNEL_INSTAGRAM: 'channel_instagram', CONTACT_CHATWOOT_SUPPORT_TEAM: 'contact_chatwoot_support_team', }; diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index 5e26a34c0..c70a2d348 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -48,6 +48,7 @@ }, "INSTAGRAM": { "CONTINUE_WITH_INSTAGRAM": "Continue with Instagram", + "CONNECT_YOUR_INSTAGRAM_PROFILE": "Connect your Instagram Profile", "HELP": "To add your Instagram profile as a channel, you need to authenticate your Instagram Profile by clicking on 'Continue with Instagram' ", "ERROR_MESSAGE": "There was an error connecting to Instagram, please try again", "ERROR_AUTH": "There was an error connecting to Instagram, please try again" diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelList.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelList.vue index 9e91f0b72..a404cbea6 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelList.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelList.vue @@ -35,6 +35,8 @@ export default { }, { key: 'telegram', name: 'Telegram' }, { key: 'line', name: 'Line' }, + // TODO: Add Instagram to the channel list after the feature is ready to use. + // { key: 'instagram', name: 'Instagram' }, ]; }, ...mapGetters({ @@ -62,7 +64,7 @@ export default {