@linktr.ee/messaging-react 4.1.7 → 4.2.0-rc-1787181113

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/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-BE8tWSEx.cjs");exports.ActionButton=e.ActionButton;exports.Avatar=e.Avatar;exports.ChannelEmptyState=e.ChannelEmptyState;exports.ChannelList=e.ChannelList;exports.ChannelView=e.ChannelView;exports.CustomMessageProvider=e.CustomMessageProvider;exports.FaqList=e.FaqList;exports.FaqListItem=e.FaqListItem;exports.LinkAttachment=e.LinkAttachment;exports.LockedAttachment=e.LockedAttachment;exports.MediaMessage=e.MediaMessage;exports.MessageAttachment=e.MessageAttachment;exports.MessageBubble=e.MessageBubble;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.RichCard=e.RichCard;exports.TipMessage=e.TipMessage;exports.buildCompactMetaLabel=e.buildCompactMetaLabel;exports.formatFileSize=e.formatFileSize;exports.formatRelativeTime=e.formatRelativeTime;exports.getFileExtensionLabel=e.getFileExtensionLabel;exports.getMessageDisplayText=e.getMessageDisplayText;exports.isLinkAttachment=e.isLinkAttachment;exports.isTipMessage=e.isTipMessage;exports.isUuidLike=e.isUuidLike;exports.messageAttachmentGroupPositionFromStream=e.bubbleGroupPositionFromStream;exports.normalizeLanguageCode=e.normalizeLanguageCode;exports.optimizeMessagingAttachmentUrl=e.optimizeMessagingAttachmentUrl;exports.resolveConversationParticipant=e.resolveConversationParticipant;exports.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useComposerLocked=e.useComposerLocked;exports.useCustomMessage=e.useCustomMessage;exports.useMessaging=e.useMessaging;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./index-BcU18EjV.cjs");exports.ActionButton=e.ActionButton;exports.Avatar=e.Avatar;exports.ChannelEmptyState=e.ChannelEmptyState;exports.ChannelList=e.ChannelList;exports.ChannelView=e.ChannelView;exports.CustomMessageProvider=e.CustomMessageProvider;exports.FaqList=e.FaqList;exports.FaqListItem=e.FaqListItem;exports.LinkAttachment=e.LinkAttachment;exports.LockedAttachment=e.LockedAttachment;exports.MediaMessage=e.MediaMessage;exports.MessageAttachment=e.MessageAttachment;exports.MessageBubble=e.MessageBubble;exports.MessagingLoggerProvider=e.MessagingLoggerProvider;exports.MessagingProvider=e.MessagingProvider;exports.MessagingShell=e.MessagingShell;exports.RichCard=e.RichCard;exports.TipMessage=e.TipMessage;exports.buildCompactMetaLabel=e.buildCompactMetaLabel;exports.formatFileSize=e.formatFileSize;exports.formatRelativeTime=e.formatRelativeTime;exports.getFileExtensionLabel=e.getFileExtensionLabel;exports.getMessageDisplayText=e.getMessageDisplayText;exports.isLinkAttachment=e.isLinkAttachment;exports.isTipMessage=e.isTipMessage;exports.isUuidLike=e.isUuidLike;exports.messageAttachmentGroupPositionFromStream=e.bubbleGroupPositionFromStream;exports.normalizeLanguageCode=e.normalizeLanguageCode;exports.optimizeMessagingAttachmentUrl=e.optimizeMessagingAttachmentUrl;exports.resolveConversationParticipant=e.resolveConversationParticipant;exports.resolveLinkAttachment=e.resolveLinkAttachment;exports.resolveMediaFromMessage=e.resolveMediaFromMessage;exports.resolveParticipantDisplayName=e.resolveParticipantDisplayName;exports.useComposerLocked=e.useComposerLocked;exports.useCustomMessage=e.useCustomMessage;exports.useMessaging=e.useMessaging;exports.useMessagingLogger=e.useMessagingLogger;
2
2
  //# sourceMappingURL=index.cjs.map
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ import { FC } from 'react';
13
13
  import { InfiniteScrollProps } from 'stream-chat-react';
