@planningcenter/chat-react-native 3.36.1-rc.0 → 3.36.2-qa-726.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/components/conversation/message.d.ts.map +1 -1
- package/build/components/conversation/message.js +3 -3
- package/build/components/conversation/message.js.map +1 -1
- package/build/components/display/action_button.d.ts +3 -1
- package/build/components/display/action_button.d.ts.map +1 -1
- package/build/components/display/action_button.js +8 -1
- package/build/components/display/action_button.js.map +1 -1
- package/build/components/display/index.d.ts +1 -0
- package/build/components/display/index.d.ts.map +1 -1
- package/build/components/display/index.js +1 -0
- package/build/components/display/index.js.map +1 -1
- package/build/components/index.d.ts +2 -0
- package/build/components/index.d.ts.map +1 -1
- package/build/components/index.js +2 -0
- package/build/components/index.js.map +1 -1
- package/build/components/page/component_error_boundary.d.ts +4 -0
- package/build/components/page/component_error_boundary.d.ts.map +1 -0
- package/build/components/page/component_error_boundary.js +8 -0
- package/build/components/page/component_error_boundary.js.map +1 -0
- package/build/components/page/error_boundary.d.ts +13 -10
- package/build/components/page/error_boundary.d.ts.map +1 -1
- package/build/components/page/error_boundary.js +20 -90
- package/build/components/page/error_boundary.js.map +1 -1
- package/build/components/page/page_error_boundary.d.ts +4 -0
- package/build/components/page/page_error_boundary.d.ts.map +1 -0
- package/build/components/page/page_error_boundary.js +80 -0
- package/build/components/page/page_error_boundary.js.map +1 -0
- package/build/hooks/groups/use_group_chat_conversation_payload.d.ts +168 -0
- package/build/hooks/groups/use_group_chat_conversation_payload.d.ts.map +1 -0
- package/build/hooks/groups/use_group_chat_conversation_payload.js +23 -0
- package/build/hooks/groups/use_group_chat_conversation_payload.js.map +1 -0
- package/build/hooks/groups/use_group_members_for_new_conversation.d.ts +0 -4
- package/build/hooks/groups/use_group_members_for_new_conversation.d.ts.map +1 -1
- package/build/hooks/groups/use_group_members_for_new_conversation.js +6 -18
- package/build/hooks/groups/use_group_members_for_new_conversation.js.map +1 -1
- package/build/hooks/groups/use_groups_conversation_create.js +1 -1
- package/build/hooks/groups/use_groups_conversation_create.js.map +1 -1
- package/build/hooks/index.d.ts +2 -1
- package/build/hooks/index.d.ts.map +1 -1
- package/build/hooks/index.js +2 -1
- package/build/hooks/index.js.map +1 -1
- package/build/hooks/services/use_find_or_create_services_conversation.d.ts +11 -3
- package/build/hooks/services/use_find_or_create_services_conversation.d.ts.map +1 -1
- package/build/hooks/services/use_find_or_create_services_conversation.js +10 -14
- package/build/hooks/services/use_find_or_create_services_conversation.js.map +1 -1
- package/build/hooks/services/use_services_chat_conversation_payload.d.ts +164 -0
- package/build/hooks/services/use_services_chat_conversation_payload.d.ts.map +1 -0
- package/build/hooks/services/use_services_chat_conversation_payload.js +16 -0
- package/build/hooks/services/use_services_chat_conversation_payload.js.map +1 -0
- package/build/hooks/services/use_team_members_for_new_conversation.d.ts.map +1 -1
- package/build/hooks/services/use_team_members_for_new_conversation.js +11 -4
- package/build/hooks/services/use_team_members_for_new_conversation.js.map +1 -1
- package/build/hooks/use_conversation_validate.d.ts +12 -0
- package/build/hooks/use_conversation_validate.d.ts.map +1 -0
- package/build/hooks/use_conversation_validate.js +28 -0
- package/build/hooks/use_conversation_validate.js.map +1 -0
- package/build/hooks/use_enrich_people.d.ts +13 -0
- package/build/hooks/use_enrich_people.d.ts.map +1 -0
- package/build/hooks/use_enrich_people.js +25 -0
- package/build/hooks/use_enrich_people.js.map +1 -0
- package/build/hooks/use_features.d.ts +9 -6
- package/build/hooks/use_features.d.ts.map +1 -1
- package/build/hooks/use_features.js +1 -0
- package/build/hooks/use_features.js.map +1 -1
- package/build/hooks/use_jolt.d.ts +2 -1
- package/build/hooks/use_jolt.d.ts.map +1 -1
- package/build/hooks/use_jolt.js.map +1 -1
- package/build/hooks/use_product_analytics.d.ts +7 -1
- package/build/hooks/use_product_analytics.d.ts.map +1 -1
- package/build/hooks/use_product_analytics.js +4 -0
- package/build/hooks/use_product_analytics.js.map +1 -1
- package/build/index.d.ts +3 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -1
- package/build/index.js.map +1 -1
- package/build/navigation/screenLayout.d.ts.map +1 -1
- package/build/navigation/screenLayout.js +5 -3
- package/build/navigation/screenLayout.js.map +1 -1
- package/build/screens/conversation_details_screen.js +1 -1
- package/build/screens/conversation_details_screen.js.map +1 -1
- package/build/screens/conversation_new/components/groups_form.d.ts.map +1 -1
- package/build/screens/conversation_new/components/groups_form.js +14 -1
- package/build/screens/conversation_new/components/groups_form.js.map +1 -1
- package/build/screens/conversation_new/components/services_form.d.ts.map +1 -1
- package/build/screens/conversation_new/components/services_form.js +20 -2
- package/build/screens/conversation_new/components/services_form.js.map +1 -1
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +2 -2
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/screens/conversations/conversations_screen.js +2 -2
- package/build/screens/conversations/conversations_screen.js.map +1 -1
- package/build/screens/team_conversation_screen.d.ts.map +1 -1
- package/build/screens/team_conversation_screen.js +6 -3
- package/build/screens/team_conversation_screen.js.map +1 -1
- package/build/types/jolt_events/index.d.ts +2 -0
- package/build/types/jolt_events/index.d.ts.map +1 -1
- package/build/types/jolt_events/index.js.map +1 -1
- package/build/types/resources/conversation_validate.d.ts +10 -0
- package/build/types/resources/conversation_validate.d.ts.map +1 -0
- package/build/types/resources/conversation_validate.js +2 -0
- package/build/types/resources/conversation_validate.js.map +1 -0
- package/build/types/resources/index.d.ts +1 -0
- package/build/types/resources/index.d.ts.map +1 -1
- package/build/types/resources/index.js +1 -0
- package/build/types/resources/index.js.map +1 -1
- package/build/utils/cache/messages_cache.d.ts +1 -1
- package/build/utils/cache/messages_cache.d.ts.map +1 -1
- package/build/utils/client/instrumented_fetch.d.ts +2 -0
- package/build/utils/client/instrumented_fetch.d.ts.map +1 -0
- package/build/utils/client/instrumented_fetch.js +64 -0
- package/build/utils/client/instrumented_fetch.js.map +1 -0
- package/build/utils/client/request_helpers.d.ts.map +1 -1
- package/build/utils/client/request_helpers.js +2 -1
- package/build/utils/client/request_helpers.js.map +1 -1
- package/build/utils/native_adapters/log.d.ts +11 -0
- package/build/utils/native_adapters/log.d.ts.map +1 -1
- package/build/utils/native_adapters/log.js +9 -0
- package/build/utils/native_adapters/log.js.map +1 -1
- package/build/utils/performance_tracking.d.ts +1 -1
- package/build/utils/performance_tracking.d.ts.map +1 -1
- package/build/utils/performance_tracking.js.map +1 -1
- package/build/utils/request/get_chat_configuration.d.ts +1 -1
- package/build/utils/request/get_chat_configuration.d.ts.map +1 -1
- package/build/utils/request/get_features.d.ts +1 -1
- package/build/utils/request/get_features.d.ts.map +1 -1
- package/build/utils/request/get_message.d.ts +1 -1
- package/build/utils/request/get_message.d.ts.map +1 -1
- package/build/utils/request/get_messages.d.ts +1 -1
- package/build/utils/request/get_messages.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/__tests__/hooks/use_conversation_validate.test.tsx +117 -0
- package/src/__tests__/hooks/use_enrich_people.test.tsx +95 -0
- package/src/components/conversation/message.tsx +6 -4
- package/src/components/display/action_button.tsx +18 -0
- package/src/components/display/index.ts +1 -0
- package/src/components/index.tsx +2 -0
- package/src/components/page/__tests__/component_error_boundary.test.tsx +46 -0
- package/src/components/page/__tests__/error_boundary.test.tsx +93 -0
- package/src/components/page/__tests__/page_error_boundary.test.tsx +77 -0
- package/src/components/page/component_error_boundary.tsx +13 -0
- package/src/components/page/error_boundary.tsx +34 -118
- package/src/components/page/page_error_boundary.tsx +112 -0
- package/src/hooks/groups/use_group_chat_conversation_payload.ts +38 -0
- package/src/hooks/groups/use_group_members_for_new_conversation.ts +9 -23
- package/src/hooks/groups/use_groups_conversation_create.ts +1 -1
- package/src/hooks/index.ts +2 -1
- package/src/hooks/services/use_find_or_create_services_conversation.ts +27 -24
- package/src/hooks/services/use_services_chat_conversation_payload.ts +26 -0
- package/src/hooks/services/use_team_members_for_new_conversation.ts +18 -7
- package/src/hooks/use_conversation_validate.ts +45 -0
- package/src/hooks/use_enrich_people.ts +35 -0
- package/src/hooks/use_features.ts +5 -2
- package/src/hooks/use_jolt.ts +2 -1
- package/src/hooks/use_product_analytics.ts +13 -3
- package/src/index.tsx +3 -2
- package/src/navigation/screenLayout.tsx +6 -3
- package/src/screens/conversation_details_screen.tsx +1 -1
- package/src/screens/conversation_new/components/groups_form.tsx +17 -1
- package/src/screens/conversation_new/components/services_form.tsx +26 -2
- package/src/screens/conversation_screen.tsx +2 -1
- package/src/screens/conversations/conversations_screen.tsx +2 -2
- package/src/screens/team_conversation_screen.tsx +6 -6
- package/src/types/jolt_events/index.ts +3 -0
- package/src/types/resources/conversation_validate.ts +11 -0
- package/src/types/resources/index.ts +1 -0
- package/src/utils/client/__tests__/instrumented_fetch.test.ts +84 -0
- package/src/utils/client/instrumented_fetch.ts +69 -0
- package/src/utils/client/request_helpers.ts +2 -1
- package/src/utils/native_adapters/__tests__/log.test.ts +62 -0
- package/src/utils/native_adapters/log.ts +22 -0
- package/src/utils/performance_tracking.ts +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"performance_tracking.js","sourceRoot":"","sources":["../../src/utils/performance_tracking.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Performance tracking utilities for client-side metrics (React Native)\n *\n * This module provides functionality to track performance metrics and send them\n * to Datadog via the backend API for aggregation.\n */\n\nimport { Platform } from 'react-native'\nimport DeviceInfo from 'react-native-device-info'\nimport { ApiClient } from '../hooks'\nconst appName = DeviceInfo.getApplicationName()\n\ninterface PerformanceMetric {\n name: string\n value: number\n unit: 'milliseconds' | 'seconds' | 'count'\n}\n\ninterface PendingMessageMetric {\n idempotentKey: string\n startTime: number\n}\n\nconst pendingMetrics = new Map<string, PendingMessageMetric>()\n\n/**\n * Start tracking message creation performance\n */\nexport function startMessageCreationTracking(idempotentKey: string): void {\n if (!idempotentKey) return\n const startTime = Date.now()\n pendingMetrics.set(`message:${idempotentKey}`, { idempotentKey, startTime })\n pendingMetrics.set(`conversation:${idempotentKey}`, { idempotentKey, startTime })\n cleanupStaleMetrics()\n}\n\n/**\n * Complete message creation tracking when the message.created event is received\n */\nexport function completeMessageCreationTracking({\n apiClient,\n idempotentKey,\n}: {\n apiClient: ApiClient\n idempotentKey: string\n}): void {\n if (!idempotentKey) return\n const duration = durationForIdempotentKey(`message:${idempotentKey}`)\n if (!duration) return\n sendPerformanceMetric(apiClient, {\n name: 'chat.message.creation.message_roundtrip_time',\n value: duration,\n unit: 'milliseconds',\n })\n}\n\n/**\n * Complete message creation tracking when the conversation.updated event is received\n */\nexport function completeMessageCreationConversationTracking({\n apiClient,\n idempotentKey,\n}: {\n apiClient: ApiClient\n idempotentKey: string\n}): void {\n if (!idempotentKey) return\n const duration = durationForIdempotentKey(`conversation:${idempotentKey}`)\n if (!duration) return\n sendPerformanceMetric(apiClient, {\n name: 'chat.message.creation.conversation_roundtrip_time',\n value: duration,\n unit: 'milliseconds',\n })\n}\n\nfunction durationForIdempotentKey(idempotentKey: string): number | undefined {\n const metric = pendingMetrics.get(idempotentKey)\n if (!metric) return undefined\n pendingMetrics.delete(idempotentKey)\n const endTime = Date.now()\n return endTime - metric.startTime\n}\n\n/**\n * Send performance metric to backend for Datadog submission\n */\nasync function sendPerformanceMetric(\n apiClient: ApiClient,\n metric: PerformanceMetric\n): Promise<void> {\n try {\n await apiClient.chat.post({\n url: `/me/metrics`,\n data: {\n data: {\n type: 'Metric',\n attributes: {\n metric: {\n name: metric.name,\n value: metric.value,\n unit: metric.unit,\n platform: Platform.OS,\n app: appName,\n },\n },\n },\n },\n })\n } catch (_e) {\n // Ignore errors\n }\n}\n\n/**\n * Clean up any stale metrics (older than 5 minutes)\n * This prevents memory leaks if messages fail to complete for any reason\n */\nfunction cleanupStaleMetrics(): void {\n const now = Date.now()\n const staleThreshold = 5 * 60 * 1000 // 5 minutes in milliseconds\n\n for (const [key, metric] of pendingMetrics.entries()) {\n if (now - metric.startTime > staleThreshold) {\n pendingMetrics.delete(key)\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"performance_tracking.js","sourceRoot":"","sources":["../../src/utils/performance_tracking.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AACvC,OAAO,UAAU,MAAM,0BAA0B,CAAA;AAEjD,MAAM,OAAO,GAAG,UAAU,CAAC,kBAAkB,EAAE,CAAA;AAa/C,MAAM,cAAc,GAAG,IAAI,GAAG,EAAgC,CAAA;AAE9D;;GAEG;AACH,MAAM,UAAU,4BAA4B,CAAC,aAAqB;IAChE,IAAI,CAAC,aAAa;QAAE,OAAM;IAC1B,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IAC5B,cAAc,CAAC,GAAG,CAAC,WAAW,aAAa,EAAE,EAAE,EAAE,aAAa,EAAE,SAAS,EAAE,CAAC,CAAA;IAC5E,cAAc,CAAC,GAAG,CAAC,gBAAgB,aAAa,EAAE,EAAE,EAAE,aAAa,EAAE,SAAS,EAAE,CAAC,CAAA;IACjF,mBAAmB,EAAE,CAAA;AACvB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,+BAA+B,CAAC,EAC9C,SAAS,EACT,aAAa,GAId;IACC,IAAI,CAAC,aAAa;QAAE,OAAM;IAC1B,MAAM,QAAQ,GAAG,wBAAwB,CAAC,WAAW,aAAa,EAAE,CAAC,CAAA;IACrE,IAAI,CAAC,QAAQ;QAAE,OAAM;IACrB,qBAAqB,CAAC,SAAS,EAAE;QAC/B,IAAI,EAAE,8CAA8C;QACpD,KAAK,EAAE,QAAQ;QACf,IAAI,EAAE,cAAc;KACrB,CAAC,CAAA;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,2CAA2C,CAAC,EAC1D,SAAS,EACT,aAAa,GAId;IACC,IAAI,CAAC,aAAa;QAAE,OAAM;IAC1B,MAAM,QAAQ,GAAG,wBAAwB,CAAC,gBAAgB,aAAa,EAAE,CAAC,CAAA;IAC1E,IAAI,CAAC,QAAQ;QAAE,OAAM;IACrB,qBAAqB,CAAC,SAAS,EAAE;QAC/B,IAAI,EAAE,mDAAmD;QACzD,KAAK,EAAE,QAAQ;QACf,IAAI,EAAE,cAAc;KACrB,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,wBAAwB,CAAC,aAAqB;IACrD,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,aAAa,CAAC,CAAA;IAChD,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAA;IAC7B,cAAc,CAAC,MAAM,CAAC,aAAa,CAAC,CAAA;IACpC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IAC1B,OAAO,OAAO,GAAG,MAAM,CAAC,SAAS,CAAA;AACnC,CAAC;AAED;;GAEG;AACH,KAAK,UAAU,qBAAqB,CAClC,SAAoB,EACpB,MAAyB;IAEzB,IAAI,CAAC;QACH,MAAM,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;YACxB,GAAG,EAAE,aAAa;YAClB,IAAI,EAAE;gBACJ,IAAI,EAAE;oBACJ,IAAI,EAAE,QAAQ;oBACd,UAAU,EAAE;wBACV,MAAM,EAAE;4BACN,IAAI,EAAE,MAAM,CAAC,IAAI;4BACjB,KAAK,EAAE,MAAM,CAAC,KAAK;4BACnB,IAAI,EAAE,MAAM,CAAC,IAAI;4BACjB,QAAQ,EAAE,QAAQ,CAAC,EAAE;4BACrB,GAAG,EAAE,OAAO;yBACb;qBACF;iBACF;aACF;SACF,CAAC,CAAA;IACJ,CAAC;IAAC,OAAO,EAAE,EAAE,CAAC;QACZ,gBAAgB;IAClB,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,mBAAmB;IAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IACtB,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAA,CAAC,4BAA4B;IAEjE,KAAK,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,cAAc,CAAC,OAAO,EAAE,EAAE,CAAC;QACrD,IAAI,GAAG,GAAG,MAAM,CAAC,SAAS,GAAG,cAAc,EAAE,CAAC;YAC5C,cAAc,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAC5B,CAAC;IACH,CAAC;AACH,CAAC","sourcesContent":["/**\n * Performance tracking utilities for client-side metrics (React Native)\n *\n * This module provides functionality to track performance metrics and send them\n * to Datadog via the backend API for aggregation.\n */\n\nimport { Platform } from 'react-native'\nimport DeviceInfo from 'react-native-device-info'\nimport type { ApiClient } from '../hooks/use_api_client'\nconst appName = DeviceInfo.getApplicationName()\n\ninterface PerformanceMetric {\n name: string\n value: number\n unit: 'milliseconds' | 'seconds' | 'count'\n}\n\ninterface PendingMessageMetric {\n idempotentKey: string\n startTime: number\n}\n\nconst pendingMetrics = new Map<string, PendingMessageMetric>()\n\n/**\n * Start tracking message creation performance\n */\nexport function startMessageCreationTracking(idempotentKey: string): void {\n if (!idempotentKey) return\n const startTime = Date.now()\n pendingMetrics.set(`message:${idempotentKey}`, { idempotentKey, startTime })\n pendingMetrics.set(`conversation:${idempotentKey}`, { idempotentKey, startTime })\n cleanupStaleMetrics()\n}\n\n/**\n * Complete message creation tracking when the message.created event is received\n */\nexport function completeMessageCreationTracking({\n apiClient,\n idempotentKey,\n}: {\n apiClient: ApiClient\n idempotentKey: string\n}): void {\n if (!idempotentKey) return\n const duration = durationForIdempotentKey(`message:${idempotentKey}`)\n if (!duration) return\n sendPerformanceMetric(apiClient, {\n name: 'chat.message.creation.message_roundtrip_time',\n value: duration,\n unit: 'milliseconds',\n })\n}\n\n/**\n * Complete message creation tracking when the conversation.updated event is received\n */\nexport function completeMessageCreationConversationTracking({\n apiClient,\n idempotentKey,\n}: {\n apiClient: ApiClient\n idempotentKey: string\n}): void {\n if (!idempotentKey) return\n const duration = durationForIdempotentKey(`conversation:${idempotentKey}`)\n if (!duration) return\n sendPerformanceMetric(apiClient, {\n name: 'chat.message.creation.conversation_roundtrip_time',\n value: duration,\n unit: 'milliseconds',\n })\n}\n\nfunction durationForIdempotentKey(idempotentKey: string): number | undefined {\n const metric = pendingMetrics.get(idempotentKey)\n if (!metric) return undefined\n pendingMetrics.delete(idempotentKey)\n const endTime = Date.now()\n return endTime - metric.startTime\n}\n\n/**\n * Send performance metric to backend for Datadog submission\n */\nasync function sendPerformanceMetric(\n apiClient: ApiClient,\n metric: PerformanceMetric\n): Promise<void> {\n try {\n await apiClient.chat.post({\n url: `/me/metrics`,\n data: {\n data: {\n type: 'Metric',\n attributes: {\n metric: {\n name: metric.name,\n value: metric.value,\n unit: metric.unit,\n platform: Platform.OS,\n app: appName,\n },\n },\n },\n },\n })\n } catch (_e) {\n // Ignore errors\n }\n}\n\n/**\n * Clean up any stale metrics (older than 5 minutes)\n * This prevents memory leaks if messages fail to complete for any reason\n */\nfunction cleanupStaleMetrics(): void {\n const now = Date.now()\n const staleThreshold = 5 * 60 * 1000 // 5 minutes in milliseconds\n\n for (const [key, metric] of pendingMetrics.entries()) {\n if (now - metric.startTime > staleThreshold) {\n pendingMetrics.delete(key)\n }\n }\n}\n"]}
|
|
@@ -6,5 +6,5 @@ export declare const getChatConfigurationRequestArgs: () => {
|
|
|
6
6
|
};
|
|
7
7
|
};
|
|
8
8
|
};
|
|
9
|
-
export declare const getChatConfigurationQueryKey: () => import("
|
|
9
|
+
export declare const getChatConfigurationQueryKey: () => import("../..").RequestQueryKey;
|
|
10
10
|
//# sourceMappingURL=get_chat_configuration.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get_chat_configuration.d.ts","sourceRoot":"","sources":["../../../src/utils/request/get_chat_configuration.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,+BAA+B;;;;;;;CAe3C,CAAA;AAED,eAAO,MAAM,4BAA4B,
|
|
1
|
+
{"version":3,"file":"get_chat_configuration.d.ts","sourceRoot":"","sources":["../../../src/utils/request/get_chat_configuration.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,+BAA+B;;;;;;;CAe3C,CAAA;AAED,eAAO,MAAM,4BAA4B,uCAGxC,CAAA"}
|
|
@@ -7,5 +7,5 @@ export declare const getFeaturesRequestArgs: () => {
|
|
|
7
7
|
};
|
|
8
8
|
};
|
|
9
9
|
};
|
|
10
|
-
export declare const getFeaturesQueryKey: () => import("
|
|
10
|
+
export declare const getFeaturesQueryKey: () => import("../..").RequestQueryKey;
|
|
11
11
|
//# sourceMappingURL=get_features.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get_features.d.ts","sourceRoot":"","sources":["../../../src/utils/request/get_features.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,sBAAsB;;;;;;;;CAYlC,CAAA;AAED,eAAO,MAAM,mBAAmB,
|
|
1
|
+
{"version":3,"file":"get_features.d.ts","sourceRoot":"","sources":["../../../src/utils/request/get_features.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,sBAAsB;;;;;;;;CAYlC,CAAA;AAED,eAAO,MAAM,mBAAmB,uCAG/B,CAAA"}
|
|
@@ -16,5 +16,5 @@ export declare const getMessageRequestArgs: ({ conversation_id, messageId, }: {
|
|
|
16
16
|
export declare const getMessageQueryKey: ({ conversation_id, messageId, }: {
|
|
17
17
|
conversation_id: number;
|
|
18
18
|
messageId: string;
|
|
19
|
-
}) => import("
|
|
19
|
+
}) => import("../..").RequestQueryKey;
|
|
20
20
|
//# sourceMappingURL=get_message.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get_message.d.ts","sourceRoot":"","sources":["../../../src/utils/request/get_message.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,qBAAqB,GAAI,iCAGnC;IACD,eAAe,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;CAClB;;;;;;;;;;;CAWA,CAAA;AAED,eAAO,MAAM,kBAAkB,GAAI,iCAGhC;IACD,eAAe,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;CAClB,
|
|
1
|
+
{"version":3,"file":"get_message.d.ts","sourceRoot":"","sources":["../../../src/utils/request/get_message.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,qBAAqB,GAAI,iCAGnC;IACD,eAAe,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;CAClB;;;;;;;;;;;CAWA,CAAA;AAED,eAAO,MAAM,kBAAkB,GAAI,iCAGhC;IACD,eAAe,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;CAClB,oCAGA,CAAA"}
|
|
@@ -17,5 +17,5 @@ export declare const getMessagesRequestArgs: ({ conversation_id, reply_root_id,
|
|
|
17
17
|
export declare const getMessagesQueryKey: ({ conversation_id, reply_root_id, }: {
|
|
18
18
|
conversation_id: number;
|
|
19
19
|
reply_root_id?: string | null;
|
|
20
|
-
}) => import("
|
|
20
|
+
}) => import("../..").RequestQueryKey;
|
|
21
21
|
//# sourceMappingURL=get_messages.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get_messages.d.ts","sourceRoot":"","sources":["../../../src/utils/request/get_messages.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,sBAAsB,GAAI,qCAGpC;IACD,eAAe,EAAE,MAAM,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;;;;;;;;;;;;CAeA,CAAA;AAED,eAAO,MAAM,mBAAmB,GAAI,qCAGjC;IACD,eAAe,EAAE,MAAM,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B,
|
|
1
|
+
{"version":3,"file":"get_messages.d.ts","sourceRoot":"","sources":["../../../src/utils/request/get_messages.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,sBAAsB,GAAI,qCAGpC;IACD,eAAe,EAAE,MAAM,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;;;;;;;;;;;;CAeA,CAAA;AAED,eAAO,MAAM,mBAAmB,GAAI,qCAGjC;IACD,eAAe,EAAE,MAAM,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B,oCAGA,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/chat-react-native",
|
|
3
|
-
"version": "3.36.
|
|
3
|
+
"version": "3.36.2-qa-726.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "build/index.js",
|
|
6
|
+
"react-native": "./src/index.tsx",
|
|
6
7
|
"types": "build/index.d.ts",
|
|
7
8
|
"files": [
|
|
8
9
|
"build",
|
|
@@ -71,5 +72,5 @@
|
|
|
71
72
|
"react-native-url-polyfill": "^2.0.0",
|
|
72
73
|
"typescript": "~5.9.2"
|
|
73
74
|
},
|
|
74
|
-
"gitHead": "
|
|
75
|
+
"gitHead": "00e3f3c134a6d61868f4124be4c17c8ac47967c6"
|
|
75
76
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { QueryClientProvider } from '@tanstack/react-query'
|
|
2
|
+
import { renderHook, waitFor } from '@testing-library/react-native'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { buildTestQueryClient } from '../../__utils__/query_client'
|
|
5
|
+
import * as useApiClientModule from '../../hooks/use_api_client'
|
|
6
|
+
import { useConversationValidate } from '../../hooks/use_conversation_validate'
|
|
7
|
+
|
|
8
|
+
const createWrapper = () => {
|
|
9
|
+
const queryClient = buildTestQueryClient()
|
|
10
|
+
return ({ children }: { children: React.ReactNode }) => (
|
|
11
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const mockChatPost = (impl: jest.Mock) => {
|
|
16
|
+
jest.spyOn(useApiClientModule, 'useApiClient').mockReturnValue({
|
|
17
|
+
chat: { post: impl },
|
|
18
|
+
} as unknown as ReturnType<typeof useApiClientModule.useApiClient>)
|
|
19
|
+
return impl
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const resolveWith = (warnings: { attribute: string; fullMessage: string }[]) =>
|
|
23
|
+
jest.fn().mockResolvedValue({
|
|
24
|
+
data: { type: 'ConversationValidate', id: '1', warnings },
|
|
25
|
+
links: {},
|
|
26
|
+
meta: {},
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('useConversationValidate', () => {
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
jest.restoreAllMocks()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('does not fire the request when disabled', () => {
|
|
35
|
+
const post = mockChatPost(resolveWith([]))
|
|
36
|
+
|
|
37
|
+
const { result } = renderHook(
|
|
38
|
+
() => useConversationValidate({ payload: 'abc', enabled: false }),
|
|
39
|
+
{ wrapper: createWrapper() }
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
expect(post).not.toHaveBeenCalled()
|
|
43
|
+
expect(result.current.warnings).toEqual([])
|
|
44
|
+
expect(result.current.validationPending).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('does not fire the request when payload is undefined', () => {
|
|
48
|
+
const post = mockChatPost(resolveWith([]))
|
|
49
|
+
|
|
50
|
+
const { result } = renderHook(
|
|
51
|
+
() => useConversationValidate({ payload: undefined, isLoadingPayload: true }),
|
|
52
|
+
{ wrapper: createWrapper() }
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
expect(post).not.toHaveBeenCalled()
|
|
56
|
+
// validationPending stays true while the upstream payload query is still loading
|
|
57
|
+
expect(result.current.validationPending).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('returns warnings from the response and clears validationPending', async () => {
|
|
61
|
+
const warnings = [{ attribute: 'safety_policy', fullMessage: 'Needs a second adult.' }]
|
|
62
|
+
mockChatPost(resolveWith(warnings))
|
|
63
|
+
|
|
64
|
+
const { result } = renderHook(() => useConversationValidate({ payload: 'abc' }), {
|
|
65
|
+
wrapper: createWrapper(),
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
await waitFor(() => expect(result.current.warnings).toEqual(warnings))
|
|
69
|
+
expect(result.current.validationPending).toBe(false)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('sends gender_id in attributes when provided', async () => {
|
|
73
|
+
const post = mockChatPost(resolveWith([]))
|
|
74
|
+
|
|
75
|
+
renderHook(() => useConversationValidate({ payload: 'abc', genderId: '42' }), {
|
|
76
|
+
wrapper: createWrapper(),
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
await waitFor(() => expect(post).toHaveBeenCalledTimes(1))
|
|
80
|
+
expect(post).toHaveBeenCalledWith(
|
|
81
|
+
expect.objectContaining({
|
|
82
|
+
url: '/me/conversation_validate',
|
|
83
|
+
data: {
|
|
84
|
+
data: {
|
|
85
|
+
type: 'ConversationValidate',
|
|
86
|
+
attributes: { payload: 'abc', gender_id: '42' },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('omits gender_id when not provided', async () => {
|
|
94
|
+
const post = mockChatPost(resolveWith([]))
|
|
95
|
+
|
|
96
|
+
renderHook(() => useConversationValidate({ payload: 'abc' }), {
|
|
97
|
+
wrapper: createWrapper(),
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
await waitFor(() => expect(post).toHaveBeenCalledTimes(1))
|
|
101
|
+
const [call] = post.mock.calls
|
|
102
|
+
expect(call[0].data.data.attributes).toEqual({ payload: 'abc' })
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('falls back to empty warnings and unblocks on request error', async () => {
|
|
106
|
+
// Documents the current fail-open behavior: if validate errors, the user is allowed to proceed.
|
|
107
|
+
const post = mockChatPost(jest.fn().mockRejectedValue(new Error('boom')))
|
|
108
|
+
|
|
109
|
+
const { result } = renderHook(() => useConversationValidate({ payload: 'abc' }), {
|
|
110
|
+
wrapper: createWrapper(),
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
await waitFor(() => expect(post).toHaveBeenCalledTimes(1))
|
|
114
|
+
await waitFor(() => expect(result.current.validationPending).toBe(false))
|
|
115
|
+
expect(result.current.warnings).toEqual([])
|
|
116
|
+
})
|
|
117
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { QueryClientProvider } from '@tanstack/react-query'
|
|
2
|
+
import { renderHook, waitFor } from '@testing-library/react-native'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { buildTestQueryClient } from '../../__utils__/query_client'
|
|
5
|
+
import { useApiClient } from '../../hooks/use_api_client'
|
|
6
|
+
import { useEnrichPeople } from '../../hooks/use_enrich_people'
|
|
7
|
+
|
|
8
|
+
jest.mock('../../hooks/use_api_client')
|
|
9
|
+
|
|
10
|
+
const mockedUseApiClient = useApiClient as jest.MockedFunction<typeof useApiClient>
|
|
11
|
+
const mockPost = jest.fn()
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
mockPost.mockReset()
|
|
15
|
+
mockedUseApiClient.mockReturnValue({ chat: { post: mockPost } } as any)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const createWrapper = () => {
|
|
19
|
+
const queryClient = buildTestQueryClient()
|
|
20
|
+
return ({ children }: { children: React.ReactNode }) => (
|
|
21
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('returns empty map and skips the request when personIds is empty', () => {
|
|
26
|
+
const { result } = renderHook(() => useEnrichPeople({ personIds: [] }), {
|
|
27
|
+
wrapper: createWrapper(),
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
expect(result.current.size).toBe(0)
|
|
31
|
+
expect(mockPost).not.toHaveBeenCalled()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('returns enrichment data keyed by person id', async () => {
|
|
35
|
+
mockPost.mockResolvedValue({
|
|
36
|
+
data: [
|
|
37
|
+
{ id: '1', type: 'PersonEnrichment', badges: [{ title: 'Leader' }] },
|
|
38
|
+
{ id: '2', type: 'PersonEnrichment', badges: [] },
|
|
39
|
+
],
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const { result } = renderHook(() => useEnrichPeople({ personIds: [1, 2] }), {
|
|
43
|
+
wrapper: createWrapper(),
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
await waitFor(() => expect(result.current.size).toBe(2))
|
|
47
|
+
|
|
48
|
+
expect(result.current.get(1)?.badges).toEqual([{ title: 'Leader' }])
|
|
49
|
+
expect(result.current.get(2)?.badges).toEqual([])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('includes group_id in the request when provided', async () => {
|
|
53
|
+
mockPost.mockResolvedValue({ data: [] })
|
|
54
|
+
|
|
55
|
+
renderHook(() => useEnrichPeople({ personIds: [1], groupId: 42 }), {
|
|
56
|
+
wrapper: createWrapper(),
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
await waitFor(() => expect(mockPost).toHaveBeenCalled())
|
|
60
|
+
|
|
61
|
+
expect(mockPost).toHaveBeenCalledWith(
|
|
62
|
+
expect.objectContaining({
|
|
63
|
+
data: expect.objectContaining({
|
|
64
|
+
data: expect.objectContaining({
|
|
65
|
+
attributes: expect.objectContaining({ group_id: 42 }),
|
|
66
|
+
}),
|
|
67
|
+
}),
|
|
68
|
+
})
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('omits group_id from the request when not provided', async () => {
|
|
73
|
+
mockPost.mockResolvedValue({ data: [] })
|
|
74
|
+
|
|
75
|
+
renderHook(() => useEnrichPeople({ personIds: [1] }), {
|
|
76
|
+
wrapper: createWrapper(),
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
await waitFor(() => expect(mockPost).toHaveBeenCalled())
|
|
80
|
+
|
|
81
|
+
const attributes = mockPost.mock.calls[0][0].data.data.attributes
|
|
82
|
+
expect(attributes).not.toHaveProperty('group_id')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('returns empty map when the request fails', async () => {
|
|
86
|
+
mockPost.mockRejectedValue(new Error('network error'))
|
|
87
|
+
|
|
88
|
+
const { result } = renderHook(() => useEnrichPeople({ personIds: [1] }), {
|
|
89
|
+
wrapper: createWrapper(),
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
await waitFor(() => expect(mockPost).toHaveBeenCalled())
|
|
93
|
+
|
|
94
|
+
expect(result.current.size).toBe(0)
|
|
95
|
+
})
|
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
MESSAGE_AUTHOR_AVATAR_COLUMN_WIDTH,
|
|
32
32
|
platformFontWeightMedium,
|
|
33
33
|
} from '../../utils/styles'
|
|
34
|
-
import
|
|
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} ${
|
|
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
|
-
<
|
|
242
|
+
<ComponentErrorBoundary>
|
|
241
243
|
<MessageAttachments
|
|
242
244
|
attachments={attachments}
|
|
243
245
|
metaProps={metaProps}
|
|
244
246
|
onMessageAttachmentLongPress={handleMessageAttachmentLongPress}
|
|
245
247
|
onMessageLongPress={handleMessageLongPress}
|
|
246
248
|
/>
|
|
247
|
-
</
|
|
249
|
+
</ComponentErrorBoundary>
|
|
248
250
|
{text && (
|
|
249
251
|
<View style={styles.messageText}>
|
|
250
252
|
<MessageMarkdown text={text} />
|
|
@@ -3,7 +3,9 @@ import { useEffect } from 'react'
|
|
|
3
3
|
import { Animated, LayoutAnimation, StyleSheet, useWindowDimensions, View } from 'react-native'
|
|
4
4
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
5
5
|
import { useTheme } from '../../hooks'
|
|
6
|
+
import { ConversationWarning } from '../../types'
|
|
6
7
|
import { MAX_FONT_SIZE_MULTIPLIER_LANDMARK } from '../../utils'
|
|
8
|
+
import { Banner } from './banner'
|
|
7
9
|
import { Button } from './button'
|
|
8
10
|
import { IconString } from './icon'
|
|
9
11
|
import { Text } from './text'
|
|
@@ -17,6 +19,7 @@ export const ActionButton = ({
|
|
|
17
19
|
buttonIconNameLeft,
|
|
18
20
|
secondaryButton,
|
|
19
21
|
loading = false,
|
|
22
|
+
warnings = [],
|
|
20
23
|
}: {
|
|
21
24
|
visible?: boolean
|
|
22
25
|
disabled?: boolean
|
|
@@ -26,6 +29,7 @@ export const ActionButton = ({
|
|
|
26
29
|
buttonIconNameLeft?: IconString
|
|
27
30
|
secondaryButton?: React.ReactNode
|
|
28
31
|
loading?: boolean
|
|
32
|
+
warnings?: ConversationWarning[]
|
|
29
33
|
}) => {
|
|
30
34
|
const styles = useStyles()
|
|
31
35
|
const [show, setShow] = useState(visible)
|
|
@@ -41,6 +45,17 @@ export const ActionButton = ({
|
|
|
41
45
|
|
|
42
46
|
return (
|
|
43
47
|
<Animated.View style={styles.container}>
|
|
48
|
+
{warnings.length > 0 && (
|
|
49
|
+
<View style={styles.warnings}>
|
|
50
|
+
{warnings.map((warning, index) => (
|
|
51
|
+
<Banner
|
|
52
|
+
key={`${warning.attribute}-${index}`}
|
|
53
|
+
appearance="error"
|
|
54
|
+
description={warning.fullMessage}
|
|
55
|
+
/>
|
|
56
|
+
))}
|
|
57
|
+
</View>
|
|
58
|
+
)}
|
|
44
59
|
{Boolean(infoText) && (
|
|
45
60
|
<Text style={styles.infoText} variant="footnote">
|
|
46
61
|
{infoText}
|
|
@@ -95,5 +110,8 @@ const useStyles = () => {
|
|
|
95
110
|
infoText: {
|
|
96
111
|
textAlign: 'center',
|
|
97
112
|
},
|
|
113
|
+
warnings: {
|
|
114
|
+
gap: 8,
|
|
115
|
+
},
|
|
98
116
|
})
|
|
99
117
|
}
|
package/src/components/index.tsx
CHANGED
|
@@ -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
|
+
}
|