From 45081de62ab8e83b5e00a8ec9fd2be0c5f958fd4 Mon Sep 17 00:00:00 2001 From: pandeymangg Date: Mon, 18 May 2026 11:38:24 +0530 Subject: [PATCH] client api fixes --- .../src/components/survey-web-view.tsx | 6 +-- packages/react-native/src/lib/common/setup.ts | 4 +- .../lib/common/tests/__mocks__/config.mock.ts | 1 - .../src/lib/common/tests/utils.test.ts | 38 ++++++++++++++++--- packages/react-native/src/lib/common/utils.ts | 22 ++++++++--- .../react-native/src/lib/survey/action.ts | 2 +- .../lib/survey/tests/__mocks__/store.mock.ts | 1 - .../src/lib/survey/tests/action.test.ts | 3 +- .../src/lib/survey/tests/store.test.ts | 33 ++++++++-------- packages/react-native/src/types/survey.ts | 13 ++----- 10 files changed, 74 insertions(+), 49 deletions(-) diff --git a/packages/react-native/src/components/survey-web-view.tsx b/packages/react-native/src/components/survey-web-view.tsx index 445ffe1..5b9c469 100644 --- a/packages/react-native/src/components/survey-web-view.tsx +++ b/packages/react-native/src/components/survey-web-view.tsx @@ -53,7 +53,7 @@ export function SurveyWebView(props: SurveyWebViewProps): JSX.Element | null { const displayLanguage = getLanguageCode(props.survey, language); if (!displayLanguage) { logger.debug( - `Survey "${props.survey.name}" is not available in specified language.`, + `Survey "${props.survey.id}" is not available in specified language.`, ); setIsSurveyRunning(false); setShowSurvey(false); @@ -75,7 +75,7 @@ export function SurveyWebView(props: SurveyWebViewProps): JSX.Element | null { if (props.survey.delay) { logger.debug( - `Delaying survey "${props.survey.name}" by ${String(props.survey.delay)} seconds`, + `Delaying survey "${props.survey.id}" by ${String(props.survey.delay)} seconds`, ); const timerId = setTimeout(() => { setShowSurvey(true); @@ -87,7 +87,7 @@ export function SurveyWebView(props: SurveyWebViewProps): JSX.Element | null { } setShowSurvey(true); - }, [props.survey.delay, isSurveyRunning, props.survey.name]); + }, [props.survey.delay, isSurveyRunning, props.survey.id]); if (!appConfig) { return null; diff --git a/packages/react-native/src/lib/common/setup.ts b/packages/react-native/src/lib/common/setup.ts index c49fd24..51ff6ec 100644 --- a/packages/react-native/src/lib/common/setup.ts +++ b/packages/react-native/src/lib/common/setup.ts @@ -150,9 +150,9 @@ const updateAppConfigWithSyncedStates = ( filteredSurveys, }); - const surveyNames = filteredSurveys.map((s) => s.name); + const surveyIds = filteredSurveys.map((s) => s.id); logger.debug( - `Fetched ${surveyNames.length.toString()} surveys during sync: ${surveyNames.join(", ")}`, + `Fetched ${surveyIds.length.toString()} surveys during sync: ${surveyIds.join(", ")}`, ); }; diff --git a/packages/react-native/src/lib/common/tests/__mocks__/config.mock.ts b/packages/react-native/src/lib/common/tests/__mocks__/config.mock.ts index 30b098d..99129b3 100644 --- a/packages/react-native/src/lib/common/tests/__mocks__/config.mock.ts +++ b/packages/react-native/src/lib/common/tests/__mocks__/config.mock.ts @@ -16,7 +16,6 @@ export const mockConfig: TConfig = { surveys: [ { id: mockSurveyId, - name: "Onboarding Survey", welcomeCard: null, questions: [], variables: [], diff --git a/packages/react-native/src/lib/common/tests/utils.test.ts b/packages/react-native/src/lib/common/tests/utils.test.ts index 25de39b..83ee6fd 100644 --- a/packages/react-native/src/lib/common/tests/utils.test.ts +++ b/packages/react-native/src/lib/common/tests/utils.test.ts @@ -135,7 +135,7 @@ describe("utils.ts", () => { { ...baseSurvey, id: mockSurveyId1, - segment: { id: mockSegmentId1 }, + segment: { id: mockSegmentId1, hasFilters: false }, } as TSurvey, ]; @@ -158,14 +158,14 @@ describe("utils.ts", () => { expect(result[0].id).toBe(mockSurveyId1); }); - test("filters out surveys that have a segment with filters if userId is not set", () => { + test("filters out surveys that have a segment with filters if userId is not set (new shape: hasFilters=true)", () => { environment.data.surveys = [ { ...baseSurvey, id: mockSurveyId1, segment: { id: mockSegmentId1, - filters: [{ type: "string", key: "name", value: "John" }], + hasFilters: true, }, } as TSurvey, ]; @@ -174,6 +174,32 @@ describe("utils.ts", () => { expect(result).toHaveLength(0); }); + test("filters out surveys with segment filters when cached payload uses legacy shape (filters array)", () => { + // Simulates an AsyncStorage payload written by an older SDK version that + // still has `segment.filters` and no `hasFilters`. The defensive check + // must fall back to the legacy shape so anonymous users don't receive + // segment-targeted surveys. + environment.data.surveys = [ + { + ...baseSurvey, + id: mockSurveyId1, + segment: { + id: mockSegmentId1, + filters: [{ type: "attribute", value: "plan" }], + }, + } as unknown as TSurvey, + { + ...baseSurvey, + id: mockSurveyId2, + segment: { id: mockSegmentId2, filters: [] }, + } as unknown as TSurvey, + ]; + + const result = filterSurveys(environment, user); + expect(result).toHaveLength(1); + expect(result[0].id).toBe(mockSurveyId2); + }); + test("includes surveys without segment filters for anonymous users", () => { environment.data.surveys = [ { @@ -184,7 +210,7 @@ describe("utils.ts", () => { { ...baseSurvey, id: mockSurveyId2, - segment: { id: mockSegmentId1 }, // Segment but no filters + segment: { id: mockSegmentId1, hasFilters: false }, // Segment but no filters } as TSurvey, ]; @@ -276,13 +302,13 @@ describe("utils.ts", () => { { ...baseSurvey, id: mockSurveyId1, - segment: { id: mockSegmentId1 }, + segment: { id: mockSegmentId1, hasFilters: false }, displayOption: "respondMultiple", } as TSurvey, { ...baseSurvey, id: mockSurveyId2, - segment: { id: mockSegmentId2 }, + segment: { id: mockSegmentId2, hasFilters: false }, displayOption: "respondMultiple", } as TSurvey, ]; diff --git a/packages/react-native/src/lib/common/utils.ts b/packages/react-native/src/lib/common/utils.ts index 0ee5386..21c47f5 100644 --- a/packages/react-native/src/lib/common/utils.ts +++ b/packages/react-native/src/lib/common/utils.ts @@ -29,6 +29,22 @@ export const wrapThrowsAsync = } }; +/** + * Detect whether a survey's segment has filters. Handles both the current + * minimal shape (`{ id, hasFilters }`) and the legacy shape with a `filters` + * array — older SDKs cached the full segment in AsyncStorage and may still be + * read back here within the cache window after an SDK upgrade. + */ +export const surveyHasSegmentFilters = (survey: TSurvey): boolean => { + const segment = survey.segment as + | { hasFilters?: boolean; filters?: unknown[] } + | null + | undefined; + if (!segment) return false; + if (typeof segment.hasFilters === "boolean") return segment.hasFilters; + return Array.isArray(segment.filters) && segment.filters.length > 0; +}; + /** * Filters surveys based on the displayOption, recontactDays, and segments * @param environmentSate - The environment state @@ -111,11 +127,7 @@ export const filterSurveys = ( if (!userId) { // exclude surveys that have a segment with filters return filteredSurveys.filter((survey) => { - const segmentFilters = survey.segment?.filters; - const segmentFiltersLength = Array.isArray(segmentFilters) - ? segmentFilters.length - : 0; - return segmentFiltersLength === 0; + return !surveyHasSegmentFilters(survey); }); } diff --git a/packages/react-native/src/lib/survey/action.ts b/packages/react-native/src/lib/survey/action.ts index e5c8c8f..2d92496 100644 --- a/packages/react-native/src/lib/survey/action.ts +++ b/packages/react-native/src/lib/survey/action.ts @@ -27,7 +27,7 @@ export const triggerSurvey = (survey: TSurvey): void => { ); if (!shouldDisplaySurvey) { logger.debug( - `Survey display of "${survey.name}" skipped based on displayPercentage.`, + `Survey display of "${survey.id}" skipped based on displayPercentage.`, ); return; // skip displaying the survey } diff --git a/packages/react-native/src/lib/survey/tests/__mocks__/store.mock.ts b/packages/react-native/src/lib/survey/tests/__mocks__/store.mock.ts index 8b0aefc..d11b850 100644 --- a/packages/react-native/src/lib/survey/tests/__mocks__/store.mock.ts +++ b/packages/react-native/src/lib/survey/tests/__mocks__/store.mock.ts @@ -1,2 +1 @@ export const mockSurveyId = "jgocyoxk9uifo6u381qahmes"; -export const mockSurveyName = "Test Survey"; diff --git a/packages/react-native/src/lib/survey/tests/action.test.ts b/packages/react-native/src/lib/survey/tests/action.test.ts index f8fd706..312ce7c 100644 --- a/packages/react-native/src/lib/survey/tests/action.test.ts +++ b/packages/react-native/src/lib/survey/tests/action.test.ts @@ -45,7 +45,6 @@ vi.mock("@react-native-community/netinfo", () => ({ describe("survey/action.ts", () => { const mockSurvey = { id: "survey_001", - name: "Test Survey", displayPercentage: 50, triggers: [ { @@ -95,7 +94,7 @@ describe("survey/action.ts", () => { // Ensure survey is not set expect(mockSurveyStore.setSurvey).not.toHaveBeenCalled(); expect(mockLogger.debug).toHaveBeenCalledWith( - 'Survey display of "Test Survey" skipped based on displayPercentage.', + 'Survey display of "survey_001" skipped based on displayPercentage.', ); }); diff --git a/packages/react-native/src/lib/survey/tests/store.test.ts b/packages/react-native/src/lib/survey/tests/store.test.ts index 3933ed6..a7ecd6f 100644 --- a/packages/react-native/src/lib/survey/tests/store.test.ts +++ b/packages/react-native/src/lib/survey/tests/store.test.ts @@ -1,9 +1,6 @@ import { beforeEach, describe, expect, test, vi } from "vitest"; import { SurveyStore } from "@/lib/survey/store"; -import { - mockSurveyId, - mockSurveyName, -} from "@/lib/survey/tests/__mocks__/store.mock"; +import { mockSurveyId } from "@/lib/survey/tests/__mocks__/store.mock"; import type { TSurvey } from "@/types/survey"; describe("SurveyStore", () => { @@ -32,8 +29,8 @@ describe("SurveyStore", () => { test("returns current survey when set", () => { const mockSurvey: TSurvey = { id: mockSurveyId, - name: mockSurveyName, - } as TSurvey; + displayPercentage: 100, + } as unknown as TSurvey; store.setSurvey(mockSurvey); expect(store.getSurvey()).toBe(mockSurvey); @@ -45,8 +42,8 @@ describe("SurveyStore", () => { const listener = vi.fn(); const mockSurvey: TSurvey = { id: mockSurveyId, - name: mockSurveyName, - } as TSurvey; + displayPercentage: 100, + } as unknown as TSurvey; store.subscribe(listener); store.setSurvey(mockSurvey); @@ -59,8 +56,8 @@ describe("SurveyStore", () => { const listener = vi.fn(); const mockSurvey: TSurvey = { id: mockSurveyId, - name: mockSurveyName, - } as TSurvey; + displayPercentage: 100, + } as unknown as TSurvey; store.setSurvey(mockSurvey); store.subscribe(listener); @@ -75,8 +72,8 @@ describe("SurveyStore", () => { const listener = vi.fn(); const mockSurvey: TSurvey = { id: mockSurveyId, - name: mockSurveyName, - } as TSurvey; + displayPercentage: 100, + } as unknown as TSurvey; store.setSurvey(mockSurvey); store.subscribe(listener); @@ -101,8 +98,8 @@ describe("SurveyStore", () => { const listener = vi.fn(); const mockSurvey: TSurvey = { id: mockSurveyId, - name: mockSurveyName, - } as TSurvey; + displayPercentage: 100, + } as unknown as TSurvey; const unsubscribe = store.subscribe(listener); store.setSurvey(mockSurvey); @@ -111,8 +108,8 @@ describe("SurveyStore", () => { unsubscribe(); store.setSurvey({ ...mockSurvey, - name: "Updated Survey", - } as TSurvey); + displayPercentage: 50, + } as unknown as TSurvey); expect(listener).toHaveBeenCalledTimes(1); // Still 1, not called after unsubscribe }); @@ -121,8 +118,8 @@ describe("SurveyStore", () => { const listener2 = vi.fn(); const mockSurvey: TSurvey = { id: mockSurveyId, - name: mockSurveyName, - } as TSurvey; + displayPercentage: 100, + } as unknown as TSurvey; store.subscribe(listener1); store.subscribe(listener2); diff --git a/packages/react-native/src/types/survey.ts b/packages/react-native/src/types/survey.ts index 006c513..6ebafce 100644 --- a/packages/react-native/src/types/survey.ts +++ b/packages/react-native/src/types/survey.ts @@ -83,7 +83,6 @@ export interface SurveyContainerProps export interface TSurvey { id: string; - name: string; welcomeCard: { enabled: boolean; headline?: Record; @@ -283,17 +282,11 @@ export interface TSurvey { environmentId: string; }; }[]; + // Minimal segment shape — full filter logic is evaluated server-side and must not reach the client segment?: { id: string; - title: string; - description: string | null; - isPrivate: boolean; - filters: unknown; // recursive, optional to expand - environmentId: string; - createdAt: string; - updatedAt: string; - surveys: string[]; - }; + hasFilters: boolean; + } | null; displayPercentage: number; styling?: { brandColor?: {