From 5c00a1601ad32f2ea0a1833dfa8735c1f24f384c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:58:07 +0530 Subject: [PATCH 001/143] chore(deps): bump dompurify from 3.4.0 to 3.4.11 (#14798) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.0 to 3.4.11.
Release notes

Sourced from dompurify's releases.

DOMPurify 3.4.11

DOMPurify 3.4.10

DOMPurify 3.4.9

DOMPurify 3.4.8

DOMPurify 3.4.7

DOMPurify 3.4.6

DOMPurify 3.4.5

Note that this is a security release for an issue introduced in 3.4.4 and should be upgraded to immediately.

... (truncated)

Commits
Install script changes

This version adds prepare script that runs during installation. Review the package contents before updating.


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dompurify&package-manager=npm_and_yarn&previous-version=3.4.0&new-version=3.4.11)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/chatwoot/chatwoot/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sony Mathew --- package.json | 2 +- pnpm-lock.yaml | 22 +++++++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 65dd48d27..917a1b97d 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "countries-and-timezones": "^3.6.0", "date-fns": "2.21.1", "date-fns-tz": "^1.3.3", - "dompurify": "3.4.0", + "dompurify": "3.4.11", "flag-icons": "^7.2.3", "floating-vue": "^5.2.2", "highlight.js": "^11.10.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c7bebb79..80fbf318a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -130,8 +130,8 @@ importers: specifier: ^1.3.3 version: 1.3.8(date-fns@2.21.1) dompurify: - specifier: 3.4.0 - version: 3.4.0 + specifier: 3.4.11 + version: 3.4.11 flag-icons: specifier: ^7.2.3 version: 7.2.3 @@ -1635,6 +1635,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + activestorage@5.2.8: resolution: {integrity: sha512-bueFOxBGIAUdrjbLyBZ8Xlkcecy8vr05sCk5VV37BbFi+RehPoEjfvKX3iYYPY7RFVhl+L43W9/ZbN3xNNLPtQ==} @@ -2218,8 +2223,8 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.4.0: - resolution: {integrity: sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==} + dompurify@3.4.11: + resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==} domutils@3.1.0: resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} @@ -6380,6 +6385,9 @@ snapshots: acorn@8.16.0: {} + acorn@8.17.0: + optional: true + activestorage@5.2.8: dependencies: spark-md5: 3.0.2 @@ -6999,7 +7007,7 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.4.0: + dompurify@3.4.11: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -9511,7 +9519,7 @@ snapshots: terser@5.33.0: dependencies: '@jridgewell/source-map': 0.3.11 - acorn: 8.16.0 + acorn: 8.17.0 commander: 2.20.3 source-map-support: 0.5.21 optional: true @@ -9898,7 +9906,7 @@ snapshots: vue-dompurify-html@5.3.0(vue@3.5.12(typescript@5.6.2)): dependencies: - dompurify: 3.4.0 + dompurify: 3.4.11 vue: 3.5.12(typescript@5.6.2) vue-eslint-parser@9.4.3(eslint@8.57.0): From 5b7430f3765b9a4b6e1812cf3c78aac5c81ee310 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:08:06 +0530 Subject: [PATCH 002/143] chore(deps): bump concurrent-ruby from 1.3.5 to 1.3.7 (#14810) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby) from 1.3.5 to 1.3.7.
Release notes

Sourced from concurrent-ruby's releases.

v1.3.7

There are 3 security fixes in this release, so updating is recommended. These security vulnerabilities are not very likely to be hit in practice and have a corresponding Low severity score.

What's Changed

New Contributors

Full Changelog: https://github.com/ruby-concurrency/concurrent-ruby/compare/v1.3.6...v1.3.7

v1.3.6

What's Changed

New Contributors

Full Changelog: https://github.com/ruby-concurrency/concurrent-ruby/compare/v1.3.5...v1.3.6

Changelog

Sourced from concurrent-ruby's changelog.

Release v1.3.7 (16 June 2026)

concurrent-ruby:

Release v1.3.6 (13 December 2025)

concurrent-ruby:

