Commit Graph

4360 Commits

Author SHA1 Message Date
Shivam Mishra
c218eff5ec feat: add per-webhook secret with backfill migration (#13573) 2026-02-26 17:26:12 +05:30
Muhsin Keloth
6b3f1114fd fix(slack): Show correct sender name and avatar for Slack replies (#13624) 2026-02-26 16:15:15 +05:30
Sivin Varghese
109b43aadb chore: Disable API channel reply editor outside 24h window (#13664) 2026-02-26 16:05:05 +05:30
Vishnu Narayanan
3ddab3ab26 fix: show upgrade prompt when email transcript returns 402 (#13650)
- Show a specific upgrade prompt when free-plan users attempt to send an
email transcript and the API returns a 402 Payment Required error
- Previously, a generic "There was an error, please try again" message
was shown for all failures, including plan restrictions

Fixes
https://linear.app/chatwoot/issue/CW-6538/show-ui-feedback-for-email-transcript-402-plan-restriction
2026-02-26 12:54:40 +05:30
Pranav
e2dd2ccb42 feat: Add a priority + created at sort for conversations (#13658)
- Add a new conversation sort option "Priority: Highest first, Created:
Oldest first" that sorts by priority descending (urgent > high > medium
> low > none) with created_at ascending as the tiebreaker
2026-02-25 18:22:41 -08:00
Pranav
9fab70aebf fix: Use search API instead of filter in the filter in the endpoints (#13651)
- Replace `POST /contacts/filter` with `GET /contacts/search` for
contact lookup in compose new conversation
- Remove client-side input-type detection logic (`generateContactQuery`,
key filtering by email/phone/name) — the search API handles matching
across name, email, phone_number, and identifier server-side via a
single `ILIKE` query
- Filter the contacts with emails in cc and bcc fields.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-02-25 09:08:24 -08:00
Sivin Varghese
5aef9d2dd0 fix: Conversation list overlay issue with Virtua virtualizer (#13648) 2026-02-25 20:18:34 +05:30
Sivin Varghese
ba804e0f30 fix: Upgrade pico-search to 0.6.0 (#13645) 2026-02-25 16:52:45 +05:30
Sojan Jose
a44cb2c738 feat(inbox): Enable conversation continuity for social channels (#11079)
## Summary
This PR enables and surfaces **conversation workflow** for social-style
channels that should support either:
- `Create new conversations` after resolve, or
- `Reopen same conversation`

## What is included
- Adds the conversation workflow setting UI as card-based options in
Inbox Settings.
- Expands channel availability in settings to include channels like:
  - Telegram
  - TikTok
  - Instagram
  - Line
  - WhatsApp
  - Facebook
- Updates conversation selection behavior for Line incoming messages to
respect the workflow (reopen vs create-new-after-resolved).
- Updates TikTok conversation selection behavior to respect the workflow
(reopen vs create-new-after-resolved).
- Keeps email behavior unchanged (always starts a new thread).

Fixes: https://github.com/chatwoot/chatwoot/issues/8426

## Screenshot

<img width="1400" height="900" alt="pr11079-workflow-sender-clear-tight"
src="https://github.com/user-attachments/assets/9456821f-8d83-4924-8dcf-7503c811a7b1"
/>


## How To Reproduce
1. Open `Settings -> Inboxes ->
<Telegram/TikTok/Instagram/Line/Facebook/WhatsApp inbox> -> Settings`.
2. Verify **Conversation workflow** is visible with the two card
options.
3. Toggle between both options and save.
4. For Line and TikTok, verify resolved-conversation behavior follows
the selected workflow.

## Testing
- `RAILS_ENV=test bundle exec rspec
spec/builders/messages/instagram/message_builder_spec.rb:213
spec/builders/messages/instagram/message_builder_spec.rb:255
spec/builders/messages/instagram/messenger/message_builder_spec.rb:228
spec/builders/messages/instagram/messenger/message_builder_spec.rb:293
spec/services/tiktok/message_service_spec.rb`
- Result: `16 examples, 0 failures`

## Follow-up
- Migrate Website Live Chat workflow settings into this same
conversation-workflow settings model.
- Add Voice channel support for this workflow setting.

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-02-25 13:56:51 +04:00
Sivin Varghese
172ff87b5b feat: Replace vue-virtual-scroller with virtua for chat list virtualization (#13642)
# Pull Request Template

## Description

This PR replaces `vue-virtual-scroller` with
[`virtua`](https://github.com/inokawa/virtua/#benchmark) for the
conversation list virtualization.

### Changes
- Replace `vue-virtual-scroller`
(`DynamicScroller`/`DynamicScrollerItem`) with `virtua`'s `Virtualizer`
component
- Remove `IntersectionObserver`-based infinite scroll in favor of
`Virtualizer`'s `@scroll` event with offset-based bottom detection
- Remove `useEventListener` scroll binding and
`intersectionObserverOptions` computed
- Simplify item rendering — no more `DynamicScrollerItem` wrapper or
`size-dependencies` tracking; `virtua` measures items automatically


## Type of change

- [x] New feature (non-breaking change which adds functionality)


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-02-25 12:59:02 +04:00
Muhsin Keloth
76f129efaf feat(tiktok): Enable outgoing image attachments (#13620)
- Enabled the attachment button for TikTok conversations in the reply
box
- Auto-split messages when both text and an image are composed together,
since the TikTok API rejects mixed text+media in a single message.
Fixes
https://linear.app/chatwoot/issue/CW-6528/enable-outgoing-image-attachments
2026-02-24 20:13:58 +04:00
Muhsin Keloth
6be95e79f8 feat(csat): Add WhatsApp utility template analyzer with rewrite guidance (#13575)
CSAT templates for WhatsApp are submitted as Utility, but Meta may
reclassify them as Marketing based on content, which can significantly
increase messaging costs.
This PR introduces a Captain-powered CSAT template analyzer for
WhatsApp/Twilio WhatsApp that predicts utility fit, explains likely
risks, and suggests safer rewrites before submission. The flow is manual
(button-triggered), Captain-gated, and applies rewrites only on explicit
user action. It also updates UX copy to clearly set expectations: the
system submits as Utility, Meta makes the final categorization decision.

Fixes
https://linear.app/chatwoot/issue/CW-6424/ai-powered-whatsapp-template-classifier-for-csat-submissions


https://github.com/user-attachments/assets/8fd1d6db-2f91-447c-9771-3de271b16fd9
2026-02-24 15:11:04 +04:00
Tanmay Deep Sharma
2b85275e26 feat: show assignment policy name in auto-assignment activity messages (#13598) 2026-02-24 13:32:54 +05:30
Muhsin Keloth
5b167b5b5b fix(contacts): Show telegram id in contact details form (#13611)
## Summary
This change fixes a mismatch in contact details where Telegram data
could be shown in the contact profile/social icon area but was not
available in the editable contact form.

### What changed
- Added Telegram to the social links section of the next-gen contact
form so agents can view and edit it alongside Facebook, Instagram,
TikTok, Twitter, GitHub, and LinkedIn.
- Added Telegram support to the legacy conversation contact edit form
for parity between both contact editing experiences.
- Mapped social_telegram_user_name into the editable socialProfiles
payload when preparing contact form state, so Telegram usernames sourced
from channel attributes are visible in the form.
- Updated the conversation contact social profile merge logic so
Telegram display prefers an explicitly saved social profile value and
falls back to social_telegram_user_name when needed.
- Added the missing English i18n placeholder: Add Telegram.

### Why
Without this, users could see Telegram info in some contact views but
could not reliably edit it in contact details, creating inconsistent
behavior between display and edit states.

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2026-02-23 19:26:45 +04:00
Muhsin Keloth
b220663785 fix: Skip notifications for private notes (#13617)
When agents or integrations create private notes on a conversation,
every note was sending a notification to the assigned agent and all
conversation participants. The fix ensures that private notes no longer
trigger new message notifications. If someone explicitly mentions a
teammate in a private note, that person will still get notified as
expected.
2026-02-23 15:40:54 +04:00
Shivam Mishra
40da358dc2 feat: better errors for SMTP (#13401)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-02-23 16:00:17 +05:30
Tanmay Deep Sharma
957a1b17c9 perf: add default configs for assignment V2 (#13577)
## Description

AutoAssignment::RateLimiter#within_limit? returned true early for
inboxes without an AssignmentPolicy, bypassing fair distribution
entirely and allowing unlimited conversation assignment.

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)

## Script

- Script to enable users with V2 assignment:
https://www.notion.so/chatwoot/Script-to-migrate-account-to-assignment-V2-30ca5f274c9280f5b8ecfd15e28eeb9c?source=copy_link

## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-02-23 15:08:11 +05:30
Vishnu Narayanan
2441487a76 perf: skip conversation loading in /meta endpoint (#13564)
# Pull Request Template

## Summary
- Adds `perform_meta_only` method to `ConversationFinder` that runs
setup and counts without loading the paginated conversation list
- Updates `/api/v1/conversations/meta` to use `perform_meta_only`
instead of `perform`

## Problem
The `/meta` endpoint calls `ConversationFinder#perform` which:
1. Runs all filters and setup (`set_up`)
2. Computes 3 COUNT queries (`set_count_for_all_conversations`)
3. Filters by assignee type
4. **Builds the full paginated conversation list** with
`.includes(:taggings, :inbox, {assignee: {avatar_attachment: [:blob]}},
{contact: {avatar_attachment: [:blob]}}, :team, :contact_inbox)` +
sorting + pagination

The controller then **discards the conversations** and only uses the
counts:
```ruby
def meta
result = conversation_finder.perform
@conversations_count = result[:count]  # conversations thrown away
end
```

## Type of change

- [x] Performance fix

## How Has This Been Tested?

- [ ] Verify /meta returns correct mine/unassigned/assigned/all counts
- [ ] Verify counts update when switching inbox, team, or status filters
- [ ] Verify conversation list still loads correctly (uses perform, not
affected)
- [ ] Monitor response time reduction for /meta in NewRelic after deploy

## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Pranav <pranav@chatwoot.com>
2026-02-20 21:20:19 +05:30
Sivin Varghese
418bd177f8 fix: Adjust inbox settings pages layout width (#13590)
# Pull Request Template

## Description

This PR includes,

1. Adjusting the inbox settings page layout width from 3xl to 4xl for
the collaborators, configuration, and bot configuration sections.
2. Adding a dynamic max-width for inbox settings banners based on the
selected tab.
3. Making the sender name preview layout responsive.
4. Reordering automation rule row buttons so Clone appears before
Delete.
5. Update the Gmail icon ratio.
6. Fix height issues with team/inbox pages
7. The delete button changes to red on hover
8. Add border to conversation header when no dashboard apps present


## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-02-20 20:20:32 +05:30
Shivam Mishra
572f5b2709 Merge branch 'hotfix/4.11.1' into develop 2026-02-20 20:02:39 +05:30
Shivam Mishra
280ca06e5b fix: url endpoint
fix: spec
2026-02-20 20:01:14 +05:30
Natã
dbab0fe8da fix: search header overlap with new conversation form (#13548) 2026-02-20 11:24:37 +05:30
Sivin Varghese
6902969a09 chore: Remove vue-multiselect package and styles from codebase (#13585) 2026-02-19 15:42:34 +05:30
Sivin Varghese
7b2b3ac37d feat(V5): Update settings pages UI (#13396)
# Pull Request Template

## Description

This PR updates settings page UI


## Type of change

- [x] New feature (non-breaking change which adds functionality)


## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-02-19 15:04:40 +05:30
Muhsin Keloth
e75e8a77f6 feat(shopify): Add mandatory compliance webhooks with HMAC verification (#13549)
Fixes
https://linear.app/chatwoot/issue/CW-6494/add-shopify-mandatory-compliance-webhooks-for-app-store-listing

Shopify requires all public apps to handle three GDPR compliance
webhooks before they can be listed on the App Store. Their automated
review checks for these endpoints and verifies that apps validate HMAC
signatures on incoming requests. We were failing both checks.

This PR adds a single webhook endpoint at `POST /webhooks/shopify` that
receives all three compliance events. When Shopify sends a webhook, it
signs the payload with our app's client secret and includes the
signature in the `X-Shopify-Hmac-SHA256` header. Our controller reads
the raw body, computes the expected HMAC-SHA256 digest, and rejects
mismatched requests with a 401.

Shopify identifies the event type through the `X-Shopify-Topic` header.
For `customers/data_request` and `customers/redact`, we simply
acknowledge with a 200—Chatwoot doesn't persist any Shopify customer
data. All order lookups happen as live API calls at query time. For
`shop/redact`, which Shopify sends after a merchant uninstalls the app,
we delete the integration hook for that shop domain and remove the
stored access token and configuration.


### How to test via Rails console
```
secret = GlobalConfigService.load('SHOPIFY_CLIENT_SECRET', nil)
body = '{"shop_domain":"test.myshopify.com"}'
valid_hmac = Base64.strict_encode64(OpenSSL::HMAC.digest('SHA256', secret, body))
```

  #### Test 1: No HMAC → 401
```
app.post '/webhooks/shopify', params: body, headers: { 'Content-Type' => 'application/json', 'X-Shopify-Topic' => 'customers/data_request' }
app.response.code  # => "401"
```
  ####  Test 2: Invalid HMAC → 401
```
app.post '/webhooks/shopify', params: body, headers: { 'Content-Type' => 'application/json', 'X-Shopify-Hmac-SHA256' => 'invalid', 'X-Shopify-Topic' => 'customers/data_request' }
app.response.code  # => "401"
```
  ####  Test 3: Valid HMAC, customers/data_request → 200
```
app.post '/webhooks/shopify', params: body, headers: { 'Content-Type' => 'application/json', 'X-Shopify-Hmac-SHA256' => valid_hmac, 'X-Shopify-Topic' => 'customers/data_request' }
app.response.code  # => "200"
```

####  Test 4: Valid HMAC, customers/redact → 200
```
app.post '/webhooks/shopify', params: body, headers: { 'Content-Type' => 'application/json', 'X-Shopify-Hmac-SHA256' => valid_hmac, 'X-Shopify-Topic' => 'customers/redact' }
app.response.code  # => "200"
```

#### Test 5: Valid HMAC, shop/redact → 200 (deletes hook)
```  
# First check if a hook exists for this domain:
Integrations::Hook.where(app_id: 'shopify', reference_id: 'test.myshopify.com').count
app.post '/webhooks/shopify', params: body, headers: { 'Content-Type' => 'application/json', 'X-Shopify-Hmac-SHA256' => valid_hmac, 'X-Shopify-Topic' => 'shop/redact' }
app.response.code  # => "200"
```

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-02-17 16:52:13 +05:30
Sivin Varghese
229f56d6e3 chore: Remove vue-multiselect and migrate to next components (#13506)
# Pull Request Template

## Description

This PR includes:
1. Removes multiselect usage from the Merge Contact modal (Conversation
sidebar) and replaces it with the existing component used on the Contact
Details page.
2. Replaces legacy form and multiselect elements in Add and Edit
automations flows with next components.**(Also check Macros)**
3. Replace multiselect with ComboBox in contact form country field.
4. Replace multiselect with TagInput in create/edit attribute form.
5. Replace multiselect with TagInput for agent selection in inbox
creation.
6. Replace multiselect with ComboBox in Facebook channel page selection

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

**Screenshots**

1. **Merge modal**
<img width="741" height="449" alt="image"
src="https://github.com/user-attachments/assets/a05a96ec-0692-4d94-9e27-d3e85fd143e4"
/>
<img width="741" height="449" alt="image"
src="https://github.com/user-attachments/assets/fc1dc977-689d-4440-869d-2124e4ca9083"
/>

2. **Automations**
<img width="849" height="1089" alt="image"
src="https://github.com/user-attachments/assets/b0155f06-ab21-4f90-a2c8-5bfbd97b08f7"
/>
<img width="813" height="879" alt="image"
src="https://github.com/user-attachments/assets/0921ac4a-88f5-49ac-a776-cc02941b479c"
/>
<img width="849" height="826" alt="image"
src="https://github.com/user-attachments/assets/44358dae-a076-4e10-b7ba-a4e40ccd817f"
/>

3. **Country field**
<img width="462" height="483" alt="image"
src="https://github.com/user-attachments/assets/d5db9aa1-b859-4327-9960-957d7091678f"
/>

4. **Add/Edit attribute form**
<img width="619" height="646" alt="image"
src="https://github.com/user-attachments/assets/6ab2ea94-73e5-40b8-ac29-399c0543fa7b"
/>
<img width="619" height="646" alt="image"
src="https://github.com/user-attachments/assets/b4c5bb0e-baa0-4ef7-a6a2-adb0f0203243"
/>
<img width="635" height="731" alt="image"
src="https://github.com/user-attachments/assets/74890c80-b213-4567-bf5f-4789dda39d2d"
/>

5. **Agent selection in inbox creation**
<img width="635" height="534" alt="image"
src="https://github.com/user-attachments/assets/0003bad1-1a75-4f20-b014-587e1c19a620"
/>
<img width="809" height="602" alt="image"
src="https://github.com/user-attachments/assets/5e7ab635-7340-420a-a191-e6cd49c02704"
/>

7. **Facebook channel page selection**
<img width="597" height="444" alt="image"
src="https://github.com/user-attachments/assets/f7ec8d84-0a7d-4bc6-92a1-a1365178e319"
/>
<img width="597" height="444" alt="image"
src="https://github.com/user-attachments/assets/d0596c4d-94c1-4544-8b50-e7103ff207a6"
/>
<img width="597" height="444" alt="image"
src="https://github.com/user-attachments/assets/be097921-011b-4dbe-b5f1-5d1306e25349"
/>



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-02-17 16:40:12 +05:30
Sivin Varghese
c5f6844877 fix: Disable reply editor outside WhatsApp reply window (#13454) 2026-02-17 14:07:36 +05:30
Shivam Mishra
39243b9e71 fix: duplicate message_created webhooks for WhatsApp messages (#13523)
Some customers using WhatsApp inboxes with account-level webhooks were
reporting receiving duplicate `message_created` webhook deliveries for
every incoming message. Upon inspection, here's what we found

- Both payloads are identical.
- No errors appear in the application logs
- Webhook URL is only configured in one place. 

This meant, the system was sending the webhooks twice. For some context,
there's a know related issue... Meta's WhatsApp Business API can deliver
the same webhook notification multiple times for a single message. The
codebase already acknowledges this — there's a comment in
`IncomingMessageBaseService#process_messages` noting that "multiple
webhook events can be received against the same message due to
misconfigurations in the Meta business manager account." A deduplication
guard exists, but it doesn't actually work under concurrency.

### Rationale

The existing dedup was a three-step sequence: check Redis (`GET`), check
the database, then set a Redis flag (`SETEX`). Two Sidekiq workers
processing duplicate Meta webhooks simultaneously would both complete
the `GET` before either executed the `SETEX`, so both would proceed to
create a message. The `source_id` column has a non-unique index, so the
database wouldn't catch the duplicate either. Each message then
independently fires `after_create_commit`, dispatching two
`message_created` webhook events to the customer.

```
             Worker A                          Worker B
                │                                 │
                ▼                                 ▼
        Redis GET key ──► nil               Redis GET key ──► nil
                │                                 │
                │    ◄── both pass guard ──►      │
                │                                 │
                ▼                                 ▼
        Redis SETEX key                    Redis SETEX key
                │                                 │
                ▼                                 ▼
        BEGIN transaction               BEGIN transaction
        INSERT message                   INSERT message
        DELETE Redis key ◄─┐                      │
        COMMIT             │             DELETE Redis key
                           │             COMMIT
                           │                      │
                           └── key gone before ───┘
                              B's commit lands

                ▼                                 ▼
        after_create_commit              after_create_commit
        dispatch MESSAGE_CREATED         dispatch MESSAGE_CREATED
                │                                 │
                ▼                                 ▼
        WebhookJob ──► n8n               WebhookJob ──► n8n
                    (duplicate!)
```

There was a second, subtler problem visible in the diagram: the Redis
key was cleared *inside* the database transaction, before the
transaction committed. This opened a window where neither the Redis
check nor the database check would see the in-flight message.

The fix collapses the check-and-set into a single `SET NX EX` call,
which is atomic in Redis. The key is no longer eagerly cleared — it
expires naturally after 24 hours. The database lookup
(`find_message_by_source_id`) remains as a fallback for messages that
were created before the lock expired.

```
             Worker A                          Worker B
                │                                 │
                ▼                                 ▼
        Redis SET NX ──► OK              Redis SET NX ──► nil
                │                                 │
                ▼                                 ▼
        proceeds to create              returns early
        message normally                (lock already held)
```

### Implementation Notes

The lock logic is extracted into `Whatsapp::MessageDedupLock`, a small
class that wraps a single `Redis SET NX EX` call. This makes the
concurrency guarantee testable in isolation — the spec uses a
`CyclicBarrier` to race two threads against the same key and asserts
exactly one wins, without needing database writes,
`use_transactional_tests = false`, or monkey-patching.

Because the Redis lock now persists (instead of being cleared
mid-transaction), existing WhatsApp specs needed an `after` hook to
clean up `MESSAGE_SOURCE_KEY::*` keys between examples. Transactional
fixtures only roll back the database, not Redis.
2026-02-17 14:01:10 +05:30
Sivin Varghese
fb2f5e1d42 fix: Persist compose form state on accidental outside click (#13529) 2026-02-17 13:57:44 +05:30
Sivin Varghese
cfe3061b5d feat: Allow removing labels via conversation context menu (#13525)
# Pull Request Template

## Description

This PR adds support for removing labels from the conversation card
context menu. Assigned labels now show a checkmark, and clicking an
already-selected label will remove it.

Fixes
https://linear.app/chatwoot/issue/CW-6400/allow-removing-labels-directly-from-the-right-click-menu
https://github.com/chatwoot/chatwoot/issues/13367
## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

**Screencast**


https://github.com/user-attachments/assets/4e3a6080-a67d-4851-9d10-d8dbf3ceeb04




## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-02-17 13:30:55 +05:30
Aakash Bakhle
101eca3003 feat: add captain editor events (#13524)
## Description

Adds missing analytics instrumentation for the editor AI funnel so we
can measure end-to-end usage and outcome quality.

### What was added

- Captain: Editor AI menu opened
- Captain: Generation failed
- Captain: AI-assisted message sent

### Behavior covered

- Tracks AI button click + menu open from both entry points:
    - top panel sparkle button
    - inline editor copilot button
- Tracks generation failures (initial + follow-up stages).
- Tracks whether accepted AI content was sent as-is or edited before
send.

### Notes

- Applies to editor Captain accept/send flow
(rewrite/summarize/reply_suggestion + follow-ups).
- Does not change Copilot sidebar flow instrumentation.

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality not to work as expected)
- [ ] This change requires a documentation update

## How Has This Been Tested?

### Manual verification steps

<img width="1906" height="832" alt="image"
src="https://github.com/user-attachments/assets/f0ade43b-aa8d-41be-8ca2-20a091a81f60"
/>

<img width="828" height="280" alt="image"
src="https://github.com/user-attachments/assets/be76219e-fb61-4a6e-bff5-dc085b0a3cc9"
/>

<img width="415" height="147" alt="image"
src="https://github.com/user-attachments/assets/36802c5c-33a7-49ed-bf7e-f0b02d86dccc"
/>

<img width="2040" height="516" alt="image"
src="https://github.com/user-attachments/assets/74b95288-bc86-4312-a282-14211ae8f25c"
/>


1. Open a conversation with Captain tasks enabled.
2. Click AI button in top panel and inline editor.
3. Confirm analytics events fire for:
    - AI menu opened
4. Run an AI action and force a failure scenario (or empty response
path) and confirm generation-failed event.
5. Accept AI output, then:
    - send without changes -> editedBeforeSend: false
    - edit then send -> editedBeforeSend: true

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
2026-02-17 13:26:56 +05:30
Tanmay Deep Sharma
9cd7c4ef89 fix: Enhance notification emails with message details and handle failed messages (#13273)
## Description

Handle messages with null content properly in UI and email notifications

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)

## Relevant Screenshots:
<img width="688" height="765" alt="Screenshot 2026-01-21 at 4 43 00 PM"
src="https://github.com/user-attachments/assets/6a27c22e-2ae6-4377-a05d-cfa44bf181fe"
/>


## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches notification email templates and message rendering conditions;
mistakes could lead to missing content/attachments in emails or
incorrect UI visibility, but changes are localized and non-auth/security
related.
> 
> **Overview**
> Agent notification emails for *assigned* and *participating* new
messages now include the actual message details (sender name, rendered
text when present, and attachment links) and gracefully fall back when
content is unavailable.
> 
> To support this, the mailer now passes `@message` into Liquid via
`MessageDrop` (adding `attachments` URLs), and the dashboard message UI
now renders failed/external-error messages even when `content` is `null`
while tightening retry eligibility to require content or attachments
(and still within 1 day).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
475c8cedda54eb5e806990f977faf8098d0b27d8. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2026-02-16 14:47:33 +05:30
Tanmay Deep Sharma
f4538ae2c5 fix: Enforce team boundaries to prevent cross-team assignments (#13353)
## Description

Fixes a critical bug where conversations assigned to a team could be
auto-assigned to agents outside that team when all team members were at
capacity.

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)

## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes core assignment selection for both legacy and v2 flows;
misconfiguration of `allow_auto_assign` or team membership could cause
conversations to remain unassigned.
> 
> **Overview**
> Prevents auto-assignment from crossing team boundaries by filtering
eligible agents to the conversation’s `team` members (and requiring
`team.allow_auto_assign`) in both the legacy `AutoAssignmentHandler`
path and the v2 `AutoAssignment::AssignmentService` (including the
Enterprise override).
> 
> Adds test coverage to ensure team-scoped conversations only assign to
team members, and are skipped when team auto-assign is disabled or no
team members are available; also updates the conversations controller
spec setup to include team membership.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
67ed2bda0cd8ffd56c7e0253b86369dead2e6155. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2026-02-16 14:39:20 +05:30
Sojan Jose
fd5ac2a8a3 fix: apply installation branding replacement in tooltip copy (#13538)
## Summary
Fix hardcoded `Chatwoot` branding in two UI tooltips using the existing
`useBranding` flow so self-hosted/white-label deployments no longer show
the wrong brand text.

## Changes
- LabelSuggestion tooltip now uses:
  - `replaceInstallationName($t('LABEL_MGMT.SUGGESTIONS.POWERED_BY'))`
- Message avatar tooltip (native app/external echo) now uses:
  - `replaceInstallationName(t('CONVERSATION.NATIVE_APP_ADVISORY'))`

## Why
This follows the existing branding pattern already used in the product
and keeps behavior consistent across deployments.

## Notes
- No change to message logic or API behavior.
- `AGENTS.md` updated with a branding guidance note.

## Fixes
- Fixes https://github.com/chatwoot/chatwoot/issues/13306
- Fixes https://github.com/chatwoot/chatwoot/issues/13466

## Testing

<img width="195" height="155" alt="Screenshot 2026-02-13 at 3 55 39 PM"
src="https://github.com/user-attachments/assets/5b295cdd-6e5d-42c0-bbd7-23ba7052e1c3"
/>
<img width="721" height="152" alt="Screenshot 2026-02-13 at 3 55 48 PM"
src="https://github.com/user-attachments/assets/19cec2a0-451f-4fb3-bd61-7c2e591fc3c7"
/>
2026-02-13 16:47:25 -08:00
Sojan Jose
6b7180d051 fix(twilio): prevent dead jobs on missing channel lookup (#13522)
## Why
We observed `Webhooks::TwilioEventsJob` failures ending up in Sidekiq
dead jobs when Twilio callback payloads could not be mapped to a
`Channel::TwilioSms` record. In this scenario, channel lookup raised
`ActiveRecord::RecordNotFound`, which caused retries and eventual dead
jobs instead of a graceful drop.

Related Sentry issue/search:
-
https://chatwoot-p3.sentry.io/issues/?project=6382945&query=Webhooks%3A%3ATwilioEventsJob%20ActiveRecord%3A%3ARecordNotFound

## What changed
This PR keeps the existing lookup flow but makes it non-raising:
- `app/services/twilio/incoming_message_service.rb`
  - `find_by!` -> `find_by` for account SID + phone lookup
  - Added warning log when channel lookup misses
- `app/services/twilio/delivery_status_service.rb`
  - `find_by!` -> `find_by` for account SID + phone lookup
  - Added warning log when channel lookup misses

## Reproduction
Configure a Twilio webhook callback that reaches Chatwoot but does not
match an existing Twilio channel lookup path. Before this change, the
job raises `RecordNotFound` and can end up in dead jobs after retries.
After this change, the job logs the miss and exits safely.

## Testing
- `bundle exec rspec
spec/services/twilio/incoming_message_service_spec.rb
spec/services/twilio/delivery_status_service_spec.rb`
- `bundle exec rubocop app/services/twilio/incoming_message_service.rb
app/services/twilio/delivery_status_service.rb`
2026-02-13 14:06:12 -08:00
João Pedro Baza Garcia Rodrigues
4d362da9f0 fix: Prevent user enumeration on password reset endpoint (#13528)
## Description

The current password reset endpoint returns different HTTP status codes
and messages depending on whether the email exists in the system (200
for existing emails, 404 for non-existing ones). This allows attackers
to enumerate valid email addresses via the password reset form.

## Changes

### `app/controllers/devise_overrides/passwords_controller.rb`
- Removed the `if/else` branch that returned different responses based
on email existence
- Now always returns a generic `200 OK` response with the same message
regardless of whether the email exists
- Uses safe navigation operator (`&.`) to send reset instructions only
if the user exists

### `config/locales/en.yml`
- Consolidated `reset_password_success` and `reset_password_failure`
into a single generic `reset_password` key
- New message does not reveal whether the email exists in the system

## Security Impact
- **Before**: An attacker could determine if an email was registered by
observing the HTTP status code (200 vs 404) and response message
- **After**: All requests receive the same 200 response with a generic
message, preventing user enumeration

This follows [OWASP guidelines for authentication error
messages](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#authentication-responses).

Fixes #13527
2026-02-13 13:45:40 +05:30
Shivam Mishra
2c2f0547f7 fix: Captain not responding to campaign conversations (#13489)
Co-authored-by: Aakash Bakhle <48802744+aakashb95@users.noreply.github.com>
2026-02-12 10:07:56 +05:30
Sojan Jose
d272a64ff7 fix(mailbox): handle malformed sender address headers (#13486)
## How to reproduce
When an inbound email has malformed sender headers (for example `From:
McDonald <info@example.com` without a closing `>`), mailbox
processing can raise `Mail::Field::IncompleteParseError` while resolving
sender data in `MailPresenter`.

## What changed
This PR hardens sender parsing in `MailPresenter` with a small, readable
implementation:
- Added/used a safe parser (`parse_mail_address`) that rescues
`Mail::Field::ParseError` and `Mail::Field::IncompleteParseError`.
- `sender_name` now uses the same safe parser path.
- `original_sender` now resolves candidates in order via a compact
`filter_map` flow:
  - `Reply-To`
  - `X-Original-Sender`
  - `From`
- All three candidates are parsed as email addresses before use
(including `X-Original-Sender`), and invalid values are ignored.
- `notification_email_from_chatwoot?` now compares sender addresses
case-insensitively (`casecmp?`) to avoid case-only mismatches.

## Test coverage
Added focused presenter specs for:
- malformed `From` header returns nil sender values and does not
classify as notification sender
- malformed `Reply-To` falls back to valid `From`
- valid `X-Original-Sender` is used when present
- invalid `X-Original-Sender` falls back to valid `From`
- mixed-case sender address still matches configured
`MAILER_SENDER_EMAIL`

## How this was tested
Ran:
- `bundle exec rspec spec/presenters/mail_presenter_spec.rb`
- `bundle exec rubocop app/presenters/mail_presenter.rb
spec/presenters/mail_presenter_spec.rb`

Sentry issue:
[CHATWOOT-B9Y](https://chatwoot-p3.sentry.io/issues/7005483640/)
2026-02-11 11:02:38 -08:00
Vishnu Narayanan
00ed074d72 fix: disable email transcript for free plans (#13509)
- Block email transcript functionality for accounts without a paid plan
to prevent SES abuse.
2026-02-11 21:21:36 +05:30
Tanmay Deep Sharma
7b512bd00e fix: V2 Assignment service enhancements (#13036)
## Linear Ticket:
https://linear.app/chatwoot/issue/CW-6081/review-feedback

## Description

Assignment V2 Service Enhancements

- Enable Assignment V2 on plan upgrade
- Fix UI issue with fair distribution policy display
- Add advanced assignment feature flag and enhance Assignment V2
capabilities

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

This has been tested using the UI.

## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes auto-assignment execution paths, rate limiting defaults, and
feature-flag gating (including premium plan behavior), which could
affect which conversations get assigned and when. UI rewires inbox
settings and policy flows, so regressions are possible around
navigation/linking and feature visibility.
> 
> **Overview**
> **Adds a new premium `advanced_assignment` feature flag** and uses it
to gate capacity/balanced assignment features in the UI (sidebar entry,
settings routes, assignment-policy landing cards) and backend
(Enterprise balanced selector + capacity filtering).
`advanced_assignment` is marked premium, included in Business plan
entitlements, and auto-synced in Enterprise accounts when
`assignment_v2` is toggled.
> 
> **Improves Assignment V2 policy UX** by adding an inbox-level
“Conversation Assignment” section (behind `assignment_v2`) that can
link/unlink an assignment policy, navigate to create/edit policy flows
with `inboxId` query context, and show an inbox-link prompt after
creating a policy. The policy form now defaults to enabled, disables the
`balanced` option with a premium badge/message when unavailable, and
inbox lists support click-to-navigate.
> 
> **Tightens/adjusts auto-assignment behavior**: bulk assignment now
requires `inbox.enable_auto_assignment?`, conversation ordering uses the
attached `assignment_policy` priority, and rate limiting uses
`assignment_policy` config with an infinite default limit while still
tracking assignments. Tests and i18n strings are updated accordingly.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
23bc03bf75ee4376071e4d7fc7cd564c601d33d7. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Pranav <pranav@chatwoot.com>
Co-authored-by: iamsivin <iamsivin@gmail.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-02-11 12:24:45 +05:30
Pranav
8f95fafff4 feat: Add a setting to keep conversations pending on bot failures (#13512)
Adds an account-level setting `keep_pending_on_bot_failure` to control
whether conversations should move from pending to open when agent bot
webhooks fail.

Some users experience occasional message drops and don't want
conversations to automatically reopen due to transient bot failures.
This setting gives accounts control over that behavior. This is a
temporary setting which will be removed in future once a proper fix for
it is done, so it is not added in the UI.
2026-02-10 17:27:42 -08:00
Muhsin Keloth
0ad47d87f4 fix: Use Faraday for Telegram document uploads to fix large file failures (#13397)
Fixes
https://linear.app/chatwoot/issue/CW-6415/sending-large-attachments-11mb-via-telegram-channels-fails-with-http

 #### Issue
Sending large attachments (~11MB) via Telegram channels fails with HTTP
502 (Bad Gateway) and 413 (Request Entity Too Large) errors. The issue
is caused by HTTParty's built-in multipart encoding, which reads the
entire file into an in-memory string before constructing the request
body. For large files, this produces a malformed multipart request that
Telegram's API proxy rejects.

#### Solution

Replace HTTParty with Faraday + multipart-post (both already available
in the project) for the sendDocument multipart upload. The
multipart-post gem streams file content directly from disk into the HTTP
request, producing a correctly formed multipart body that Telegram
accepts for large files.

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-02-10 14:25:25 -08:00
Sivin Varghese
e65ea24360 fix: Wrong assignee displayed after switching conversations (#13501) 2026-02-10 15:23:55 +05:30
Sivin Varghese
b252656984 fix: Prevent race condition in conversation dataFetched flag (#13492)
Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2026-02-10 15:23:14 +05:30
Tanmay Deep Sharma
04c456e0a3 fix: handle 404 errors gracefully in avatar download job (#13491)
## Description

Fixes `Avatar::AvatarFromUrlJob` logging 404 errors as ERROR when
avatars don't exist

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)


## Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Small logging-only behavior change that doesn’t affect attachment flow
or persisted data beyond existing sync-attribute updates.
> 
> **Overview**
> Updates `Avatar::AvatarFromUrlJob` error handling to treat
`Down::NotFound` (404/missing avatar URL) as a non-error: it now logs an
INFO message instead of logging as ERROR.
> 
> Other `Down::Error` failures continue to be logged as ERROR, and the
job still runs `update_avatar_sync_attributes` in `ensure`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
675f41041ae3dd4ead6e0dee5f1586dcad9750cd. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2026-02-09 13:27:23 +05:30
Sojan Jose
656ae41b24 fix(imap): handle IMAP parser/read errors without exception tracking (#13473)
When an IMAP server returns malformed or partial protocol responses,
Inboxes::FetchImapEmailsJob can raise Net::IMAP::ResponseParseError,
Net::IMAP::ResponseReadError, or Net::IMAP::ResponseTooLargeError.
Previously these errors fell through to the generic StandardError
handler and were captured repeatedly in Sentry.

This PR updates app/jobs/inboxes/fetch_imap_emails_job.rb to treat those
parser/read exceptions as handled IMAP failures by adding them to the
existing IMAP/network rescue block, so they are logged and retried on
the next scheduled run without exception tracking noise.

fixes:
https://chatwoot-p3.sentry.io/issues/7132037793/events/104fb9b4d80a4fb6ba3861c44c6c9b83/

How to reproduce:
- Use an IMAP server/inbox that intermittently returns malformed or
truncated protocol responses during search/fetch.
- Run Inboxes::FetchImapEmailsJob for that channel and observe the
raised parser/read exception.

How this was tested:
- bundle exec ruby -c app/jobs/inboxes/fetch_imap_emails_job.rb
- bundle exec rubocop app/jobs/inboxes/fetch_imap_emails_job.rb
2026-02-07 17:30:54 -08:00
Sojan Jose
f83415f299 fix(account-deletion): normalize deleted email suffix and handle collisions safely (#13472)
## Summary
This PR fixes account deletion failures by changing how orphaned user
emails are rewritten during `AccountDeletionService`.

Ref:
https://chatwoot-p3.sentry.io/issues/6715254765/events/e228a5d045ad47348d6c32448bc33b7a/

## Changes (develop -> this branch)
- Updated soft-delete email rewrite from:
  - `#{original_email}-deleted.com`
- To deterministic value:
  - `#{user.id}@chatwoot-deleted.invalid`
- Added reserved non-deliverable domain constant:
  - `@chatwoot-deleted.invalid`
- Replaced the "other accounts" check from `count.zero?` to `exists?`
(same behavior, cheaper query).
- Updated service spec expectation to match deterministic email value
and assert it differs from original email.

## Files changed
- `app/services/account_deletion_service.rb`
- `spec/services/account_deletion_service_spec.rb`

## How to verify
- Run: `bundle exec rspec
spec/services/account_deletion_service_spec.rb`
- Run: `bundle exec rubocop app/services/account_deletion_service.rb
spec/services/account_deletion_service_spec.rb`
2026-02-07 17:29:27 -08:00
Vishnu Narayanan
0a910c3763 fix: Add email rate limiting to automation rule actions (#13474) 2026-02-07 10:02:40 -08:00
Pranav
6a7cbcf5ba fix: Fixes reply-to in WhatsApp Cloud API (#13467)
This change https://github.com/chatwoot/chatwoot/pull/13371 broke the
functionality. When a user replies to a WhatsApp message, the reply
context wasn't being properly stored in Chatwoot due to #13371

WhatsApp sends reply messages with a `context` field containing the
original message ID:
```json
{
    "messages": [{
      "context": {
        "from": "phone_number",
        "id": "wamid.ORIGINAL_MESSAGE_ID"
      },
      "from": "phone_number",
      "id": "wamid.REPLY_MESSAGE_ID",
      "text": { "body": "This is a reply" }
    }]
  }
```
However, the in_reply_to_external_id was being overridden when building
the message because content_attributes was explicitly set to either {
external_echo: true } or {}, which discarded the reply-to information.
2026-02-06 14:01:01 -08:00
Shivam Mishra
0e30e3c00a fix: add loading and silent retry to summary reports (#13455)
For large accounts, summary report queries can take several seconds to
complete, often times hitting the 15-second production request timeout.
The existing implementation silently swallows these failures and
provides no feedback during loading. Users see stale data with no
indication that a fetch is in progress, and if they interact with
filters while a request is in flight, they trigger race conditions that
can result in mismatched data being displayed.

This is a UX-level fix for what is fundamentally a performance problem.
While the underlying query performance is addressed separately, users
need proper feedback either way

## Approach

The PR adds three things: 

1. A loading overlay on the table, to provide feedback on loading state
2. Disabled filter inputs during loading so that the user does not
request new information that can cause race conditions in updating the
store
3. Silent retry before showing an error.

The retry exists because these queries often succeed on the second
attempt—likely due to database query caching. Rather than immediately
showing an error and forcing the user to manually retry, we do it
automatically. If the second attempt also fails, we show a toast so the
user knows something went wrong.

The store previously caught and discarded errors entirely. It now
rethrows them after resetting the loading flag, allowing components to
handle failures as they see fit.

### Previews

#### Double Retry and Error


https://github.com/user-attachments/assets/c189b173-8017-44b7-9493-417d65582c95

#### Loading State


https://github.com/user-attachments/assets/9f899c20-fbad-469b-93cc-f0d05d0853b0

---------

Co-authored-by: iamsivin <iamsivin@gmail.com>
2026-02-06 19:53:46 +05:30