diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d87be83e16a..189c91ed117 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -405,6 +405,9 @@ catalogs:
jotai:
specifier: 2.20.1
version: 2.20.1
+ jotai-effect:
+ specifier: 2.3.1
+ version: 2.3.1
jotai-scope:
specifier: 0.11.0
version: 0.11.0
@@ -1252,6 +1255,9 @@ importers:
jotai:
specifier: 'catalog:'
version: 2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7)
+ jotai-effect:
+ specifier: 'catalog:'
+ version: 2.3.1(jotai@2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7))
jotai-scope:
specifier: 'catalog:'
version: 0.11.0(jotai@2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7))(react@19.2.7)
@@ -7364,6 +7370,12 @@ packages:
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
hasBin: true
+ jotai-effect@2.3.1:
+ resolution: {integrity: sha512-FBBVXDM2podbbxJsZ19+uDv45LxeXoVA5yh6CfkQ35AVCuRHj7Lanlcjiea3b67Q7+/MMGXWf8+GeB73JcbeMg==}
+ engines: {node: '>=12.20.0'}
+ peerDependencies:
+ jotai: '>=2.20.0'
+
jotai-scope@0.11.0:
resolution: {integrity: sha512-ofiW0Z0i3lTw509Gx0+T6fqsDPMDxMn+AHmNs9iF9OA8CmK1/0xRprPxuZ89UZdBzt6jcrTYdunNZSF2255zJQ==}
engines: {node: '>=14.0.0'}
@@ -16663,6 +16675,10 @@ snapshots:
jiti@2.7.0: {}
+ jotai-effect@2.3.1(jotai@2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7)):
+ dependencies:
+ jotai: 2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7)
+
jotai-scope@0.11.0(jotai@2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7))(react@19.2.7):
dependencies:
jotai: 2.20.1(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.17)(react@19.2.7)
@@ -20034,6 +20050,7 @@ time:
i18next@26.3.3: '2026-06-26T11:23:58.220Z'
iconify-import-svg@0.2.0: '2026-04-20T06:18:25.132Z'
immer@11.1.8: '2026-05-08T15:09:33.021Z'
+ jotai-effect@2.3.1: '2026-05-13T18:51:15.911Z'
jotai-scope@0.11.0: '2026-05-13T18:43:15.331Z'
jotai-tanstack-query@0.11.0: '2025-08-01T02:55:49.826Z'
jotai@2.20.1: '2026-06-11T06:30:45.782Z'
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index df50f9c4228..54eba537ae1 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -185,6 +185,7 @@ catalog:
iconify-import-svg: 0.2.0
immer: 11.1.8
jotai: 2.20.1
+ jotai-effect: 2.3.1
jotai-scope: 0.11.0
jotai-tanstack-query: 0.11.0
js-audio-recorder: 1.0.7
diff --git a/web/app/(commonLayout)/external-service-sync.tsx b/web/app/(commonLayout)/external-service-sync.tsx
new file mode 100644
index 00000000000..b0ab3f166c5
--- /dev/null
+++ b/web/app/(commonLayout)/external-service-sync.tsx
@@ -0,0 +1,12 @@
+'use client'
+
+import { useAtomValue } from 'jotai'
+import { amplitudeIdentitySyncAtom } from '@/context/amplitude-identity-sync'
+import { zendeskConversationSyncAtom } from '@/context/zendesk-conversation-sync'
+
+export function ExternalServiceSync() {
+ useAtomValue(zendeskConversationSyncAtom)
+ useAtomValue(amplitudeIdentitySyncAtom)
+
+ return null
+}
diff --git a/web/app/(commonLayout)/hydration-boundary.tsx b/web/app/(commonLayout)/hydration-boundary.tsx
index 4f6156dd8a8..ea6d9a5cbed 100644
--- a/web/app/(commonLayout)/hydration-boundary.tsx
+++ b/web/app/(commonLayout)/hydration-boundary.tsx
@@ -61,28 +61,22 @@ export async function CommonLayoutHydrationBoundary({ children }: { children: Re
const queryClient = getQueryClientServer()
const accountProfileUrl = resolveServerConsoleApiUrl(ACCOUNT_PROFILE_PATH)
- if (!accountProfileUrl) {
- return (
-
- {children}
-
- )
- }
+ if (accountProfileUrl) {
+ try {
+ const context = await getServerConsoleClientContext()
- try {
- const context = await getServerConsoleClientContext()
-
- await Promise.all([
- queryClient.fetchQuery(serverUserProfileQueryOptions()),
- queryClient.prefetchQuery(serverSystemFeaturesQueryOptions()),
- queryClient.prefetchQuery(serverConsoleQuery.workspaces.current.post.queryOptions({
- context,
- retry: false,
- })),
- ])
- }
- catch (error) {
- await handleProfileError(error)
+ await Promise.all([
+ queryClient.fetchQuery(serverUserProfileQueryOptions()),
+ queryClient.prefetchQuery(serverSystemFeaturesQueryOptions()),
+ queryClient.prefetchQuery(serverConsoleQuery.workspaces.current.post.queryOptions({
+ context,
+ retry: false,
+ })),
+ ])
+ }
+ catch (error) {
+ await handleProfileError(error)
+ }
}
return (
diff --git a/web/app/(commonLayout)/layout.tsx b/web/app/(commonLayout)/layout.tsx
index e70e660f1c8..966ee0b4b43 100644
--- a/web/app/(commonLayout)/layout.tsx
+++ b/web/app/(commonLayout)/layout.tsx
@@ -1,18 +1,10 @@
import * as React from 'react'
-import AmplitudeProvider from '@/app/components/base/amplitude'
-import { GoogleAnalyticsScripts } from '@/app/components/base/ga'
import Zendesk from '@/app/components/base/zendesk'
-import { EducationVerifyActionRecorder } from '@/app/components/education-verify-action-recorder'
import MaintenanceNotice from '@/app/components/header/maintenance-notice'
import MainNavLayout from '@/app/components/main-nav/layout'
import { NextRouteStateBridge } from '@/app/components/next-route-state'
-import { OAuthRegistrationAnalytics } from '@/app/components/oauth-registration-analytics'
-import { AppBootstrapEffects } from '@/context/app-bootstrap-effects'
-import { EventEmitterContextProvider } from '@/context/event-emitter-provider'
-import { ModalContextProvider } from '@/context/modal-context-provider'
-import { ProviderContextProvider } from '@/context/provider-context-provider'
import { CommonLayoutGlobalMounts } from './global-mounts'
-import { CommonLayoutHydrationBoundary } from './hydration-boundary'
+import { ConsoleContextProviders, ConsoleRuntimeProviders } from './providers'
export default async function Layout({
children,
@@ -23,28 +15,19 @@ export default async function Layout({
}) {
return (
-
-
-
-
-
+
-
-
-
-
-
- {children}
-
-
-
-
-
+
+
+ {children}
+
+
+
-
+
)
diff --git a/web/app/(commonLayout)/providers.tsx b/web/app/(commonLayout)/providers.tsx
new file mode 100644
index 00000000000..6b0b579b2d0
--- /dev/null
+++ b/web/app/(commonLayout)/providers.tsx
@@ -0,0 +1,37 @@
+import type { ReactNode } from 'react'
+import AmplitudeProvider from '@/app/components/base/amplitude'
+import { GoogleAnalyticsScripts } from '@/app/components/base/ga'
+import { EducationVerifyActionRecorder } from '@/app/components/education-verify-action-recorder'
+import { OAuthRegistrationAnalytics } from '@/app/components/oauth-registration-analytics'
+import { EventEmitterContextProvider } from '@/context/event-emitter-provider'
+import { ModalContextProvider } from '@/context/modal-context-provider'
+import { ProviderContextProvider } from '@/context/provider-context-provider'
+import { ExternalServiceSync } from './external-service-sync'
+import { CommonLayoutHydrationBoundary } from './hydration-boundary'
+
+export async function ConsoleRuntimeProviders({ children }: { children: ReactNode }) {
+ return (
+ <>
+
+
+
+
+
+
+ {children}
+
+ >
+ )
+}
+
+export function ConsoleContextProviders({ children }: { children: ReactNode }) {
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
diff --git a/web/app/account/(commonLayout)/layout.tsx b/web/app/account/(commonLayout)/layout.tsx
index 292010773c7..06ec2ae4e5d 100644
--- a/web/app/account/(commonLayout)/layout.tsx
+++ b/web/app/account/(commonLayout)/layout.tsx
@@ -1,42 +1,25 @@
import * as React from 'react'
-import { CommonLayoutHydrationBoundary } from '@/app/(commonLayout)/hydration-boundary'
-import AmplitudeProvider from '@/app/components/base/amplitude'
-import { GoogleAnalyticsScripts } from '@/app/components/base/ga'
-import { EducationVerifyActionRecorder } from '@/app/components/education-verify-action-recorder'
+import { ConsoleContextProviders, ConsoleRuntimeProviders } from '@/app/(commonLayout)/providers'
import HeaderWrapper from '@/app/components/header/header-wrapper'
import MaintenanceNotice from '@/app/components/header/maintenance-notice'
-import { OAuthRegistrationAnalytics } from '@/app/components/oauth-registration-analytics'
-import { AppBootstrapEffects } from '@/context/app-bootstrap-effects'
-import { EventEmitterContextProvider } from '@/context/event-emitter-provider'
-import { ModalContextProvider } from '@/context/modal-context-provider'
-import { ProviderContextProvider } from '@/context/provider-context-provider'
import Header from './header'
export default async function Layout({ children }: { children: React.ReactNode }) {
return (
-
-
-
-
-
+
-
-
-
-
-
-
-
-
- {children}
-
-
-
-
+
+
+
+
+
+ {children}
+
+
-
+
)
}
diff --git a/web/app/components/oauth-registration-analytics.tsx b/web/app/components/oauth-registration-analytics.tsx
index 2c0d84edd52..ff6b3b036c0 100644
--- a/web/app/components/oauth-registration-analytics.tsx
+++ b/web/app/components/oauth-registration-analytics.tsx
@@ -48,8 +48,8 @@ export function OAuthRegistrationAnalytics() {
const eventName = utmInfo ? 'user_registration_success_with_utm' : 'user_registration_success'
- // Defer the Amplitude event until the user ID is attached. It is flushed in
- // AppBootstrapEffects after setUserId runs. Firing it here would record it under an
+ // Defer the Amplitude event until the user ID is attached. The app context
+ // external sync replays it after setUserId runs. Firing it here would record it under an
// anonymous Amplitude profile (no user ID set yet).
rememberRegistrationSuccess({ method: 'oauth', utmInfo })
diff --git a/web/app/signup/set-password/page.tsx b/web/app/signup/set-password/page.tsx
index f0087156a85..697aa8e2bdf 100644
--- a/web/app/signup/set-password/page.tsx
+++ b/web/app/signup/set-password/page.tsx
@@ -78,8 +78,8 @@ const ChangePasswordForm = () => {
if (result === 'success') {
const utmInfo = parseUtmInfo()
rememberCreateAppExternalAttribution({ utmInfo })
- // Defer the Amplitude event until the user ID is attached. It is flushed in
- // AppBootstrapEffects after setUserId runs once the redirect lands on /apps.
+ // Defer the Amplitude event until the user ID is attached. The app context
+ // external sync replays it after setUserId runs once the redirect lands on /apps.
// Firing it here would record it under an anonymous Amplitude profile.
rememberRegistrationSuccess({ method: 'email', utmInfo })
diff --git a/web/context/__tests__/app-bootstrap-effects.spec.tsx b/web/context/__tests__/console-bootstrap.spec.tsx
similarity index 96%
rename from web/context/__tests__/app-bootstrap-effects.spec.tsx
rename to web/context/__tests__/console-bootstrap.spec.tsx
index 108c2ecf618..617ff9abdbb 100644
--- a/web/context/__tests__/app-bootstrap-effects.spec.tsx
+++ b/web/context/__tests__/console-bootstrap.spec.tsx
@@ -5,11 +5,11 @@ import { Provider as JotaiProvider, useAtomValue, useSetAtom } from 'jotai'
import { queryClientAtom } from 'jotai-tanstack-query'
import { useHydrateAtoms } from 'jotai/react/utils'
import { Suspense } from 'react'
+import { ExternalServiceSync } from '@/app/(commonLayout)/external-service-sync'
import { setUserId, setUserProperties } from '@/app/components/base/amplitude'
import { flushRegistrationSuccess } from '@/app/components/base/amplitude/registration-tracking'
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
import { ZENDESK_FIELD_IDS } from '@/config'
-import { AppBootstrapEffects } from '../app-bootstrap-effects'
import { initialWorkspaceInfo } from '../app-context-defaults'
import {
currentWorkspaceAtom,
@@ -167,7 +167,7 @@ vi.mock('@/app/components/header/maintenance-notice', () => ({
default: () => null,
}))
-function AppContextProbe() {
+function ConsoleBootstrapProbe() {
const userProfile = useAtomValue(userProfileAtom)
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
const roleFlags = useAtomValue(workspaceRoleFlagsAtom)
@@ -257,15 +257,15 @@ function createTestQueryClient() {
})
}
-function renderBootstrapEffects() {
+function renderConsoleBootstrap() {
const queryClient = createTestQueryClient()
const view = render(
loading}>
-
-
+
+
@@ -278,7 +278,7 @@ function renderBootstrapEffects() {
}
}
-describe('AppBootstrapEffects', () => {
+describe('Console bootstrap', () => {
beforeEach(() => {
vi.clearAllMocks()
mockPermissionKeysState.isPending = false
@@ -339,7 +339,7 @@ describe('AppBootstrapEffects', () => {
describe('Bootstrap atoms', () => {
it('should provide profile, workspace, permissions, loading state, and version metadata', async () => {
- renderBootstrapEffects()
+ renderConsoleBootstrap()
expect(await screen.findByText('user:user@example.com')).toBeInTheDocument()
expect(await screen.findByText('workspace:Workspace')).toBeInTheDocument()
@@ -360,7 +360,7 @@ describe('AppBootstrapEffects', () => {
mockPermissionKeysState.permissionKeys = []
mockLangGeniusVersionState.data = undefined
- renderBootstrapEffects()
+ renderConsoleBootstrap()
expect(await screen.findByText('user:')).toBeInTheDocument()
expect(screen.getByText(`workspace:${initialWorkspaceInfo.name}`)).toBeInTheDocument()
@@ -375,7 +375,7 @@ describe('AppBootstrapEffects', () => {
role: 'unsupported-role',
}
- renderBootstrapEffects()
+ renderConsoleBootstrap()
expect(await screen.findByText(`role:${initialWorkspaceInfo.role}`)).toBeInTheDocument()
})
@@ -386,7 +386,7 @@ describe('AppBootstrapEffects', () => {
role: 'owner',
}
- renderBootstrapEffects()
+ renderConsoleBootstrap()
expect(await screen.findByText('manager:true')).toBeInTheDocument()
expect(screen.getByText('owner:true')).toBeInTheDocument()
@@ -398,7 +398,7 @@ describe('AppBootstrapEffects', () => {
mockPermissionKeysState.isPending = true
mockCurrentWorkspaceQueryState.isPending = true
- renderBootstrapEffects()
+ renderConsoleBootstrap()
expect(await screen.findByText('workspace loading:true')).toBeInTheDocument()
expect(screen.getByText('permission loading:true')).toBeInTheDocument()
@@ -407,7 +407,7 @@ describe('AppBootstrapEffects', () => {
describe('Refresh actions', () => {
it('should invalidate the source queries when refresh actions are called', async () => {
- const { queryClient } = renderBootstrapEffects()
+ const { queryClient } = renderConsoleBootstrap()
const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries')
fireEvent.click(await screen.findByRole('button', { name: /refresh user/i }))
@@ -420,7 +420,7 @@ describe('AppBootstrapEffects', () => {
describe('External side effects', () => {
it('should sync Zendesk fields and Amplitude identity when bootstrap data is available', async () => {
- renderBootstrapEffects()
+ renderConsoleBootstrap()
await waitFor(() => {
expect(setZendeskConversationFields).toHaveBeenCalledWith([{
@@ -469,7 +469,7 @@ describe('AppBootstrapEffects', () => {
},
}
- renderBootstrapEffects()
+ renderConsoleBootstrap()
await screen.findByText('user:')
expect(setUserId).not.toHaveBeenCalled()
diff --git a/web/context/amplitude-identity-sync.ts b/web/context/amplitude-identity-sync.ts
new file mode 100644
index 00000000000..7ccbf570fba
--- /dev/null
+++ b/web/context/amplitude-identity-sync.ts
@@ -0,0 +1,65 @@
+'use client'
+
+import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen'
+import type { ICurrentWorkspace } from '@/models/common'
+import { atom } from 'jotai'
+import { atomEffect } from 'jotai-effect'
+import { setUserId, setUserProperties } from '@/app/components/base/amplitude'
+import { flushRegistrationSuccess } from '@/app/components/base/amplitude/registration-tracking'
+import {
+ currentWorkspaceAtom,
+ userProfileAtom,
+} from './app-context-state'
+
+type AmplitudeProperties = Record
+
+const amplitudeIdentityAtom = atom(undefined)
+
+function buildAmplitudeProperties({
+ currentWorkspace,
+ userProfile,
+}: {
+ currentWorkspace: ICurrentWorkspace
+ userProfile: GetAccountProfileResponse
+}) {
+ const properties: AmplitudeProperties = {
+ email: userProfile.email,
+ name: userProfile.name,
+ has_password: userProfile.is_password_set,
+ }
+
+ if (currentWorkspace.id) {
+ properties.workspace_id = currentWorkspace.id
+ properties.workspace_name = currentWorkspace.name
+ properties.workspace_plan = currentWorkspace.plan
+ properties.workspace_status = currentWorkspace.status
+ properties.workspace_role = currentWorkspace.role
+ }
+
+ return properties
+}
+
+export const amplitudeIdentitySyncAtom = atomEffect((get, set) => {
+ const userProfile = get(userProfileAtom)
+ const currentWorkspace = get(currentWorkspaceAtom)
+
+ if (!userProfile.id)
+ return
+
+ const properties = buildAmplitudeProperties({
+ currentWorkspace,
+ userProfile,
+ })
+ const identity = JSON.stringify({
+ userId: userProfile.email,
+ properties,
+ })
+
+ if (identity === get.peek(amplitudeIdentityAtom))
+ return
+
+ setUserId(userProfile.email)
+ setUserProperties(properties)
+ flushRegistrationSuccess()
+ set(amplitudeIdentityAtom, identity)
+})
diff --git a/web/context/app-bootstrap-effects.tsx b/web/context/app-bootstrap-effects.tsx
deleted file mode 100644
index 0493cfffc99..00000000000
--- a/web/context/app-bootstrap-effects.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-'use client'
-
-import {
- useSyncAmplitudeIdentity,
- useSyncZendeskFields,
-} from './app-context-effects'
-
-export function AppBootstrapEffects() {
- useSyncZendeskFields()
- useSyncAmplitudeIdentity()
-
- return null
-}
diff --git a/web/context/app-context-effects.ts b/web/context/app-context-effects.ts
deleted file mode 100644
index 875cc2ec638..00000000000
--- a/web/context/app-context-effects.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-'use client'
-
-import { useAtomValue } from 'jotai'
-import { useEffect } from 'react'
-import { setUserId, setUserProperties } from '@/app/components/base/amplitude'
-import { flushRegistrationSuccess } from '@/app/components/base/amplitude/registration-tracking'
-import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
-import { ZENDESK_FIELD_IDS } from '@/config'
-import {
- currentWorkspaceAtom,
- langGeniusVersionInfoAtom,
- userProfileAtom,
-} from './app-context-state'
-
-export function useSyncZendeskFields() {
- const userProfile = useAtomValue(userProfileAtom)
- const currentWorkspace = useAtomValue(currentWorkspaceAtom)
- const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
-
- useEffect(() => {
- if (ZENDESK_FIELD_IDS.ENVIRONMENT && langGeniusVersionInfo?.current_env) {
- setZendeskConversationFields([{
- id: ZENDESK_FIELD_IDS.ENVIRONMENT,
- value: langGeniusVersionInfo.current_env.toLowerCase(),
- }])
- }
- }, [langGeniusVersionInfo?.current_env])
-
- useEffect(() => {
- if (ZENDESK_FIELD_IDS.VERSION && langGeniusVersionInfo?.version) {
- setZendeskConversationFields([{
- id: ZENDESK_FIELD_IDS.VERSION,
- value: langGeniusVersionInfo.version,
- }])
- }
- }, [langGeniusVersionInfo?.version])
-
- useEffect(() => {
- if (ZENDESK_FIELD_IDS.EMAIL && userProfile?.email) {
- setZendeskConversationFields([{
- id: ZENDESK_FIELD_IDS.EMAIL,
- value: userProfile.email,
- }])
- }
- }, [userProfile?.email])
-
- useEffect(() => {
- if (ZENDESK_FIELD_IDS.WORKSPACE_ID && currentWorkspace?.id) {
- setZendeskConversationFields([{
- id: ZENDESK_FIELD_IDS.WORKSPACE_ID,
- value: currentWorkspace.id,
- }])
- }
- }, [currentWorkspace?.id])
-}
-
-export function useSyncAmplitudeIdentity() {
- const userProfile = useAtomValue(userProfileAtom)
- const currentWorkspace = useAtomValue(currentWorkspaceAtom)
-
- useEffect(() => {
- if (userProfile?.id) {
- setUserId(userProfile.email)
- const properties: Record = {
- email: userProfile.email,
- name: userProfile.name,
- has_password: userProfile.is_password_set,
- }
-
- if (currentWorkspace?.id) {
- properties.workspace_id = currentWorkspace.id
- properties.workspace_name = currentWorkspace.name
- properties.workspace_plan = currentWorkspace.plan
- properties.workspace_status = currentWorkspace.status
- properties.workspace_role = currentWorkspace.role
- }
-
- setUserProperties(properties)
- flushRegistrationSuccess()
- }
- }, [userProfile, currentWorkspace])
-}
diff --git a/web/context/zendesk-conversation-sync.ts b/web/context/zendesk-conversation-sync.ts
new file mode 100644
index 00000000000..535731b4518
--- /dev/null
+++ b/web/context/zendesk-conversation-sync.ts
@@ -0,0 +1,88 @@
+'use client'
+
+import { atom } from 'jotai'
+import { atomEffect } from 'jotai-effect'
+import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
+import { ZENDESK_FIELD_IDS } from '@/config'
+import {
+ currentWorkspaceAtom,
+ langGeniusVersionInfoAtom,
+ userProfileAtom,
+} from './app-context-state'
+
+type ZendeskSyncState = {
+ email?: string
+ environment?: string
+ version?: string
+ workspaceId?: string
+}
+
+const zendeskConversationSyncStateAtom = atom({})
+
+function syncZendeskField({
+ fieldId,
+ previousValue,
+ setNextValue,
+ value,
+}: {
+ fieldId: string | undefined
+ previousValue: string | undefined
+ setNextValue: (value: string) => void
+ value: string
+}) {
+ if (!fieldId || !value || value === previousValue)
+ return false
+
+ setZendeskConversationFields([{
+ id: fieldId,
+ value,
+ }])
+ setNextValue(value)
+
+ return true
+}
+
+export const zendeskConversationSyncAtom = atomEffect((get, set) => {
+ const userProfile = get(userProfileAtom)
+ const currentWorkspace = get(currentWorkspaceAtom)
+ const langGeniusVersionInfo = get(langGeniusVersionInfoAtom)
+ const state = get.peek(zendeskConversationSyncStateAtom)
+ const nextState = { ...state }
+
+ let didSync = false
+ didSync = syncZendeskField({
+ fieldId: ZENDESK_FIELD_IDS.ENVIRONMENT,
+ value: langGeniusVersionInfo.current_env.toLowerCase(),
+ previousValue: state.environment,
+ setNextValue: (value) => {
+ nextState.environment = value
+ },
+ }) || didSync
+ didSync = syncZendeskField({
+ fieldId: ZENDESK_FIELD_IDS.VERSION,
+ value: langGeniusVersionInfo.version,
+ previousValue: state.version,
+ setNextValue: (value) => {
+ nextState.version = value
+ },
+ }) || didSync
+ didSync = syncZendeskField({
+ fieldId: ZENDESK_FIELD_IDS.EMAIL,
+ value: userProfile.email,
+ previousValue: state.email,
+ setNextValue: (value) => {
+ nextState.email = value
+ },
+ }) || didSync
+ didSync = syncZendeskField({
+ fieldId: ZENDESK_FIELD_IDS.WORKSPACE_ID,
+ value: currentWorkspace.id,
+ previousValue: state.workspaceId,
+ setNextValue: (value) => {
+ nextState.workspaceId = value
+ },
+ }) || didSync
+
+ if (didSync)
+ set(zendeskConversationSyncStateAtom, nextState)
+})
diff --git a/web/package.json b/web/package.json
index f4ff8e96e73..7f61d6ed3ab 100644
--- a/web/package.json
+++ b/web/package.json
@@ -109,6 +109,7 @@
"i18next-resources-to-backend": "catalog:",
"immer": "catalog:",
"jotai": "catalog:",
+ "jotai-effect": "catalog:",
"jotai-scope": "catalog:",
"jotai-tanstack-form": "workspace:*",
"jotai-tanstack-query": "catalog:",