@cstar.help/react 0.10.0 → 0.10.1

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/README.md CHANGED
@@ -367,7 +367,7 @@ component.
367
367
 
368
368
  - React 18+
369
369
  - @cstar.help/js 0.1.0+
370
- - Node.js 18+ (for SSR)
370
+ - Node.js 22+ for SSR with `@cstar.help/js` 0.17
371
371
 
372
372
  ## License
373
373
 
package/dist/index.cjs CHANGED
@@ -246,36 +246,48 @@ function useMessages(ticketId) {
246
246
  const [error, setError] = (0, import_react5.useState)(null);
247
247
  const mountedRef = (0, import_react5.useRef)(true);
248
248
  const seenIdsRef = (0, import_react5.useRef)(/* @__PURE__ */ new Set());
249
+ const requestRef = (0, import_react5.useRef)(0);
250
+ const scopeRef = (0, import_react5.useRef)(0);
251
+ const optimisticRef = (0, import_react5.useRef)(0);
249
252
  const refresh = (0, import_react5.useCallback)(async () => {
250
253
  if (!ticketId) return;
254
+ const request = ++requestRef.current;
251
255
  setIsLoading(true);
252
256
  setError(null);
253
257
  try {
254
258
  const msgs = await client.messages.list(ticketId);
255
- if (mountedRef.current) {
259
+ if (mountedRef.current && request === requestRef.current) {
256
260
  seenIdsRef.current = new Set(msgs.map((m) => m.id));
257
261
  setMessages(msgs);
258
262
  }
259
263
  } catch (err) {
260
- if (mountedRef.current) {
264
+ if (mountedRef.current && request === requestRef.current) {
261
265
  setError(err instanceof Error ? err : new Error(String(err)));
262
266
  }
263
267
  } finally {
264
- if (mountedRef.current) {
268
+ if (mountedRef.current && request === requestRef.current) {
265
269
  setIsLoading(false);
266
270
  }
267
271
  }
268
272
  }, [client, ticketId]);
269
273
  (0, import_react5.useEffect)(() => {
270
274
  mountedRef.current = true;
275
+ const scope = ++scopeRef.current;
276
+ seenIdsRef.current = /* @__PURE__ */ new Set();
277
+ setMessages([]);
278
+ setError(null);
271
279
  if (!ticketId) {
272
280
  setMessages([]);
273
281
  setIsLoading(false);
274
- return;
282
+ return () => {
283
+ mountedRef.current = false;
284
+ requestRef.current++;
285
+ scopeRef.current++;
286
+ };
275
287
  }
276
288
  refresh();
277
289
  const unsubscribe = client.onMessage(ticketId, (msg) => {
278
- if (mountedRef.current && !seenIdsRef.current.has(msg.id)) {
290
+ if (mountedRef.current && scope === scopeRef.current && !seenIdsRef.current.has(msg.id)) {
279
291
  seenIdsRef.current.add(msg.id);
280
292
  setMessages((prev) => {
281
293
  const optimisticIdx = prev.findIndex(
@@ -292,14 +304,17 @@ function useMessages(ticketId) {
292
304
  });
293
305
  return () => {
294
306
  mountedRef.current = false;
307
+ requestRef.current++;
308
+ scopeRef.current++;
295
309
  unsubscribe();
296
310
  };
297
311
  }, [client, ticketId, refresh]);
298
312
  const send = (0, import_react5.useCallback)(
299
313
  async (content) => {
300
314
  if (!ticketId) throw new Error("No ticket selected");
315
+ const scope = scopeRef.current;
301
316
  const optimistic = {
302
- id: `optimistic-${Date.now()}`,
317
+ id: `optimistic-${++optimisticRef.current}`,
303
318
  content,
304
319
  sender: "customer",
305
320
  sender_name: "",
@@ -310,11 +325,14 @@ function useMessages(ticketId) {
310
325
  setMessages((prev) => [...prev, optimistic]);
311
326
  try {
312
327
  const msg = await client.messages.send(ticketId, content);
328
+ if (!mountedRef.current || scope !== scopeRef.current) return msg;
313
329
  seenIdsRef.current.add(msg.id);
314
330
  setMessages((prev) => prev.map((m) => m.id === optimistic.id ? msg : m));
315
331
  return msg;
316
332
  } catch (err) {
317
- setMessages((prev) => prev.filter((m) => m.id !== optimistic.id));
333
+ if (mountedRef.current && scope === scopeRef.current) {
334
+ setMessages((prev) => prev.filter((m) => m.id !== optimistic.id));
335
+ }
318
336
  throw err;
319
337
  }
320
338
  },
package/dist/index.d.cts CHANGED
@@ -6,7 +6,6 @@ import { LibraryClient, LibraryConfig, Category, LibraryArticle, LibraryArticleL
6
6
  export { Category, LibraryArticle, LibraryArticleListParams, LibraryConfig, LibrarySearchParams, LibrarySearchResult } from '@cstar.help/js/library';
7
7
  import { CommunityClient, CommunityConfig, CommunityTopic, CommunityPost, CommunityPostListParams, CommunityPostDetail, CommunitySearchResult } from '@cstar.help/js/community';
8
8
  export { CommunityComment, CommunityConfig, CommunityPost, CommunityPostDetail, CommunityPostListParams, CommunitySearchResult, CommunityTopic } from '@cstar.help/js/community';
9
- import * as react_jsx_runtime from 'react/jsx-runtime';
10
9
  import { CStarClientConfig, CStarClient, RealtimeEvent, Boss, Player, LeaderboardEntry } from '@cstar.help/js';
11
10
 
12
11
  declare const CStarChatContext: react.Context<ChatClient | null>;
@@ -41,7 +40,7 @@ interface CStarChatProviderProps extends ChatConfig {
41
40
  * </CStarChatProvider>
42
41
  * ```
43
42
  */
44
- declare function CStarChatProvider({ children, ...config }: CStarChatProviderProps): react_jsx_runtime.JSX.Element;
43
+ declare function CStarChatProvider({ children, ...config }: CStarChatProviderProps): react.JSX.Element;
45
44
 
46
45
  interface UseChatReturn {
47
46
  /** Identify a customer with HMAC signature. */
@@ -178,7 +177,7 @@ interface CStarLibraryProviderProps extends LibraryConfig {
178
177
  * </CStarLibraryProvider>
179
178
  * ```
180
179
  */
181
- declare function CStarLibraryProvider({ children, ...config }: CStarLibraryProviderProps): react_jsx_runtime.JSX.Element;
180
+ declare function CStarLibraryProvider({ children, ...config }: CStarLibraryProviderProps): react.JSX.Element;
182
181
 
183
182
  interface UseCategoriesReturn {
184
183
  /** All public categories. */
@@ -271,7 +270,7 @@ interface CStarCommunityProviderProps extends CommunityConfig {
271
270
  * </CStarCommunityProvider>
272
271
  * ```
273
272
  */
274
- declare function CStarCommunityProvider({ children, ...config }: CStarCommunityProviderProps): react_jsx_runtime.JSX.Element;
273
+ declare function CStarCommunityProvider({ children, ...config }: CStarCommunityProviderProps): react.JSX.Element;
275
274
 
276
275
  interface UseTopicsReturn {
277
276
  /** Community discussion topics. */
@@ -360,6 +359,7 @@ interface CStarDashboardProviderProps {
360
359
  * provider builds the `CStarClient` internally so this prop can be passed
361
360
  * across the React Server Components boundary without tripping the
362
361
  * "Classes or null prototypes are not supported" runtime error.
362
+ * Config is visible to the browser: use only a publishable key here.
363
363
  *
364
364
  * Either `config` or `client` is required.
365
365
  */
@@ -378,19 +378,22 @@ interface CStarDashboardProviderProps {
378
378
  /**
379
379
  * Provides a `CStarClient` to dashboard hooks.
380
380
  *
381
- * Unlike `CStarChatProvider` (which uses a publishable key for customer-facing
382
- * chat), this provider wraps a full admin client with a secret key for building
383
- * custom dashboards.
381
+ * Provider props and bundled clients are visible to the browser. Pass only
382
+ * a publishable key; supported operations are limited by that key's permissions.
383
+ * Run privileged dashboard reads and mutations through your authenticated
384
+ * backend, where a secret-key `CStarClient` stays on the server.
384
385
  *
385
386
  * ```tsx
386
- * // Recommended: pass serializable config — works in any React tree, including
387
- * // App Router server components that hand the props to a client component.
388
- * <CStarDashboardProvider config={{ apiKey: process.env.CSTAR_API_KEY!, teamId }}>
387
+ * // app/providers.tsx
388
+ * 'use client';
389
+ * <CStarDashboardProvider
390
+ * config={{ apiKey: process.env.NEXT_PUBLIC_CSTAR_PUBLISHABLE_KEY!, teamId }}
391
+ * >
389
392
  * <TicketList />
390
393
  * </CStarDashboardProvider>
391
394
  * ```
392
395
  */
393
- declare function CStarDashboardProvider({ config, client: providedClient, children }: CStarDashboardProviderProps): react_jsx_runtime.JSX.Element;
396
+ declare function CStarDashboardProvider({ config, client: providedClient, children }: CStarDashboardProviderProps): react__default.JSX.Element;
394
397
  /**
395
398
  * Access the `CStarClient` from the nearest `CStarDashboardProvider`.
396
399
  * Throws if used outside a provider.
package/dist/index.d.ts CHANGED
@@ -6,7 +6,6 @@ import { LibraryClient, LibraryConfig, Category, LibraryArticle, LibraryArticleL
6
6
  export { Category, LibraryArticle, LibraryArticleListParams, LibraryConfig, LibrarySearchParams, LibrarySearchResult } from '@cstar.help/js/library';
7
7
  import { CommunityClient, CommunityConfig, CommunityTopic, CommunityPost, CommunityPostListParams, CommunityPostDetail, CommunitySearchResult } from '@cstar.help/js/community';
8
8
  export { CommunityComment, CommunityConfig, CommunityPost, CommunityPostDetail, CommunityPostListParams, CommunitySearchResult, CommunityTopic } from '@cstar.help/js/community';
9
- import * as react_jsx_runtime from 'react/jsx-runtime';
10
9
  import { CStarClientConfig, CStarClient, RealtimeEvent, Boss, Player, LeaderboardEntry } from '@cstar.help/js';
11
10
 
12
11
  declare const CStarChatContext: react.Context<ChatClient | null>;
@@ -41,7 +40,7 @@ interface CStarChatProviderProps extends ChatConfig {
41
40
  * </CStarChatProvider>
42
41
  * ```
43
42
  */
44
- declare function CStarChatProvider({ children, ...config }: CStarChatProviderProps): react_jsx_runtime.JSX.Element;
43
+ declare function CStarChatProvider({ children, ...config }: CStarChatProviderProps): react.JSX.Element;
45
44
 
46
45
  interface UseChatReturn {
47
46
  /** Identify a customer with HMAC signature. */
@@ -178,7 +177,7 @@ interface CStarLibraryProviderProps extends LibraryConfig {
178
177
  * </CStarLibraryProvider>
179
178
  * ```
180
179
  */
181
- declare function CStarLibraryProvider({ children, ...config }: CStarLibraryProviderProps): react_jsx_runtime.JSX.Element;
180
+ declare function CStarLibraryProvider({ children, ...config }: CStarLibraryProviderProps): react.JSX.Element;
182
181
 
183
182
  interface UseCategoriesReturn {
184
183
  /** All public categories. */
@@ -271,7 +270,7 @@ interface CStarCommunityProviderProps extends CommunityConfig {
271
270
  * </CStarCommunityProvider>
272
271
  * ```
273
272
  */
274
- declare function CStarCommunityProvider({ children, ...config }: CStarCommunityProviderProps): react_jsx_runtime.JSX.Element;
273
+ declare function CStarCommunityProvider({ children, ...config }: CStarCommunityProviderProps): react.JSX.Element;
275
274
 
276
275
  interface UseTopicsReturn {
277
276
  /** Community discussion topics. */
@@ -360,6 +359,7 @@ interface CStarDashboardProviderProps {
360
359
  * provider builds the `CStarClient` internally so this prop can be passed
361
360
  * across the React Server Components boundary without tripping the
362
361
  * "Classes or null prototypes are not supported" runtime error.
362
+ * Config is visible to the browser: use only a publishable key here.
363
363
  *
364
364
  * Either `config` or `client` is required.
365
365
  */
@@ -378,19 +378,22 @@ interface CStarDashboardProviderProps {
378
378
  /**
379
379
  * Provides a `CStarClient` to dashboard hooks.
380
380
  *
381
- * Unlike `CStarChatProvider` (which uses a publishable key for customer-facing
382
- * chat), this provider wraps a full admin client with a secret key for building
383
- * custom dashboards.
381
+ * Provider props and bundled clients are visible to the browser. Pass only
382
+ * a publishable key; supported operations are limited by that key's permissions.
383
+ * Run privileged dashboard reads and mutations through your authenticated
384
+ * backend, where a secret-key `CStarClient` stays on the server.
384
385
  *
385
386
  * ```tsx
386
- * // Recommended: pass serializable config — works in any React tree, including
387
- * // App Router server components that hand the props to a client component.
388
- * <CStarDashboardProvider config={{ apiKey: process.env.CSTAR_API_KEY!, teamId }}>
387
+ * // app/providers.tsx
388
+ * 'use client';
389
+ * <CStarDashboardProvider
390
+ * config={{ apiKey: process.env.NEXT_PUBLIC_CSTAR_PUBLISHABLE_KEY!, teamId }}
391
+ * >
389
392
  * <TicketList />
390
393
  * </CStarDashboardProvider>
391
394
  * ```
392
395
  */
393
- declare function CStarDashboardProvider({ config, client: providedClient, children }: CStarDashboardProviderProps): react_jsx_runtime.JSX.Element;
396
+ declare function CStarDashboardProvider({ config, client: providedClient, children }: CStarDashboardProviderProps): react__default.JSX.Element;
394
397
  /**
395
398
  * Access the `CStarClient` from the nearest `CStarDashboardProvider`.
396
399
  * Throws if used outside a provider.
package/dist/index.js CHANGED
@@ -191,36 +191,48 @@ function useMessages(ticketId) {
191
191
  const [error, setError] = useState3(null);
192
192
  const mountedRef = useRef3(true);
193
193
  const seenIdsRef = useRef3(/* @__PURE__ */ new Set());
194
+ const requestRef = useRef3(0);
195
+ const scopeRef = useRef3(0);
196
+ const optimisticRef = useRef3(0);
194
197
  const refresh = useCallback3(async () => {
195
198
  if (!ticketId) return;
199
+ const request = ++requestRef.current;
196
200
  setIsLoading(true);
197
201
  setError(null);
198
202
  try {
199
203
  const msgs = await client.messages.list(ticketId);
200
- if (mountedRef.current) {
204
+ if (mountedRef.current && request === requestRef.current) {
201
205
  seenIdsRef.current = new Set(msgs.map((m) => m.id));
202
206
  setMessages(msgs);
203
207
  }
204
208
  } catch (err) {
205
- if (mountedRef.current) {
209
+ if (mountedRef.current && request === requestRef.current) {
206
210
  setError(err instanceof Error ? err : new Error(String(err)));
207
211
  }
208
212
  } finally {
209
- if (mountedRef.current) {
213
+ if (mountedRef.current && request === requestRef.current) {
210
214
  setIsLoading(false);
211
215
  }
212
216
  }
213
217
  }, [client, ticketId]);
214
218
  useEffect4(() => {
215
219
  mountedRef.current = true;
220
+ const scope = ++scopeRef.current;
221
+ seenIdsRef.current = /* @__PURE__ */ new Set();
222
+ setMessages([]);
223
+ setError(null);
216
224
  if (!ticketId) {
217
225
  setMessages([]);
218
226
  setIsLoading(false);
219
- return;
227
+ return () => {
228
+ mountedRef.current = false;
229
+ requestRef.current++;
230
+ scopeRef.current++;
231
+ };
220
232
  }
221
233
  refresh();
222
234
  const unsubscribe = client.onMessage(ticketId, (msg) => {
223
- if (mountedRef.current && !seenIdsRef.current.has(msg.id)) {
235
+ if (mountedRef.current && scope === scopeRef.current && !seenIdsRef.current.has(msg.id)) {
224
236
  seenIdsRef.current.add(msg.id);
225
237
  setMessages((prev) => {
226
238
  const optimisticIdx = prev.findIndex(
@@ -237,14 +249,17 @@ function useMessages(ticketId) {
237
249
  });
238
250
  return () => {
239
251
  mountedRef.current = false;
252
+ requestRef.current++;
253
+ scopeRef.current++;
240
254
  unsubscribe();
241
255
  };
242
256
  }, [client, ticketId, refresh]);
243
257
  const send = useCallback3(
244
258
  async (content) => {
245
259
  if (!ticketId) throw new Error("No ticket selected");
260
+ const scope = scopeRef.current;
246
261
  const optimistic = {
247
- id: `optimistic-${Date.now()}`,
262
+ id: `optimistic-${++optimisticRef.current}`,
248
263
  content,
249
264
  sender: "customer",
250
265
  sender_name: "",
@@ -255,11 +270,14 @@ function useMessages(ticketId) {
255
270
  setMessages((prev) => [...prev, optimistic]);
256
271
  try {
257
272
  const msg = await client.messages.send(ticketId, content);
273
+ if (!mountedRef.current || scope !== scopeRef.current) return msg;
258
274
  seenIdsRef.current.add(msg.id);
259
275
  setMessages((prev) => prev.map((m) => m.id === optimistic.id ? msg : m));
260
276
  return msg;
261
277
  } catch (err) {
262
- setMessages((prev) => prev.filter((m) => m.id !== optimistic.id));
278
+ if (mountedRef.current && scope === scopeRef.current) {
279
+ setMessages((prev) => prev.filter((m) => m.id !== optimistic.id));
280
+ }
263
281
  throw err;
264
282
  }
265
283
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cstar.help/react",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "React hooks for the cStar customer support platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -35,14 +35,15 @@
35
35
  "devDependencies": {
36
36
  "react": "^18.3.1",
37
37
  "react-dom": "^18.3.1",
38
- "@cstar.help/js": "file:../js",
38
+ "@cstar.help/js": "^0.17.0",
39
39
  "@testing-library/react": "^16.1.0",
40
40
  "@types/react": "^18.3.0",
41
41
  "@types/react-dom": "^18.3.0",
42
42
  "tsup": "^8.5.0",
43
43
  "typescript": "^5.7.0",
44
44
  "vitest": "^3.2.0",
45
- "jsdom": "^25.0.0"
45
+ "jsdom": "^25.0.0",
46
+ "@testing-library/dom": "^10.4.1"
46
47
  },
47
48
  "license": "MIT",
48
49
  "engines": {
@@ -51,11 +52,11 @@
51
52
  "homepage": "https://cstar.help/developers/sdk/react",
52
53
  "repository": {
53
54
  "type": "git",
54
- "url": "https://github.com/cstar-help/cstar",
55
+ "url": "https://github.com/msjoshlopez/cstar",
55
56
  "directory": "packages/react"
56
57
  },
57
58
  "bugs": {
58
- "url": "https://github.com/cstar-help/cstar/issues"
59
+ "url": "https://www.cstar.help/support"
59
60
  },
60
61
  "keywords": [
61
62
  "cstar",