14
14
  import { JSX as JSX_2 } from 'react/jsx-runtime';
15
15
  import { LocalMessage } from 'stream-chat';
16
+ import { Logger } from '@linktr.ee/messaging-core';
16
17
  import { MessageCustomType } from '@linktr.ee/messaging-taxonomy';
17
18
  import { MessagingUser } from '@linktr.ee/messaging-core';
18
19
  import { OfficialCtaActionKind } from '@linktr.ee/messaging-taxonomy';
@@ -872,6 +873,8 @@ export declare interface LockedAttachmentSource {
872
873
  thumbnailUrl?: string;
873
874
  }
874
875
 
876
+ export { Logger }
877
+
875
878
  declare interface MediaCardBaseProps {
876
879
  /** Attachment title. */
877
880
  title?: string;
@@ -1442,6 +1445,16 @@ declare interface MessagingContextValue {
1442
1445
  debug: boolean;
1443
1446
  }
1444
1447
 
1448
+ /**
1449
+ * Provides the resolved logger sink to the messaging subtree. Rendered by
1450
+ * `MessagingProvider`; usable standalone in tests or in a subtree that needs a
1451
+ * different sink.
1452
+ */
1453
+ export declare const MessagingLoggerProvider: default_2.FC<{
1454
+ logger?: Logger;
1455
+ children: default_2.ReactNode;
1456
+ }>;
1457
+
1445
1458
  /**
1446
1459
  * Provider component that wraps messaging-core with React state management
1447
1460
  */
@@ -1463,6 +1476,13 @@ declare interface MessagingProviderBaseProps {
1463
1476
  user: MessagingUser | null;
1464
1477
  capabilities?: MessagingCapabilities;
1465
1478
  debug?: boolean;
1479
+ /**
1480
+ * Optional sink for the package's own log output. Levels left undefined fall
1481
+ * back to console, which is also the default when the prop is omitted. In
1482
+ * apiKey mode it is forwarded to the underlying StreamChatService unless
1483
+ * `serviceConfig.logger` is set, which wins.
1484
+ */
1485
+ logger?: Logger;
1466
1486
  }
1467
1487
 
1468
1488
  /**
@@ -1825,6 +1845,12 @@ export declare function useCustomMessage<K extends keyof CustomMessageRegistry>(
1825
1845
  */
1826
1846
  export declare const useMessaging: () => MessagingContextValue;
1827
1847
 
1848
+ /**
1849
+ * The logger sink for the current subtree. Falls back to console when no
1850
+ * provider (or no `logger` prop) is present, so call sites never branch.
1851
+ */
1852
+ export declare const useMessagingLogger: () => Required<Logger>;
1853
+
1828
1854
  declare interface VideoAttachmentSharedProps extends MessageAttachmentBaseProps {
1829
1855
  /** Single video — convenience for the most common case. */
1830
1856
  src?: string;
package/dist/index.js CHANGED
@@ -1,39 +1,41 @@
1
- import { a as e, b as t, C as i, c as n, d as o, e as m, F as r, f as g, L as l, h as M, M as c, i as h, j as p, k as L, l as u, R as d, T as C, m as v, n as A, p as k, q as F, s as b, t as P, u as T, v as f, w as x, x as y, o as z, y as S, z as q, B, D, E, G as R, H as w } from "./index-xcIpBghu.js";
1
+ import { a as e, b as t, C as i, c as n, d as o, e as g, F as r, f as m, L as l, h as M, M as c, i as L, j as h, k as p, l as u, m as d, R as v, T as C, n as A, p as k, q as F, s as b, t as P, u as T, v as f, w as x, x as y, y as z, o as S, z as q, B, D, E, G as R, H as w, I as G, J as I } from "./index-6RdyZh6F.js";
2
2
  export {
3
3
  e as ActionButton,
4
4
  t as Avatar,
5
5
  i as ChannelEmptyState,
6
6
  n as ChannelList,
7
7
  o as ChannelView,
8
- m as CustomMessageProvider,
8
+ g as CustomMessageProvider,
9
9
  r as FaqList,
10
- g as FaqListItem,
10
+ m as FaqListItem,
11
11
  l as LinkAttachment,
12
12
  M as LockedAttachment,
13
13
  c as MediaMessage,
14
- h as MessageAttachment,
15
- p as MessageBubble,
16
- L as MessagingProvider,
17
- u as MessagingShell,
18
- d as RichCard,
14
+ L as MessageAttachment,
15
+ h as MessageBubble,
16
+ p as MessagingLoggerProvider,
17
+ u as MessagingProvider,
18
+ d as MessagingShell,
19
+ v as RichCard,
19
20
  C as TipMessage,
20
- v as buildCompactMetaLabel,
21
- A as formatFileSize,
22
- k as formatRelativeTime,
23
- F as getFileExtensionLabel,
24
- b as getMessageDisplayText,
25
- P as isLinkAttachment,
26
- T as isTipMessage,
27
- f as isUuidLike,
28
- x as messageAttachmentGroupPositionFromStream,
29
- y as normalizeLanguageCode,
30
- z as optimizeMessagingAttachmentUrl,
31
- S as resolveConversationParticipant,
32
- q as resolveLinkAttachment,
33
- B as resolveMediaFromMessage,
34
- D as resolveParticipantDisplayName,
35
- E as useComposerLocked,
36
- R as useCustomMessage,
37
- w as useMessaging
21
+ A as buildCompactMetaLabel,
22
+ k as formatFileSize,
23
+ F as formatRelativeTime,
24
+ b as getFileExtensionLabel,
25
+ P as getMessageDisplayText,
26
+ T as isLinkAttachment,
27
+ f as isTipMessage,
28
+ x as isUuidLike,
29
+ y as messageAttachmentGroupPositionFromStream,
30
+ z as normalizeLanguageCode,
31
+ S as optimizeMessagingAttachmentUrl,
32
+ q as resolveConversationParticipant,
33
+ B as resolveLinkAttachment,
34
+ D as resolveMediaFromMessage,
35
+ E as resolveParticipantDisplayName,
36
+ R as useComposerLocked,
37
+ w as useCustomMessage,
38
+ G as useMessaging,
39
+ I as useMessagingLogger
38
40
  };
39
41
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "4.1.7",
3
+ "version": "4.2.0-rc-1787181113",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -50,7 +50,7 @@
50
50
  "chromatic:local": "yarn storybook:build && chromatic"
51
51
  },
52
52
  "dependencies": {
53
- "@linktr.ee/messaging-core": "^2.5.0",
53
+ "@linktr.ee/messaging-core": "2.5.0-rc-1787181113",
54
54
  "@linktr.ee/messaging-taxonomy": "^0.10.0",
55
55
  "@phosphor-icons/react": "^2.1.10"
56
56
  },
package/src/index.ts CHANGED
@@ -29,11 +29,13 @@ export type {
29
29
 
30
30
  // Providers
31
31
  export { MessagingProvider } from './providers/MessagingProvider'
32
+ export { MessagingLoggerProvider } from './logging'
32
33
  export { CustomMessageProvider } from './components/CustomMessage/context'
33
34
 
34
35
  // Hooks
35
36
  export { useComposerLocked } from './components/CustomMessageInput'
36
37
  export { useMessaging } from './hooks/useMessaging'
38
+ export { useMessagingLogger } from './logging'
37
39
  export { useCustomMessage } from './components/CustomMessage/context'
38
40
 
39
41
  // Utils
@@ -136,3 +138,4 @@ export type { CustomMessageRegistry } from './components/CustomMessage/context'
136
138
  export type { AttachmentSourceType } from './components/AttachmentCard/utils/mimeType'
137
139
  export type { Faq, FaqListProps } from './components/FaqList'
138
140
  export type { FaqListItemProps } from './components/FaqList/FaqListItem'
141
+ export type { Logger } from '@linktr.ee/messaging-core'
@@ -0,0 +1,136 @@
1
+ import type { Logger } from '@linktr.ee/messaging-core'
2
+ import { render } from '@testing-library/react'
3
+ import React from 'react'
4
+ import { describe, it, expect, vi, afterEach } from 'vitest'
5
+
6
+ import {
7
+ MessagingLoggerProvider,
8
+ consoleLogger,
9
+ resolveLogger,
10
+ useMessagingLogger,
11
+ } from './index'
12
+
13
+ const ERROR_MESSAGE = '[MessagingShell] Failed to load channels:'
14
+
15
+ /** Logs one error through the seam the way a call site would. */
16
+ const ErrorEmitter: React.FC<{ error: Error }> = ({ error }) => {
17
+ const logger = useMessagingLogger()
18
+ logger.error(ERROR_MESSAGE, error)
19
+ return null
20
+ }
21
+
22
+ /** Emits one line at every level so partial-sink fallback is observable. */
23
+ const AllLevelsEmitter: React.FC = () => {
24
+ const logger = useMessagingLogger()
25
+ logger.debug('debug line', 1)
26
+ logger.info('info line', 2)
27
+ logger.warn('warn line', 3)
28
+ logger.error('error line', 4)
29
+ return null
30
+ }
31
+
32
+ afterEach(() => {
33
+ vi.restoreAllMocks()
34
+ })
35
+
36
+ describe('logger seam', () => {
37
+ it('routes error to an injected sink with the exact message and raw error object', () => {
38
+ const error = new Error('boom')
39
+ const sink = { error: vi.fn() }
40
+
41
+ render(
42
+ <MessagingLoggerProvider logger={sink}>
43
+ <ErrorEmitter error={error} />
44
+ </MessagingLoggerProvider>
45
+ )
46
+
47
+ expect(sink.error).toHaveBeenCalledTimes(1)
48
+ expect(sink.error).toHaveBeenCalledWith(ERROR_MESSAGE, error)
49
+ // The raw Error instance, not a serialized copy.
50
+ expect(sink.error.mock.calls[0][1]).toBe(error)
51
+ })
52
+
53
+ it('passes identical arguments to console.error when no sink is injected', () => {
54
+ const error = new Error('boom')
55
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
56
+ const sink = { error: vi.fn() }
57
+
58
+ render(
59
+ <MessagingLoggerProvider logger={sink}>
60
+ <ErrorEmitter error={error} />
61
+ </MessagingLoggerProvider>
62
+ )
63
+ const injectedArgs = sink.error.mock.calls[0]
64
+
65
+ // No provider at all, and a provider with no logger prop: both must produce
66
+ // byte-identical console output to what the injected sink received.
67
+ render(<ErrorEmitter error={error} />)
68
+ render(
69
+ <MessagingLoggerProvider>
70
+ <ErrorEmitter error={error} />
71
+ </MessagingLoggerProvider>
72
+ )
73
+
74
+ expect(consoleError).toHaveBeenCalledTimes(2)
75
+ expect(consoleError.mock.calls[0]).toEqual(injectedArgs)
76
+ expect(consoleError.mock.calls[1]).toEqual(injectedArgs)
77
+ expect(consoleError.mock.calls[0][1]).toBe(error)
78
+ })
79
+
80
+ it('falls back to console for the levels a partial sink omits', () => {
81
+ const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => {})
82
+ const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {})
83
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
84
+ const sink: Logger = { warn: vi.fn(), error: vi.fn() }
85
+
86
+ render(
87
+ <MessagingLoggerProvider logger={sink}>
88
+ <AllLevelsEmitter />
89
+ </MessagingLoggerProvider>
90
+ )
91
+
92
+ expect(sink.warn).toHaveBeenCalledWith('warn line', 3)
93
+ expect(sink.error).toHaveBeenCalledWith('error line', 4)
94
+ expect(consoleWarn).not.toHaveBeenCalled()
95
+ expect(consoleError).not.toHaveBeenCalled()
96
+ // debug and info are unset on the sink, so both land on console.log.
97
+ expect(consoleLog.mock.calls).toEqual([
98
+ ['debug line', 1],
99
+ ['info line', 2],
100
+ ])
101
+ })
102
+
103
+ it('defaults every level to console when the hook is used with no provider', () => {
104
+ const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => {})
105
+ const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {})
106
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
107
+
108
+ render(<AllLevelsEmitter />)
109
+
110
+ expect(consoleLog.mock.calls).toEqual([
111
+ ['debug line', 1],
112
+ ['info line', 2],
113
+ ])
114
+ expect(consoleWarn).toHaveBeenCalledWith('warn line', 3)
115
+ expect(consoleError).toHaveBeenCalledWith('error line', 4)
116
+ })
117
+
118
+ it('resolveLogger keeps provided levels and fills the rest from console', () => {
119
+ const warn = vi.fn()
120
+ const resolved = resolveLogger({ warn })
121
+
122
+ expect(resolved.warn).toBe(warn)
123
+ expect(resolved.debug).toBe(consoleLogger.debug)
124
+ expect(resolved.info).toBe(consoleLogger.info)
125
+ expect(resolved.error).toBe(consoleLogger.error)
126
+ expect(resolveLogger()).toEqual(consoleLogger)
127
+ })
128
+
129
+ it('consoleLogger dispatches through console at call time, not bind time', () => {
130
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
131
+
132
+ consoleLogger.error('late spy', 'arg')
133
+
134
+ expect(consoleError).toHaveBeenCalledWith('late spy', 'arg')
135
+ })
136
+ })
@@ -0,0 +1,48 @@
1
+ import type { Logger } from '@linktr.ee/messaging-core'
2
+ import React, { createContext, useContext, useMemo } from 'react'
3
+
4
+ /**
5
+ * Default sink: the console calls the package makes today. Wrappers, not
6
+ * `console.error.bind(console)`, so a later-installed spy is still observed.
7
+ */
8
+ export const consoleLogger: Required<Logger> = {
9
+ debug: (message, ...args) => console.log(message, ...args),
10
+ info: (message, ...args) => console.log(message, ...args),
11
+ warn: (message, ...args) => console.warn(message, ...args),
12
+ error: (message, ...args) => console.error(message, ...args),
13
+ }
14
+
15
+ /**
16
+ * Merge a partial sink over the console defaults, so a consumer can route only
17
+ * the levels it cares about (e.g. `error`) and leave the rest on console.
18
+ */
19
+ export const resolveLogger = (logger?: Logger): Required<Logger> => ({
20
+ debug: logger?.debug ?? consoleLogger.debug,
21
+ info: logger?.info ?? consoleLogger.info,
22
+ warn: logger?.warn ?? consoleLogger.warn,
23
+ error: logger?.error ?? consoleLogger.error,
24
+ })
25
+
26
+ const LoggerContext = createContext<Required<Logger>>(consoleLogger)
27
+
28
+ /**
29
+ * Provides the resolved logger sink to the messaging subtree. Rendered by
30
+ * `MessagingProvider`; usable standalone in tests or in a subtree that needs a
31
+ * different sink.
32
+ */
33
+ export const MessagingLoggerProvider: React.FC<{
34
+ logger?: Logger
35
+ children: React.ReactNode
36
+ }> = ({ logger, children }) => {
37
+ const value = useMemo(() => resolveLogger(logger), [logger])
38
+ return (
39
+ <LoggerContext.Provider value={value}>{children}</LoggerContext.Provider>
40
+ )
41
+ }
42
+
43
+ /**
44
+ * The logger sink for the current subtree. Falls back to console when no
45
+ * provider (or no `logger` prop) is present, so call sites never branch.
46
+ */
47
+ export const useMessagingLogger = (): Required<Logger> =>
48
+ useContext(LoggerContext)
@@ -1,10 +1,16 @@
1
1
  import { StreamChatService } from '@linktr.ee/messaging-core'
