Skip to content

feat(ui-react): add chatwoot live-chat support widget#6249

Open
luizhf42 wants to merge 4 commits intomasterfrom
feat/ui-react/chatwoot-support
Open

feat(ui-react): add chatwoot live-chat support widget#6249
luizhf42 wants to merge 4 commits intomasterfrom
feat/ui-react/chatwoot-support

Conversation

@luizhf42
Copy link
Copy Markdown
Member

What

In-app Chatwoot live chat for cloud users with active billing. Closes the largest remaining feature-parity gap with the Vue UI.

Depends on #6232

This branch is stacked on top of the billing PR and must merge after #6232. Several touchpoints reuse readNamespaceBilling, the useNamespace().billing shape, and the ClientConfig extension pattern introduced there. Until #6232 lands and this branch rebases, the diff against master includes the billing files too.

Changes

  • Lifecycle module (hooks/chatwootRuntime.ts): script latch, 15s bootstrap watchdog, and tearDownChatwoot() extracted outside the React tree so authStore.logout() can reset it directly. Watchdog flips status to unavailable if chatwoot:ready never fires (invalid token, content blocker, CSP) — the button never sits spinning forever.
  • Hook (hooks/useChatwoot.ts): orchestrates injection, identifies the user via /api/namespaces/:tenant/support HMAC, sets conversation custom attributes (namespace, tenant, domain) on the first message after each tenant change. Status: non-cloud | unavailable | no-subscription | loading | ready.
  • AppBar button (components/layout/SupportButton.tsx): three branches — ready opens the widget; no-subscription opens a paywall dialog mirroring Vue's PaywallChat and linking to /pricing; non-cloud links to the GitHub issue tracker.
  • Logout cleanup: tearDownChatwoot() removes the script tag, the SDK-injected DOM (chat bubble holders + iframe), and clears window.\$chatwoot / window.chatwootSDK globals. Called from logout() and from the loginWithToken() failure path so a stale widget never survives into the login screen.
  • Infrastructure: chatwootWebsiteToken + chatwootBaseUrl added to ClientConfig; gen-config.sh reads them from SHELLHUB_CHATWOOT_WEBSITE_TOKEN and SHELLHUB_CHATWOOT_BASEURL (same names the Vue UI uses); docker-compose.yml forwards them to the ui-react container.

Backend dependency

/api/namespaces/:tenant/support requires SHELLHUB_CHATWOOT_IDENTITY_KEY on the api service for HMAC minting. The env loader already reads it; the cloud docker-compose overlay needs a one-line addition to wire it through, tracked in a separate cloud-repo change. Without that env, the endpoint returns 400 "chatwoot is disabled" — the frontend handles this gracefully (useSupportIdentifier flips to error → status becomes unavailable and the button hides).

Testing

  • 69 unit tests covering status state machine, watchdog timeout, bootstrapFailed recovery on retry, identity refresh on profile/tenant edits, paywall auto-close on status flip, and logout teardown.
  • Manual: with cloud + SHELLHUB_CHATWOOT_* set on ui-react, SHELLHUB_CHATWOOT_IDENTITY_KEY on api, and an active subscription — Support icon → widget opens, identified, custom attrs reach the Chatwoot dashboard. Cancel subscription → icon → paywall dialog → Upgrade opens /pricing in a new tab. Log out → widget DOM disappears, no leftover iframe on /login. Operator forgets IDENTITY_KEY → exactly one 400 in the network tab, button hides instead of spinning.

@luizhf42 luizhf42 requested review from a team as code owners April 28, 2026 11:46
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

@luizhf42 luizhf42 force-pushed the feat/ui-react/chatwoot-support branch from 7d6c32c to e3c9148 Compare April 28, 2026 12:26
@luizhf42 luizhf42 self-assigned this Apr 28, 2026
@luizhf42 luizhf42 added kind/feature New feature or request area/ui status/work-in-progress product/cloud Issue/PR related to Cloud Edition labels Apr 28, 2026
@luizhf42 luizhf42 force-pushed the feat/ui-react/chatwoot-support branch from e3c9148 to 5fe0abd Compare April 28, 2026 13:01
@shellhub-io shellhub-io deleted a comment from github-actions Bot Apr 28, 2026
@shellhub-io shellhub-io deleted a comment from github-actions Bot Apr 28, 2026
@gustavosbarreto
Copy link
Copy Markdown
Member

/review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

@gustavosbarreto
Copy link
Copy Markdown
Member

/review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

Claude finished @gustavosbarreto's task in 8m 55s —— View job


Code Review Complete

Reviewed 46 files across code quality, security, testing, TypeScript/React patterns, and architecture — 2 inline issues posted (1 high, 1 medium).

Inline comments

Severity File Issue
high chatwootRuntime.ts:50-62 subscribeChatwootState double-registers each callback (via listeners Set + direct window.addEventListener), causing every useSyncExternalStore consumer to fire twice per chatwoot:ready event
medium BillingPayment.tsx:199-203 as unknown as Record<string, unknown> cast reads .message which is always undefined — backend sends {"error": "..."}, so the error message never reaches the user

Additional notes

  • The gen-config.sh heredoc interpolation pattern (no JSON escaping of env vars) is a pre-existing concern across all string fields — not introduced by this PR, so not flagged inline.
  • The docker-compose.yml env vars for ui-react use ${VAR} without empty defaults (vs ${VAR-} in the ui service), but this is consistent with all existing ui-react env vars, so not a new inconsistency.
  • Security review found no exploitable vulnerabilities in the Chatwoot/Stripe integration: external URLs use noopener,noreferrer, Stripe publishable keys are correctly public, logout teardown properly cleans up widget state and DOM.
  • Testing review found comprehensive coverage (69 tests) with no correctness issues in test assertions.
  • Architecture review confirmed no cross-repo breakage: no pkg/ changes, cloud backend endpoints match the new UI hooks, and ClientConfig extensions default gracefully.

