Files
leadchat/app/javascript/widget/composables/useAvailability.js
Rob Coenen 5608f5a1a2 fix: Widget shows 'away' on initial load despite agents being online (#12869)
## Description

Fixes #12868

This PR fixes a Vue 3 reactivity bug that causes the widget to display
"We are away at the moment" on initial page load, even when agents are
online and the API correctly returns their availability.

## Problem

The widget welcome screen shows "We are away" on first render, only
updating to show correct agent status after navigating to the
conversation view and back. This misleads visitors into thinking no
agents are available.

**Reproduction:** Open website with widget in fresh incognito window,
click bubble immediately → shows "away" despite agents being online.

## Root Cause

Vue 3 reactivity chain breaks in the `useAvailability` composable:

**Before (broken):**
```javascript
// AvailabilityContainer.vue
const { isOnline } = useAvailability(props.agents); // Passes VALUE

// useAvailability.js  
const availableAgents = toRef(agents); // Creates ref from VALUE, doesn't track changes
```

When the API responds and updates the Vuex store, the parent component's
computed `props.agents` updates correctly, but the composable's
`toRef()` doesn't know about the change because it was created from a
static value, not a reactive source.

## Solution

**After (fixed):**
```javascript
// AvailabilityContainer.vue
const { isOnline } = useAvailability(toRef(props, 'agents')); // Passes REACTIVE REF

// useAvailability.js
const availableAgents = computed(() => unref(agents)); // Unwraps ref and tracks changes
```

Now when `props.agents` updates after the API response, the `computed()`
re-evaluates and all downstream reactive properties (`hasOnlineAgents`,
`isOnline`) update correctly.

## Testing

-  Initial page load shows correct agent status immediately
-  Status changes via WebSocket update correctly  
-  No configuration changes or workarounds needed
-  Tested with network monitoring (Puppeteer) confirming API returns
correct data

## Files Changed

1.
`app/javascript/widget/components/Availability/AvailabilityContainer.vue`
   - Pass `toRef(props, 'agents')` instead of `props.agents`

2. `app/javascript/widget/composables/useAvailability.js`
   - Use `computed(() => unref(agents))` instead of `toRef(agents)`
   - Added explanatory comments

## Related Issues

- #5918 - Similar symptoms, closed with workaround (business hours
toggle) rather than fixing root cause
- #5763 - Different issue (mobile app presence)

This is a genuine Vue 3 reactivity bug affecting all widgets,
independent of business hours configuration.

Co-authored-by: rcoenen <rcoenen@users.noreply.github.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2025-11-20 20:27:44 +05:30

74 lines
2.2 KiB
JavaScript

import { computed, unref } from 'vue';
import {
isOnline as checkIsOnline,
isInWorkingHours as checkInWorkingHours,
} from 'widget/helpers/availabilityHelpers';
import { useCamelCase } from 'dashboard/composables/useTransformKeys';
const DEFAULT_TIMEZONE = 'UTC';
const DEFAULT_REPLY_TIME = 'in_a_few_minutes';
/**
* Composable for availability-related logic
* @param {Ref|Array} agents - Available agents (can be ref or raw array)
* @returns {Object} Availability utilities and computed properties
*/
export function useAvailability(agents = []) {
// Now receives toRef(props, 'agents') from caller, which maintains reactivity.
// Use unref() inside computed to unwrap the ref value properly.
// This ensures availableAgents updates when the parent's agents prop changes
// (e.g., after API response updates the Vuex store).
const availableAgents = computed(() => unref(agents));
const channelConfig = computed(() => window.chatwootWebChannel || {});
const inboxConfig = computed(() => ({
workingHours: channelConfig.value.workingHours?.map(useCamelCase) || [],
workingHoursEnabled: channelConfig.value.workingHoursEnabled || false,
timezone: channelConfig.value.timezone || DEFAULT_TIMEZONE,
utcOffset:
channelConfig.value.utcOffset ||
channelConfig.value.timezone ||
DEFAULT_TIMEZONE,
replyTime: channelConfig.value.replyTime || DEFAULT_REPLY_TIME,
}));
const currentTime = computed(() => new Date());
const hasOnlineAgents = computed(() => {
const agentList = availableAgents.value || [];
return Array.isArray(agentList) ? agentList.length > 0 : false;
});
const isInWorkingHours = computed(() =>
checkInWorkingHours(
currentTime.value,
inboxConfig.value.utcOffset,
inboxConfig.value.workingHours
)
);
// Check if online (considering both working hours and agents)
const isOnline = computed(() =>
checkIsOnline(
inboxConfig.value.workingHoursEnabled,
currentTime.value,
inboxConfig.value.utcOffset,
inboxConfig.value.workingHours,
hasOnlineAgents.value
)
);
return {
channelConfig,
inboxConfig,
currentTime,
availableAgents,
hasOnlineAgents,
isOnline,
isInWorkingHours,
};
}