@vendure/dashboard 3.5.3-master-202601070244 → 3.5.3-master-202601100240

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ import type { Plugin } from 'vite';
2
+ /**
3
+ * @description
4
+ * A custom Vite plugin that transforms Lingui macros in files using Babel instead of SWC.
5
+ *
6
+ * This plugin solves a critical compatibility issue with SWC plugins:
7
+ * - SWC plugins are compiled Wasm binaries that require exact version matching with `@swc/core`
8
+ * - When users have different SWC versions in their projects (e.g., from Next.js, Nx, etc.),
9
+ * the Lingui SWC plugin fails with "failed to invoke plugin" errors
10
+ * - Babel has no such binary compatibility issues, making it much more reliable for library code
11
+ *
12
+ * The plugin runs BEFORE `@vitejs/plugin-react` and transforms files containing Lingui macros
13
+ * (imports from `@lingui/core/macro` or `@lingui/react/macro`) using the Babel-based
14
+ * `@lingui/babel-plugin-lingui-macro`.
15
+ *
16
+ * Files processed:
17
+ * - `@vendure/dashboard/src` files (in node_modules for external projects)
18
+ * - `packages/dashboard/src` files (in monorepo development)
19
+ * - User's dashboard extension files (e.g., custom plugins using Lingui)
20
+ *
21
+ * Files NOT processed:
22
+ * - Other node_modules packages (they shouldn't contain Lingui macros)
23
+ *
24
+ * @see https://github.com/vendurehq/vendure/issues/3929
25
+ * @see https://github.com/lingui/swc-plugin/issues/179
26
+ */
27
+ export declare function linguiBabelPlugin(): Plugin;
@@ -0,0 +1,86 @@
1
+ import * as babel from '@babel/core';
2
+ /**
3
+ * @description
4
+ * A custom Vite plugin that transforms Lingui macros in files using Babel instead of SWC.
5
+ *
6
+ * This plugin solves a critical compatibility issue with SWC plugins:
7
+ * - SWC plugins are compiled Wasm binaries that require exact version matching with `@swc/core`
8
+ * - When users have different SWC versions in their projects (e.g., from Next.js, Nx, etc.),
9
+ * the Lingui SWC plugin fails with "failed to invoke plugin" errors
10
+ * - Babel has no such binary compatibility issues, making it much more reliable for library code
11
+ *
12
+ * The plugin runs BEFORE `@vitejs/plugin-react` and transforms files containing Lingui macros
13
+ * (imports from `@lingui/core/macro` or `@lingui/react/macro`) using the Babel-based
14
+ * `@lingui/babel-plugin-lingui-macro`.
15
+ *
16
+ * Files processed:
17
+ * - `@vendure/dashboard/src` files (in node_modules for external projects)
18
+ * - `packages/dashboard/src` files (in monorepo development)
19
+ * - User's dashboard extension files (e.g., custom plugins using Lingui)
20
+ *
21
+ * Files NOT processed:
22
+ * - Other node_modules packages (they shouldn't contain Lingui macros)
23
+ *
24
+ * @see https://github.com/vendurehq/vendure/issues/3929
25
+ * @see https://github.com/lingui/swc-plugin/issues/179
26
+ */
27
+ export function linguiBabelPlugin() {
28
+ return {
29
+ name: 'vendure:lingui-babel',
30
+ // Run BEFORE @vitejs/plugin-react so the macros are already transformed
31
+ // when the react plugin processes the file
32
+ enforce: 'pre',
33
+ async transform(code, id) {
34
+ // Strip query params for path matching (Vite adds ?v=xxx for cache busting)
35
+ const cleanId = id.split('?')[0];
36
+ // Only process TypeScript/JavaScript files
37
+ if (!/\.[tj]sx?$/.test(cleanId)) {
38
+ return null;
39
+ }
40
+ // Only process files that actually contain Lingui macro imports
41
+ // This is a fast check to avoid running Babel on files that don't need it
42
+ if (!code.includes('@lingui/') || !code.includes('/macro')) {
43
+ return null;
44
+ }
45
+ // Skip node_modules files EXCEPT for @vendure/dashboard source
46
+ // This ensures:
47
+ // 1. Dashboard source files get transformed (both in monorepo and external projects)
48
+ // 2. User's extension files get transformed (not in node_modules)
49
+ // 3. Other node_modules packages are left alone
50
+ if (cleanId.includes('node_modules')) {
51
+ const isVendureDashboard = cleanId.includes('@vendure/dashboard/src') || cleanId.includes('packages/dashboard/src');
52
+ if (!isVendureDashboard) {
53
+ return null;
54
+ }
55
+ }
56
+ try {
57
+ const result = await babel.transformAsync(code, {
58
+ filename: id,
59
+ presets: [
60
+ ['@babel/preset-typescript', { isTSX: true, allExtensions: true }],
61
+ ['@babel/preset-react', { runtime: 'automatic' }],
62
+ ],
63
+ plugins: ['@lingui/babel-plugin-lingui-macro'],
64
+ sourceMaps: true,
65
+ // Don't look for babel config files - we want to control the config completely
66
+ configFile: false,
67
+ babelrc: false,
68
+ });
69
+ if (!(result === null || result === void 0 ? void 0 : result.code)) {
70
+ return null;
71
+ }
72
+ return {
73
+ code: result.code,
74
+ map: result.map,
75
+ };
76
+ }
77
+ catch (error) {
78
+ // Log the error but don't crash - let the build continue
79
+ // The lingui vite plugin will catch untransformed macros later
80
+ // eslint-disable-next-line no-console
81
+ console.error(`[vendure:lingui-babel] Failed to transform ${id}:`, error);
82
+ return null;
83
+ }
84
+ },
85
+ };
86
+ }
@@ -105,6 +105,7 @@ export type VitePluginVendureDashboardOptions = {
105
105
  */
106
106
  disablePlugins?: {
107
107
  tanstackRouter?: boolean;
108
+ linguiBabel?: boolean;
108
109
  react?: boolean;
109
110
  lingui?: boolean;
110
111
  themeVariables?: boolean;
@@ -1,7 +1,7 @@
1
1
  import { lingui } from '@lingui/vite-plugin';
2
2
  import tailwindcss from '@tailwindcss/vite';
3
3
  import { tanstackRouter } from '@tanstack/router-plugin/vite';
4
- import react from '@vitejs/plugin-react-swc';
4
+ import react from '@vitejs/plugin-react';
5
5
  import path from 'path';
6
6
  import { adminApiSchemaPlugin } from './vite-plugin-admin-api-schema.js';
7
7
  import { configLoaderPlugin } from './vite-plugin-config-loader.js';
@@ -9,6 +9,7 @@ import { viteConfigPlugin } from './vite-plugin-config.js';
9
9
  import { dashboardMetadataPlugin } from './vite-plugin-dashboard-metadata.js';
10
10
  import { gqlTadaPlugin } from './vite-plugin-gql-tada.js';
11
11
  import { hmrPlugin } from './vite-plugin-hmr.js';
12
+ import { linguiBabelPlugin } from './vite-plugin-lingui-babel.js';
12
13
  import { dashboardTailwindSourcePlugin } from './vite-plugin-tailwind-source.js';
13
14
  import { themeVariablesPlugin } from './vite-plugin-theme.js';
14
15
  import { transformIndexHtmlPlugin } from './vite-plugin-transform-index.js';
@@ -47,11 +48,18 @@ export function vendureDashboardPlugin(options) {
47
48
  generatedRouteTree: path.join(packageRoot, 'src/app/routeTree.gen.ts'),
48
49
  }),
49
50
  },
