diff --git a/app/controllers/instagram/callbacks_controller.rb b/app/controllers/instagram/callbacks_controller.rb index 02add933f..4dc8ece1c 100644 --- a/app/controllers/instagram/callbacks_controller.rb +++ b/app/controllers/instagram/callbacks_controller.rb @@ -26,8 +26,13 @@ class Instagram::CallbacksController < ApplicationController ) @long_lived_token_response = exchange_for_long_lived_token(@response.token) - inbox, = create_channel_with_inbox - redirect_to app_instagram_inbox_agents_url(account_id: account_id, inbox_id: inbox.id) + inbox, already_exists = find_or_create_inbox + + if already_exists + redirect_to app_instagram_inbox_settings_url(account_id: account_id, inbox_id: inbox.id) + else + redirect_to app_instagram_inbox_agents_url(account_id: account_id, inbox_id: inbox.id) + end end # Handle all errors that might occur during authorization @@ -82,12 +87,45 @@ class Instagram::CallbacksController < ApplicationController ) end - def create_channel_with_inbox + def find_or_create_inbox + user_details = fetch_instagram_user_details(@long_lived_token_response['access_token']) + channel_instagram = find_channel_by_instagram_id(user_details['user_id'].to_s) + channel_exists = channel_instagram.present? + + if channel_instagram + update_channel(channel_instagram, user_details) + else + channel_instagram = create_channel_with_inbox(user_details) + end + + # reauthorize channel, this code path only triggers when instagram auth is successful + # reauthorized will also update cache keys for the associated inbox + channel_instagram.reauthorized! + + [channel_instagram.inbox, channel_exists] + end + + def find_channel_by_instagram_id(instagram_id) + Channel::Instagram.find_by(instagram_id: instagram_id, account: account) + end + + def update_channel(channel_instagram, user_details) + expires_at = Time.current + @long_lived_token_response['expires_in'].seconds + + channel_instagram.update!( + access_token: @long_lived_token_response['access_token'], + expires_at: expires_at + ) + + # Update inbox name if username changed + channel_instagram.inbox.update!(name: user_details['username']) + channel_instagram + end + + def create_channel_with_inbox(user_details) ActiveRecord::Base.transaction do expires_at = Time.current + @long_lived_token_response['expires_in'].seconds - user_details = fetch_instagram_user_details(@long_lived_token_response['access_token']) - channel_instagram = Channel::Instagram.create!( access_token: @long_lived_token_response['access_token'], instagram_id: user_details['user_id'].to_s, @@ -100,6 +138,8 @@ class Instagram::CallbacksController < ApplicationController channel: channel_instagram, name: user_details['username'] ) + + channel_instagram end end diff --git a/app/javascript/dashboard/helper/inbox.js b/app/javascript/dashboard/helper/inbox.js index b608f4110..ff6d73c84 100644 --- a/app/javascript/dashboard/helper/inbox.js +++ b/app/javascript/dashboard/helper/inbox.js @@ -9,6 +9,7 @@ export const INBOX_TYPES = { TELEGRAM: 'Channel::Telegram', LINE: 'Channel::Line', SMS: 'Channel::Sms', + INSTAGRAM: 'Channel::Instagram', }; const INBOX_ICON_MAP_FILL = { @@ -20,6 +21,7 @@ const INBOX_ICON_MAP_FILL = { [INBOX_TYPES.EMAIL]: 'i-ri-mail-fill', [INBOX_TYPES.TELEGRAM]: 'i-ri-telegram-fill', [INBOX_TYPES.LINE]: 'i-ri-line-fill', + [INBOX_TYPES.INSTAGRAM]: 'i-ri-instagram-fill', }; const DEFAULT_ICON_FILL = 'i-ri-chat-1-fill'; @@ -33,6 +35,7 @@ const INBOX_ICON_MAP_LINE = { [INBOX_TYPES.EMAIL]: 'i-ri-mail-line', [INBOX_TYPES.TELEGRAM]: 'i-ri-telegram-line', [INBOX_TYPES.LINE]: 'i-ri-line-line', + [INBOX_TYPES.INSTAGRAM]: 'i-ri-instagram-line', }; const DEFAULT_ICON_LINE = 'i-ri-chat-1-line'; @@ -118,6 +121,9 @@ export const getInboxClassByType = (type, phoneNumber) => { case INBOX_TYPES.LINE: return 'brand-line'; + case INBOX_TYPES.INSTAGRAM: + return 'brand-instagram'; + default: return 'chat'; } diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index 3834b0a3d..056fcc78a 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -49,7 +49,7 @@ "CONTINUE_WITH_INSTAGRAM": "Continue with Instagram", "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": "Something went wrong with your Instagram authentication, please try again" + "ERROR_AUTH": "There was an error connecting to Instagram, please try again" }, "TWITTER": { "HELP": "To add your Twitter profile as a channel, you need to authenticate your Twitter Profile by clicking on 'Sign in with Twitter' ", diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue index 8bd2f3e6f..845206d1c 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue @@ -7,6 +7,7 @@ import SettingIntroBanner from 'dashboard/components/widgets/SettingIntroBanner. import SettingsSection from '../../../../components/SettingsSection.vue'; import inboxMixin from 'shared/mixins/inboxMixin'; import FacebookReauthorize from './facebook/Reauthorize.vue'; +import InstagramReauthorize from './channels/instagram/Reauthorize.vue'; import MicrosoftReauthorize from './channels/microsoft/Reauthorize.vue'; import GoogleReauthorize from './channels/google/Reauthorize.vue'; import PreChatFormSettings from './PreChatForm/Settings.vue'; @@ -36,6 +37,7 @@ export default { MicrosoftReauthorize, GoogleReauthorize, NextButton, + InstagramReauthorize, }, mixins: [inboxMixin], setup() { @@ -202,6 +204,9 @@ export default { return true; return false; }, + instagramUnauthorized() { + return this.isAInstagramChannel && this.inbox.reauthorization_required; + }, microsoftUnauthorized() { return this.isAMicrosoftInbox && this.inbox.reauthorization_required; }, @@ -383,10 +388,11 @@ export default { /> -
+
+
+import { ref } from 'vue'; +import InboxReconnectionRequired from '../../components/InboxReconnectionRequired.vue'; + +import instagramClient from 'dashboard/api/channel/instagramClient'; + +import { useI18n } from 'vue-i18n'; +import { useAlert } from 'dashboard/composables'; + +const { t } = useI18n(); + +const isRequestingAuthorization = ref(false); + +async function requestAuthorization() { + try { + isRequestingAuthorization.value = true; + const response = await instagramClient.generateAuthorization(); + + const { + data: { url }, + } = response; + + window.location.href = url; + } catch (error) { + useAlert(t('INBOX_MGMT.ADD.INSTAGRAM.ERROR_AUTH')); + } finally { + isRequestingAuthorization.value = false; + } +} + + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/ChannelName.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/ChannelName.vue index 91cc2282e..b7b0a2c1d 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/components/ChannelName.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/components/ChannelName.vue @@ -28,6 +28,7 @@ const i18nMap = { 'Channel::Telegram': 'TELEGRAM', 'Channel::Line': 'LINE', 'Channel::Api': 'API', + 'Channel::Instagram': 'INSTAGRAM', }; const twilioChannelName = () => { diff --git a/app/javascript/shared/mixins/inboxMixin.js b/app/javascript/shared/mixins/inboxMixin.js index 82ee9db9e..94f5997f7 100644 --- a/app/javascript/shared/mixins/inboxMixin.js +++ b/app/javascript/shared/mixins/inboxMixin.js @@ -121,6 +121,9 @@ export default { this.isATwilioWhatsAppChannel ); }, + isAInstagramChannel() { + return this.channelType === INBOX_TYPES.INSTAGRAM; + }, }, methods: { inboxHasFeature(feature) { diff --git a/app/mailers/administrator_notifications/channel_notifications_mailer.rb b/app/mailers/administrator_notifications/channel_notifications_mailer.rb index e884b3df9..05508a4ce 100644 --- a/app/mailers/administrator_notifications/channel_notifications_mailer.rb +++ b/app/mailers/administrator_notifications/channel_notifications_mailer.rb @@ -4,6 +4,11 @@ class AdministratorNotifications::ChannelNotificationsMailer < AdministratorNoti send_notification(subject, action_url: inbox_url(inbox)) end + def instagram_disconnect(inbox) + subject = 'Your Instagram connection has expired' + send_notification(subject, action_url: inbox_url(inbox)) + end + def whatsapp_disconnect(inbox) subject = 'Your Whatsapp connection has expired' send_notification(subject, action_url: inbox_url(inbox)) diff --git a/app/models/channel/instagram.rb b/app/models/channel/instagram.rb index b5ce02ce7..092973fcd 100644 --- a/app/models/channel/instagram.rb +++ b/app/models/channel/instagram.rb @@ -19,6 +19,8 @@ class Channel::Instagram < ApplicationRecord include Reauthorizable self.table_name = 'channel_instagram' + AUTHORIZATION_ERROR_THRESHOLD = 1 + validates :access_token, presence: true validates :instagram_id, uniqueness: true, presence: true diff --git a/app/models/concerns/reauthorizable.rb b/app/models/concerns/reauthorizable.rb index 32de1a8ef..94bbed0d9 100644 --- a/app/models/concerns/reauthorizable.rb +++ b/app/models/concerns/reauthorizable.rb @@ -39,18 +39,7 @@ module Reauthorizable def prompt_reauthorization! ::Redis::Alfred.set(reauthorization_required_key, true) - case self.class.name - when 'Integrations::Hook' - process_integration_hook_reauthorization_emails - when 'Channel::FacebookPage' - send_channel_reauthorization_email(:facebook_disconnect) - when 'Channel::Whatsapp' - send_channel_reauthorization_email(:whatsapp_disconnect) - when 'Channel::Email' - send_channel_reauthorization_email(:email_disconnect) - when 'AutomationRule' - handle_automation_rule_reauthorization - end + reauthorization_handlers[self.class.name]&.call(self) invalidate_inbox_cache unless instance_of?(::AutomationRule) end @@ -82,6 +71,17 @@ module Reauthorizable private + def reauthorization_handlers + { + 'Integrations::Hook' => ->(obj) { obj.process_integration_hook_reauthorization_emails }, + 'Channel::FacebookPage' => ->(obj) { obj.send_channel_reauthorization_email(:facebook_disconnect) }, + 'Channel::Instagram' => ->(obj) { obj.send_channel_reauthorization_email(:instagram_disconnect) }, + 'Channel::Whatsapp' => ->(obj) { obj.send_channel_reauthorization_email(:whatsapp_disconnect) }, + 'Channel::Email' => ->(obj) { obj.send_channel_reauthorization_email(:email_disconnect) }, + 'AutomationRule' => ->(obj) { obj.handle_automation_rule_reauthorization } + } + end + def invalidate_inbox_cache inbox.update_account_cache if inbox.present? end diff --git a/app/models/inbox.rb b/app/models/inbox.rb index 508675858..20c8dc610 100644 --- a/app/models/inbox.rb +++ b/app/models/inbox.rb @@ -107,7 +107,11 @@ class Inbox < ApplicationRecord end def instagram? - facebook? && channel.instagram_id.present? + (facebook? || instagram_direct?) && channel.instagram_id.present? + end + + def instagram_direct? + channel_type == 'Channel::Instagram' end def web_widget? diff --git a/app/views/api/v1/models/_inbox.json.jbuilder b/app/views/api/v1/models/_inbox.json.jbuilder index 9cad3edc1..9527b0787 100644 --- a/app/views/api/v1/models/_inbox.json.jbuilder +++ b/app/views/api/v1/models/_inbox.json.jbuilder @@ -54,6 +54,9 @@ if resource.facebook? json.reauthorization_required resource.channel.try(:reauthorization_required?) end +## Instagram Attributes +json.reauthorization_required resource.channel.try(:reauthorization_required?) if resource.instagram? + ## Twilio Attributes json.messaging_service_sid resource.channel.try(:messaging_service_sid) json.phone_number resource.channel.try(:phone_number) diff --git a/app/views/mailers/administrator_notifications/channel_notifications_mailer/instagram_disconnect.liquid b/app/views/mailers/administrator_notifications/channel_notifications_mailer/instagram_disconnect.liquid new file mode 100644 index 000000000..d1d4e6345 --- /dev/null +++ b/app/views/mailers/administrator_notifications/channel_notifications_mailer/instagram_disconnect.liquid @@ -0,0 +1,8 @@ +

Hello,

+ +

Your Instagram Inbox Access has expired.

+

Please reconnect Instagram to continue receiving messages.

+ +

+Click here to re-connect. +

\ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 375d60b0c..3ec088890 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -22,6 +22,7 @@ Rails.application.routes.draw do get '/app/accounts/:account_id/settings/inboxes/new/:inbox_id/agents', to: 'dashboard#index', as: 'app_twitter_inbox_agents' get '/app/accounts/:account_id/settings/inboxes/new/:inbox_id/agents', to: 'dashboard#index', as: 'app_email_inbox_agents' get '/app/accounts/:account_id/settings/inboxes/new/:inbox_id/agents', to: 'dashboard#index', as: 'app_instagram_inbox_agents' + get '/app/accounts/:account_id/settings/inboxes/:inbox_id', to: 'dashboard#index', as: 'app_instagram_inbox_settings' get '/app/accounts/:account_id/settings/inboxes/:inbox_id', to: 'dashboard#index', as: 'app_email_inbox_settings' resource :widget, only: [:show] diff --git a/spec/mailers/administrator_notifications/channel_notifications_mailer_spec.rb b/spec/mailers/administrator_notifications/channel_notifications_mailer_spec.rb index 1be1314da..e5cd7327b 100644 --- a/spec/mailers/administrator_notifications/channel_notifications_mailer_spec.rb +++ b/spec/mailers/administrator_notifications/channel_notifications_mailer_spec.rb @@ -44,4 +44,18 @@ RSpec.describe AdministratorNotifications::ChannelNotificationsMailer do expect(mail.to).to eq([administrator.email]) end end + + describe 'instagram_disconnect' do + let!(:instagram_channel) { create(:channel_instagram, account: account) } + let!(:instagram_inbox) { create(:inbox, channel: instagram_channel, account: account) } + let(:mail) { described_class.with(account: account).instagram_disconnect(instagram_inbox).deliver_now } + + it 'renders the subject' do + expect(mail.subject).to eq('Your Instagram connection has expired') + end + + it 'renders the receiver email' do + expect(mail.to).to eq([administrator.email]) + end + end end diff --git a/spec/models/channel/instagram_spec.rb b/spec/models/channel/instagram_spec.rb index 901fe392e..a3cea9e92 100644 --- a/spec/models/channel/instagram_spec.rb +++ b/spec/models/channel/instagram_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'rails_helper' +require Rails.root.join 'spec/models/concerns/reauthorizable_shared.rb' RSpec.describe Channel::Instagram do let(:channel) { create(:channel_instagram) } @@ -14,4 +15,21 @@ RSpec.describe Channel::Instagram do it 'has a valid name' do expect(channel.name).to eq('Instagram') end + + describe 'concerns' do + it_behaves_like 'reauthorizable' + + context 'when prompt_reauthorization!' do + it 'calls channel notifier mail for instagram' do + admin_mailer = double + mailer_double = double + + expect(AdministratorNotifications::ChannelNotificationsMailer).to receive(:with).and_return(admin_mailer) + expect(admin_mailer).to receive(:instagram_disconnect).with(channel.inbox).and_return(mailer_double) + expect(mailer_double).to receive(:deliver_later) + + channel.prompt_reauthorization! + end + end + end end diff --git a/spec/models/concerns/reauthorizable_shared.rb b/spec/models/concerns/reauthorizable_shared.rb index a71800267..558312e6c 100644 --- a/spec/models/concerns/reauthorizable_shared.rb +++ b/spec/models/concerns/reauthorizable_shared.rb @@ -48,10 +48,12 @@ shared_examples_for 'reauthorizable' do facebook_mailer_response = instance_double(ActionMailer::MessageDelivery, deliver_later: true) whatsapp_mailer_response = instance_double(ActionMailer::MessageDelivery, deliver_later: true) email_mailer_response = instance_double(ActionMailer::MessageDelivery, deliver_later: true) + instagram_mailer_response = instance_double(ActionMailer::MessageDelivery, deliver_later: true) allow(AdministratorNotifications::ChannelNotificationsMailer).to receive(:with).and_return(channel_mailer) allow(channel_mailer).to receive(:facebook_disconnect).and_return(facebook_mailer_response) allow(channel_mailer).to receive(:whatsapp_disconnect).and_return(whatsapp_mailer_response) allow(channel_mailer).to receive(:email_disconnect).and_return(email_mailer_response) + allow(channel_mailer).to receive(:instagram_disconnect).and_return(instagram_mailer_response) end describe 'prompt_reauthorization!' do