@planningcenter/chat-react-native 3.37.0-rc.1 → 3.37.0-rc.3

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.
Files changed (38) hide show
  1. package/build/components/conversation/message.d.ts.map +1 -1
  2. package/build/components/conversation/message.js +3 -3
  3. package/build/components/conversation/message.js.map +1 -1
  4. package/build/components/index.d.ts +2 -0
  5. package/build/components/index.d.ts.map +1 -1
  6. package/build/components/index.js +2 -0
  7. package/build/components/index.js.map +1 -1
  8. package/build/components/page/component_error_boundary.d.ts +4 -0
  9. package/build/components/page/component_error_boundary.d.ts.map +1 -0
  10. package/build/components/page/component_error_boundary.js +8 -0
  11. package/build/components/page/component_error_boundary.js.map +1 -0
  12. package/build/components/page/error_boundary.d.ts +13 -10
  13. package/build/components/page/error_boundary.d.ts.map +1 -1
  14. package/build/components/page/error_boundary.js +20 -90
  15. package/build/components/page/error_boundary.js.map +1 -1
  16. package/build/components/page/page_error_boundary.d.ts +4 -0
  17. package/build/components/page/page_error_boundary.d.ts.map +1 -0
  18. package/build/components/page/page_error_boundary.js +80 -0
  19. package/build/components/page/page_error_boundary.js.map +1 -0
  20. package/build/navigation/screenLayout.d.ts.map +1 -1
  21. package/build/navigation/screenLayout.js +5 -3
  22. package/build/navigation/screenLayout.js.map +1 -1
  23. package/build/utils/native_adapters/log.d.ts +11 -0
  24. package/build/utils/native_adapters/log.d.ts.map +1 -1
  25. package/build/utils/native_adapters/log.js +9 -0
  26. package/build/utils/native_adapters/log.js.map +1 -1
  27. package/package.json +2 -2
  28. package/src/components/conversation/message.tsx +6 -4
  29. package/src/components/index.tsx +2 -0
  30. package/src/components/page/__tests__/component_error_boundary.test.tsx +46 -0
  31. package/src/components/page/__tests__/error_boundary.test.tsx +93 -0
  32. package/src/components/page/__tests__/page_error_boundary.test.tsx +77 -0
  33. package/src/components/page/component_error_boundary.tsx +13 -0
  34. package/src/components/page/error_boundary.tsx +34 -118
  35. package/src/components/page/page_error_boundary.tsx +112 -0
  36. package/src/navigation/screenLayout.tsx +6 -3
  37. package/src/utils/native_adapters/__tests__/log.test.ts +62 -0
  38. package/src/utils/native_adapters/log.ts +22 -0
