From d442fa0159224e5cffe50df5dbdbdac23ec33c47 Mon Sep 17 00:00:00 2001 From: Abdullah Date: Tue, 30 Jun 2026 22:49:16 +0530 Subject: [PATCH 1/2] fix: handle missing features in feed settings response Normalize feed settings when the API returns a partial body (e.g. on flaky networks) and guard branding_required access in NotificationFeed components. Fixes #1018 Co-authored-by: Cursor --- .changeset/fix-feed-settings-features.md | 7 +++++++ .../src/modules/feed/hooks/useFeedSettings.ts | 10 ++++++++-- .../test/feed/useFeedSettings.test.ts | 20 +++++++++++++++++++ .../NotificationFeed.tsx | 2 +- .../NotificationFeed/NotificationFeed.tsx | 2 +- 5 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 .changeset/fix-feed-settings-features.md 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..598f9db00 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 @@ -29,7 +29,13 @@ function useFeedSettings(feedClient: Feed): { }); if (!response.error) { - setSettings(response.body); + 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..4730c309e 100644 --- a/packages/react-core/test/feed/useFeedSettings.test.ts +++ b/packages/react-core/test/feed/useFeedSettings.test.ts @@ -55,4 +55,24 @@ describe("useFeedSettings", () => { expect(result.current.settings).toBeNull(); }); + + it("handles partial settings response by defaulting features", async () => { + const { feed, mockApiClient } = createMockFeed("feed_123"); + + mockNetworkSuccess(mockApiClient, {}); + + const { result } = renderHook(() => + useFeedSettings(feed as unknown as Feed), + ); + + await waitFor(() => { + expect(result.current.loading).toBe(false); + }); + + expect(result.current.settings).toEqual({ + features: { + branding_required: false, + }, + }); + }); }); 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"} From 4c856e205100253ebc5b9b81b6232ca8990c6089 Mon Sep 17 00:00:00 2001 From: Kyle McDonald Date: Wed, 8 Jul 2026 09:00:09 -0500 Subject: [PATCH 2/2] fix: only store well-formed feed settings responses A flaky connection (captive portal, proxy) can return an HTTP 200 whose body is not the feed settings payload. Fabricating branding_required: false from such a body silently suppresses branding when it is required. Only store settings on a genuine success whose body actually contains a features object; otherwise leave settings null (which the optional chaining in the feed components already renders safely). --- .../src/modules/feed/hooks/useFeedSettings.ts | 9 +++++++-- packages/react-core/test/feed/useFeedSettings.test.ts | 11 +++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/react-core/src/modules/feed/hooks/useFeedSettings.ts b/packages/react-core/src/modules/feed/hooks/useFeedSettings.ts index 598f9db00..0f4013e4f 100644 --- a/packages/react-core/src/modules/feed/hooks/useFeedSettings.ts +++ b/packages/react-core/src/modules/feed/hooks/useFeedSettings.ts @@ -28,11 +28,16 @@ function useFeedSettings(feedClient: Feed): { url: feedSettingsPath, }); - if (!response.error) { + // 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, + response.body.features.branding_required, ), }, }); diff --git a/packages/react-core/test/feed/useFeedSettings.test.ts b/packages/react-core/test/feed/useFeedSettings.test.ts index 4730c309e..fd56b2aa8 100644 --- a/packages/react-core/test/feed/useFeedSettings.test.ts +++ b/packages/react-core/test/feed/useFeedSettings.test.ts @@ -56,9 +56,12 @@ describe("useFeedSettings", () => { expect(result.current.settings).toBeNull(); }); - it("handles partial settings response by defaulting features", async () => { + 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(() => @@ -69,10 +72,6 @@ describe("useFeedSettings", () => { expect(result.current.loading).toBe(false); }); - expect(result.current.settings).toEqual({ - features: { - branding_required: false, - }, - }); + expect(result.current.settings).toBeNull(); }); });