@chatlychat/widget-react 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/LICENSE +21 -0
- package/README.md +202 -2
- package/dist/ChatlyWidget.d.ts +55 -0
- package/dist/ChatlyWidget.d.ts.map +1 -0
- package/dist/ChatlyWidget.js +159 -0
- package/dist/ChatlyWidget.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/useChatly.d.ts +46 -0
- package/dist/useChatly.d.ts.map +1 -0
- package/dist/useChatly.js +58 -0
- package/dist/useChatly.js.map +1 -0
- package/package.json +72 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# @chatlychat/widget-react — changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — unreleased
|
|
4
|
+
|
|
5
|
+
- Initial release: `<ChatlyWidget />`, `useChatly()`, `useChatlyEvent()`.
|
|
6
|
+
- React 18 StrictMode safe (one boot, not two), tears the widget down on
|
|
7
|
+
unmount, and turns an `identity`/`locale` prop change into `identify()` /
|
|
8
|
+
`setLocale()` rather than a re-init.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chatly Engineering
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,203 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @chatlychat/widget-react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Chatly chat widget as a React component.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm add @chatlychat/widget-react
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { ChatlyWidget, useChatly } from '@chatlychat/widget-react';
|
|
11
|
+
|
|
12
|
+
export function App() {
|
|
13
|
+
return (
|
|
14
|
+
<>
|
|
15
|
+
{/* One of these, high in the tree. */}
|
|
16
|
+
<ChatlyWidget publicId="acme-web" visitor={{ externalId: user.id, email: user.email }} />
|
|
17
|
+
<Routes />
|
|
18
|
+
</>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function HelpButton() {
|
|
23
|
+
const chatly = useChatly();
|
|
24
|
+
return <button onClick={() => chatly.open('help')}>Help</button>;
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
That is the whole integration. No `<script>` tag, no `window.Chatly`, no
|
|
29
|
+
effect of your own — the component injects the loader, mounts the widget and
|
|
30
|
+
tears it down when it unmounts.
|
|
31
|
+
|
|
32
|
+
## Why this exists
|
|
33
|
+
|
|
34
|
+
Without it, the integration is an effect you write by hand:
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
// Don't. This is the version that was in our own docs, and it has three bugs.
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
window.Chatly?.init({ publicId });
|
|
40
|
+
return () => window.Chatly?.destroy();
|
|
41
|
+
}, [publicId]);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
1. **It boots twice in StrictMode.** React 18 runs every effect mount →
|
|
45
|
+
cleanup → mount in development, so that is `init` → `destroy` → `init`:
|
|
46
|
+
two visitor sessions minted, and a launcher that visibly flickers.
|
|
47
|
+
2. **A `theme` in the deps re-initialises on every render.** `theme={{…}}` is
|
|
48
|
+
a new object each time, so the widget is destroyed and rebuilt underneath a
|
|
49
|
+
visitor who was mid-sentence.
|
|
50
|
+
3. **It can never identify anybody.** The only place to put an identity is
|
|
51
|
+
`init`, which has already run by the time your user signs in.
|
|
52
|
+
|
|
53
|
+
This component fixes all three, and the mechanism is shared with the custom
|
|
54
|
+
element in [`@chatlychat/widget-element`](../widget-element) so the two cannot
|
|
55
|
+
drift on the part that is hard.
|
|
56
|
+
|
|
57
|
+
## React is a peer dependency
|
|
58
|
+
|
|
59
|
+
`react` is in `peerDependencies`, never `dependencies`, and that is a
|
|
60
|
+
deliberate exception to this repo's "no new dependencies" rule.
|
|
61
|
+
|
|
62
|
+
A real dependency resolves a **second copy of React** in any consumer whose
|
|
63
|
+
version differs by so much as a patch, and two Reacts on one page break hooks
|
|
64
|
+
with `Invalid hook call` — thrown from a component the consumer did not write
|
|
65
|
+
and cannot debug. A peer dependency on the framework the consumer is already
|
|
66
|
+
using adds nothing to their install and cannot duplicate.
|
|
67
|
+
|
|
68
|
+
The range is `^18.0.0 || ^19.0.0`. The component uses `useRef` and
|
|
69
|
+
`useEffect` and nothing newer, so it works on both.
|
|
70
|
+
|
|
71
|
+
Everything else here has **one** dependency, `@chatlychat/widget-element`, which
|
|
72
|
+
itself has none.
|
|
73
|
+
|
|
74
|
+
## `<ChatlyWidget />`
|
|
75
|
+
|
|
76
|
+
Every [init option](https://chatlychat.com/docs/channel-web) is a prop:
|
|
77
|
+
`publicId` (required), `apiUrl`, `realtimeUrl`, `visitor`, `theme`,
|
|
78
|
+
`behavior`, `quickLinks`, `copy`, `copyByLocale`, `cssOverrides`.
|
|
79
|
+
|
|
80
|
+
Plus four of its own:
|
|
81
|
+
|
|
82
|
+
| prop | type |
|
|
83
|
+
| --------------------- | ------------------------------------------------------------- |
|
|
84
|
+
| `loaderUrl` | Where `loader.js` is served from. **Self-hosting: set this.** |
|
|
85
|
+
| `locale` | Calls `setLocale()` once the widget is up. |
|
|
86
|
+
| `inline` | Render the chat inside this component's own box. |
|
|
87
|
+
| `className` / `style` | Applied to the inline container. |
|
|
88
|
+
|
|
89
|
+
And the events, as callbacks: `onReady`, `onOpen`, `onClose`, `onMessage`,
|
|
90
|
+
`onConversation`, `onIdentified`, `onReset`, `onAction`, `onError`. Pass
|
|
91
|
+
inline arrow functions freely — they are read through a ref, so a new one on
|
|
92
|
+
every render costs nothing and the handler that fires is always the latest.
|
|
93
|
+
|
|
94
|
+
### What a prop change actually does
|
|
95
|
+
|
|
96
|
+
This is the part worth knowing, because the three outcomes are different:
|
|
97
|
+
|
|
98
|
+
| changed prop | what happens |
|
|
99
|
+
| ----------------------- | ----------------------------------------------------- |
|
|
100
|
+
| `visitor` | `identify()`. Nothing is re-mounted. |
|
|
101
|
+
| `visitor` → `undefined` | `reset()` — a sign-out, so the next session is fresh. |
|
|
102
|
+
| `locale` | `setLocale()`. Nothing is re-mounted. |
|
|
103
|
+
| anything else | `init()` again, which **destroys and re-mounts**. |
|
|
104
|
+
|
|
105
|
+
A re-mount loses nothing durable — the conversation lives on the server — but
|
|
106
|
+
it closes an open panel. The widget has no `setTheme`, so there is no honest
|
|
107
|
+
alternative; what this component guarantees is that it only happens when a
|
|
108
|
+
value really changed, not when a parent re-rendered.
|
|
109
|
+
|
|
110
|
+
`loaderUrl` is the exception: changing it after load warns and does nothing,
|
|
111
|
+
because the bundle is already fetched and pointing the running widget at
|
|
112
|
+
another deployment would need a page reload.
|
|
113
|
+
|
|
114
|
+
### Inline mounting
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
<ChatlyWidget publicId="acme-web" inline className="h-[600px] w-full rounded-lg border" />
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The chat fills the box instead of floating in the corner — a support page, a
|
|
121
|
+
sidebar, a full-page `/chat` route. In this mode there is no launcher, no
|
|
122
|
+
corner offset, and `close()` warns instead of working (hide or unmount your
|
|
123
|
+
own container instead).
|
|
124
|
+
|
|
125
|
+
This is the React shape of the widget's `mountTo` option, and it closes
|
|
126
|
+
`mountTo`'s one sharp edge: the container must exist when `init` runs, and a
|
|
127
|
+
selector that matches nothing makes the widget refuse to mount at all. A
|
|
128
|
+
container this component renders itself cannot be missing.
|
|
129
|
+
|
|
130
|
+
Give it a height. It has a 420px minimum so a container with none is still
|
|
131
|
+
usable.
|
|
132
|
+
|
|
133
|
+
## `useChatly()`
|
|
134
|
+
|
|
135
|
+
Returns the widget's post-boot methods: `open`, `close`, `toggle`,
|
|
136
|
+
`identify`, `setUser`, `setCustomAttributes`, `deleteCustomAttribute`,
|
|
137
|
+
`setLabel`, `removeLabel`, `setLocale`, `track`, `registerPushToken`, `reset`.
|
|
138
|
+
|
|
139
|
+
The object is referentially stable, so it is safe in a dependency array.
|
|
140
|
+
|
|
141
|
+
`init` and `destroy` are deliberately **not** on it. The component owns the
|
|
142
|
+
lifecycle; calling them behind its back leaves it believing a widget is
|
|
143
|
+
mounted that is not, after which the next unmount does nothing and the next
|
|
144
|
+
mount does nothing.
|
|
145
|
+
|
|
146
|
+
Every method returns `void`, including `identify`, which the widget's own API
|
|
147
|
+
returns a promise from. Your call may be queued before the bundle has arrived
|
|
148
|
+
and there is nothing to await in that state — use `onIdentified` (or
|
|
149
|
+
`useChatlyEvent('identified', …)`) to learn that it landed, and `onError` to
|
|
150
|
+
learn that it did not.
|
|
151
|
+
|
|
152
|
+
Calling any of them with no widget mounted warns in the console and does
|
|
153
|
+
nothing. It never throws: a misconfigured chat must not break your click
|
|
154
|
+
handler.
|
|
155
|
+
|
|
156
|
+
## `useChatlyEvent(event, handler)`
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
function UnreadBadge() {
|
|
160
|
+
const [unread, setUnread] = useState(0);
|
|
161
|
+
useChatlyEvent('message', () => setUnread((n) => n + 1));
|
|
162
|
+
useChatlyEvent('open', () => setUnread(0));
|
|
163
|
+
return unread > 0 ? <span className="badge">{unread}</span> : null;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Subscribes for the life of the component. The handler is read through a ref,
|
|
168
|
+
so an inline arrow does not re-subscribe on every render.
|
|
169
|
+
|
|
170
|
+
`'ready'` fires immediately if the session is already up — the widget replays
|
|
171
|
+
the last one — which is what makes this usable from a component that mounts
|
|
172
|
+
long after the page did.
|
|
173
|
+
|
|
174
|
+
## One widget per page
|
|
175
|
+
|
|
176
|
+
`window.Chatly` is a singleton, so there is one widget per page whatever you
|
|
177
|
+
render. Two mounted `<ChatlyWidget />`s with different options is a
|
|
178
|
+
misconfiguration: the **first** one owns the configuration, the second's
|
|
179
|
+
options are ignored (its event handlers still fire), and you get a console
|
|
180
|
+
warning saying so. Render one, high in the tree, and reach for `useChatly()`
|
|
181
|
+
everywhere else.
|
|
182
|
+
|
|
183
|
+
Two mounted with the _same_ options is harmless and silent — which is what
|
|
184
|
+
makes a `<ChatlyWidget />` inside a layout that remounts on navigation safe.
|
|
185
|
+
|
|
186
|
+
## Server-side rendering
|
|
187
|
+
|
|
188
|
+
Safe. The component renders `null` (or an empty `<div>` when `inline`), and
|
|
189
|
+
everything that touches `window` happens in an effect, which never runs on the
|
|
190
|
+
server. There is no `'use client'` needed in Next.js App Router beyond the one
|
|
191
|
+
your own component already has if it uses hooks.
|
|
192
|
+
|
|
193
|
+
## Self-hosting
|
|
194
|
+
|
|
195
|
+
Set `loaderUrl` to your deployment's. `GET /v1/channels/:id` returns the right
|
|
196
|
+
value as `embed.loaderUrl` — read it from there rather than typing a host, and
|
|
197
|
+
note that a running widget talks to **three** origins (widget host, API host,
|
|
198
|
+
realtime host); the same endpoint reports all of them as `connectOrigins` for
|
|
199
|
+
your CSP.
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
MIT — see `LICENSE`.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<ChatlyWidget />` — the widget as a React component.
|
|
3
|
+
*
|
|
4
|
+
* ── What this replaces ──────────────────────────────────────────────────
|
|
5
|
+
*
|
|
6
|
+
* Without it, a React app integrates by hand-writing the effect:
|
|
7
|
+
*
|
|
8
|
+
* useEffect(() => {
|
|
9
|
+
* window.Chatly?.init({ publicId });
|
|
10
|
+
* return () => window.Chatly?.destroy();
|
|
11
|
+
* }, [publicId]);
|
|
12
|
+
*
|
|
13
|
+
* That snippet has three bugs, and all three were in the version of it we
|
|
14
|
+
* published in our own docs. In StrictMode it boots twice (two sessions, a
|
|
15
|
+
* visibly flickering launcher). With a `theme={{…}}` literal in the deps it
|
|
16
|
+
* re-inits on every parent render, closing the visitor's open panel. And
|
|
17
|
+
* nothing in it ever calls `identify()`, so a visitor who signs in after load
|
|
18
|
+
* stays anonymous — because the only place to put an identity was `init`,
|
|
19
|
+
* which already ran.
|
|
20
|
+
*
|
|
21
|
+
* The lifecycle lives in `@chatlychat/widget-element` (refcounting, deferred
|
|
22
|
+
* teardown, structural prop comparison) so the React and custom-element
|
|
23
|
+
* wrappers cannot drift on the part that is hard.
|
|
24
|
+
*
|
|
25
|
+
* ── React is a peerDependency ───────────────────────────────────────────
|
|
26
|
+
*
|
|
27
|
+
* Declared `peerDependencies`, never `dependencies`. A real dependency would
|
|
28
|
+
* resolve a SECOND copy of React in any consumer whose version differs by so
|
|
29
|
+
* much as a patch, and two Reacts on one page break hooks with
|
|
30
|
+
* "Invalid hook call" — from a component the consumer did not write. A peer
|
|
31
|
+
* dep on the framework the consumer is already using is the one case where
|
|
32
|
+
* adding to the dependency block is right, and it costs their install nothing.
|
|
33
|
+
*/
|
|
34
|
+
import { type ChatlyEmbedHandlers, type ChatlyEmbedOptions } from '@chatlychat/widget-element';
|
|
35
|
+
import type { CSSProperties, ReactElement } from 'react';
|
|
36
|
+
export interface ChatlyWidgetProps extends ChatlyEmbedOptions, ChatlyEmbedHandlers {
|
|
37
|
+
/**
|
|
38
|
+
* Render the chat INSIDE this component's own box instead of floating it in
|
|
39
|
+
* the page corner.
|
|
40
|
+
*
|
|
41
|
+
* This is the React-shaped answer to `mountTo`. The container is a `<div>`
|
|
42
|
+
* this component renders, so it provably exists when `init` runs — which is
|
|
43
|
+
* the failure the widget's `mountTo` refusal exists to make loud ("not
|
|
44
|
+
* mounted — mountTo selector matched nothing"). Give it a height through
|
|
45
|
+
* `className` or `style`; it has a 420px minimum so a container with none
|
|
46
|
+
* is still usable.
|
|
47
|
+
*/
|
|
48
|
+
inline?: boolean;
|
|
49
|
+
/** Only meaningful with `inline`. */
|
|
50
|
+
className?: string;
|
|
51
|
+
/** Only meaningful with `inline`. */
|
|
52
|
+
style?: CSSProperties;
|
|
53
|
+
}
|
|
54
|
+
export declare function ChatlyWidget(props: ChatlyWidgetProps): ReactElement | null;
|
|
55
|
+
//# sourceMappingURL=ChatlyWidget.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatlyWidget.d.ts","sourceRoot":"","sources":["../src/ChatlyWidget.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,EAGL,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACxB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAEzD,MAAM,WAAW,iBAAkB,SAAQ,kBAAkB,EAAE,mBAAmB;IAChF;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,qCAAqC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAmDD,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,YAAY,GAAG,IAAI,CAqF1E"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* `<ChatlyWidget />` — the widget as a React component.
|
|
4
|
+
*
|
|
5
|
+
* ── What this replaces ──────────────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* Without it, a React app integrates by hand-writing the effect:
|
|
8
|
+
*
|
|
9
|
+
* useEffect(() => {
|
|
10
|
+
* window.Chatly?.init({ publicId });
|
|
11
|
+
* return () => window.Chatly?.destroy();
|
|
12
|
+
* }, [publicId]);
|
|
13
|
+
*
|
|
14
|
+
* That snippet has three bugs, and all three were in the version of it we
|
|
15
|
+
* published in our own docs. In StrictMode it boots twice (two sessions, a
|
|
16
|
+
* visibly flickering launcher). With a `theme={{…}}` literal in the deps it
|
|
17
|
+
* re-inits on every parent render, closing the visitor's open panel. And
|
|
18
|
+
* nothing in it ever calls `identify()`, so a visitor who signs in after load
|
|
19
|
+
* stays anonymous — because the only place to put an identity was `init`,
|
|
20
|
+
* which already ran.
|
|
21
|
+
*
|
|
22
|
+
* The lifecycle lives in `@chatlychat/widget-element` (refcounting, deferred
|
|
23
|
+
* teardown, structural prop comparison) so the React and custom-element
|
|
24
|
+
* wrappers cannot drift on the part that is hard.
|
|
25
|
+
*
|
|
26
|
+
* ── React is a peerDependency ───────────────────────────────────────────
|
|
27
|
+
*
|
|
28
|
+
* Declared `peerDependencies`, never `dependencies`. A real dependency would
|
|
29
|
+
* resolve a SECOND copy of React in any consumer whose version differs by so
|
|
30
|
+
* much as a patch, and two Reacts on one page break hooks with
|
|
31
|
+
* "Invalid hook call" — from a component the consumer did not write. A peer
|
|
32
|
+
* dep on the framework the consumer is already using is the one case where
|
|
33
|
+
* adding to the dependency block is right, and it costs their install nothing.
|
|
34
|
+
*/
|
|
35
|
+
import { startWidget, } from '@chatlychat/widget-element';
|
|
36
|
+
import { useEffect, useRef } from 'react';
|
|
37
|
+
/**
|
|
38
|
+
* Keys that are this component's own, not the widget's.
|
|
39
|
+
*
|
|
40
|
+
* Listed so `optionsFrom` can strip them by name. Spreading the rest means a
|
|
41
|
+
* widget option added later needs no change here — the failure that costs is
|
|
42
|
+
* the reverse (a prop of ours leaking into `init`), which this prevents.
|
|
43
|
+
*/
|
|
44
|
+
const LOCAL_PROPS = ['inline', 'className', 'style'];
|
|
45
|
+
const HANDLER_PROPS = [
|
|
46
|
+
'onReady',
|
|
47
|
+
'onOpen',
|
|
48
|
+
'onClose',
|
|
49
|
+
'onMessage',
|
|
50
|
+
'onConversation',
|
|
51
|
+
'onIdentified',
|
|
52
|
+
'onReset',
|
|
53
|
+
'onAction',
|
|
54
|
+
'onError',
|
|
55
|
+
];
|
|
56
|
+
function optionsFrom(props, mountTo) {
|
|
57
|
+
const options = {};
|
|
58
|
+
const skip = new Set([...LOCAL_PROPS, ...HANDLER_PROPS]);
|
|
59
|
+
for (const [key, value] of Object.entries(props)) {
|
|
60
|
+
if (!skip.has(key) && value !== undefined) {
|
|
61
|
+
options[key] = value;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (mountTo !== undefined) {
|
|
65
|
+
options['mountTo'] = mountTo;
|
|
66
|
+
}
|
|
67
|
+
return options;
|
|
68
|
+
}
|
|
69
|
+
function handlersFrom(props) {
|
|
70
|
+
return {
|
|
71
|
+
onReady: props.onReady,
|
|
72
|
+
onOpen: props.onOpen,
|
|
73
|
+
onClose: props.onClose,
|
|
74
|
+
onMessage: props.onMessage,
|
|
75
|
+
onConversation: props.onConversation,
|
|
76
|
+
onIdentified: props.onIdentified,
|
|
77
|
+
onReset: props.onReset,
|
|
78
|
+
onAction: props.onAction,
|
|
79
|
+
onError: props.onError,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export function ChatlyWidget(props) {
|
|
83
|
+
const container = useRef(null);
|
|
84
|
+
const handle = useRef(null);
|
|
85
|
+
/*
|
|
86
|
+
* The latest callbacks, behind a stable indirection.
|
|
87
|
+
*
|
|
88
|
+
* `onMessage={(m) => …}` is a NEW function on every render. Handing it
|
|
89
|
+
* straight to the lifecycle would either re-register subscribers every
|
|
90
|
+
* commit or pin the first render's closure forever — the stale-callback bug
|
|
91
|
+
* that makes a wrapper feel haunted (the handler fires with props from
|
|
92
|
+
* three navigations ago). One stable object of forwarders that read this ref
|
|
93
|
+
* is the standard answer and the only one that is correct in both
|
|
94
|
+
* directions.
|
|
95
|
+
*/
|
|
96
|
+
const latest = useRef(handlersFrom(props));
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
latest.current = handlersFrom(props);
|
|
99
|
+
// No dependency array: this must run after EVERY commit, because that is
|
|
100
|
+
// what "latest" means. There is nothing to compare and nothing to skip.
|
|
101
|
+
});
|
|
102
|
+
const forwarders = useRef({
|
|
103
|
+
onReady: (session) => latest.current.onReady?.(session),
|
|
104
|
+
onOpen: () => latest.current.onOpen?.(),
|
|
105
|
+
onClose: () => latest.current.onClose?.(),
|
|
106
|
+
onMessage: (message) => latest.current.onMessage?.(message),
|
|
107
|
+
onConversation: (detail) => latest.current.onConversation?.(detail),
|
|
108
|
+
onIdentified: (session) => latest.current.onIdentified?.(session),
|
|
109
|
+
onReset: (detail) => latest.current.onReset?.(detail),
|
|
110
|
+
onAction: (detail) => latest.current.onAction?.(detail),
|
|
111
|
+
onError: (detail) => latest.current.onError?.(detail),
|
|
112
|
+
});
|
|
113
|
+
/*
|
|
114
|
+
* The props of the FIRST render, captured by `useRef`'s initial-value
|
|
115
|
+
* semantics rather than read from the render scope.
|
|
116
|
+
*
|
|
117
|
+
* The mount effect has an empty dependency array — mount and unmount, which
|
|
118
|
+
* is exactly right — and reading `props` inside it would make
|
|
119
|
+
* `react-hooks/exhaustive-deps` demand them as dependencies, at which point
|
|
120
|
+
* the effect re-runs on every render and we are back to re-initialising the
|
|
121
|
+
* widget for a new object literal. The update effect below is what applies
|
|
122
|
+
* later props, and it does so through a structural comparison.
|
|
123
|
+
*/
|
|
124
|
+
const initial = useRef(props);
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (initial.current.inline === true && container.current === null) {
|
|
127
|
+
/*
|
|
128
|
+
* Should be impossible — effects run after the DOM is committed — but if
|
|
129
|
+
* it ever happens the alternative is mounting a FLOATING widget on a
|
|
130
|
+
* page built to embed one, which is the exact outcome the widget's own
|
|
131
|
+
* refusal path exists to prevent. Refuse, and say which component.
|
|
132
|
+
*/
|
|
133
|
+
console.warn('[Chatly] <ChatlyWidget inline> did not get its container — not mounted');
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const started = startWidget(
|
|
137
|
+
// The claim identity. `forwarders` is a per-instance ref object, so two
|
|
138
|
+
// mounted components are two claims and one of them unmounting does not
|
|
139
|
+
// take the widget with it.
|
|
140
|
+
forwarders, optionsFrom(initial.current, container.current ?? undefined), forwarders.current);
|
|
141
|
+
handle.current = started;
|
|
142
|
+
return () => {
|
|
143
|
+
started.stop();
|
|
144
|
+
handle.current = null;
|
|
145
|
+
};
|
|
146
|
+
}, []);
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
// Again no dependency array. Every commit re-offers the props; the
|
|
149
|
+
// lifecycle decides what, if anything, that means — `visitor` becomes an
|
|
150
|
+
// `identify()`, `locale` a `setLocale()`, and only a material change to
|
|
151
|
+
// the rest costs a re-init.
|
|
152
|
+
handle.current?.update(optionsFrom(props, container.current ?? undefined), forwarders.current);
|
|
153
|
+
});
|
|
154
|
+
if (props.inline !== true) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return _jsx("div", { ref: container, className: props.className, style: props.style });
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=ChatlyWidget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ChatlyWidget.js","sourceRoot":"","sources":["../src/ChatlyWidget.tsx"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@chatlychat/widget-react` — the Chatly widget as a React component.
|
|
3
|
+
*
|
|
4
|
+
* import { ChatlyWidget, useChatly } from '@chatlychat/widget-react';
|
|
5
|
+
*
|
|
6
|
+
* function App() {
|
|
7
|
+
* return (
|
|
8
|
+
* <>
|
|
9
|
+
* <ChatlyWidget publicId="acme-web" visitor={{ externalId: user.id }} />
|
|
10
|
+
* <Routes />
|
|
11
|
+
* </>
|
|
12
|
+
* );
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* function HelpButton() {
|
|
16
|
+
* const chatly = useChatly();
|
|
17
|
+
* return <button onClick={() => chatly.open('help')}>Help</button>;
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* React is a peerDependency — see the note at the top of `ChatlyWidget.tsx`.
|
|
21
|
+
* The embed lifecycle comes from `@chatlychat/widget-element`, which has no
|
|
22
|
+
* dependencies of its own and no side effects on import; the custom element it
|
|
23
|
+
* also ships is NOT registered by anything here.
|
|
24
|
+
*/
|
|
25
|
+
export { ChatlyWidget, type ChatlyWidgetProps } from './ChatlyWidget.js';
|
|
26
|
+
export { useChatly, useChatlyEvent } from './useChatly.js';
|
|
27
|
+
/**
|
|
28
|
+
* Re-exported so a consumer types their props from one import rather than
|
|
29
|
+
* reaching into the element package for a theme type.
|
|
30
|
+
*/
|
|
31
|
+
export type { ChatlyBehavior, ChatlyCopy, ChatlyEmbedHandlers, ChatlyEmbedOptions, ChatlyEventName, ChatlyIdentity, ChatlyInitOptions, ChatlyMessageEvent, ChatlyQuickLink, ChatlyQuip, ChatlySession, ChatlyTheme, } from '@chatlychat/widget-element';
|
|
32
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAE,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACzE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAE3D;;;GAGG;AACH,YAAY,EACV,cAAc,EACd,UAAU,EACV,mBAAmB,EACnB,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,UAAU,EACV,aAAa,EACb,WAAW,GACZ,MAAM,4BAA4B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@chatlychat/widget-react` — the Chatly widget as a React component.
|
|
3
|
+
*
|
|
4
|
+
* import { ChatlyWidget, useChatly } from '@chatlychat/widget-react';
|
|
5
|
+
*
|
|
6
|
+
* function App() {
|
|
7
|
+
* return (
|
|
8
|
+
* <>
|
|
9
|
+
* <ChatlyWidget publicId="acme-web" visitor={{ externalId: user.id }} />
|
|
10
|
+
* <Routes />
|
|
11
|
+
* </>
|
|
12
|
+
* );
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* function HelpButton() {
|
|
16
|
+
* const chatly = useChatly();
|
|
17
|
+
* return <button onClick={() => chatly.open('help')}>Help</button>;
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* React is a peerDependency — see the note at the top of `ChatlyWidget.tsx`.
|
|
21
|
+
* The embed lifecycle comes from `@chatlychat/widget-element`, which has no
|
|
22
|
+
* dependencies of its own and no side effects on import; the custom element it
|
|
23
|
+
* also ships is NOT registered by anything here.
|
|
24
|
+
*/
|
|
25
|
+
export { ChatlyWidget } from './ChatlyWidget.js';
|
|
26
|
+
export { useChatly, useChatlyEvent } from './useChatly.js';
|
|
27
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAAE,YAAY,EAA0B,MAAM,mBAAmB,CAAC;AACzE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks for controlling a widget that is already mounted.
|
|
3
|
+
*
|
|
4
|
+
* The split matters: `<ChatlyWidget />` OWNS the widget (one per page, high in
|
|
5
|
+
* the tree), and everything else — a "Chat with us" button in the header, a
|
|
6
|
+
* badge in the nav, a help link inside a modal — controls it through
|
|
7
|
+
* `useChatly()`. Rendering a second `<ChatlyWidget />` to get at `open()` is
|
|
8
|
+
* the mistake this exists to make unnecessary, and the lifecycle warns when
|
|
9
|
+
* someone makes it anyway.
|
|
10
|
+
*/
|
|
11
|
+
import { controls, type ChatlyEventName } from '@chatlychat/widget-element';
|
|
12
|
+
/**
|
|
13
|
+
* The widget's post-boot methods.
|
|
14
|
+
*
|
|
15
|
+
* A module-level constant, returned as-is: it is referentially stable, so it
|
|
16
|
+
* is safe in a dependency array and in a `useCallback` closure, and there is
|
|
17
|
+
* no state for a hook to hold. It is a hook rather than a plain import purely
|
|
18
|
+
* for discoverability — and because it is where the "mount the widget first"
|
|
19
|
+
* contract is documented.
|
|
20
|
+
*
|
|
21
|
+
* Every method returns `void`, including `identify`, which the widget's own
|
|
22
|
+
* API returns a promise from. The call may be queued before the bundle has
|
|
23
|
+
* arrived, and there is nothing to await in that state; `useChatlyEvent`
|
|
24
|
+
* (or `onIdentified`) is how you learn it landed.
|
|
25
|
+
*
|
|
26
|
+
* Calling any of these with no widget mounted warns in the console and does
|
|
27
|
+
* nothing. It does not throw: a click handler on the host page must not break
|
|
28
|
+
* because the chat is misconfigured.
|
|
29
|
+
*/
|
|
30
|
+
export declare function useChatly(): typeof controls;
|
|
31
|
+
/**
|
|
32
|
+
* Subscribe to a widget event for the life of this component.
|
|
33
|
+
*
|
|
34
|
+
* The handler is read through a ref, so an inline arrow function does not
|
|
35
|
+
* re-subscribe on every render — the same reason `<ChatlyWidget />` keeps its
|
|
36
|
+
* callbacks behind one. Without it, a consumer writing the obvious
|
|
37
|
+
* `useChatlyEvent('message', (m) => setCount(c => c + 1))` would register and
|
|
38
|
+
* unregister a subscriber on every commit, and would do so against the
|
|
39
|
+
* widget's `off`, which can only remove a handler it matches by identity.
|
|
40
|
+
*
|
|
41
|
+
* `'ready'` fires immediately if the session is already up: the widget replays
|
|
42
|
+
* the last one, which is what makes this usable from a component that mounts
|
|
43
|
+
* long after the page did.
|
|
44
|
+
*/
|
|
45
|
+
export declare function useChatlyEvent(event: ChatlyEventName, handler: (detail: unknown) => void): void;
|
|
46
|
+
//# sourceMappingURL=useChatly.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useChatly.d.ts","sourceRoot":"","sources":["../src/useChatly.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,QAAQ,EAAa,KAAK,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAGvF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,SAAS,IAAI,OAAO,QAAQ,CAE3C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI,CAU/F"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks for controlling a widget that is already mounted.
|
|
3
|
+
*
|
|
4
|
+
* The split matters: `<ChatlyWidget />` OWNS the widget (one per page, high in
|
|
5
|
+
* the tree), and everything else — a "Chat with us" button in the header, a
|
|
6
|
+
* badge in the nav, a help link inside a modal — controls it through
|
|
7
|
+
* `useChatly()`. Rendering a second `<ChatlyWidget />` to get at `open()` is
|
|
8
|
+
* the mistake this exists to make unnecessary, and the lifecycle warns when
|
|
9
|
+
* someone makes it anyway.
|
|
10
|
+
*/
|
|
11
|
+
import { controls, subscribe } from '@chatlychat/widget-element';
|
|
12
|
+
import { useEffect, useRef } from 'react';
|
|
13
|
+
/**
|
|
14
|
+
* The widget's post-boot methods.
|
|
15
|
+
*
|
|
16
|
+
* A module-level constant, returned as-is: it is referentially stable, so it
|
|
17
|
+
* is safe in a dependency array and in a `useCallback` closure, and there is
|
|
18
|
+
* no state for a hook to hold. It is a hook rather than a plain import purely
|
|
19
|
+
* for discoverability — and because it is where the "mount the widget first"
|
|
20
|
+
* contract is documented.
|
|
21
|
+
*
|
|
22
|
+
* Every method returns `void`, including `identify`, which the widget's own
|
|
23
|
+
* API returns a promise from. The call may be queued before the bundle has
|
|
24
|
+
* arrived, and there is nothing to await in that state; `useChatlyEvent`
|
|
25
|
+
* (or `onIdentified`) is how you learn it landed.
|
|
26
|
+
*
|
|
27
|
+
* Calling any of these with no widget mounted warns in the console and does
|
|
28
|
+
* nothing. It does not throw: a click handler on the host page must not break
|
|
29
|
+
* because the chat is misconfigured.
|
|
30
|
+
*/
|
|
31
|
+
export function useChatly() {
|
|
32
|
+
return controls;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Subscribe to a widget event for the life of this component.
|
|
36
|
+
*
|
|
37
|
+
* The handler is read through a ref, so an inline arrow function does not
|
|
38
|
+
* re-subscribe on every render — the same reason `<ChatlyWidget />` keeps its
|
|
39
|
+
* callbacks behind one. Without it, a consumer writing the obvious
|
|
40
|
+
* `useChatlyEvent('message', (m) => setCount(c => c + 1))` would register and
|
|
41
|
+
* unregister a subscriber on every commit, and would do so against the
|
|
42
|
+
* widget's `off`, which can only remove a handler it matches by identity.
|
|
43
|
+
*
|
|
44
|
+
* `'ready'` fires immediately if the session is already up: the widget replays
|
|
45
|
+
* the last one, which is what makes this usable from a component that mounts
|
|
46
|
+
* long after the page did.
|
|
47
|
+
*/
|
|
48
|
+
export function useChatlyEvent(event, handler) {
|
|
49
|
+
const latest = useRef(handler);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
latest.current = handler;
|
|
52
|
+
});
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
return subscribe(event, (detail) => latest.current(detail));
|
|
55
|
+
// `event` only. The handler is deliberately NOT a dependency — see above.
|
|
56
|
+
}, [event]);
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=useChatly.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useChatly.js","sourceRoot":"","sources":["../src/useChatly.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAwB,MAAM,4BAA4B,CAAC;AACvF,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAE1C;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,SAAS;IACvB,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc,CAAC,KAAsB,EAAE,OAAkC;IACvF,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC/B,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,CAAC,OAAO,GAAG,OAAO,CAAC;IAC3B,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,SAAS,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QAC5D,0EAA0E;IAC5E,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;AACd,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,74 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatlychat/widget-react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The Chatly chat widget as a React component — mount, unmount, identity and locale changes handled, StrictMode-safe.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/livechat/platform/tree/main/packages/widget-react",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/livechat/platform.git",
|
|
10
|
+
"directory": "packages/widget-react"
|
|
11
|
+
},
|
|
12
|
+
"bugs": "https://github.com/livechat/platform/issues",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"chatly",
|
|
15
|
+
"livechat",
|
|
16
|
+
"chat",
|
|
17
|
+
"widget",
|
|
18
|
+
"react",
|
|
19
|
+
"support"
|
|
20
|
+
],
|
|
21
|
+
"author": "LiveChat Platform Engineering",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"module": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"!dist/**/*.spec.*",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE",
|
|
38
|
+
"CHANGELOG.md"
|
|
39
|
+
],
|
|
40
|
+
"sideEffects": false,
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc -p tsconfig.json",
|
|
43
|
+
"prepublishOnly": "pnpm build",
|
|
44
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
45
|
+
"lint": "eslint src --max-warnings=0",
|
|
46
|
+
"test": "vitest run"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public",
|
|
50
|
+
"registry": "https://registry.npmjs.org/",
|
|
51
|
+
"provenance": true
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=18"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@chatlychat/widget-element": "workspace:*"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@livechat/config": "workspace:*",
|
|
64
|
+
"@testing-library/dom": "^10.4.1",
|
|
65
|
+
"@testing-library/react": "^16.3.2",
|
|
66
|
+
"@types/react": "catalog:",
|
|
67
|
+
"@types/react-dom": "catalog:",
|
|
68
|
+
"jsdom": "^30.0.1",
|
|
69
|
+
"react": "catalog:",
|
|
70
|
+
"react-dom": "catalog:",
|
|
71
|
+
"typescript": "catalog:",
|
|
72
|
+
"vitest": "catalog:"
|
|
73
|
+
}
|
|
74
|
+
}
|