2
- import type { MessagingUser } from '@linktr.ee/messaging-core'
2
+ import type {
3
+ Logger,
4
+ MessagingUser,
5
+ StreamChatServiceConfig,
6
+ } from '@linktr.ee/messaging-core'
3
7
  import { render, waitFor, act } from '@testing-library/react'
4
8
  import React from 'react'
5
9
  import type { StreamChat } from 'stream-chat'
6
10
  import { describe, it, expect, vi, beforeEach } from 'vitest'
7
11
 
12
+ import { useMessagingLogger } from '../logging'
13
+
8
14
  import { MessagingProvider, useMessagingContext } from './MessagingProvider'
9
15
 
10
16
  // Stub stream-chat-react's <Chat /> so we don't need a real, fully-wired
@@ -486,4 +492,110 @@ describe('MessagingProvider', () => {
486
492
  expect(last.hasClient).toBe(false)
487
493
  expect(last.error).toBeNull()
488
494
  })
495
+
496
+ describe('logger seam', () => {
497
+ const ServiceProbe: React.FC<{
498
+ onService: (service: StreamChatService | null) => void
499
+ }> = ({ onService }) => {
500
+ const { service } = useMessagingContext()
501
+ React.useEffect(() => {
502
+ onService(service)
503
+ }, [service, onService])
504
+ return null
505
+ }
506
+
507
+ const LoggerProbe: React.FC<{ onLogger: (logger: Logger) => void }> = ({
508
+ onLogger,
509
+ }) => {
510
+ const logger = useMessagingLogger()
511
+ React.useEffect(() => {
512
+ onLogger(logger)
513
+ }, [logger, onLogger])
514
+ return null
515
+ }
516
+
517
+ // The service stores its resolved sink privately; read it to prove which
518
+ // logger the provider handed to the constructor.
519
+ const serviceLogger = (service: StreamChatService) =>
520
+ (service as unknown as { logger: Logger }).logger
521
+
522
+ const renderWithLogger = async (
523
+ props: Partial<React.ComponentProps<typeof MessagingProvider>> & {
524
+ serviceConfig: Omit<StreamChatServiceConfig, 'apiKey'>
525
+ }
526
+ ) => {
527
+ setupServiceMock()
528
+ const services: (StreamChatService | null)[] = []
529
+ const loggers: Logger[] = []
530
+
531
+ render(
532
+ <MessagingProvider
533
+ apiKey="mock-api-key"
534
+ user={
535
+ { type: 'guest', id: 'guest-1', name: 'Guest' } as MessagingUser
536
+ }
537
+ {...props}
538
+ >
539
+ <ServiceProbe onService={(s) => services.push(s)} />
540
+ <LoggerProbe onLogger={(l) => loggers.push(l)} />
541
+ </MessagingProvider>
542
+ )
543
+
544
+ await waitFor(() => expect(services.filter(Boolean).length).toBe(1))
545
+ return {
546
+ service: services.filter(Boolean).pop() as StreamChatService,
547
+ logger: loggers[loggers.length - 1],
548
+ }
549
+ }
550
+
551
+ const baseServiceConfig: Omit<StreamChatServiceConfig, 'apiKey'> = {
552
+ fetchToken: async () => 'token',
553
+ createChannel: async () => ({ channelId: 'ch-1' }),
554
+ }
555
+
556
+ it('forwards the logger prop into the service and to the subtree', async () => {
557
+ const sink: Logger = { error: vi.fn() }
558
+ const { service, logger } = await renderWithLogger({
559
+ serviceConfig: baseServiceConfig,
560
+ logger: sink,
561
+ })
562
+
563
+ expect(serviceLogger(service)).toBe(sink)
564
+
565
+ const error = new Error('boom')
566
+ logger.error?.('[MessagingShell] Failed:', error)
567
+ expect(sink.error).toHaveBeenCalledWith('[MessagingShell] Failed:', error)
568
+ })
569
+
570
+ it('lets an explicit serviceConfig.logger win over the logger prop', async () => {
571
+ const configSink: Logger = { error: vi.fn() }
572
+ const propSink: Logger = { error: vi.fn() }
573
+ const { service } = await renderWithLogger({
574
+ serviceConfig: { ...baseServiceConfig, logger: configSink },
575
+ logger: propSink,
576
+ })
577
+
578
+ expect(serviceLogger(service)).toBe(configSink)
579
+ })
580
+
581
+ it('leaves the service on its console defaults when no logger is given', async () => {
582
+ const consoleError = vi
583
+ .spyOn(console, 'error')
584
+ .mockImplementation(() => {})
585
+ const { service, logger } = await renderWithLogger({
586
+ serviceConfig: baseServiceConfig,
587
+ })
588
+
589
+ // Not the react seam object: the service fell back to its own defaults.
590
+ expect(serviceLogger(service)).not.toBe(logger)
591
+
592
+ const error = new Error('boom')
593
+ logger.error?.('[MessagingShell] Failed:', error)
594
+ serviceLogger(service).error?.('[StreamChatService] Failed:', error)
595
+ expect(consoleError.mock.calls).toEqual([
596
+ ['[MessagingShell] Failed:', error],
597
+ ['[StreamChatService] Failed:', error],
598
+ ])
599
+ })
600
+ })
489
601
  })
