diff --git a/.changeset/fix-feed-settings-features.md b/.changeset/fix-feed-settings-features.md new file mode 100644 index 000000000..32c9f7de4 --- /dev/null +++ b/.changeset/fix-feed-settings-features.md @@ -0,0 +1,7 @@ +--- +"@knocklabs/react-core": patch +"@knocklabs/react": patch +"@knocklabs/react-native": patch +--- + +Handle missing `features` in feed settings responses to prevent crashes on partial API responses. diff --git a/packages/react-core/src/modules/feed/hooks/useFeedSettings.ts b/packages/react-core/src/modules/feed/hooks/useFeedSettings.ts index b3a0173f2..0f4013e4f 100644 --- a/packages/react-core/src/modules/feed/hooks/useFeedSettings.ts +++ b/packages/react-core/src/modules/feed/hooks/useFeedSettings.ts @@ -11,7 +11,7 @@ function useFeedSettings(feedClient: Feed): { settings: FeedSettings | null; loading: boolean; } { - const [settings, setSettings] = useState(null); + const [settings, setSettings] = useState(null); const [isLoading, setIsLoading] = useState(false); // TODO: consider moving this into the feed client and into the feed store state when @@ -28,8 +28,19 @@ function useFeedSettings(feedClient: Feed): { url: feedSettingsPath, }); - if (!response.error) { - setSettings(response.body); + // Only trust a genuine success whose body actually contains the settings + // payload. On flaky connections a captive portal or proxy can return a + // 200 with a body that isn't the feed settings object; treat that as + // "unknown" (leave settings null) rather than fabricating a default that + // would silently suppress branding when it is required. + if (response.statusCode === "ok" && response.body?.features) { + setSettings({ + features: { + branding_required: Boolean( + response.body.features.branding_required, + ), + }, + }); } setIsLoading(false); diff --git a/packages/react-core/test/feed/useFeedSettings.test.ts b/packages/react-core/test/feed/useFeedSettings.test.ts index f2d94475a..fd56b2aa8 100644 --- a/packages/react-core/test/feed/useFeedSettings.test.ts +++ b/packages/react-core/test/feed/useFeedSettings.test.ts @@ -55,4 +55,23 @@ describe("useFeedSettings", () => { expect(result.current.settings).toBeNull(); }); + + it("leaves settings null when a success response is missing the features payload", async () => { + const { feed, mockApiClient } = createMockFeed("feed_123"); + + // A degraded connection (captive portal / proxy) can return a 200 whose + // body is not the feed settings object. We must not fabricate a default + // from it, which would silently suppress branding when it is required. + mockNetworkSuccess(mockApiClient, {}); + + const { result } = renderHook(() => + useFeedSettings(feed as unknown as Feed), + ); + + await waitFor(() => { + expect(result.current.loading).toBe(false); + }); + + expect(result.current.settings).toBeNull(); + }); }); diff --git a/packages/react-native/src/modules/feed/components/NotificationFeedComponents/NotificationFeed.tsx b/packages/react-native/src/modules/feed/components/NotificationFeedComponents/NotificationFeed.tsx index 3e84df321..a7b9c5b8a 100644 --- a/packages/react-native/src/modules/feed/components/NotificationFeedComponents/NotificationFeed.tsx +++ b/packages/react-native/src/modules/feed/components/NotificationFeedComponents/NotificationFeed.tsx @@ -160,7 +160,7 @@ export const NotificationFeed: React.FC = ({ onEndReached={onEndReached} onEndReachedThreshold={0.5} /> - {settings?.features.branding_required && ( + {settings?.features?.branding_required && ( diff --git a/packages/react/src/modules/feed/components/NotificationFeed/NotificationFeed.tsx b/packages/react/src/modules/feed/components/NotificationFeed/NotificationFeed.tsx index bb93ad531..e9a4af704 100644 --- a/packages/react/src/modules/feed/components/NotificationFeed/NotificationFeed.tsx +++ b/packages/react/src/modules/feed/components/NotificationFeed/NotificationFeed.tsx @@ -154,7 +154,7 @@ export const NotificationFeed: React.FC = ({ {!requestInFlight && noItems && EmptyComponent} - {settings?.features.branding_required && ( + {settings?.features?.branding_required && (
{t("poweredBy") || "Powered by Knock"}