If you've addressed the feedback and want a new review, tag @shellhub-io/admin and a team member can trigger it.

Comment thread ui-react/apps/console/src/hooks/chatwootRuntime.ts
Comment thread ui-react/apps/console/src/components/billing/BillingPayment.tsx
@luizhf42 luizhf42 force-pushed the feat/ui-react/chatwoot-support branch from 5fe0abd to 865dfcf Compare April 28, 2026 14:49
@gustavosbarreto
Copy link
Copy Markdown
Member

/review

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

Claude finished @gustavosbarreto's task in 2m 51s —— View job


Code Review Complete

Reviewed 46 files across code quality, security, testing, TypeScript/React patterns, and architecture — 2 inline issues posted (2 medium).

Inline comments

Severity File Issue
medium BillingDialog.tsx:78-83 canClose doesn't block ESC/backdrop on the success step despite the comment stating it should
medium useChatwoot.test.tsx:700-701 Vacuous iframe assertion — test iframe src doesn't contain "chatwoot", so the cleanup selector never matches and the assertion passes trivially

Additional notes

  • Security review found no exploitable vulnerabilities: script injection uses operator-controlled config, all dynamic values are JSX-escaped, external URLs use noopener,noreferrer, Stripe publishable keys are correctly public, logout teardown properly cleans up widget state and DOM.
  • Architecture review confirmed no cross-repo breakage: no pkg/ changes, API contracts (GET /api/namespaces/:tenant/support, POST /api/billing/portal) match the cloud backend, and ClientConfig extensions default gracefully to empty strings.
  • The two issues flagged in the previous review round (double notification in subscribeChatwootState, dead .message read in BillingPayment.tsx) are still present in this revision.

If you've addressed the feedback and want a new review, tag @shellhub-io/admin and a team member can trigger it.

Comment thread ui-react/apps/console/src/hooks/__tests__/useChatwoot.test.tsx
Comment thread ui-react/apps/console/src/components/billing/BillingDialog.tsx
Comment thread ui-react/apps/console/src/hooks/__tests__/useChatwoot.test.tsx Outdated
@luizhf42 luizhf42 force-pushed the feat/ui-react/chatwoot-support branch from 865dfcf to ad749ea Compare April 28, 2026 17:15
Implements the full billing workstream for Cloud tenants:

- New hooks/useBilling.ts: useSubscription, useCustomer,
  useCreateCustomer, useCreateSubscription, useAttachPaymentMethod,
  useDetachPaymentMethod, useSetDefaultPaymentMethod,
  useOpenBillingPortal (axios wrapper for /api/billing/portal)
- New types/billing.ts: BillingStatus union, typed Customer,
  Subscription, PaymentMethod, NamespaceBilling with safe narrowing
- New utils/stripeErrors.ts: Stripe error-code → user message map
- New BillingSection in Settings.tsx: status badge, warning banners
  (past_due / unpaid / incomplete / incomplete_expired / to_cancel),
  Subscribe button (inactive/canceled/incomplete_expired only),
  Open Portal button, hash-scroll + pageshow refetch for portal return
- New BillingDialog wizard (4 steps): Overview → Payment method →
  Review → Success; lazy-loaded to exclude Stripe from initial chunk
- New BillingPayment: Stripe Elements card form, payment-method list
  with set-default / detach, auto-creates Stripe customer on mount
- New BillingWarning: 402 device-limit dialog wired to
  DeviceActionDialog + ContainerActionDialog
- env.ts: add stripePublishableKey to ClientConfig + in-flight
  promise dedup for concurrent loadConfig callers
- App.tsx: /settings/billing redirect route for Stripe portal return
- gen-config.sh: emit SHELLHUB_STRIPE_PUBLISHABLE_KEY into config.json
- docker-compose.yml: add SHELLHUB_STRIPE_PUBLISHABLE_KEY env var

Fixes: shellhub-io/team#71
Covers the billing workstream with Vitest + React Testing Library:

- useBilling.test.ts: mutations (create customer/subscription,
  attach/detach/default PM), query enable/disable behavior,
  useOpenBillingPortal URL open + missing-URL error
- billing.test.ts (types): toCustomer and toSubscription transforms,
  readNamespaceBilling safe narrowing, getSubscriptionStatus sentinel
- stripeErrors.test.ts: known-code mapping + unknown-code fallback
- BillingDialog.test.tsx: all 4 wizard steps, Next/Back nav, error
  paths (402, generic, incomplete status), pending state, live region
  announcements, close buttons
- BillingSection.test.tsx: Subscribe/portal button visibility per
  status, non-owner view, warning banners, dialog open/close
- BillingWarning.test.tsx: renders, confirm navigates, cancel closes
- Update CreateNamespaceDialog + InvitationsMenu + Login mock getConfig
  to include new stripePublishableKey field in ClientConfig
Wires the official Chatwoot SDK into the AppBar for cloud users with an
active subscription. The SDK script is injected lazily after auth, the
user is identified via an HMAC token from /api/namespaces/:tenant/support
(matching the Vue UI's flow), and conversation custom attributes are
tagged with the active namespace on first message.

Click branches: ready -> open widget; no-subscription -> paywall dialog
linking to /pricing; non-cloud -> github issue tracker.

Requires SHELLHUB_CHATWOOT_IDENTITY_KEY on the api service for the
backend to mint identifier hashes.
@luizhf42 luizhf42 force-pushed the feat/ui-react/chatwoot-support branch from ad749ea to f4e2d4e Compare April 28, 2026 20:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ui do-not-merge kind/feature New feature or request product/cloud Issue/PR related to Cloud Edition

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants