@cstar.help/react 0.9.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
@@ -300,11 +300,74 @@ function CommunitySearch() {
300
300
  | `useLibraryClient()` | `LibraryClient` | Raw client from nearest `CStarLibraryProvider` |
301
301
  | `useCommunityClient()` | `CommunityClient` | Raw client from nearest `CStarCommunityProvider` |
302
302
 
303
+ ## Using with Next.js App Router (RSC)
304
+
305
+ The dashboard provider (`CStarDashboardProvider`) wraps a `CStarClient`,
306
+ which is a class instance — and React Server Components can't serialize
307
+ class instances across the server/client boundary. The provider therefore
308
+ takes one of two prop shapes:
309
+
310
+ **Recommended — pass a plain config object.** The provider builds (and
311
+ disposes) the client inside the `'use client'` boundary, so nothing
312
+ class-shaped crosses the RSC boundary:
313
+
314
+ ```tsx
315
+ // app/providers.tsx
316
+ 'use client';
317
+ import { CStarDashboardProvider } from '@cstar.help/react';
318
+
319
+ export function Providers({ children }: { children: React.ReactNode }) {
320
+ return (
321
+ <CStarDashboardProvider
322
+ config={{
323
+ apiKey: process.env.NEXT_PUBLIC_CSTAR_PUBLISHABLE_KEY!,
324
+ teamId: process.env.NEXT_PUBLIC_CSTAR_TEAM_ID!
325
+ }}
326
+ >
327
+ {children}
328
+ </CStarDashboardProvider>
329
+ );
330
+ }
331
+ ```
332
+
333
+ Use the `apiKey` field for a publishable key (`pk_live_*` / `pk_test_*`),
334
+ NOT a secret key — the bundle ships to the browser. For server-only flows
335
+ (an admin route handler, a server action), construct `CStarClient` with the
336
+ secret key inline; never thread it into a provider.
337
+
338
+ **Legacy — pass a pre-built client (deprecated).** If you already have a
339
+ client instance you want to share, the `client` prop still works, but you
340
+ have to construct it inside `'use client'` and you take on responsibility
341
+ for tearing down the realtime subscription:
342
+
343
+ ```tsx
344
+ 'use client';
345
+ import { useMemo } from 'react';
346
+ import { CStarClient } from '@cstar.help/js';
347
+ import { CStarDashboardProvider } from '@cstar.help/react';
348
+
349
+ export function Providers({ children }) {
350
+ const client = useMemo(
351
+ () =>
352
+ new CStarClient({
353
+ apiKey: process.env.NEXT_PUBLIC_CSTAR_PUBLISHABLE_KEY!,
354
+ teamId: process.env.NEXT_PUBLIC_CSTAR_TEAM_ID!
355
+ }),
356
+ []
357
+ );
358
+ return <CStarDashboardProvider client={client}>{children}</CStarDashboardProvider>;
359
+ }
360
+ ```
361
+
362
+ Either way, mark the file `'use client'`. Server components can sit above
363
+ the provider in the tree — only the provider boundary needs to be a client
364
+ component.
365
+
303
366
  ## Requirements
304
367
 
305
368
  - React 18+
306
369
  - @cstar.help/js 0.1.0+
307
- - Node.js 18+ (for SSR)
370
+ - Node.js 22+ for SSR with `@cstar.help/js` 0.17
308
371
 
309
372
  ## License
310
373
 
package/dist/index.cjs CHANGED
@@ -107,9 +107,14 @@ function useChat() {
107
107
  const [isRealtimeReady, setIsRealtimeReady] = (0, import_react3.useState)(false);
108
108
  const [error, setError] = (0, import_react3.useState)(null);
109
109
  (0, import_react3.useEffect)(() => {
110
- const current = client.getAuthMode();
111
- if (current !== authMode) setAuthMode(current);
112
- }, [client, authMode]);
110
+ const subscribe = client.subscribe?.bind(client);
111
+ if (!subscribe) return;
112
+ return subscribe(() => {
113
+ const mode = client.getAuthMode();
114
+ setAuthMode(mode);
115
+ setIsAuthenticated(mode !== null);
116
+ });
117
+ }, [client]);
113
118
  const wireRealtime = (0, import_react3.useCallback)(() => {
114
119
  client.waitForRealtime().then(() => setIsRealtimeReady(true)).catch(() => {
115
120
  });
@@ -199,7 +204,7 @@ function useTickets() {
199
204
  try {
200
205
  const result = await client.tickets.list();
201
206
  if (mountedRef.current) {
202
- setTickets(result.tickets);
207
+ setTickets(result.data);
203
208
  setHasMore(result.hasMore);
204
209
  }
205
210
  } catch (err) {
@@ -241,36 +246,48 @@ function useMessages(ticketId) {
241
246
  const [error, setError] = (0, import_react5.useState)(null);
242
247
  const mountedRef = (0, import_react5.useRef)(true);
243
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);
244
252
  const refresh = (0, import_react5.useCallback)(async () => {
245
253
  if (!ticketId) return;
254
+ const request = ++requestRef.current;
246
255
  setIsLoading(true);
247
256
  setError(null);
248
257
  try {
249
258
  const msgs = await client.messages.list(ticketId);
250
- if (mountedRef.current) {
259
+ if (mountedRef.current && request === requestRef.current) {
251
260
  seenIdsRef.current = new Set(msgs.map((m) => m.id));
252
261
  setMessages(msgs);
253
262
  }
254
263
  } catch (err) {
255
- if (mountedRef.current) {
264
+ if (mountedRef.current && request === requestRef.current) {
256
265
  setError(err instanceof Error ? err : new Error(String(err)));
257
266
  }
258
267
  } finally {
259
- if (mountedRef.current) {
268
+ if (mountedRef.current && request === requestRef.current) {
260
269
  setIsLoading(false);
261
270
  }
262
271
  }
263
272
  }, [client, ticketId]);
264
273
  (0, import_react5.useEffect)(() => {
265
274
  mountedRef.current = true;
275
+ const scope = ++scopeRef.current;
276
+ seenIdsRef.current = /* @__PURE__ */ new Set();
277
+ setMessages([]);
278
+ setError(null);
266
279
  if (!ticketId) {
267
280
  setMessages([]);
268
281
  setIsLoading(false);
269
- return;
282
+ return () => {
283
+ mountedRef.current = false;
284
+ requestRef.current++;
285
+ scopeRef.current++;
286
+ };
270
287
  }
271
288
  refresh();
272
289
  const unsubscribe = client.onMessage(ticketId, (msg) => {
273
- if (mountedRef.current && !seenIdsRef.current.has(msg.id)) {
290
+ if (mountedRef.current && scope === scopeRef.current && !seenIdsRef.current.has(msg.id)) {
274
291
  seenIdsRef.current.add(msg.id);
275
292
  setMessages((prev) => {
276
293
  const optimisticIdx = prev.findIndex(
@@ -287,14 +304,17 @@ function useMessages(ticketId) {
287
304
  });
288
305
  return () => {
289
306
  mountedRef.current = false;
307
+ requestRef.current++;
308
+ scopeRef.current++;
290
309
  unsubscribe();
291
310
  };
292
311
  }, [client, ticketId, refresh]);
293
312
  const send = (0, import_react5.useCallback)(
294
313
  async (content) => {
295
314
  if (!ticketId) throw new Error("No ticket selected");
315
+ const scope = scopeRef.current;
296
316
  const optimistic = {
297
- id: `optimistic-${Date.now()}`,
317
+ id: `optimistic-${++optimisticRef.current}`,
298
318
  content,
299
319
  sender: "customer",
300
320
  sender_name: "",
@@ -305,11 +325,14 @@ function useMessages(ticketId) {
305
325
  setMessages((prev) => [...prev, optimistic]);
306
326
  try {
307
327
  const msg = await client.messages.send(ticketId, content);
328
+ if (!mountedRef.current || scope !== scopeRef.current) return msg;
308
329
  seenIdsRef.current.add(msg.id);
309
330
  setMessages((prev) => prev.map((m) => m.id === optimistic.id ? msg : m));
310
331
  return msg;
311
332
  } catch (err) {
312
- 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
+ }
313
336
  throw err;
314
337
  }
315
338
  },
@@ -727,20 +750,38 @@ function useCommunitySearch() {
727
750
 
728
751
  // src/dashboard/CStarDashboardProvider.tsx
729
752
  var import_react17 = require("react");
753
+ var import_js = require("@cstar.help/js");
730
754
  var import_jsx_runtime4 = require("react/jsx-runtime");
731
755
  var CStarDashboardContext = (0, import_react17.createContext)(null);
732
- function CStarDashboardProvider({ client, children }) {
733
- if (!client) {
756
+ function CStarDashboardProvider({
757
+ config,
758
+ client: providedClient,
759
+ children
760
+ }) {
761
+ if (!config && !providedClient) {
734
762
  throw new Error(
735
- "CStarDashboardProvider requires a `client` prop. Build it server-side with `new CStarClient({ apiKey: process.env.CSTAR_API_KEY, teamId })` and pass via SSR. See https://cstar.help/developers/sdk/react#dashboard"
763
+ "CStarDashboardProvider requires either a `config` prop (`{ apiKey, teamId }` \u2014 recommended) or a `client` prop (a pre-built CStarClient instance, deprecated). See https://cstar.help/developers/sdk/react#dashboard"
736
764
  );
737
765
  }
738
- const contextValue = (0, import_react17.useRef)({ client }).current;
766
+ const client = (0, import_react17.useMemo)(() => {
767
+ if (providedClient) return providedClient;
768
+ return new import_js.CStarClient(config);
769
+ }, [
770
+ providedClient,
771
+ config?.apiKey,
772
+ config?.teamId,
773
+ config?.baseUrl,
774
+ config?.version,
775
+ config?.maxRetries,
776
+ config?.timeout
777
+ ]);
778
+ const contextValue = (0, import_react17.useMemo)(() => ({ client }), [client]);
739
779
  (0, import_react17.useEffect)(() => {
780
+ if (providedClient) return;
740
781
  return () => {
741
782
  client?.realtime?.disconnect?.();
742
783
  };
743
- }, [client]);
784
+ }, [client, providedClient]);
744
785
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CStarDashboardContext.Provider, { value: contextValue, children });
745
786
  }
746
787
  function useDashboardClient() {
@@ -753,6 +794,7 @@ function useDashboardClient() {
753
794
 
754
795
  // src/dashboard/useCStarQuery.ts
755
796
  var import_react18 = require("react");
797
+ var import_js2 = require("@cstar.help/js");
756
798
  function resolveResource(client, path) {
757
799
  const parts = path.split(".");
758
800
  if (parts.length < 2 || parts[1] !== "list") return null;
@@ -764,6 +806,11 @@ function resolveResource(client, path) {
764
806
  return { resource, basePath: `/${resourceName}` };
765
807
  }
766
808
  function useCStarQuery(resource, params) {
809
+ if (typeof resource !== "string") {
810
+ throw new import_js2.CStarValidationError(
811
+ `useCStarQuery: \`resource\` must be a string (e.g. 'tickets.list'). Received ${resource === null ? "null" : typeof resource}. The signature is useCStarQuery(resource: string, params?: object).`
812
+ );
813
+ }
767
814
  const client = useDashboardClient();
768
815
  const [data, setData] = (0, import_react18.useState)(null);
769
816
  const [loading, setLoading] = (0, import_react18.useState)(true);
package/dist/index.d.cts CHANGED
@@ -6,8 +6,7 @@ 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
- import { CStarClient, RealtimeEvent, Boss, Player, LeaderboardEntry } from '@cstar.help/js';
9
+ import { CStarClientConfig, CStarClient, RealtimeEvent, Boss, Player, LeaderboardEntry } from '@cstar.help/js';
11
10
 
12
11
  declare const CStarChatContext: react.Context<ChatClient | null>;
13
12
  declare const CStarLibraryContext: react.Context<LibraryClient | 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. */
@@ -355,28 +354,48 @@ interface UseCommunitySearchReturn {
355
354
  declare function useCommunitySearch(): UseCommunitySearchReturn;
356
355
 
357
356
  interface CStarDashboardProviderProps {
358
- /** A pre-configured CStarClient instance (server-side, secret key). */
359
- client: CStarClient;
357
+ /**
358
+ * Serializable client config — `{ apiKey, teamId, baseUrl?, ... }`. The
359
+ * provider builds the `CStarClient` internally so this prop can be passed
360
+ * across the React Server Components boundary without tripping the
361
+ * "Classes or null prototypes are not supported" runtime error.
362
+ * Config is visible to the browser: use only a publishable key here.
363
+ *
364
+ * Either `config` or `client` is required.
365
+ */
366
+ config?: CStarClientConfig;
367
+ /**
368
+ * A pre-built `CStarClient` instance.
369
+ *
370
+ * @deprecated since 0.9.0 — pass `config` instead. Class instances cannot
371
+ * cross the RSC boundary, which makes this prop unsafe to use from a
372
+ * Next.js App Router server component. Kept as an escape hatch for
373
+ * integrators who already wired it; will be removed in 1.0.
374
+ */
375
+ client?: CStarClient;
360
376
  children: react__default.ReactNode;
361
377
  }
362
378
  /**
363
- * Provides a CStarClient to dashboard hooks.
379
+ * Provides a `CStarClient` to dashboard hooks.
364
380
  *
365
- * Unlike `CStarChatProvider` (which uses a publishable key for customer-facing
366
- * chat), this provider wraps a full admin client with a secret key for building
367
- * 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.
368
385
  *
369
386
  * ```tsx
370
- * const client = new CStarClient({ apiKey: 'sk_test_...', teamId: '...' });
371
- *
372
- * <CStarDashboardProvider client={client}>
387
+ * // app/providers.tsx
388
+ * 'use client';
389
+ * <CStarDashboardProvider
390
+ * config={{ apiKey: process.env.NEXT_PUBLIC_CSTAR_PUBLISHABLE_KEY!, teamId }}
391
+ * >
373
392
  * <TicketList />
374
393
  * </CStarDashboardProvider>
375
394
  * ```
376
395
  */
377
- declare function CStarDashboardProvider({ client, children }: CStarDashboardProviderProps): react_jsx_runtime.JSX.Element;
396
+ declare function CStarDashboardProvider({ config, client: providedClient, children }: CStarDashboardProviderProps): react__default.JSX.Element;
378
397
  /**
379
- * Access the CStarClient from the nearest CStarDashboardProvider.
398
+ * Access the `CStarClient` from the nearest `CStarDashboardProvider`.
380
399
  * Throws if used outside a provider.
381
400
  */
382
401
  declare function useDashboardClient(): CStarClient;
package/dist/index.d.ts CHANGED
@@ -6,8 +6,7 @@ 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
- import { CStarClient, RealtimeEvent, Boss, Player, LeaderboardEntry } from '@cstar.help/js';
9
+ import { CStarClientConfig, CStarClient, RealtimeEvent, Boss, Player, LeaderboardEntry } from '@cstar.help/js';
11
10
 
12
11
  declare const CStarChatContext: react.Context<ChatClient | null>;
13
12
  declare const CStarLibraryContext: react.Context<LibraryClient | 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. */
@@ -355,28 +354,48 @@ interface UseCommunitySearchReturn {
355
354
  declare function useCommunitySearch(): UseCommunitySearchReturn;
356
355
 
357
356
  interface CStarDashboardProviderProps {
358
- /** A pre-configured CStarClient instance (server-side, secret key). */
359
- client: CStarClient;
357
+ /**
358
+ * Serializable client config — `{ apiKey, teamId, baseUrl?, ... }`. The
359
+ * provider builds the `CStarClient` internally so this prop can be passed
360
+ * across the React Server Components boundary without tripping the
361
+ * "Classes or null prototypes are not supported" runtime error.
362
+ * Config is visible to the browser: use only a publishable key here.
363
+ *
364
+ * Either `config` or `client` is required.
365
+ */
366
+ config?: CStarClientConfig;
367
+ /**
368
+ * A pre-built `CStarClient` instance.
369
+ *
370
+ * @deprecated since 0.9.0 — pass `config` instead. Class instances cannot
371
+ * cross the RSC boundary, which makes this prop unsafe to use from a
372
+ * Next.js App Router server component. Kept as an escape hatch for
373
+ * integrators who already wired it; will be removed in 1.0.
374
+ */
375
+ client?: CStarClient;
360
376
  children: react__default.ReactNode;
361
377
  }
362
378
  /**
363
- * Provides a CStarClient to dashboard hooks.
379
+ * Provides a `CStarClient` to dashboard hooks.
364
380
  *
365
- * Unlike `CStarChatProvider` (which uses a publishable key for customer-facing
366
- * chat), this provider wraps a full admin client with a secret key for building
367
- * 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.
368
385
  *
369
386
  * ```tsx
370
- * const client = new CStarClient({ apiKey: 'sk_test_...', teamId: '...' });
371
- *
372
- * <CStarDashboardProvider client={client}>
387
+ * // app/providers.tsx
388
+ * 'use client';
389
+ * <CStarDashboardProvider
390
+ * config={{ apiKey: process.env.NEXT_PUBLIC_CSTAR_PUBLISHABLE_KEY!, teamId }}
391
+ * >
373
392
  * <TicketList />
374
393
  * </CStarDashboardProvider>
375
394
  * ```
376
395
  */
377
- declare function CStarDashboardProvider({ client, children }: CStarDashboardProviderProps): react_jsx_runtime.JSX.Element;
396
+ declare function CStarDashboardProvider({ config, client: providedClient, children }: CStarDashboardProviderProps): react__default.JSX.Element;
378
397
  /**
379
- * Access the CStarClient from the nearest CStarDashboardProvider.
398
+ * Access the `CStarClient` from the nearest `CStarDashboardProvider`.
380
399
  * Throws if used outside a provider.
381
400
  */
382
401
  declare function useDashboardClient(): CStarClient;
package/dist/index.js CHANGED
@@ -52,9 +52,14 @@ function useChat() {
52
52
  const [isRealtimeReady, setIsRealtimeReady] = useState(false);
53
53
  const [error, setError] = useState(null);
54
54
  useEffect2(() => {
55
- const current = client.getAuthMode();
56
- if (current !== authMode) setAuthMode(current);
57
- }, [client, authMode]);
55
+ const subscribe = client.subscribe?.bind(client);
56
+ if (!subscribe) return;
57
+ return subscribe(() => {
58
+ const mode = client.getAuthMode();
59
+ setAuthMode(mode);
60
+ setIsAuthenticated(mode !== null);
61
+ });
62
+ }, [client]);
58
63
  const wireRealtime = useCallback(() => {
59
64
  client.waitForRealtime().then(() => setIsRealtimeReady(true)).catch(() => {
60
65
  });
@@ -144,7 +149,7 @@ function useTickets() {
144
149
  try {
145
150
  const result = await client.tickets.list();
146
151
  if (mountedRef.current) {
147
- setTickets(result.tickets);
152
+ setTickets(result.data);
148
153
  setHasMore(result.hasMore);
149
154
  }
150
155
  } catch (err) {
@@ -186,36 +191,48 @@ function useMessages(ticketId) {
186
191
  const [error, setError] = useState3(null);
187
192
  const mountedRef = useRef3(true);
188
193
  const seenIdsRef = useRef3(/* @__PURE__ */ new Set());
194
+ const requestRef = useRef3(0);
195
+ const scopeRef = useRef3(0);
196
+ const optimisticRef = useRef3(0);
189
197
  const refresh = useCallback3(async () => {
190
198
  if (!ticketId) return;
199
+ const request = ++requestRef.current;
191
200
  setIsLoading(true);
192
201
  setError(null);
193
202
  try {
194
203
  const msgs = await client.messages.list(ticketId);
195
- if (mountedRef.current) {
204
+ if (mountedRef.current && request === requestRef.current) {
196
205
  seenIdsRef.current = new Set(msgs.map((m) => m.id));
197
206
  setMessages(msgs);
198
207
  }
199
208
  } catch (err) {
200
- if (mountedRef.current) {
209
+ if (mountedRef.current && request === requestRef.current) {
201
210
  setError(err instanceof Error ? err : new Error(String(err)));
202
211
  }
203
212
  } finally {
204
- if (mountedRef.current) {
213
+ if (mountedRef.current && request === requestRef.current) {
205
214
  setIsLoading(false);
206
215
  }
207
216
  }
208
217
  }, [client, ticketId]);
209
218
  useEffect4(() => {
210
219
  mountedRef.current = true;
220
+ const scope = ++scopeRef.current;
221
+ seenIdsRef.current = /* @__PURE__ */ new Set();
222
+ setMessages([]);
223
+ setError(null);
211
224
  if (!ticketId) {
212
225
  setMessages([]);
213
226
  setIsLoading(false);
214
- return;
227
+ return () => {
228
+ mountedRef.current = false;
229
+ requestRef.current++;
230
+ scopeRef.current++;
231
+ };
215
232
  }
216
233
  refresh();
217
234
  const unsubscribe = client.onMessage(ticketId, (msg) => {
218
- if (mountedRef.current && !seenIdsRef.current.has(msg.id)) {
235
+ if (mountedRef.current && scope === scopeRef.current && !seenIdsRef.current.has(msg.id)) {
219
236
  seenIdsRef.current.add(msg.id);
220
237
  setMessages((prev) => {
221
238
  const optimisticIdx = prev.findIndex(
@@ -232,14 +249,17 @@ function useMessages(ticketId) {
232
249
  });
233
250
  return () => {
234
251
  mountedRef.current = false;
252
+ requestRef.current++;
253
+ scopeRef.current++;
235
254
  unsubscribe();
236
255
  };
237
256
  }, [client, ticketId, refresh]);
238
257
  const send = useCallback3(
239
258
  async (content) => {
240
259
  if (!ticketId) throw new Error("No ticket selected");
260
+ const scope = scopeRef.current;
241
261
  const optimistic = {
242
- id: `optimistic-${Date.now()}`,
262
+ id: `optimistic-${++optimisticRef.current}`,
243
263
  content,
244
264
  sender: "customer",
245
265
  sender_name: "",
@@ -250,11 +270,14 @@ function useMessages(ticketId) {
250
270
  setMessages((prev) => [...prev, optimistic]);
251
271
  try {
252
272
  const msg = await client.messages.send(ticketId, content);
273
+ if (!mountedRef.current || scope !== scopeRef.current) return msg;
253
274
  seenIdsRef.current.add(msg.id);
254
275
  setMessages((prev) => prev.map((m) => m.id === optimistic.id ? msg : m));
255
276
  return msg;
256
277
  } catch (err) {
257
- 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
+ }
258
281
  throw err;
259
282
  }
260
283
  },
@@ -671,21 +694,39 @@ function useCommunitySearch() {
671
694
  }
672
695
 
673
696
  // src/dashboard/CStarDashboardProvider.tsx
674
- import { createContext as createContext2, useContext as useContext2, useEffect as useEffect14, useRef as useRef15 } from "react";
697
+ import { createContext as createContext2, useContext as useContext2, useEffect as useEffect14, useMemo } from "react";
698
+ import { CStarClient } from "@cstar.help/js";
675
699
  import { jsx as jsx4 } from "react/jsx-runtime";
676
700
  var CStarDashboardContext = createContext2(null);
677
- function CStarDashboardProvider({ client, children }) {
678
- if (!client) {
701
+ function CStarDashboardProvider({
702
+ config,
703
+ client: providedClient,
704
+ children
705
+ }) {
706
+ if (!config && !providedClient) {
679
707
  throw new Error(
680
- "CStarDashboardProvider requires a `client` prop. Build it server-side with `new CStarClient({ apiKey: process.env.CSTAR_API_KEY, teamId })` and pass via SSR. See https://cstar.help/developers/sdk/react#dashboard"
708
+ "CStarDashboardProvider requires either a `config` prop (`{ apiKey, teamId }` \u2014 recommended) or a `client` prop (a pre-built CStarClient instance, deprecated). See https://cstar.help/developers/sdk/react#dashboard"
681
709
  );
682
710
  }
683
- const contextValue = useRef15({ client }).current;
711
+ const client = useMemo(() => {
712
+ if (providedClient) return providedClient;
713
+ return new CStarClient(config);
714
+ }, [
715
+ providedClient,
716
+ config?.apiKey,
717
+ config?.teamId,
718
+ config?.baseUrl,
719
+ config?.version,
720
+ config?.maxRetries,
721
+ config?.timeout
722
+ ]);
723
+ const contextValue = useMemo(() => ({ client }), [client]);
684
724
  useEffect14(() => {
725
+ if (providedClient) return;
685
726
  return () => {
686
727
  client?.realtime?.disconnect?.();
687
728
  };
688
- }, [client]);
729
+ }, [client, providedClient]);
689
730
  return /* @__PURE__ */ jsx4(CStarDashboardContext.Provider, { value: contextValue, children });
690
731
  }
691
732
  function useDashboardClient() {
@@ -697,7 +738,8 @@ function useDashboardClient() {
697
738
  }
698
739
 
699
740
  // src/dashboard/useCStarQuery.ts
700
- import { useState as useState13, useEffect as useEffect15, useRef as useRef16, useCallback as useCallback12 } from "react";
741
+ import { useState as useState13, useEffect as useEffect15, useRef as useRef15, useCallback as useCallback12 } from "react";
742
+ import { CStarValidationError } from "@cstar.help/js";
701
743
  function resolveResource(client, path) {
702
744
  const parts = path.split(".");
703
745
  if (parts.length < 2 || parts[1] !== "list") return null;
@@ -709,11 +751,16 @@ function resolveResource(client, path) {
709
751
  return { resource, basePath: `/${resourceName}` };
710
752
  }
711
753
  function useCStarQuery(resource, params) {
754
+ if (typeof resource !== "string") {
755
+ throw new CStarValidationError(
756
+ `useCStarQuery: \`resource\` must be a string (e.g. 'tickets.list'). Received ${resource === null ? "null" : typeof resource}. The signature is useCStarQuery(resource: string, params?: object).`
757
+ );
758
+ }
712
759
  const client = useDashboardClient();
713
760
  const [data, setData] = useState13(null);
714
761
  const [loading, setLoading] = useState13(true);
715
762
  const [error, setError] = useState13(null);
716
- const subscriptionRef = useRef16(null);
763
+ const subscriptionRef = useRef15(null);
717
764
  const paramsKey = params ? JSON.stringify(params) : "";
718
765
  useEffect15(() => {
719
766
  const resolved = resolveResource(client, resource);
@@ -841,13 +888,13 @@ function useCStarEvents(types) {
841
888
  }
842
889
 
843
890
  // src/dashboard/useBossBattle.ts
844
- import { useState as useState16, useEffect as useEffect17, useRef as useRef17, useCallback as useCallback14 } from "react";
891
+ import { useState as useState16, useEffect as useEffect17, useRef as useRef16, useCallback as useCallback14 } from "react";
845
892
  function useBossBattle() {
846
893
  const client = useDashboardClient();
847
894
  const [boss, setBoss] = useState16(null);
848
895
  const [loading, setLoading] = useState16(true);
849
896
  const [error, setError] = useState16(null);
850
- const subRef = useRef17(null);
897
+ const subRef = useRef16(null);
851
898
  useEffect17(() => {
852
899
  const sub = client.game.subscribeBoss(client.realtime);
853
900
  subRef.current = sub;
@@ -872,13 +919,13 @@ function useBossBattle() {
872
919
  }
873
920
 
874
921
  // src/dashboard/usePlayerStats.ts
875
- import { useState as useState17, useEffect as useEffect18, useRef as useRef18, useCallback as useCallback15 } from "react";
922
+ import { useState as useState17, useEffect as useEffect18, useRef as useRef17, useCallback as useCallback15 } from "react";
876
923
  function usePlayerStats(memberId) {
877
924
  const client = useDashboardClient();
878
925
  const [player, setPlayer] = useState17(null);
879
926
  const [loading, setLoading] = useState17(true);
880
927
  const [error, setError] = useState17(null);
881
- const subRef = useRef18(null);
928
+ const subRef = useRef17(null);
882
929
  useEffect18(() => {
883
930
  const sub = client.game.subscribePlayer(client.realtime, memberId);
884
931
  subRef.current = sub;
@@ -903,13 +950,13 @@ function usePlayerStats(memberId) {
903
950
  }
904
951
 
905
952
  // src/dashboard/useLeaderboard.ts
906
- import { useState as useState18, useEffect as useEffect19, useRef as useRef19, useCallback as useCallback16 } from "react";
953
+ import { useState as useState18, useEffect as useEffect19, useRef as useRef18, useCallback as useCallback16 } from "react";
907
954
  function useLeaderboard(period) {
908
955
  const client = useDashboardClient();
909
956
  const [entries, setEntries] = useState18([]);
910
957
  const [loading, setLoading] = useState18(true);
911
958
  const [error, setError] = useState18(null);
912
- const subRef = useRef19(null);
959
+ const subRef = useRef18(null);
913
960
  useEffect19(() => {
914
961
  const sub = client.game.subscribeLeaderboard(client.realtime, period ? { period } : void 0);
915
962
  subRef.current = sub;
@@ -934,13 +981,13 @@ function useLeaderboard(period) {
934
981
  }
935
982
 
936
983
  // src/dashboard/useGameActivity.ts
937
- import { useState as useState19, useEffect as useEffect20, useRef as useRef20, useCallback as useCallback17 } from "react";
984
+ import { useState as useState19, useEffect as useEffect20, useRef as useRef19, useCallback as useCallback17 } from "react";
938
985
  function useGameActivity() {
939
986
  const client = useDashboardClient();
940
987
  const [events, setEvents] = useState19([]);
941
988
  const [loading, setLoading] = useState19(true);
942
989
  const [error, setError] = useState19(null);
943
- const subRef = useRef20(null);
990
+ const subRef = useRef19(null);
944
991
  useEffect20(() => {
945
992
  const sub = client.game.subscribeActivity(client.realtime);
946
993
  subRef.current = sub;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cstar.help/react",
3
- "version": "0.9.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",
@@ -30,19 +30,20 @@
30
30
  },
31
31
  "peerDependencies": {
32
32
  "react": ">=18.0.0",
33
- "@cstar.help/js": ">=0.14.0"
33
+ "@cstar.help/js": ">=0.15.0"
34
34
  },
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",