@@ -13,6 +13,7 @@ import React, {
13
13
  import { StreamChat } from 'stream-chat'
14
14
  import { Chat } from 'stream-chat-react'
15
15
 
16
+ import { MessagingLoggerProvider } from '../logging'
16
17
  import type { MessagingProviderProps, MessagingCapabilities } from '../types'
17
18
 
18
19
  /**
@@ -56,6 +57,7 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
56
57
  capabilities = {},
57
58
  debug = false,
58
59
  client: injectedClient,
60
+ logger,
59
61
  }) => {
60
62
  // Create debug logger that respects the debug prop
61
63
  const debugLog = useCallback(
@@ -94,6 +96,8 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
94
96
  serviceConfigRef.current = serviceConfig
95
97
  const debugRef = useRef(debug)
96
98
  debugRef.current = debug
99
+ const loggerRef = useRef(logger)
100
+ loggerRef.current = logger
97
101
 
98
102
  // Track renders and prop changes
99
103
  const prevPropsRef = useRef({
@@ -199,6 +203,10 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
199
203
  apiKey,
200
204
  debug: debugRef.current,
201
205
  client: chatClient,
206
+ // An explicit serviceConfig.logger wins — existing injectors keep working.
207
+ ...(currentConfig.logger || !loggerRef.current
208
+ ? {}
209
+ : { logger: loggerRef.current }),
202
210
  })
203
211
 
204
212
  serviceRef.current = newService
@@ -404,20 +412,22 @@ export const MessagingProvider: React.FC<MessagingProviderProps> = ({
404
412
  })
405
413
 
406
414
  return (
407
- <MessagingContext.Provider value={contextValue}>
408
- {chatClient ? (
409
- <Chat
410
- client={chatClient}
411
- customClasses={{
412
- channelList:
413
- 'str-chat__channel-list str-chat__channel-list-react bg-transparent lg:border-r-2 border-r-0 border-[#0000000A]',
414
- }}
415
- >
416
- {children}
417
- </Chat>
418
- ) : (
419
- children
420
- )}
421
- </MessagingContext.Provider>
415
+ <MessagingLoggerProvider logger={logger}>
416
+ <MessagingContext.Provider value={contextValue}>
417
+ {chatClient ? (
418
+ <Chat
419
+ client={chatClient}
420
+ customClasses={{
421
+ channelList:
422
+ 'str-chat__channel-list str-chat__channel-list-react bg-transparent lg:border-r-2 border-r-0 border-[#0000000A]',
423
+ }}
424
+ >
425
+ {children}
426
+ </Chat>
427
+ ) : (
428
+ children
429
+ )}
430
+ </MessagingContext.Provider>
431
+ </MessagingLoggerProvider>
422
432
  )
423
433
  }
package/src/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ Logger,
2
3
  MessagingUser,
3
4
  StreamChatServiceConfig,
4
5
  } from '@linktr.ee/messaging-core'
@@ -501,6 +502,13 @@ interface MessagingProviderBaseProps {
501
502
  user: MessagingUser | null
502
503
  capabilities?: MessagingCapabilities
503
504
  debug?: boolean
505
+ /**
506
+ * Optional sink for the package's own log output. Levels left undefined fall
507
+ * back to console, which is also the default when the prop is omitted. In
508
+ * apiKey mode it is forwarded to the underlying StreamChatService unless
509
+ * `serviceConfig.logger` is set, which wins.
510
+ */
511
+ logger?: Logger
504
512
  }
505
513
 
506
514
  /**