@@ -2,10 +2,12 @@ export class LogAdapter {
2
2
  enabled;
3
3
  onError;
4
4
  onInfo;
5
+ onReportError;
5
6
  constructor(config = {}) {
6
7
  this.enabled = config.enabled ?? false;
7
8
  this.onError = config.error;
8
9
  this.onInfo = config.info;
10
+ this.onReportError = config.reportError;
9
11
  }
10
12
  error(message, ...args) {
11
13
  if (!this.enabled)
@@ -17,5 +19,12 @@ export class LogAdapter {
17
19
  return;
18
20
  this.onInfo?.(message, ...args);
19
21
  }
22
+ reportError(error, context) {
23
+ if (this.onReportError) {
24
+ this.onReportError(error, context);
25
+ return;
26
+ }
27
+ console.error(error, context);
28
+ }
20
29
  }
21
30
  //# sourceMappingURL=log.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"log.js","sourceRoot":"","sources":["../../../src/utils/native_adapters/log.ts"],"names":[],"mappings":"AAMA,MAAM,OAAO,UAAU;IACrB,OAAO,CAAS;IAChB,OAAO,CAA4B;IACnC,MAAM,CAA2B;IAEjC,YAAY,SAAoC,EAAE;QAChD,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,KAAK,CAAA;QACtC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,KAAK,CAAA;QAC3B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,IAAI,CAAA;IAC3B,CAAC;IAED,KAAK,CAAC,OAAe,EAAE,GAAG,IAAW;QACnC,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAM;QAEzB,IAAI,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,CAAA;IAClC,CAAC;IAED,IAAI,CAAC,OAAe,EAAE,GAAG,IAAW;QAClC,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAM;QAEzB,IAAI,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,CAAA;IACjC,CAAC;CACF","sourcesContent":["interface LogAdapterConfig {\n enabled: boolean\n error: (message: string, ...args: any[]) => void\n info: (message: string, ...args: any[]) => void\n}\n\nexport class LogAdapter {\n enabled: boolean\n onError?: LogAdapterConfig['error']\n onInfo?: LogAdapterConfig['info']\n\n constructor(config: Partial<LogAdapterConfig> = {}) {\n this.enabled = config.enabled ?? false\n this.onError = config.error\n this.onInfo = config.info\n }\n\n error(message: string, ...args: any[]) {\n if (!this.enabled) return\n\n this.onError?.(message, ...args)\n }\n\n info(message: string, ...args: any[]) {\n if (!this.enabled) return\n\n this.onInfo?.(message, ...args)\n }\n}\n"]}
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../../../src/utils/native_adapters/log.ts"],"names":[],"mappings":"AAiBA,MAAM,OAAO,UAAU;IACrB,OAAO,CAAS;IAChB,OAAO,CAA4B;IACnC,MAAM,CAA2B;IACjC,aAAa,CAAkC;IAE/C,YAAY,SAAoC,EAAE;QAChD,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,KAAK,CAAA;QACtC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,KAAK,CAAA;QAC3B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,IAAI,CAAA;QACzB,IAAI,CAAC,aAAa,GAAG,MAAM,CAAC,WAAW,CAAA;IACzC,CAAC;IAED,KAAK,CAAC,OAAe,EAAE,GAAG,IAAW;QACnC,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAM;QAEzB,IAAI,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,CAAA;IAClC,CAAC;IAED,IAAI,CAAC,OAAe,EAAE,GAAG,IAAW;QAClC,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE,OAAM;QAEzB,IAAI,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,CAAA;IACjC,CAAC;IAED,WAAW,CAAC,KAAY,EAAE,OAA4B;QACpD,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACvB,IAAI,CAAC,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;YAClC,OAAM;QACR,CAAC;QAED,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;IAC/B,CAAC;CACF","sourcesContent":["export type ReportErrorScope = 'screen' | 'component' | 'http'\n\nexport type ReportErrorContext = {\n componentStack?: string\n scope?: ReportErrorScope\n screenName?: string\n tags?: Record<string, string>\n extra?: Record<string, unknown>\n}\n\ninterface LogAdapterConfig {\n enabled: boolean\n error: (message: string, ...args: any[]) => void\n info: (message: string, ...args: any[]) => void\n reportError: (error: Error, context?: ReportErrorContext) => void\n}\n\nexport class LogAdapter {\n enabled: boolean\n onError?: LogAdapterConfig['error']\n onInfo?: LogAdapterConfig['info']\n onReportError?: LogAdapterConfig['reportError']\n\n constructor(config: Partial<LogAdapterConfig> = {}) {\n this.enabled = config.enabled ?? false\n this.onError = config.error\n this.onInfo = config.info\n this.onReportError = config.reportError\n }\n\n error(message: string, ...args: any[]) {\n if (!this.enabled) return\n\n this.onError?.(message, ...args)\n }\n\n info(message: string, ...args: any[]) {\n if (!this.enabled) return\n\n this.onInfo?.(message, ...args)\n }\n\n reportError(error: Error, context?: ReportErrorContext) {\n if (this.onReportError) {\n this.onReportError(error, context)\n return\n }\n\n console.error(error, context)\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/chat-react-native",
3
- "version": "3.37.0-rc.1",
3
+ "version": "3.37.0-rc.3",
4
4
  "description": "",
5
5
  "main": "build/index.js",
6
6
  "react-native": "./src/index.tsx",
@@ -72,5 +72,5 @@
72
72
  "react-native-url-polyfill": "^2.0.0",
73
73
  "typescript": "~5.9.2"
74
74
  },
75
- "gitHead": "7c6773155a5b589fe9a58a28921ddaac1d134e86"
75
+ "gitHead": "0dbe10dae358e399f7fc130a434045c9aa6931e1"
76
76
  }
@@ -31,7 +31,7 @@ import {
31
31
  MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH,
32
32
  platformFontWeightMedium,
33
33
  } from '../../utils/styles'
34
- import ErrorBoundary from '../page/error_boundary'
34
+ import { ComponentErrorBoundary } from '../page/component_error_boundary'
35
35
  import { MessageAttachments } from './message_attachments'
36
36
  import { MessageMarkdown } from './message_markdown'
37
37
  import { MessageReadReceipts } from './message_read_receipts'
@@ -103,7 +103,9 @@ export function Message({
103
103
  const messageIsReplyLabel = replyToReplyRootMessage ? 'Reply' : ''
104
104
  const attachmentLabel = some(attachments) ? pluralize(attachments.length, 'attachment') : ''
105
105
  const replyCountLabel = isReplyRootMessage ? replyCountText : ''
106
- const accessibilityLabel = `${author?.name || ''} ${messageIsReplyLabel} ${attachmentLabel} ${messageText || ''} ${timestamp} ${replyCountLabel}`
106
+ const accessibilityLabel = `${author?.name || ''} ${messageIsReplyLabel} ${attachmentLabel} ${
107
+ messageText || ''
108
+ } ${timestamp} ${replyCountLabel}`
107
109
 
108
110
  useEffect(() => {
109
111
  if (pending) {
@@ -237,14 +239,14 @@ export function Message({
237
239
  </View>
238
240
  ) : (
239
241
  <>
240
- <ErrorBoundary>
242
+ <ComponentErrorBoundary>
241
243
  <MessageAttachments
242
244
  attachments={attachments}
243
245
  metaProps={metaProps}
244
246
  onMessageAttachmentLongPress={handleMessageAttachmentLongPress}
245
247
  onMessageLongPress={handleMessageLongPress}
246
248
  />
247
- </ErrorBoundary>
249
+ </ComponentErrorBoundary>
248
250
  {text && (
249
251
  <View style={styles.messageText}>
250
252
  <MessageMarkdown text={text} />
@@ -1,4 +1,6 @@
1
1
  export * from './conversations/conversations'
2
+ export * from './page/component_error_boundary'
2
3
  export * from './page/error_boundary'
4
+ export * from './page/page_error_boundary'
3
5
  export * from './display'
4
6
  export * from './group_conversation_list'
@@ -0,0 +1,46 @@
1
+ import { render } from '@testing-library/react-native'
2
+ import React from 'react'
3
+ import { Text } from 'react-native'
4
+ import { Log } from '../../../utils/native_adapters/configuration'
5
+ import { ComponentErrorBoundary } from '../component_error_boundary'
6
+
7
+ function Boom({ thrown }: { thrown: unknown }) {
8
+ throw thrown
9
+ }
10
+
11
+ describe('ComponentErrorBoundary', () => {
12
+ let reportError: jest.SpyInstance
13
+ let consoleError: jest.SpyInstance
14
+
15
+ beforeEach(() => {
16
+ reportError = jest.spyOn(Log, 'reportError').mockImplementation(() => {})
17
+ consoleError = jest.spyOn(console, 'error').mockImplementation(() => {})
18
+ })
19
+
20
+ afterEach(() => {
21
+ reportError.mockRestore()
22
+ consoleError.mockRestore()
23
+ })
24
+
25
+ it('defaults the reporter scope to "component" and renders nothing on catch', () => {
26
+ const { toJSON } = render(
27
+ <ComponentErrorBoundary>
28
+ <Boom thrown={new Error('attachment boom')} />
29
+ </ComponentErrorBoundary>
30
+ )
31
+
32
+ expect(toJSON()).toBeNull()
33
+ expect(reportError.mock.calls[0][1]).toMatchObject({ scope: 'component' })
34
+ })
35
+
36
+ it('lets callers override the defaults', () => {
37
+ const { getByText } = render(
38
+ <ComponentErrorBoundary scope="screen" fallback={<Text>overridden</Text>}>
39
+ <Boom thrown={new Error('boom')} />
40
+ </ComponentErrorBoundary>
41
+ )
42
+
43
+ expect(getByText('overridden')).toBeTruthy()
44
+ expect(reportError.mock.calls[0][1]).toMatchObject({ scope: 'screen' })
45
+ })
46
+ })
@@ -0,0 +1,93 @@
1
+ import { render } from '@testing-library/react-native'
2
+ import React from 'react'
3
+ import { Text } from 'react-native'
4
+ import { FailedResponse } from '../../../types/api_primitives'
5
+ import { Log } from '../../../utils/native_adapters/configuration'
6
+ import { ResponseError } from '../../../utils/response_error'
7
+ import { ErrorBoundary } from '../error_boundary'
8
+
9
+ function Boom({ thrown }: { thrown: unknown }) {
10
+ throw thrown
11
+ }
12
+
13
+ describe('ErrorBoundary', () => {
14
+ let reportError: jest.SpyInstance
15
+ let consoleError: jest.SpyInstance
16
+
17
+ beforeEach(() => {
18
+ reportError = jest.spyOn(Log, 'reportError').mockImplementation(() => {})
19
+ consoleError = jest.spyOn(console, 'error').mockImplementation(() => {})
20
+ })
21
+
22
+ afterEach(() => {
23
+ reportError.mockRestore()
24
+ consoleError.mockRestore()
25
+ })
26
+
27
+ it('reports caught errors with the component stack and chat tags', () => {
28
+ const error = new TypeError('Invalid time value')
29
+
30
+ render(
31
+ <ErrorBoundary scope="screen" screenName="ConversationScreen" fallback={null}>
32
+ <Boom thrown={error} />
33
+ </ErrorBoundary>
34
+ )
35
+
36
+ expect(reportError).toHaveBeenCalledTimes(1)
37
+ const [reportedError, context] = reportError.mock.calls[0]
38
+ expect(reportedError).toBe(error)
39
+ expect(context).toMatchObject({
40
+ scope: 'screen',
41
+ screenName: 'ConversationScreen',
42
+ tags: { team: 'chat', package: 'chat-react-native' },
43
+ })
44
+ expect(typeof context.componentStack).toBe('string')
45
+ expect(context.componentStack.length).toBeGreaterThan(0)
46
+ })
47
+
48
+ it('does not report ResponseError — those are owned by the HTTP layer', () => {
49
+ const failure = { status: 500, statusText: 'Server Error', errors: [] } as FailedResponse
50
+
51
+ render(
52
+ <ErrorBoundary fallback={null}>
53
+ <Boom thrown={new ResponseError(failure)} />
54
+ </ErrorBoundary>
55
+ )
56
+
57
+ expect(reportError).not.toHaveBeenCalled()
58
+ })
59
+
60
+ it('renders the provided fallback element when a child throws', () => {
61
+ const { getByText } = render(
62
+ <ErrorBoundary fallback={<Text>custom fallback</Text>}>
63
+ <Boom thrown={new Error('boom')} />
64
+ </ErrorBoundary>
65
+ )
66
+
67
+ expect(getByText('custom fallback')).toBeTruthy()
68
+ })
69
+
70
+ it('renders nothing when no fallback is provided', () => {
71
+ const { toJSON } = render(
72
+ <ErrorBoundary>
73
+ <Boom thrown={new Error('boom')} />
74
+ </ErrorBoundary>
75
+ )
76
+
77
+ expect(toJSON()).toBeNull()
78
+ })
79
+
80
+ it('calls a fallback render function with the caught error and a reset handler', () => {
81
+ const fallback = jest.fn().mockReturnValue(<Text>fn fallback</Text>)
82
+ const error = new Error('boom')
83
+
84
+ const { getByText } = render(
85
+ <ErrorBoundary fallback={fallback}>
86
+ <Boom thrown={error} />
87
+ </ErrorBoundary>
88
+ )
89
+
90
+ expect(getByText('fn fallback')).toBeTruthy()
91
+ expect(fallback).toHaveBeenCalledWith(error, expect.any(Function))
92
+ })
93
+ })
@@ -0,0 +1,77 @@
1
+ import { NavigationContainer } from '@react-navigation/native'
2
+ import { QueryClientProvider } from '@tanstack/react-query'
3
+ import { render } from '@testing-library/react-native'
4
+ import React from 'react'
5
+ import { buildTestQueryClient } from '../../../__utils__/query_client'
6
+ import { Log } from '../../../utils/native_adapters/configuration'
7
+ import { PageErrorBoundary } from '../page_error_boundary'
8
+
9
+ jest.mock('../../primitive/blank_state_primitive', () => {
10
+ const { Text } = require('react-native')
11
+ const { createElement } = require('react')
12
+ const passthrough = ({ children }: { children?: unknown }) => children
13
+ const asText = ({ children }: { children?: unknown }) => createElement(Text, null, children)
14
+ const empty = () => null
15
+ return {
16
+ __esModule: true,
17
+ default: {
18
+ Root: passthrough,
19
+ Imagery: empty,
20
+ Content: passthrough,
21
+ Heading: asText,
22
+ Text: asText,
23
+ Button: empty,
24
+ TextButton: asText,
25
+ },
26
+ }
27
+ })
28
+
29
+ function Boom({ thrown }: { thrown: unknown }) {
30
+ throw thrown
31
+ }
32
+
33
+ const renderWithProviders = (ui: React.ReactElement) =>
34
+ render(
35
+ <QueryClientProvider client={buildTestQueryClient()}>
36
+ <NavigationContainer>{ui}</NavigationContainer>
37
+ </QueryClientProvider>
38
+ )
39
+
40
+ describe('PageErrorBoundary', () => {
41
+ let reportError: jest.SpyInstance
42
+ let consoleError: jest.SpyInstance
43
+
44
+ beforeEach(() => {
45
+ reportError = jest.spyOn(Log, 'reportError').mockImplementation(() => {})
46
+ consoleError = jest.spyOn(console, 'error').mockImplementation(() => {})
47
+ })
48
+
49
+ afterEach(() => {
50
+ reportError.mockRestore()
51
+ consoleError.mockRestore()
52
+ })
53
+
54
+ it('defaults the reporter scope to "screen"', () => {
55
+ renderWithProviders(
56
+ <PageErrorBoundary screenName="ConversationScreen">
57
+ <Boom thrown={new Error('boom')} />
58
+ </PageErrorBoundary>
59
+ )
60
+
61
+ expect(reportError.mock.calls[0][1]).toMatchObject({
62
+ scope: 'screen',
63
+ screenName: 'ConversationScreen',
64
+ })
65
+ })
66
+
67
+ it('renders the full Oops fallback when no fallback is provided', () => {
68
+ const { getByText } = renderWithProviders(
69
+ <PageErrorBoundary>
70
+ <Boom thrown={new Error('boom')} />
71
+ </PageErrorBoundary>
72
+ )
73
+
74
+ expect(getByText('Oops!')).toBeTruthy()
75
+ expect(getByText('Something unexpected happened.')).toBeTruthy()
76
+ })
77
+ })
@@ -0,0 +1,13 @@
1
+ import React, { PropsWithChildren } from 'react'
2
+ import { ErrorBoundary, ErrorBoundaryProps } from './error_boundary'
3
+
4
+ export function ComponentErrorBoundary({
5
+ children,
6
+ ...props
7
+ }: PropsWithChildren<ErrorBoundaryProps>) {
8
+ return (
9
+ <ErrorBoundary scope="component" fallback={null} {...props}>
10
+ {children}
11
+ </ErrorBoundary>
12
+ )
13
+ }
@@ -1,137 +1,53 @@
1
- import { useNavigation } from '@react-navigation/native'
2
- import { useQueryErrorResetBoundary } from '@tanstack/react-query'
3
- import React, { PropsWithChildren, useCallback, useEffect, useMemo } from 'react'
4
- import { onAuthRefresh } from '../../utils/auth_events'
1
+ import React, { PropsWithChildren } from 'react'
2
+ import { Log } from '../../utils/native_adapters/configuration'
3
+ import { ReportErrorScope } from '../../utils/native_adapters/log'
5
4
  import { ResponseError } from '../../utils/response_error'
6
- import BlankState from '../primitive/blank_state_primitive'
5
+
6
+ export type ErrorBoundaryFallback =
7
+ | React.ReactNode
8
+ | ((error: Error, reset: () => void) => React.ReactNode)
9
+
10
+ export type ErrorBoundaryProps = {
11
+ scope?: ReportErrorScope
12
+ screenName?: string
13
+ fallback?: ErrorBoundaryFallback
14
+ }
7
15
 
8
16
  type ErrorBoundaryState = {
9
- error: ResponseError | Error | TypeError | null
10
- unsubscriber: () => void
17
+ error: Error | null
11
18
  }
12
19
 
13
- class ErrorBoundary extends React.Component<PropsWithChildren<{ onReset?: () => void }>> {
14
- state: ErrorBoundaryState = {
15
- error: null,
16
- unsubscriber: () => {},
17
- }
20
+ export class ErrorBoundary extends React.Component<
21
+ PropsWithChildren<ErrorBoundaryProps>,
22
+ ErrorBoundaryState
23
+ > {
24
+ state: ErrorBoundaryState = { error: null }
18
25
 
19
- componentDidCatch(error: any) {
20
- this.handleError(error)
26
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
27
+ return { error }
21
28
  }
22
29
 
23
- handleError(error: any) {
24
- this.setState({ error })
30
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
31
+ if (error instanceof ResponseError) return
32
+
33
+ Log.reportError(error, {
34
+ componentStack: errorInfo.componentStack ?? undefined,
35
+ scope: this.props.scope,
36
+ screenName: this.props.screenName,
37
+ tags: { team: 'chat', package: 'chat-react-native' },
38
+ })
25
39
  }
26
40
 
27
41
  handleReset = () => {
28
- this.props.onReset?.()
29
42
  this.setState({ error: null })
30
43
  }
31
44
 
32
45
  render() {
33
- if (this.state.error) {
34
- return <ErrorView error={this.state.error} onReset={this.handleReset} />
35
- } else {
36
- return this.props.children
37
- }
38
- }
39
- }
46
+ if (!this.state.error) return this.props.children
40
47
 
41
- function ErrorView({ error, onReset }: { error: Error | ResponseError; onReset: () => void }) {
42
- const { reset } = useQueryErrorResetBoundary()
48
+ const { fallback } = this.props
49
+ if (fallback === undefined) return null
43
50
 
44
- const handleReset = useCallback(() => {
45
- reset()
46
- onReset()
47
- }, [reset, onReset])
48
-
49
- useEffect(() => handleReset, [handleReset])
50
-
51
- if (error instanceof ResponseError) {
52
- return <ResponseErrorView response={error.response} onReset={handleReset} />
53
- }
54
-
55
- if (isNetworkError(error)) {
56
- return (
57
- <ErrorContent
58
- heading={'Problem connecting!'}
59
- body={'Check your internet connection and try again.'}
60
- />
61
- )
51
+ return typeof fallback === 'function' ? fallback(this.state.error, this.handleReset) : fallback
62
52
  }
63
-
64
- return <ErrorContent heading={'Oops!'} body={'Something unexpected happened.'} />
65
53
  }
66
-
67
- function isNetworkError(error: ResponseError | Error | TypeError | null) {
68
- const isError = error instanceof Error
69
- const networkFailedMessages = [
70
- 'Network request failed',
71
- 'Network request timed out',
72
- 'Failed to fetch',
73
- ]
74
-
75
- if (!isError) return false
76
-
77
- return new RegExp(networkFailedMessages.join('|'), 'i').test(error.message)
78
- }
79
-
80
- function ResponseErrorView({ response, onReset }: { response: Response; onReset: () => void }) {
81
- const { status } = response
82
-
83
- const heading = useMemo(() => {
84
- switch (status) {
85
- case 403:
86
- return 'Permission required'
87
- case 404:
88
- return 'Content not found'
89
- default:
90
- return 'Oops!'
91
- }
92
- }, [status])
93
-
94
- const body = useMemo(() => {
95
- switch (status) {
96
- case 403:
97
- return 'Contact your administrator for access.'
98
- case 404:
99
- return 'If you believe something should be here, please reach out to your administrator.'
100
- default:
101
- return 'Something unexpected happened.'
102
- }
103
- }, [status])
104
-
105
- useEffect(() => {
106
- if (status !== 401) return
107
-
108
- return onAuthRefresh(onReset)
109
- }, [status, onReset])
110
-
111
- return <ErrorContent heading={heading} body={body} />
112
- }
113
-
114
- function ErrorContent({ heading, body }: { heading: string; body: string }) {
115
- const navigation = useNavigation()
116
-
117
- return (
118
- <BlankState.Root>
119
- <BlankState.Imagery name="people.noTextMessage" />
120
- <BlankState.Content>
121
- <BlankState.Heading>{heading}</BlankState.Heading>
122
- <BlankState.Text>{body}</BlankState.Text>
123
- </BlankState.Content>
124
- <BlankState.Button
125
- title="Go back"
126
- onPress={navigation.goBack}
127
- size="md"
128
- accessibilityRole="link"
129
- />
130
- <BlankState.TextButton onPress={() => navigation.navigate('BugReport')}>
131
- Report a bug
132
- </BlankState.TextButton>
133
- </BlankState.Root>
134
- )
135
- }
136
-
137
- export default ErrorBoundary
@@ -0,0 +1,112 @@
1
+ import { useNavigation } from '@react-navigation/native'
2
+ import { useQueryErrorResetBoundary } from '@tanstack/react-query'
3
+ import React, { PropsWithChildren, useCallback, useEffect, useMemo } from 'react'
4
+ import { onAuthRefresh } from '../../utils/auth_events'
5
+ import { ResponseError } from '../../utils/response_error'
6
+ import BlankState from '../primitive/blank_state_primitive'
7
+ import { ErrorBoundary, ErrorBoundaryFallback, ErrorBoundaryProps } from './error_boundary'
8
+
9
+ const renderPageFallback: ErrorBoundaryFallback = (error, reset) => (
10
+ <ErrorView error={error} reset={reset} />
11
+ )
12
+
13
+ export function PageErrorBoundary({ children, ...props }: PropsWithChildren<ErrorBoundaryProps>) {
14
+ return (
15
+ <ErrorBoundary scope="screen" fallback={renderPageFallback} {...props}>
16
+ {children}
17
+ </ErrorBoundary>
18
+ )
19
+ }
20
+
21
+ function ErrorView({ error, reset }: { error: Error; reset: () => void }) {
22
+ const { reset: resetQueries } = useQueryErrorResetBoundary()
23
+
24
+ const handleReset = useCallback(() => {
25
+ resetQueries()
26
+ reset()
27
+ }, [resetQueries, reset])
28
+
29
+ useEffect(() => handleReset, [handleReset])
30
+
31
+ if (error instanceof ResponseError) {
32
+ return <ResponseErrorView response={error.response} onReset={handleReset} />
33
+ }
34
+
35
+ if (isNetworkError(error)) {
36
+ return (
37
+ <ErrorContent
38
+ heading={'Problem connecting!'}
39
+ body={'Check your internet connection and try again.'}
40
+ />
41
+ )
42
+ }
43
+
44
+ return <ErrorContent heading={'Oops!'} body={'Something unexpected happened.'} />
45
+ }
46
+
47
+ function isNetworkError(error: Error) {
48
+ const networkFailedMessages = [
49
+ 'Network request failed',
50
+ 'Network request timed out',
51
+ 'Failed to fetch',
52
+ ]
53
+
54
+ return new RegExp(networkFailedMessages.join('|'), 'i').test(error.message)
55
+ }
56
+
57
+ function ResponseErrorView({ response, onReset }: { response: Response; onReset: () => void }) {
58
+ const { status } = response
59
+
60
+ const heading = useMemo(() => {
61
+ switch (status) {
62
+ case 403:
63
+ return 'Permission required'
64
+ case 404:
65
+ return 'Content not found'
66
+ default:
67
+ return 'Oops!'
68
+ }
69
+ }, [status])
70
+
71
+ const body = useMemo(() => {
72
+ switch (status) {
73
+ case 403:
74
+ return 'Contact your administrator for access.'
75
+ case 404:
76
+ return 'If you believe something should be here, please reach out to your administrator.'
77
+ default:
78
+ return 'Something unexpected happened.'
79
+ }
80
+ }, [status])
81
+
82
+ useEffect(() => {
83
+ if (status !== 401) return
84
+
85
+ return onAuthRefresh(onReset)
86
+ }, [status, onReset])
87
+
88
+ return <ErrorContent heading={heading} body={body} />
89
+ }
90
+
91
+ function ErrorContent({ heading, body }: { heading: string; body: string }) {
92
+ const navigation = useNavigation()
93
+
94
+ return (
95
+ <BlankState.Root>
96
+ <BlankState.Imagery name="people.noTextMessage" />
97
+ <BlankState.Content>
98
+ <BlankState.Heading>{heading}</BlankState.Heading>
99
+ <BlankState.Text>{body}</BlankState.Text>
100
+ </BlankState.Content>
101
+ <BlankState.Button
102
+ title="Go back"
103
+ onPress={navigation.goBack}
104
+ size="md"
105
+ accessibilityRole="link"
106
+ />
107
+ <BlankState.TextButton onPress={() => navigation.navigate('BugReport')}>
108
+ Report a bug
109
+ </BlankState.TextButton>
110
+ </BlankState.Root>
111
+ )
112
+ }
@@ -1,14 +1,17 @@
1
+ import { useRoute } from '@react-navigation/native'
1
2
  import React from 'react'
2
3
  import { Suspense } from 'react'
3
- import ErrorBoundary from '../components/page/error_boundary'
4
4
  import { DefaultLoading } from '../components/page/loading'
5
+ import { PageErrorBoundary } from '../components/page/page_error_boundary'
5
6
  import { ChatAccessGate } from './chat_access_gate'
6
7
 
7
8
  export function ScreenLayout({ children }: { children: React.ReactElement }) {
9
+ const route = useRoute()
10
+
8
11
  return (
9
- <ErrorBoundary>
12
+ <PageErrorBoundary screenName={route.name}>
10
13
  <Suspense fallback={<DefaultLoading />}>{children}</Suspense>
11
- </ErrorBoundary>
14
+ </PageErrorBoundary>
12
15
  )
13
16
  }
14
17