Compare commits

...
Author SHA1 Message Date
Shivam MishraandGitHub 97671f3dc0 Merge branch 'develop' into fix/article-view 2024-10-09 15:01:16 +05:30
Shivam MishraandGitHub f18ed01eb7 feat: use of imap login as default if present (#10249)
When moving form using Gmail Legacy auth to using OAuth, we need the
email address that will be used to connect. This is because we need to
store this email address in the cache and reuse when we get the callback
to find the associated inbox.

However there are cases where the imap login might be
`support@company.com` and the email used to communicate will be
`contact@company.com` (Probably an alias) In that case, we need to send
the correct email address to Chatwoot when re-authenticating

At the moment, we used the inbox email. This PR adds a check that
defaults to to `imap_login` if that is available and imap is enabled

This PR also fixes an unrelated problem where the email inbox creation
flow was not working

---

Tested it, it is working correctly

![CleanShot 2024-10-09 at 14 23
47@2x](https://github.com/user-attachments/assets/0e2cb6c8-1224-4b45-b34a-7b19611249bc)
2024-10-09 15:01:11 +05:30
aa5fa0c758 feat: Use CJS build for SDK (#10247)
The UMD build was causing issues for a few customers, this PR reverts to
using CJS like used in Webpack 4 before the vite migration

---------

Co-authored-by: Pranav <pranavrajs@gmail.com>
Co-authored-by: Pranav <pranav@chatwoot.com>
2024-10-09 14:09:52 +05:30
Shivam MishraandGitHub 42eca69763 fix: Import for vue-letter (#10246) 2024-10-08 09:40:37 -07:00
Shivam MishraandGitHub 3a78192e74 fix: Resolve accountId from the route, initialize route-sync before the app is loaded (#10245)
On production on multiple instances it may happen that the UI is
rendered in correctly, with a lot of options in the sidebar not
available. On further investigation I found out that the feature flag
checks were disabling multiple of those, and also we could see many
correlated errors that pointed towards missing information.

So, there were two problems here

1. The `vuex-router-sync` was not very reliable in some cases
2. In `App.vue` the watch on `currentAccountId` didn't always trigger.

## Fix Tested on Staging

Basically tried to reload the page ~50 times with cache enabled,
disabled, throttling, navigating different pages.


https://www.loom.com/share/1bb27294aa364ac4acfb647780d6385a?sid=87e31330-8cb7-4ded-8616-5e95e2ae3516

<details><summary>

#### What I thought was the fix

</summary>
<p>

### My chain of actions

Replacing vuex-router-sync at first worked fine, but then I saw it was
still failing in some cases, I assumed (I was half-correct tho) that the
rendering of the `App.vue` and syncing of the route to the store was not
happening in a synchronous pattern. So I decided, let's not rely on the
store when the route is directly available in the App context.

Following this, I refactored `useAccount` composable to use `useRoute`
directly, instead of the store, and then replaced the getter inside
`App.vue`. What this did was surface the issue but more consistently 🤯

I saw the watcher, added some console logs, and turns out it was not
getting triggered in all those cases. So I added an `immediate` to it.
And viola, it works!

At the moment, this is deployed to staging and seems to be working
correctly. But we still need to verify it for sure, since how this issue
was surfaced is still a mystery. All we know is that it shows up when
the widget is also loaded alongside the app (if it loads before or after
the app, it works fine)

### What about the route in the store?

Well I have used the `route` usage there with fallback to the store
state. Since Vuex exists in the app context, the route should always be
available to it. But after today I have lost all trust in JavaScript and
will worship rails until end of my life, so I added that in a
`try-catch` block, logged the error to Sentry

</p>
</details> 

## Here's the real fix

If you read the explanation I wrote earlier, I thought I fixed the
issue, but then the chat list navigation completely broke. So I removed
the custom route sync implementation and added the original package
back. Turns out the vuex-router-sync earlier was placed after the app
was initalized, however for it to work, the vue app context is not
required. And it's best to run it before the app is even bootstrapped,
so I added it back and placed it correctly.

So the following changes fixes this problem

1. Hoisting the `sync` function call to before we call `createApp` this
ensures that the stores and route hooks are in place before even the app
is created
2. Ensuring the `initializeAccount` is run immediately when watching
`currentAccountId`
4. Source `currentAccountId` for critical top of the tree components
directly from the route instead of the store
2024-10-08 09:25:51 -07:00
Shivam MishraandGitHub 02894ba221 Merge branch 'develop' into fix/article-view 2024-10-07 15:36:24 +05:30
Shivam Mishra 1c3f6f46fe feat: use article props 2024-10-05 18:42:03 +05:30
16 changed files with 72 additions and 39 deletions
@@ -1,6 +1,15 @@
class Google::CallbacksController < OauthCallbackController
include GoogleConcern
def find_channel_by_email
# find by imap_login first, and then by email
# this ensures the legacy users can migrate correctly even if inbox email address doesn't match
imap_channel = Channel::Email.find_by(imap_login: users_data['email'], account: account)
return imap_channel if imap_channel
Channel::Email.find_by(email: users_data['email'], account: account)
end
private
def provider_name
+5 -1
View File
@@ -25,7 +25,7 @@ class OauthCallbackController < ApplicationController
end
def find_or_create_inbox
channel_email = Channel::Email.find_by(email: users_data['email'], account: account)
channel_email = find_channel_by_email
# we need this value to know where to redirect on sucessful processing of the callback
channel_exists = channel_email.present?
@@ -39,6 +39,10 @@ class OauthCallbackController < ApplicationController
[channel_email.inbox, channel_exists]
end
def find_channel_by_email
Channel::Email.find_by(email: users_data['email'], account: account)
end
def update_channel(channel_email)
channel_email.update!({
imap_login: users_data['email'], imap_address: imap_address,
+10 -6
View File
@@ -13,6 +13,7 @@ import { useStore } from 'dashboard/composables/store';
import WootSnackbarBox from './components/SnackbarContainer.vue';
import { setColorTheme } from './helper/themeHelper';
import { isOnOnboardingView } from 'v3/helpers/RouteHelper';
import { useAccount } from 'dashboard/composables/useAccount';
import {
registerSubscription,
verifyServiceWorkerExistence,
@@ -35,8 +36,9 @@ export default {
setup() {
const router = useRouter();
const store = useStore();
const { accountId } = useAccount();
return { router, store };
return { router, store, currentAccountId: accountId };
},
data() {
return {
@@ -52,7 +54,6 @@ export default {
currentUser: 'getCurrentUser',
authUIFlags: 'getAuthUIFlags',
accountUIFlags: 'accounts/getUIFlags',
currentAccountId: 'getCurrentAccountId',
}),
hasAccounts() {
const { accounts = [] } = this.currentUser || {};
@@ -69,10 +70,13 @@ export default {
this.showAddAccountModal = true;
}
},
currentAccountId() {
if (this.currentAccountId) {
this.initializeAccount();
}
currentAccountId: {
immediate: true,
handler() {
if (this.currentAccountId) {
this.initializeAccount();
}
},
},
},
mounted() {
@@ -2,6 +2,7 @@
import { mapGetters } from 'vuex';
import { getSidebarItems } from './config/default-sidebar';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import { useAccount } from 'dashboard/composables/useAccount';
import { useRoute, useRouter } from 'vue-router';
import PrimarySidebar from './sidebarComponents/Primary.vue';
@@ -33,6 +34,7 @@ export default {
setup(props, { emit }) {
const route = useRoute();
const router = useRouter();
const { accountId } = useAccount();
const toggleKeyShortcutModal = () => {
emit('openKeyShortcutModal');
@@ -72,6 +74,7 @@ export default {
return {
toggleKeyShortcutModal,
accountId,
};
},
data() {
@@ -82,7 +85,6 @@ export default {
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
currentUser: 'getCurrentUser',
globalConfig: 'globalConfig/get',
inboxes: 'inboxes/getInboxes',
@@ -1,5 +1,5 @@
<script>
import Letter from 'vue-letter';
import { Letter } from 'vue-letter';
import GalleryView from '../components/GalleryView.vue';
export default {
@@ -1,14 +1,15 @@
import { ref } from 'vue';
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useAccount } from '../useAccount';
import { useStoreGetters } from 'dashboard/composables/store';
import { useRoute } from 'vue-router';
vi.mock('dashboard/composables/store');
vi.mock('vue-router');
describe('useAccount', () => {
beforeEach(() => {
useStoreGetters.mockReturnValue({
getCurrentAccountId: ref(123),
useRoute.mockReturnValue({
params: {
accountId: 123,
},
});
});
@@ -1,18 +1,20 @@
import { computed } from 'vue';
import { useStoreGetters } from 'dashboard/composables/store';
import { useRoute } from 'vue-router';
/**
* Composable for account-related operations.
* @returns {Object} An object containing account-related properties and methods.
*/
export function useAccount() {
const getters = useStoreGetters();
/**
* Computed property for the current account ID.
* @type {import('vue').ComputedRef<number>}
*/
const accountId = computed(() => getters.getCurrentAccountId.value);
const route = useRoute();
const accountId = computed(() => {
return Number(route.params.accountId);
});
/**
* Generates an account-scoped URL.
@@ -5,6 +5,7 @@ import { mapGetters } from 'vuex';
import { useAlert } from 'dashboard/composables';
import { useUISettings } from 'dashboard/composables/useUISettings';
import { useConfig } from 'dashboard/composables/useConfig';
import { useAccount } from 'dashboard/composables/useAccount';
import { FEATURE_FLAGS } from '../../../../featureFlags';
import semver from 'semver';
import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
@@ -13,9 +14,10 @@ export default {
setup() {
const { updateUISettings } = useUISettings();
const { enabledLanguages } = useConfig();
const { accountId } = useAccount();
const v$ = useVuelidate();
return { updateUISettings, v$, enabledLanguages };
return { updateUISettings, v$, enabledLanguages, accountId };
},
data() {
return {
@@ -46,7 +48,6 @@ export default {
globalConfig: 'globalConfig/get',
getAccount: 'accounts/getAccount',
uiFlags: 'accounts/getUIFlags',
accountId: 'getCurrentAccountId',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
}),
showAutoResolutionConfig() {
@@ -47,7 +47,7 @@ const emailProviderList = computed(() => {
function onClick(emailProvider) {
if (emailProvider.isEnabled) {
this.provider = emailProvider.key;
provider.value = emailProvider.key;
}
}
</script>
@@ -1,5 +1,5 @@
<script setup>
import { ref } from 'vue';
import { ref, computed } from 'vue';
import InboxReconnectionRequired from '../../components/InboxReconnectionRequired.vue';
import googleClient from 'dashboard/api/channel/googleClient';
@@ -17,11 +17,18 @@ const { t } = useI18n();
const isRequestingAuthorization = ref(false);
const inboxEmail = computed(() => {
if (props.inbox.imap_login && props.inbox.imap_enabled) {
return props.inbox.imap_login;
}
return props.inbox.email;
});
async function requestAuthorization() {
try {
isRequestingAuthorization.value = true;
const response = await googleClient.generateAuthorization({
email: props.inbox.email,
email: inboxEmail.value,
});
const {
+3 -2
View File
@@ -10,7 +10,6 @@ import Multiselect from 'vue-multiselect';
import { plugin, defaultConfig } from '@formkit/vue';
import WootSwitch from 'components/ui/Switch.vue';
import WootWizard from 'components/ui/Wizard.vue';
import { sync } from 'vuex-router-sync';
import FloatingVue from 'floating-vue';
import WootUiKit from 'dashboard/components';
import App from 'dashboard/App.vue';
@@ -18,6 +17,7 @@ import i18nMessages from 'dashboard/i18n';
import createAxios from 'dashboard/helper/APIHelper';
import commonHelpers, { isJSONValid } from 'dashboard/helper/commons';
import { sync } from 'vuex-router-sync';
import router, { initalizeRouter } from 'dashboard/routes';
import store from 'dashboard/store';
import constants from 'dashboard/constants/globals';
@@ -42,6 +42,8 @@ const i18n = createI18n({
messages: i18nMessages,
});
sync(store, router);
const app = createApp(App);
app.use(i18n);
app.use(store);
@@ -97,7 +99,6 @@ app.component('fluent-icon', FluentIcon);
app.directive('resize', vResizeObserver);
app.directive('on-clickaway', onClickaway);
sync(store, router);
// load common helpers into js
commonHelpers();
window.WOOT_STORE = store;
+2 -1
View File
@@ -34,8 +34,9 @@ export default createRouter({
component: () => import('./views/Messages.vue'),
},
{
path: '/article',
path: '/article/:link',
name: 'article-viewer',
props: true,
component: () => import('./views/ArticleViewer.vue'),
},
],
@@ -1,16 +1,16 @@
<script>
<script setup>
import IframeLoader from 'shared/components/IframeLoader.vue';
export default {
name: 'ArticleViewer',
components: {
IframeLoader,
defineProps({
link: {
type: String,
required: true,
},
};
});
</script>
<template>
<div class="bg-white h-full">
<IframeLoader :url="$route.query.link" />
<IframeLoader :url="link" />
</div>
</template>
+1 -1
View File
@@ -79,7 +79,7 @@ export default {
}
this.$router.push({
name: 'article-viewer',
query: { link: linkToOpen },
params: { link: linkToOpen },
});
},
viewAllArticles() {
@@ -39,8 +39,9 @@ RSpec.describe 'Google::CallbacksController', type: :request do
expect(Redis::Alfred.get(cache_key)).to be_nil
end
it 'creates updates inbox channel config if inbox exists and authentication is successful' do
inbox = create(:channel_email, account: account, email: email)&.inbox
it 'updates inbox channel config if inbox exists with imap_login and authentication is successful' do
channel_email = create(:channel_email, account: account, imap_login: email)
inbox = channel_email.inbox
expect(inbox.channel.provider_config).to eq({})
stub_request(:post, 'https://accounts.google.com/o/oauth2/token')
@@ -50,7 +51,7 @@ RSpec.describe 'Google::CallbacksController', type: :request do
get google_callback_url, params: { code: code }
expect(response).to redirect_to app_email_inbox_settings_url(account_id: account.id, inbox_id: account.inboxes.last.id)
expect(response).to redirect_to app_email_inbox_settings_url(account_id: account.id, inbox_id: inbox.id)
expect(account.inboxes.count).to be 1
expect(inbox.channel.reload.provider_config.keys).to include('access_token', 'refresh_token', 'expires_on')
expect(inbox.channel.reload.provider_config['access_token']).to eq response_body_success[:access_token]
+1 -1
View File
@@ -66,7 +66,7 @@ export default defineConfig({
lib: isLibraryMode
? {
entry: path.resolve(__dirname, './app/javascript/entrypoints/sdk.js'),
formats: ['umd'], // UMD format for single file
formats: ['cjs'], // CJS format for single file
name: 'sdk',
}
: undefined,