Commits
  • 4c8fc28 Release 1.3.7
  • d91ca94 Fix AtomicReference#update livelock when stored value is Float::NAN on JRuby ...
  • 7e4d711 Fix ReentrantReadWriteLock read hold overflow into write-lock bit
  • 6e37e06 Fix AtomicReference#update livelock when stored value is Float::NAN
  • 2825cfa Cleanup spec
  • 3fd4932 Fix ReadWriteLock wrong-thread write release and stray read release
  • 1974b47 Add Ruby 4.0 in CI
  • df8706d Add SECURITY.md (#1104)
  • 7a1b789 Bump actions/upload-pages-artifact from 4 to 5
  • 9b2dbf7 Bump actions/deploy-pages from 4 to 5
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=concurrent-ruby&package-manager=bundler&previous-version=1.3.5&new-version=1.3.7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/chatwoot/chatwoot/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sony Mathew --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 4fdcf804a..ffaebe115 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -193,7 +193,7 @@ GEM climate_control (1.2.0) coderay (1.1.3) commonmarker (0.23.10) - concurrent-ruby (1.3.5) + concurrent-ruby (1.3.7) connection_pool (2.5.5) crack (1.0.0) bigdecimal From 44b32eacec053f75558ab343129fad43234f389b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:36:08 +0530 Subject: [PATCH 003/143] chore(deps): bump faraday from 2.14.2 to 2.14.3 (#14811) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [faraday](https://github.com/lostisland/faraday) from 2.14.2 to 2.14.3.
Release notes

Sourced from faraday's releases.

v2.14.3

Security Note

This release contains a security fix, we recommend all users to upgrade as soon as possible. A Security Advisory with more details will be posted shortly.

What's Changed

New Contributors

Full Changelog: https://github.com/lostisland/faraday/compare/v2.14.2...v2.14.3

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=faraday&package-manager=bundler&previous-version=2.14.2&new-version=2.14.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/chatwoot/chatwoot/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sony Mathew --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index ffaebe115..8da80f52c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -304,7 +304,7 @@ GEM railties (>= 5.0.0) faker (3.2.0) i18n (>= 1.8.11, < 2) - faraday (2.14.2) + faraday (2.14.3) faraday-net_http (>= 2.0, < 3.5) json logger @@ -474,7 +474,7 @@ GEM rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (2.19.8) + json (2.19.9) json_refs (0.1.8) hana json_schemer (0.2.24) From 2b37fef0e0ce7ce5b9461f522a228facfd35f2b6 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Mon, 22 Jun 2026 06:49:52 -0700 Subject: [PATCH 004/143] fix(companies): sync contact company names (#14759) Fixes company-contact name drift when a company is renamed or deleted. Closes: N/A ## Why Contacts keep a denormalized `additional_attributes.company_name` for display and filtering. Company rename/delete flows could leave that copied value stale even though the actual `company_id` relationship changed. ## What changed - Enqueues an async company contact-name sync job when a company name changes. - Moves company deletion into `Companies::DeleteJob`. - The delete job unlinks linked contacts, clears only the copied `company_name`, and then deletes the company. - Uses bulk JSON updates for the cleanup path so contact records are not saved, which avoids contact update callbacks, webhook dispatch, and automation side effects. ## How to test - Link a contact to a company, rename the company, and confirm the contact company name updates after the job runs. - Delete a company with linked contacts and confirm the delete job removes the company, unassigns linked contacts, and preserves other contact additional attributes. --------- Co-authored-by: Sony Mathew Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com> --- .../api/v1/accounts/companies_controller.rb | 2 +- enterprise/app/jobs/companies/delete_job.rb | 28 ++++++++++++++ .../jobs/companies/sync_contact_names_job.rb | 33 ++++++++++++++++ enterprise/app/models/company.rb | 5 +++ .../v1/accounts/companies_controller_spec.rb | 6 +-- .../jobs/companies/delete_job_spec.rb | 19 ++++++++++ .../companies/sync_contact_names_job_spec.rb | 38 +++++++++++++++++++ spec/enterprise/models/company_spec.rb | 11 ++++++ 8 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 enterprise/app/jobs/companies/delete_job.rb create mode 100644 enterprise/app/jobs/companies/sync_contact_names_job.rb create mode 100644 spec/enterprise/jobs/companies/delete_job_spec.rb create mode 100644 spec/enterprise/jobs/companies/sync_contact_names_job_spec.rb diff --git a/enterprise/app/controllers/api/v1/accounts/companies_controller.rb b/enterprise/app/controllers/api/v1/accounts/companies_controller.rb index 1df0e7d88..1f2c38c91 100644 --- a/enterprise/app/controllers/api/v1/accounts/companies_controller.rb +++ b/enterprise/app/controllers/api/v1/accounts/companies_controller.rb @@ -49,7 +49,7 @@ class Api::V1::Accounts::CompaniesController < Api::V1::Accounts::EnterpriseAcco end def destroy - @company.destroy! + Companies::DeleteJob.perform_later(company_id: @company.id) head :ok end diff --git a/enterprise/app/jobs/companies/delete_job.rb b/enterprise/app/jobs/companies/delete_job.rb new file mode 100644 index 000000000..62c36750d --- /dev/null +++ b/enterprise/app/jobs/companies/delete_job.rb @@ -0,0 +1,28 @@ +class Companies::DeleteJob < ApplicationJob + queue_as :low + + BATCH_SIZE = 1000 + CONTACT_COMPANY_CLEAR_SQL = <<~SQL.squish.freeze + company_id = NULL, + additional_attributes = COALESCE(additional_attributes, '{}'::jsonb) - 'company_name' + SQL + + def perform(company_id:) + company = Company.find_by(id: company_id) + return if company.blank? + + clear_contact_company_names(company) + company.destroy! + end + + private + + # Avoid contact callbacks so this cleanup does not dispatch contact automations/webhooks. + # rubocop:disable Rails/SkipsModelValidations + def clear_contact_company_names(company) + company.contacts.in_batches(of: BATCH_SIZE) do |contacts| + contacts.update_all(CONTACT_COMPANY_CLEAR_SQL) + end + end + # rubocop:enable Rails/SkipsModelValidations +end diff --git a/enterprise/app/jobs/companies/sync_contact_names_job.rb b/enterprise/app/jobs/companies/sync_contact_names_job.rb new file mode 100644 index 000000000..36f38aacb --- /dev/null +++ b/enterprise/app/jobs/companies/sync_contact_names_job.rb @@ -0,0 +1,33 @@ +class Companies::SyncContactNamesJob < ApplicationJob + queue_as :low + + BATCH_SIZE = 1000 + CONTACT_COMPANY_NAME_UPDATE_SQL = <<~SQL.squish.freeze + additional_attributes = jsonb_set( + COALESCE(additional_attributes, '{}'::jsonb), + '{company_name}', + ?::jsonb, + true + ) + SQL + + def perform(company_id:) + return if company_id.blank? + + company = Company.find_by(id: company_id) + return if company.blank? + + sync_company_name(company) + end + + private + + # Denormalized display field sync; avoid contact validations, callbacks, and webhook/automation side effects. + # rubocop:disable Rails/SkipsModelValidations + def sync_company_name(company) + company.contacts.in_batches(of: BATCH_SIZE) do |contacts| + contacts.update_all([CONTACT_COMPANY_NAME_UPDATE_SQL, company.name.to_json]) + end + end + # rubocop:enable Rails/SkipsModelValidations +end diff --git a/enterprise/app/models/company.rb b/enterprise/app/models/company.rb index c60e9423c..b4c42ff3d 100644 --- a/enterprise/app/models/company.rb +++ b/enterprise/app/models/company.rb @@ -39,6 +39,7 @@ class Company < ApplicationRecord has_many :contacts, dependent: :nullify before_validation :prepare_jsonb_attributes after_create_commit :fetch_favicon, if: -> { domain.present? } + after_update_commit :enqueue_contact_company_name_sync, if: :saved_change_to_name? scope :ordered_by_name, -> { order(:name) } scope :search_by_name_or_domain, lambda { |query| @@ -76,4 +77,8 @@ class Company < ApplicationRecord def fetch_favicon Avatar::AvatarFromFaviconJob.set(wait: 5.seconds).perform_later(self) end + + def enqueue_contact_company_name_sync + Companies::SyncContactNamesJob.perform_later(company_id: id) + end end diff --git a/spec/enterprise/controllers/api/v1/accounts/companies_controller_spec.rb b/spec/enterprise/controllers/api/v1/accounts/companies_controller_spec.rb index 84b182669..1d6f4870c 100644 --- a/spec/enterprise/controllers/api/v1/accounts/companies_controller_spec.rb +++ b/spec/enterprise/controllers/api/v1/accounts/companies_controller_spec.rb @@ -385,13 +385,13 @@ RSpec.describe 'Companies API', type: :request do let(:admin) { create(:user, account: account, role: :administrator) } let(:company) { create(:company, account: account) } - it 'deletes the company' do - company + it 'enqueues company deletion' do expect do delete "/api/v1/accounts/#{account.id}/companies/#{company.id}", headers: admin.create_new_auth_token, as: :json - end.to change(Company, :count).by(-1) + end.to have_enqueued_job(Companies::DeleteJob).with(company_id: company.id) + expect(response).to have_http_status(:ok) end end diff --git a/spec/enterprise/jobs/companies/delete_job_spec.rb b/spec/enterprise/jobs/companies/delete_job_spec.rb new file mode 100644 index 000000000..1d4e4249d --- /dev/null +++ b/spec/enterprise/jobs/companies/delete_job_spec.rb @@ -0,0 +1,19 @@ +require 'rails_helper' + +RSpec.describe Companies::DeleteJob, type: :job do + describe '#perform' do + it 'unlinks contacts, clears company names, and deletes the company' do + account = create(:account) + company = create(:company, account: account, name: 'Acme') + contact = create(:contact, account: account, company: company, additional_attributes: { 'company_name' => 'Acme', 'city' => 'Berlin' }) + other_contact = create(:contact, account: account, additional_attributes: { 'company_name' => 'Acme' }) + + described_class.perform_now(company_id: company.id) + + expect { company.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect(contact.reload.company_id).to be_nil + expect(contact.additional_attributes).to eq('city' => 'Berlin') + expect(other_contact.reload.additional_attributes).to eq('company_name' => 'Acme') + end + end +end diff --git a/spec/enterprise/jobs/companies/sync_contact_names_job_spec.rb b/spec/enterprise/jobs/companies/sync_contact_names_job_spec.rb new file mode 100644 index 000000000..39ed4fc08 --- /dev/null +++ b/spec/enterprise/jobs/companies/sync_contact_names_job_spec.rb @@ -0,0 +1,38 @@ +require 'rails_helper' + +RSpec.describe Companies::SyncContactNamesJob, type: :job do + let(:account) { create(:account) } + let(:company) { create(:company, account: account, name: 'Acme') } + + describe '#perform' do + it 'updates linked contact company names' do + contact = create(:contact, account: account, company: company, additional_attributes: { 'company_name' => 'Acme', 'city' => 'Berlin' }) + + company.update!(name: 'Acme Labs') + + described_class.perform_now(company_id: company.id) + + expect(contact.reload.additional_attributes).to eq('company_name' => 'Acme Labs', 'city' => 'Berlin') + end + + it 'uses the current company name when a stale rename job runs' do + contact = create(:contact, account: account, company: company, additional_attributes: { 'company_name' => 'Acme' }) + company.update!(name: 'Acme Labs') + + described_class.perform_now(company_id: company.id) + + expect(contact.reload.additional_attributes).to eq('company_name' => 'Acme Labs') + end + + it 'does not save contacts while syncing the denormalized company name' do + contact = create(:contact, account: account, company: company, additional_attributes: { 'company_name' => 'Acme' }) + original_updated_at = contact.reload.updated_at + + company.update!(name: 'Acme Labs') + + described_class.perform_now(company_id: company.id) + + expect(contact.reload.updated_at).to eq(original_updated_at) + end + end +end diff --git a/spec/enterprise/models/company_spec.rb b/spec/enterprise/models/company_spec.rb index 1b681973d..8f65abe5f 100644 --- a/spec/enterprise/models/company_spec.rb +++ b/spec/enterprise/models/company_spec.rb @@ -46,4 +46,15 @@ RSpec.describe Company, type: :model do expect(company.reload.last_activity_at).to be_within(1.second).of(original_activity_at) end end + + describe 'contact company name sync' do + let(:account) { create(:account) } + let(:company) { create(:company, account: account, name: 'Acme') } + + it 'enqueues contact company name sync when the company name changes' do + expect do + company.update!(name: 'Acme Labs') + end.to have_enqueued_job(Companies::SyncContactNamesJob).with(company_id: company.id) + end + end end From e86222034e39b9be4837fea0c058ad9a6a27aa72 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Mon, 22 Jun 2026 06:51:09 -0700 Subject: [PATCH 005/143] fix(auth): record attribution for oauth signups (#14796) Google OAuth signups now persist the same first-party attribution cookies as email signups on Chatwoot Cloud. This keeps attribution capture owned by the website and reuses the existing Enterprise-only account attribution service. The OAuth callback only records the already-shaped first-touch and last-touch cookie payload after a new account is created. ## What changed - Added Enterprise-only attribution persistence to the Google OAuth signup account creation path. - Reuses `Internal::Accounts::MarketingAttributionService`. - Keeps attribution best-effort so failures do not interrupt OAuth signup. - Leaves existing email signup and SAML behavior unchanged. ## How to test - Start from a Chatwoot Cloud-like setup with attribution cookies present. - Sign up using the Google OAuth button. - Confirm the created account has `internal_attributes['marketing_attribution']` with `first_touch` and `last_touch`. - Confirm existing Google OAuth login still redirects normally. Validation run locally: - `bundle exec rspec spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb` - `bundle exec rspec spec/controllers/devise/omniauth_callbacks_controller_spec.rb spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb` - `bundle exec rubocop enterprise/app/controllers/enterprise/devise_overrides/omniauth_callbacks_controller.rb spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb` --- .../omniauth_callbacks_controller.rb | 13 +++++ .../google_oauth_attribution_spec.rb | 53 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb diff --git a/enterprise/app/controllers/enterprise/devise_overrides/omniauth_callbacks_controller.rb b/enterprise/app/controllers/enterprise/devise_overrides/omniauth_callbacks_controller.rb index 3e2713dd7..0b1328c0d 100644 --- a/enterprise/app/controllers/enterprise/devise_overrides/omniauth_callbacks_controller.rb +++ b/enterprise/app/controllers/enterprise/devise_overrides/omniauth_callbacks_controller.rb @@ -29,6 +29,19 @@ module Enterprise::DeviseOverrides::OmniauthCallbacksController private + def create_account_for_user + super + record_marketing_attribution + end + + def record_marketing_attribution + return if @account.blank? + + Internal::Accounts::MarketingAttributionService.new(account: @account, cookies: cookies).perform + rescue StandardError => e + ChatwootExceptionTracker.new(e).capture_exception + end + def handle_saml_auth account_id = extract_saml_account_id relay_state = saml_relay_state diff --git a/spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb b/spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb new file mode 100644 index 000000000..948e9af96 --- /dev/null +++ b/spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' +require 'base64' + +RSpec.describe 'Enterprise Google OAuth attribution', type: :request do + let(:email_validation_service) { instance_double(Account::SignUpEmailValidationService) } + let(:email) { 'oauth-attribution@example.com' } + let(:account_builder) { double } + let(:account) { create(:account) } + let(:first_touch_cookie) { encoded_cookie('source' => 'reddit', 'source_type' => 'paid_social') } + let(:last_touch_cookie) { encoded_cookie('source' => 'github', 'source_type' => 'referral') } + + before do + allow(ChatwootApp).to receive(:enterprise?).and_return(true) + allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true) + allow(Account::SignUpEmailValidationService).to receive(:new).and_return(email_validation_service) + allow(email_validation_service).to receive(:perform).and_return(true) + allow(AccountBuilder).to receive(:new).and_return(account_builder) + allow(account_builder).to receive(:perform) do + [create(:user, email: email, account: account), account] + end + + OmniAuth.config.test_mode = true + OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new( + provider: 'google', + uid: '123545', + info: { + name: 'OAuth Attribution', + email: email, + image: 'https://example.com/image.jpg' + } + ) + end + + it 'records marketing attribution for Google OAuth signups' do + cookies[Internal::Accounts::MarketingAttributionService::FIRST_TOUCH_COOKIE] = first_touch_cookie + cookies[Internal::Accounts::MarketingAttributionService::LAST_TOUCH_COOKIE] = last_touch_cookie + + with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true', FRONTEND_URL: 'http://www.example.com' do + get '/omniauth/google_oauth2/callback' + follow_redirect! + end + + attribution = account.reload.internal_attributes['marketing_attribution'] + + expect(attribution['captured_from']).to eq('cookie') + expect(attribution['first_touch']).to include('source' => 'reddit', 'source_type' => 'paid_social') + expect(attribution['last_touch']).to include('source' => 'github', 'source_type' => 'referral') + end + + def encoded_cookie(payload) + Base64.urlsafe_encode64(payload.to_json, padding: false) + end +end From 961ec86ba9c51adadb8ea5a4940cfae47eb493b7 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:35:52 +0530 Subject: [PATCH 006/143] chore: support custom variables in auto-resolution message (#14782) # Pull Request Template ## Description This PR adds support for custom variables in auto-resolution messages within Conversation Workflows. Agents can insert variables (e.g. `{{contact.name}}`) into the custom auto-resolution message using the variable picker by typing `{{`. These variables are resolved with actual conversation data when the auto-resolution message is sent. The field uses the existing editor with the formatting toolbar hidden, since auto-resolution messages are delivered across all channels, including SMS, where formatting isn't supported. Existing message content is preserved as-is, and typing `{{` opens the variable picker for quick insertion. Fixes https://linear.app/chatwoot/issue/CW-7369/support-custom-variables-in-conversations-workflow-text-fields ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? ### Screenshots image ## 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 --- app/javascript/dashboard/constants/editor.js | 5 +++++ .../dashboard/settings/account/components/AutoResolve.vue | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/javascript/dashboard/constants/editor.js b/app/javascript/dashboard/constants/editor.js index 5f4fe0e03..cb0996136 100644 --- a/app/javascript/dashboard/constants/editor.js +++ b/app/javascript/dashboard/constants/editor.js @@ -163,6 +163,11 @@ export const FORMATTING = { nodes: [], menu: [], }, + 'Context::NoToolbar': { + marks: ['strong', 'em', 'link'], + nodes: ['bulletList', 'orderedList'], + menu: [], + }, }; // Editor menu options for Full Editor diff --git a/app/javascript/dashboard/routes/dashboard/settings/account/components/AutoResolve.vue b/app/javascript/dashboard/routes/dashboard/settings/account/components/AutoResolve.vue index 034f40d35..54eafa59e 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/account/components/AutoResolve.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/account/components/AutoResolve.vue @@ -5,7 +5,7 @@ import { useI18n } from 'vue-i18n'; import { useAccount } from 'dashboard/composables/useAccount'; import { useAlert } from 'dashboard/composables'; import WithLabel from 'v3/components/Form/WithLabel.vue'; -import TextArea from 'next/textarea/TextArea.vue'; +import Editor from 'next/Editor/Editor.vue'; import Switch from 'next/switch/Switch.vue'; import NextButton from 'dashboard/components-next/button/Button.vue'; import DurationInput from 'next/input/DurationInput.vue'; @@ -162,9 +162,13 @@ const toggleAutoResolve = async () => { :label="t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE.MESSAGE.LABEL')" :help-message="t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE.MESSAGE.HELP')" > -