@taruvi/navkit 0.0.47 → 0.0.48-beta.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taruvi/navkit",
3
- "version": "0.0.47",
3
+ "version": "0.0.48-beta.0",
4
4
  "main": "src/App.tsx",
5
5
  "type": "module",
6
6
  "scripts": {
package/src/App.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import AppBar from "@mui/material/AppBar"
2
2
  import Toolbar from "@mui/material/Toolbar"
3
3
  import IconButton from "@mui/material/IconButton"
4
+ import { SvgIcon } from "@mui/material"
4
5
  import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
5
6
  import { useState } from "react"
6
7
  import { AppLauncher, Profile, ProfileMenu, Shortcuts, ShortcutsMenu, MattermostChat } from "./components"
@@ -12,8 +13,14 @@ import taruviLogo from "./assets/logo.svg";
12
13
  import taruviLogoWhite from "./assets/taruvi-logo-white.png";
13
14
  import type { Client } from "@taruvi/sdk"
14
15
 
16
+ const ChatIcon = ({ color }: { color?: string }) => (
17
+ <SvgIcon viewBox="-2 -2 24 24" sx={{ fontSize: 23, display: 'block', position: 'relative', top: 2 }}>
18
+ <path d="M18 0H2C0.9 0 0 0.9 0 2V20L4 16H18C19.1 16 20 15.1 20 14V2C20 0.9 19.1 0 18 0Z" fill={color || "#9DE5FD"} />
19
+ </SvgIcon>
20
+ )
21
+
15
22
  const NavkitContent = () => {
16
- const { isUserAuthenticated, siteSettings, appSettings, appSettingsLoaded, appName, userData, jwtToken, themeMode, navbarColor, iconColor } = useNavigation();
23
+ const { isUserAuthenticated, siteSettings, appSettings, appSettingsLoaded, appName, userData, themeMode, navbarColor, iconColor } = useNavigation();
17
24
  const resolvedAppName = appName || appSettings?.displayName
18
25
  const showTaruviLogo = appSettingsLoaded && !resolvedAppName && !appSettings?.icon
19
26
  const styles = getAppStyles(themeMode)
@@ -52,7 +59,12 @@ const NavkitContent = () => {
52
59
  ) : null}
53
60
  </Box>
54
61
  <Box component={"div"} sx={styles.rightSection}>
55
- <Shortcuts showChat={setShowChat} onMenuToggle={() => setShowShortcutsMenu(!showShortcutsMenu)} iconColor={iconColor} />
62
+ <Shortcuts onMenuToggle={() => setShowShortcutsMenu(!showShortcutsMenu)} iconColor={iconColor} />
63
+ {isUserAuthenticated && siteSettings['show-chat'] && siteSettings['chat-url'] && (
64
+ <Box onClick={() => setShowChat(true)} sx={{ ...styles.appLauncherContainer, cursor: 'pointer' }}>
65
+ <ChatIcon color={iconColor} />
66
+ </Box>
67
+ )}
56
68
  <Box
57
69
  onClick={() => {
58
70
  if (isUserAuthenticated) {
@@ -99,7 +111,7 @@ const NavkitContent = () => {
99
111
 
100
112
  {showShortcutsMenu && (
101
113
  <Box>
102
- <ShortcutsMenu showChat={setShowChat} />
114
+ <ShortcutsMenu />
103
115
  </Box>
104
116
  )}
105
117
 
@@ -110,7 +122,7 @@ const NavkitContent = () => {
110
122
  <FontAwesomeIcon icon={["fas", "external-link-alt"]} style={{ fontSize: "10px" }} />
111
123
  </IconButton>
112
124
  </Box>
113
- <MattermostChat jwtToken={jwtToken} mattermostUrl={siteSettings['chat-url']} loginId={userData?.email || ''} width="100%" height="100%" />
125
+ <MattermostChat mattermostUrl={siteSettings['chat-url']} width="100%" height="100%" />
114
126
  </DraggableResizable>
115
127
  )}
116
128
  </>
@@ -24,7 +24,7 @@ export const NavkitProvider = ({ children, client, onThemeChange, appName, navba
24
24
 
25
25
  const user = useRef<any>(null)
26
26
  const settings = useRef<any>(null)
27
- const siteSettings = useRef<SiteSettings>({
27
+ const [siteSettings, setSiteSettings] = useState<SiteSettings>({
28
28
  shortcuts: [],
29
29
  logo: '',
30
30
  'show-chat': false,
@@ -38,7 +38,7 @@ export const NavkitProvider = ({ children, client, onThemeChange, appName, navba
38
38
  const [isDesk, setIsDesk] = useState<boolean>(false)
39
39
  const [appsList, setAppsList] = useState<AppData[]>([])
40
40
  const [userData, setUserData] = useState<UserData | null>(null)
41
- const [jwtToken, setJwtToken] = useState<string>('')
41
+ const [sessionToken, setSessionToken] = useState<string>('')
42
42
  const [isUserAuthenticated, setIsUserAuthenticated] = useState<boolean>(false)
43
43
  const [themeMode, setThemeMode] = useState<ThemeMode>(getInitialTheme)
44
44
 
@@ -75,8 +75,8 @@ export const NavkitProvider = ({ children, client, onThemeChange, appName, navba
75
75
 
76
76
  // Get JWT token if authenticated from sdk
77
77
  if (authenticated) {
78
- const token = client.tokenClient.getToken() || ''
79
- setJwtToken(token)
78
+ const token = auth.getSessionToken() || ''
79
+ setSessionToken(token)
80
80
  }
81
81
  }
82
82
 
@@ -104,18 +104,31 @@ export const NavkitProvider = ({ children, client, onThemeChange, appName, navba
104
104
  }
105
105
 
106
106
  const rawSettings = (fetchedSettings?.data ?? fetchedSettings)?.settings ?? {}
107
- siteSettings.current = {
108
- ...siteSettings.current,
107
+ const newSiteSettings: SiteSettings = {
108
+ shortcuts: [],
109
+ logo: rawSettings['navkit.logo'] ?? '',
109
110
  'show-chat': rawSettings['navkit.show-chat'] ?? false,
110
111
  'chat-url': rawSettings['navkit.chat-url'] ?? '',
111
112
  frontendUrl: rawSettings['navkit.frontend-url'] ?? '',
112
- logo: rawSettings['navkit.logo'] ?? '',
113
113
  enableDarkMode: rawSettings['navkit.enable-dark-mode'] ?? false,
114
114
  }
115
+ setSiteSettings(newSiteSettings)
115
116
  if (isUserAuthenticated) {
116
117
  const userDataResponse = await auth.getCurrentUser()
117
118
  setUserData(userDataResponse?.data || null)
118
119
 
120
+ // Login to Mattermost on init if chat is configured
121
+ const chatUrl = newSiteSettings['chat-url']
122
+ const token = auth.getSessionToken() || ''
123
+ if (chatUrl && token && userDataResponse?.data?.username) {
124
+ fetch(`${chatUrl.replace(/\/$/, '')}/api/v4/users/login`, {
125
+ method: 'POST',
126
+ headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-Environment': 'Browser' },
127
+ credentials: 'omit',
128
+ body: JSON.stringify({ username: userDataResponse.data.username, password: token })
129
+ }).catch(() => {})
130
+ }
131
+
119
132
  // Fetch user apps using username from userData
120
133
  if (userDataResponse?.data?.username) {
121
134
  const appsResponse = await user.current.getUserApps?.(userDataResponse.data.username)
@@ -176,7 +189,7 @@ export const NavkitProvider = ({ children, client, onThemeChange, appName, navba
176
189
  }, [themeMode, onThemeChange])
177
190
 
178
191
  return (
179
- <NavigationContext.Provider value={{ navigateToUrl, isDesk, appsList, userData, siteSettings: siteSettings.current, appSettings, appSettingsLoaded, appName, jwtToken, isUserAuthenticated, client, auth, themeMode, toggleTheme, navbarColor, iconColor }}>
192
+ <NavigationContext.Provider value={{ navigateToUrl, isDesk, appsList, userData, siteSettings, appSettings, appSettingsLoaded, appName, sessionToken, isUserAuthenticated, client, auth, themeMode, toggleTheme, navbarColor, iconColor }}>
180
193
  {children}
181
194
  </NavigationContext.Provider>
182
195
  )
@@ -1,10 +1,8 @@
1
- import { useEffect, useRef, useState } from 'react'
2
- import { Box, Badge, CircularProgress, Typography } from '@mui/material'
1
+ import {useEffect, useRef, useState} from 'react'
2
+ import {Box, Badge} from '@mui/material'
3
3
 
4
4
  interface MattermostChatProps {
5
- jwtToken: string
6
- mattermostUrl: string
7
- loginId: string
5
+ mattermostUrl: string // Mattermost server URL (must be passed from parent app)
8
6
  onNotification?: (count: number) => void
9
7
  onUrlClick?: (url: string) => void
10
8
  width?: string | number
@@ -22,77 +20,22 @@ interface NotifyMessage {
22
20
  }
23
21
  }
24
22
 
25
- const MattermostChat = ({
26
- jwtToken,
27
- mattermostUrl,
28
- loginId,
29
- onNotification,
30
- onUrlClick,
31
- width = '100%',
32
- height = '100%'
33
- }: MattermostChatProps) => {
23
+ const MattermostChat = ({ mattermostUrl, onNotification, onUrlClick, width = '100%', height = '100%' }: MattermostChatProps) => {
34
24
  const iframeRef = useRef<HTMLIFrameElement>(null)
35
25
  const [unreadCount, setUnreadCount] = useState(0)
36
26
  const [isWindowFocused, setIsWindowFocused] = useState(true)
37
- const [authToken, setAuthToken] = useState<string | null>(null)
38
- const [authError, setAuthError] = useState<string | null>(null)
39
- const [isLoading, setIsLoading] = useState(true)
40
-
41
- // Authenticate with Mattermost on mount
42
- useEffect(() => {
43
- const login = async () => {
44
- try {
45
- const res = await fetch(`${mattermostUrl.replace(/\/$/, '')}/api/v4/users/login`, {
46
- method: 'POST',
47
- headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
48
- credentials: 'omit',
49
- body: JSON.stringify({ login_id: loginId, password: jwtToken, token: '', deviceId: '' })
50
- })
51
-
52
- if (!res.ok) {
53
- setAuthError(`Login failed (${res.status})`)
54
- setIsLoading(false)
55
- return
56
- }
57
-
58
- const token = res.headers.get('Token')
59
- if (token) {
60
- setAuthToken(token)
61
- } else {
62
- setAuthError('No token in response')
63
- }
64
- } catch (e: any) {
65
- setAuthError(e.message || 'Login failed')
66
- }
67
- setIsLoading(false)
68
- }
69
-
70
- if (loginId && jwtToken) login()
71
- else { setAuthError('Missing credentials'); setIsLoading(false) }
72
- }, [mattermostUrl, loginId, jwtToken])
73
-
74
- // Inject auth token into iframe via postMessage once loaded
75
- useEffect(() => {
76
- if (!authToken || !iframeRef.current) return
77
-
78
- const handleIframeLoad = () => {
79
- // Set the auth cookie/token in the iframe context
80
- iframeRef.current?.contentWindow?.postMessage(
81
- { type: 'token', data: authToken },
82
- new URL(mattermostUrl).origin
83
- )
84
- }
85
-
86
- const iframe = iframeRef.current
87
- iframe.addEventListener('load', handleIframeLoad)
88
- return () => iframe.removeEventListener('load', handleIframeLoad)
89
- }, [authToken, mattermostUrl])
90
27
 
91
28
  useEffect(() => {
29
+ // Handle postMessage events from Mattermost iframe
92
30
  const handleMessage = (event: MessageEvent<NotifyMessage>) => {
31
+ // Validate origin for security
93
32
  const mattermostOrigin = new URL(mattermostUrl).origin
94
- if (event.origin !== mattermostOrigin) return
33
+ if (event.origin !== mattermostOrigin) {
34
+ console.warn('Received message from untrusted origin:', event.origin)
35
+ return
36
+ }
95
37
 
38
+ // Handle Notify events
96
39
  if (event.data?.event === 'Notify') {
97
40
  if (!isWindowFocused) {
98
41
  setUnreadCount(prev => {
@@ -101,16 +44,31 @@ const MattermostChat = ({
101
44
  return newCount
102
45
  })
103
46
  }
104
- if (event.data.data?.url && onUrlClick) onUrlClick(event.data.data.url)
47
+
48
+ // Handle URL clicks if provided
49
+ if (event.data.data?.url && onUrlClick) {
50
+ onUrlClick(event.data.data.url)
51
+ }
105
52
  }
106
53
  }
107
54
 
108
- const handleFocus = () => { setIsWindowFocused(true); setUnreadCount(0); onNotification?.(0) }
109
- const handleBlur = () => setIsWindowFocused(false)
55
+ // Handle window focus/blur to track unread messages
56
+ const handleFocus = () => {
57
+ setIsWindowFocused(true)
58
+ setUnreadCount(0)
59
+ onNotification?.(0)
60
+ }
61
+
62
+ const handleBlur = () => {
63
+ setIsWindowFocused(false)
64
+ }
110
65
 
66
+ // Add event listeners
111
67
  window.addEventListener('message', handleMessage)
112
68
  window.addEventListener('focus', handleFocus)
113
69
  window.addEventListener('blur', handleBlur)
70
+
71
+ // Cleanup event listeners on unmount
114
72
  return () => {
115
73
  window.removeEventListener('message', handleMessage)
116
74
  window.removeEventListener('focus', handleFocus)
@@ -118,34 +76,34 @@ const MattermostChat = ({
118
76
  }
119
77
  }, [mattermostUrl, isWindowFocused, onNotification, onUrlClick])
120
78
 
121
- if (isLoading) {
122
- return (
123
- <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width, height }}>
124
- <CircularProgress size={32} />
125
- </Box>
126
- )
127
- }
128
-
129
- if (authError) {
130
- return (
131
- <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width, height }}>
132
- <Typography color="error">{authError}</Typography>
133
- </Box>
134
- )
135
- }
136
-
137
- // Load Mattermost with the auth token as a cookie header via the access_token param
138
- const iframeUrl = `${mattermostUrl.replace(/\/$/, '')}?access_token=${authToken}`
79
+ // Construct iframe URL with JWT authentication
80
+ // const iframeUrl = `${mattermostUrl.replace(/\/$/, '')}/login`
81
+ const iframeUrl = mattermostUrl
139
82
 
140
83
  return (
141
- <Box sx={{ position: 'relative', width, height, overflow: 'hidden' }}>
84
+ <Box
85
+ sx={{
86
+ position: 'relative',
87
+ width,
88
+ height,
89
+ overflow: 'hidden'
90
+ }}
91
+ >
142
92
  {unreadCount > 0 && (
143
93
  <Badge
144
94
  badgeContent={unreadCount}
145
95
  color="error"
146
96
  sx={{
147
- position: 'absolute', top: 16, right: 16, zIndex: 1000,
148
- '& .MuiBadge-badge': { fontSize: '1rem', height: '28px', minWidth: '28px', borderRadius: '14px' }
97
+ position: 'absolute',
98
+ top: 16,
99
+ right: 16,
100
+ zIndex: 1000,
101
+ '& .MuiBadge-badge': {
102
+ fontSize: '1rem',
103
+ height: '28px',
104
+ minWidth: '28px',
105
+ borderRadius: '14px'
106
+ }
149
107
  }}
150
108
  />
151
109
  )}
@@ -154,7 +112,12 @@ const MattermostChat = ({
154
112
  src={iframeUrl}
155
113
  title="Mattermost Chat"
156
114
  allow="clipboard-read; clipboard-write"
157
- style={{ width: '100%', height: '100%', border: 'none', display: 'block' }}
115
+ style={{
116
+ width: '100%',
117
+ height: '100%',
118
+ border: 'none',
119
+ display: 'block'
120
+ }}
158
121
  />
159
122
  </Box>
160
123
  )
@@ -5,7 +5,6 @@ export const shortcutsStyles = {
5
5
  display: { xs: 'none', md: 'flex' },
6
6
  alignItems: 'center',
7
7
  gap: '10px',
8
- ml: spacing.md,
9
8
  },
10
9
  mobileTrigger: {
11
10
  display: { xs: 'flex', md: 'none' },
@@ -16,9 +15,12 @@ export const shortcutsStyles = {
16
15
  display: 'flex',
17
16
  flexDirection: 'column' as const,
18
17
  alignItems: 'center',
18
+ justifyContent: 'center',
19
19
  textDecoration: 'none',
20
20
  cursor: 'pointer',
21
21
  color: colours.text.secondary,
22
+ minWidth: 40,
23
+ minHeight: 40,
22
24
  '&:hover': {
23
25
  opacity: 0.7,
24
26
  },
@@ -1,17 +1,10 @@
1
1
  import type { AppData } from '../../types'
2
- import { Box, Link, IconButton, SvgIcon } from '@mui/material'
2
+ import { Box, Link, IconButton } from '@mui/material'
3
3
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
4
4
  import { shortcutsStyles } from './Shortcuts.styles'
5
5
  import { useNavigation } from '../../NavkitContext'
6
6
 
7
- const ChatIcon = ({ color }: { color?: string }) => (
8
- <SvgIcon viewBox="-2 -2 24 24" sx={{ fontSize: 23, display: 'block', position: 'relative', top: 2 }}>
9
- <path d="M18 0H2C0.9 0 0 0.9 0 2V20L4 16H18C19.1 16 20 15.1 20 14V2C20 0.9 19.1 0 18 0Z" fill={color || "#9DE5FD"}/>
10
- </SvgIcon>
11
- )
12
-
13
7
  interface ShortcutsProps {
14
- showChat?: (showChat: boolean) => void
15
8
  onMenuToggle?: () => void
16
9
  triggerRef?: React.RefObject<HTMLButtonElement | null>
17
10
  iconColor?: string
@@ -19,8 +12,8 @@ interface ShortcutsProps {
19
12
 
20
13
  const Shortcuts = (props: ShortcutsProps) => {
21
14
 
22
- const { showChat, onMenuToggle, triggerRef, iconColor } = props
23
- const { navigateToUrl, siteSettings, isUserAuthenticated } = useNavigation()
15
+ const { onMenuToggle, triggerRef, iconColor } = props
16
+ const { navigateToUrl, siteSettings } = useNavigation()
24
17
  const shortcuts = siteSettings.shortcuts
25
18
 
26
19
  const handleShortcutClick = (shortcut: AppData) => {
@@ -45,19 +38,10 @@ const Shortcuts = (props: ShortcutsProps) => {
45
38
  />
46
39
  </Link>
47
40
  ))}
48
- {siteSettings['show-chat'] && siteSettings['chat-url'] && isUserAuthenticated &&
49
- <Link
50
- key={"chat"}
51
- onClick={() => showChat?.(true)}
52
- sx={shortcutsStyles.shortcut}
53
- >
54
- <ChatIcon color={iconColor} />
55
- </Link>
56
- }
57
41
  </Box>
58
42
 
59
43
  {/* Mobile view - trigger button */}
60
- {((shortcuts?.length ?? 0) > 0 || (siteSettings['show-chat'] && siteSettings['chat-url'] && isUserAuthenticated)) && (
44
+ {(shortcuts?.length ?? 0) > 0 && (
61
45
  <IconButton
62
46
  ref={triggerRef}
63
47
  onClick={onMenuToggle}
@@ -1,22 +1,13 @@
1
- import { Card, MenuItem, Typography, SvgIcon } from '@mui/material'
1
+ import { Card, MenuItem, Typography } from '@mui/material'
2
2
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
3
3
  import type { AppData } from '../../types'
4
4
  import { getShortcutsMenuStyles } from './ShortcutsMenu.styles'
5
5
  import { useNavigation } from '../../NavkitContext'
6
6
 
7
- const ChatIcon = ({ style }: { style: Record<string, any> }) => (
8
- <SvgIcon viewBox="-2 -2 24 24" sx={{ fontSize: style.fontSize, color: style.color }}>
9
- <path d="M18 0H2C0.9 0 0 0.9 0 2V20L4 16H18C19.1 16 20 15.1 20 14V2C20 0.9 19.1 0 18 0Z" fill="currentColor"/>
10
- </SvgIcon>
11
- )
7
+ interface ShortcutsMenuProps {}
12
8
 
13
- interface ShortcutsMenuProps {
14
- showChat: (showChat: boolean) => void
15
- }
16
-
17
- const ShortcutsMenu = (props: ShortcutsMenuProps) => {
18
- const { showChat } = props
19
- const { navigateToUrl, siteSettings, isUserAuthenticated, themeMode } = useNavigation()
9
+ const ShortcutsMenu = (_props: ShortcutsMenuProps) => {
10
+ const { navigateToUrl, siteSettings, themeMode } = useNavigation()
20
11
  const shortcuts = siteSettings.shortcuts
21
12
  const themedStyles = getShortcutsMenuStyles(themeMode)
22
13
 
@@ -28,18 +19,6 @@ const ShortcutsMenu = (props: ShortcutsMenuProps) => {
28
19
 
29
20
  return (
30
21
  <Card sx={themedStyles.menu}>
31
- {siteSettings['show-chat'] && siteSettings['chat-url'] && isUserAuthenticated && (
32
- <MenuItem
33
- key="chat"
34
- onClick={() => showChat(true)}
35
- sx={themedStyles.menuItem}
36
- >
37
- <ChatIcon style={themedStyles.iconStyle} />
38
- <Typography sx={themedStyles.menuItemText}>
39
- Chat
40
- </Typography>
41
- </MenuItem>
42
- )}
43
22
  {shortcuts?.map((shortcut) => (
44
23
  <MenuItem
45
24
  key={shortcut.id}
package/src/types.ts CHANGED
@@ -37,7 +37,7 @@ export interface NavigationContextType {
37
37
  appSettings: AppSettings
38
38
  appSettingsLoaded: boolean
39
39
  appName?: string
40
- jwtToken: string
40
+ sessionToken: string
41
41
  isUserAuthenticated: boolean
42
42
  client: Client
43
43
  auth: Auth