51
+ {
52
+ // Custom plugin that transforms Lingui macros using Babel instead of SWC.
53
+ // This runs BEFORE the react plugin to ensure macros are transformed first.
54
+ // Using Babel eliminates the SWC binary compatibility issues that caused
55
+ // "failed to invoke plugin" errors in external projects.
56
+ // See: https://github.com/vendurehq/vendure/issues/3929
57
+ key: 'linguiBabel',
58
+ plugin: () => linguiBabelPlugin(),
59
+ },
50
60
  {
51
61
  key: 'react',
52
- plugin: () => react({
53
- plugins: [['@lingui/swc-plugin', {}]],
54
- }),
62
+ plugin: () => react(),
55
63
  },
56
64
  {
57
65
  key: 'lingui',
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@vendure/dashboard",
3
3
  "private": false,
4
- "version": "3.5.3-master-202601070244",
4
+ "version": "3.5.3-master-202601100240",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/vendure-ecommerce/vendure"
8
+ "url": "https://github.com/vendurehq/vendure"
9
9
  },
10
10
  "homepage": "https://www.vendure.io",
11
11
  "funding": "https://github.com/sponsors/michaelbromley",
@@ -57,15 +57,18 @@
57
57
  "index.html"
58
58
  ],
59
59
  "dependencies": {
60
+ "@babel/core": "^7.26.0",
61
+ "@babel/preset-react": "^7.26.3",
62
+ "@babel/preset-typescript": "^7.26.0",
60
63
  "@dnd-kit/core": "^6.3.1",
61
64
  "@dnd-kit/modifiers": "^9.0.0",
62
65
  "@dnd-kit/sortable": "^10.0.0",
63
66
  "@hookform/resolvers": "^4.1.3",
64
- "@lingui/cli": "^5.5.0",
65
- "@lingui/core": "^5.5.0",
66
- "@lingui/react": "^5.5.0",
67
- "@lingui/swc-plugin": "5.6.1",
68
- "@lingui/vite-plugin": "^5.5.0",
67
+ "@lingui/babel-plugin-lingui-macro": "^5.7.0",
68
+ "@lingui/cli": "^5.7.0",
69
+ "@lingui/core": "^5.7.0",
70
+ "@lingui/react": "^5.7.0",
71
+ "@lingui/vite-plugin": "^5.7.0",
69
72
  "@radix-ui/react-accordion": "^1.2.11",
70
73
  "@radix-ui/react-alert-dialog": "^1.1.14",
71
74
  "@radix-ui/react-aspect-ratio": "^1.1.7",
@@ -92,7 +95,6 @@
92
95
  "@radix-ui/react-toggle": "^1.1.9",
93
96
  "@radix-ui/react-toggle-group": "^1.1.10",
94
97
  "@radix-ui/react-tooltip": "^1.2.7",
95
- "@swc/core": "1.13.5",
96
98
  "@tailwindcss/vite": "^4.1.5",
97
99
  "@tanstack/eslint-plugin-query": "^5.66.1",
98
100
  "@tanstack/react-query": "^5.66.7",
@@ -112,7 +114,6 @@
112
114
  "@types/react-dom": "^19.0.4",
113
115
  "@uidotdev/usehooks": "^2.4.1",
114
116
  "@vitejs/plugin-react": "^5.0.4",
115
- "@vitejs/plugin-react-swc": "^4.1.0",
116
117
  "acorn": "^8.11.3",
117
118
  "acorn-walk": "^8.3.2",
118
119
  "awesome-graphql-client": "^2.1.0",
@@ -156,8 +157,8 @@
156
157
  "@storybook/addon-vitest": "^10.0.0-beta.9",
157
158
  "@storybook/react-vite": "^10.0.0-beta.9",
158
159
  "@types/node": "^22.13.4",
159
- "@vendure/common": "^3.5.3-master-202601070244",
160
- "@vendure/core": "^3.5.3-master-202601070244",
160
+ "@vendure/common": "^3.5.3-master-202601100240",
161
+ "@vendure/core": "^3.5.3-master-202601100240",
161
162
  "@vitest/browser": "^3.2.4",
162
163
  "@vitest/coverage-v8": "^3.2.4",
163
164
  "eslint": "^9.19.0",
@@ -1,6 +1,7 @@
1
1
  import { ChevronsUpDown, Languages, Plus } from 'lucide-react';
2
2
 
3
3
  import { ChannelCodeLabel } from '@/vdb/components/shared/channel-code-label.js';
4
+ import { PermissionGuard } from '@/vdb/components/shared/permission-guard.js';
4
5
  import {
5
6
  DropdownMenu,
6
7
  DropdownMenuContent,
@@ -13,6 +14,7 @@ import {
13
14
  DropdownMenuTrigger,
14
15
  } from '@/vdb/components/ui/dropdown-menu.js';
15
16
  import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/vdb/components/ui/sidebar.js';
17
+ import { ScrollArea } from '@/vdb/components/ui/scroll-area.js';
16
18
  import { DEFAULT_CHANNEL_CODE } from '@/vdb/constants.js';
17
19
  import { useChannel } from '@/vdb/hooks/use-channel.js';
18
20
  import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
@@ -61,9 +63,9 @@ export function ChannelSwitcher() {
61
63
  const availableLanguages = serverConfig?.availableLanguages || [];
62
64
  const hasMultipleLanguages = availableLanguages.length > 1;
63
65
 
64
- // Reorder channels to put the currently selected one first
66
+ // Currently selected channel is displayed separately so filter it out of the list
65
67
  const orderedChannels = displayChannel
66
- ? [displayChannel, ...channels.filter(ch => ch.id !== displayChannel.id)]
68
+ ? channels.filter(ch => ch.id !== displayChannel.id)
67
69
  : channels;
68
70
 
69
71
  // Sort language codes by their formatted names and map to code and label
@@ -79,6 +81,32 @@ export function ChannelSwitcher() {
79
81
  }
80
82
  }, [activeChannel, contentLanguage]);
81
83
 
84
+ const renderChannel = (channel: typeof channels[number]) => (
85
+ <div key={channel.code}>
86
+ <DropdownMenuItem
87
+ onClick={() => setActiveChannel(channel.id)}
88
+ className="gap-2 p-2"
89
+ >
90
+ <div
91
+ className={cn(
92
+ 'flex size-8 items-center justify-center rounded border',
93
+ channel.code === DEFAULT_CHANNEL_CODE ? 'bg-primary' : '',
94
+ )}
95
+ >
96
+ <span className="truncate font-semibold text-xs uppercase">
97
+ {getChannelInitialsFromCode(channel.code)}
98
+ </span>
99
+ </div>
100
+ <ChannelCodeLabel code={channel.code} />
101
+ {channel.id === displayChannel?.id && (
102
+ <span className="ms-auto text-xs text-muted-foreground">
103
+ <Trans context="current channel">Current</Trans>
104
+ </span>
105
+ )}
106
+ </DropdownMenuItem>
107
+ </div>
108
+ );
109
+
82
110
  return (
83
111
  <>
84
112
  <SidebarMenu>
@@ -112,99 +140,82 @@ export function ChannelSwitcher() {
112
140
  </SidebarMenuButton>
113
141
  </DropdownMenuTrigger>
114
142
  <DropdownMenuContent
115
- className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
143
+ className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg pt-0 pr-0"
116
144
  align="start"
117
145
  side={isMobile ? 'bottom' : 'right'}
118
146
  sideOffset={4}
119
147
  >
120
- <DropdownMenuLabel className="text-muted-foreground text-xs">
121
- <Trans>Channels</Trans>
122
- </DropdownMenuLabel>
123
- {orderedChannels.map((channel, index) => (
124
- <div key={channel.code}>
125
- <DropdownMenuItem
126
- onClick={() => setActiveChannel(channel.id)}
127
- className="gap-2 p-2"
128
- >
129
- <div
130
- className={cn(
131
- 'flex size-8 items-center justify-center rounded border',
132
- channel.code === DEFAULT_CHANNEL_CODE ? 'bg-primary' : '',
133
- )}
134
- >
135
- <span className="truncate font-semibold text-xs uppercase">
136
- {getChannelInitialsFromCode(channel.code)}
137
- </span>
138
- </div>
139
- <ChannelCodeLabel code={channel.code} />
140
- {channel.id === displayChannel?.id && (
141
- <span className="ms-auto text-xs text-muted-foreground">
142
- <Trans context="current channel">Current</Trans>
143
- </span>
144
- )}
145
- </DropdownMenuItem>
146
- {/* Show language sub-menu for the current channel */}
147
- {channel.id === displayChannel?.id && (
148
- <DropdownMenuSub>
149
- <DropdownMenuSubTrigger className="gap-2 p-2 ps-4">
150
- <Languages className="w-4 h-4" />
151
- <div className="flex gap-1 ms-2">
152
- <span className="text-muted-foreground">Content: </span>
153
- {formatLanguageName(contentLanguage)}
154
- </div>
155
- </DropdownMenuSubTrigger>
156
- <DropdownMenuSubContent>
157
- {sortedLanguages?.map(({ code: languageCode, label }) => (
148
+ <ScrollArea className="max-h-[calc(100vh_-_24px)] overflow-y-auto pr-1">
149
+ <div className="sticky top-0 pt-1 bg-popover z-10">
150
+ <DropdownMenuLabel className="text-muted-foreground text-xs">
151
+ <Trans>Channels</Trans>
152
+ </DropdownMenuLabel>
153
+ {!!displayChannel && (
154
+ <>
155
+ {renderChannel(displayChannel)}
156
+ {/* Show language sub-menu for the current channel */}
157
+ <DropdownMenuSub>
158
+ <DropdownMenuSubTrigger className="gap-2 p-2 ps-4">
159
+ <Languages className="w-4 h-4" />
160
+ <div className="flex gap-1 ms-2">
161
+ <span className="text-muted-foreground">Content: </span>
162
+ {formatLanguageName(contentLanguage)}
163
+ </div>
164
+ </DropdownMenuSubTrigger>
165
+ <DropdownMenuSubContent>
166
+ {sortedLanguages?.map(({ code: languageCode, label }) => (
167
+ <DropdownMenuItem
168
+ key={`${displayChannel.code}-${languageCode}`}
169
+ onClick={() => setContentLanguage(languageCode)}
170
+ className={`gap-2 p-2 ${contentLanguage === languageCode ? 'bg-accent' : ''}`}
171
+ >
172
+ <div className="flex w-6 h-5 items-center justify-center rounded border">
173
+ <span className="truncate font-medium text-xs">
174
+ {languageCode.toUpperCase()}
175
+ </span>
176
+ </div>
177
+ <span>{label}</span>
178
+ {contentLanguage === languageCode && (
179
+ <span className="ms-auto text-xs text-muted-foreground">
180
+ <Trans context="active language">
181
+ Active
182
+ </Trans>
183
+ </span>
184
+ )}
185
+ </DropdownMenuItem>
186
+ ))}
187
+ <DropdownMenuSeparator />
158
188
  <DropdownMenuItem
159
- key={`${channel.code}-${languageCode}`}
160
- onClick={() => setContentLanguage(languageCode)}
161
- className={`gap-2 p-2 ${contentLanguage === languageCode ? 'bg-accent' : ''}`}
189
+ onClick={() => setShowManageLanguagesDialog(true)}
190
+ className="gap-2 p-2"
162
191
  >
163
- <div className="flex w-6 h-5 items-center justify-center rounded border">
164
- <span className="truncate font-medium text-xs">
165
- {languageCode.toUpperCase()}
166
- </span>
167
- </div>
168
- <span>{label}</span>
169
- {contentLanguage === languageCode && (
170
- <span className="ms-auto text-xs text-muted-foreground">
171
- <Trans context="active language">
172
- Active
173
- </Trans>
174
- </span>
175
- )}
192
+ <Languages className="w-4 h-4" />
193
+ <span>
194
+ <Trans>Manage Languages</Trans>
195
+ </span>
176
196
  </DropdownMenuItem>
177
- ))}
178
- <DropdownMenuSeparator />
179
- <DropdownMenuItem
180
- onClick={() => setShowManageLanguagesDialog(true)}
181
- className="gap-2 p-2"
182
- >
183
- <Languages className="w-4 h-4" />
184
- <span>
185
- <Trans>Manage Languages</Trans>
186
- </span>
187
- </DropdownMenuItem>
188
- </DropdownMenuSubContent>
189
- </DropdownMenuSub>
197
+ </DropdownMenuSubContent>
198
+ </DropdownMenuSub>
199
+ {/* Add separator after the current channel group */}
200
+ {orderedChannels.length > 0 && <DropdownMenuSeparator />}
201
+ </>
190
202
  )}
191
- {/* Add separator after the current channel group */}
192
- {channel.id === displayChannel?.id &&
193
- index === 0 &&
194
- orderedChannels.length > 1 && <DropdownMenuSeparator />}
195
203
  </div>
196
- ))}
197
- <DropdownMenuSeparator />
198
- <DropdownMenuItem className="gap-2 p-2 cursor-pointer" asChild>
199
- <Link to={'/channels/new'}>
200
- <div className="bg-background flex size-6 items-center justify-center rounded-md border">
201
- <Plus className="size-4" />
202
- </div>
203
- <div className="text-muted-foreground font-medium">
204
- <Trans>Add channel</Trans>
205
- </div>
206
- </Link>
207
- </DropdownMenuItem>
204
+ {orderedChannels.map(renderChannel)}
205
+ <PermissionGuard requires={['CreateChannel']}>
206
+ <DropdownMenuSeparator />
207
+ <DropdownMenuItem className="gap-2 p-2 cursor-pointer" asChild>
208
+ <Link to={'/channels/new'}>
209
+ <div className="bg-background flex size-6 items-center justify-center rounded-md border">
210
+ <Plus className="size-4" />
211
+ </div>
212
+ <div className="text-muted-foreground font-medium">
213
+ <Trans>Add channel</Trans>
214
+ </div>
215
+ </Link>
216
+ </DropdownMenuItem>
217
+ </PermissionGuard>
218
+ </ScrollArea>
208
219
  </DropdownMenuContent>
209
220
  </DropdownMenu>
210
221
  </SidebarMenuItem>
@@ -114,10 +114,16 @@ export function LatestOrdersWidget() {
114
114
  },
115
115
  },
116
116
  total: {
117
+ meta: {
118
+ dependencies: ['currencyCode'],
119
+ },
117
120
  header: t`Total`,
118
121
  cell: OrderMoneyCell,
119
122
  },
120
- totalWithTax: { cell: OrderMoneyCell },
123
+ totalWithTax: {
124
+ meta: { dependencies: ['currencyCode'] },
125
+ cell: OrderMoneyCell,
126
+ },
121
127
  state: { cell: OrderStateCell },
122
128
  customer: { cell: CustomerCell },
123
129
  }}
@@ -96,6 +96,19 @@ export interface ChannelContext {
96
96
  refreshChannels: () => void;
97
97
  }
98
98
 
99
+ /**
100
+ * Retrieves the channel token from localStorage.
101
+ * Returns null if localStorage is unavailable or an error occurs.
102
+ */
103
+ function getChannelTokenFromLocalStorage(): string | null {
104
+ try {
105
+ return localStorage.getItem(LS_KEY_SELECTED_CHANNEL_TOKEN);
106
+ } catch (e) {
107
+ console.error('Failed to retrieve channel token from localStorage', e);
108
+ return null;
109
+ }
110
+ }
111
+
99
112
  /**
100
113
  * Sets the channel token in localStorage, which is then used by the `api`
101
114
  * object to ensure we add the correct token header to all API calls.
@@ -183,27 +196,39 @@ export function ChannelProvider({ children }: Readonly<{ children: React.ReactNo
183
196
  // If selected channel is not valid for this user, reset it
184
197
  if (selectedChannelId && validChannelIds.length && !validChannelIds.includes(selectedChannelId)) {
185
198
  setSelectedChannelId(undefined);
186
- }
187
-
188
- // If no selected channel is set, use the first available channel
189
- if (!selectedChannelId && channels.length > 0) {
199
+ } else if (selectedChannelId && channels.length > 0) {
200
+ // Ensure channel token in localStorage stays in sync with selected channel.
201
+ // This handles the case where activeChannelId persists but the token was cleared (e.g., after logout).
202
+ const selectedChannel = channels.find(c => c.id === selectedChannelId);
203
+ if (selectedChannel) {
204
+ const currentToken = getChannelTokenFromLocalStorage();
205
+ if (currentToken !== selectedChannel.token) {
206
+ setChannelTokenInLocalStorage(selectedChannel.token);
207
+ // Invalidate queries to refetch with the corrected token
208
+ queryClient.invalidateQueries({
209
+ queryKey: ['activeChannel', isAuthenticated],
210
+ });
211
+ }
212
+ }
213
+ } else if (channels.length > 0) {
214
+ // If no selected channel is set, use the first available channel
190
215
  const defaultChannel = channels[0];
191
216
  setSelectedChannelId(defaultChannel.id);
192
217
  setChannelTokenInLocalStorage(defaultChannel.token);
193
218
  }
194
- }, [selectedChannelId, channels]);
219
+ }, [selectedChannelId, channels, queryClient, isAuthenticated]);
195
220
 
196
221
  const isLoading = isActiveChannelLoading;
197
222
 
198
223
  // Find the selected channel from the list of channels
199
224
  const selectedChannel = activeChannelData?.activeChannel;
200
225
 
201
- const refreshChannels = () => {
226
+ const refreshChannels = React.useCallback(() => {
202
227
  refreshCurrentUser();
203
228
  queryClient.invalidateQueries({
204
229
  queryKey: ['channels', isAuthenticated],
205
230
  });
206
- };
231
+ }, [refreshCurrentUser, queryClient, isAuthenticated]);
207
232
 
208
233
  const contextValue: ChannelContext = React.useMemo(
209
234
  () => ({