Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/gentle-context-updates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@reflag/browser-sdk": patch
"@reflag/react-sdk": patch
"@reflag/vue-sdk": patch
---

Accept JSON-compatible attributes consistently in context update methods and React/Vue update hooks. Typed user and company contexts can be passed directly, and partial updates still work. Keep user/company IDs restricted to strings or numbers. Also align other-context updates and deprecated provider props with the existing context types.
16 changes: 10 additions & 6 deletions packages/browser-sdk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { deepEqual } from "fast-equals";

import { BulkEvent, BulkQueue } from "./bulkQueue";
import { API_BASE_URL, APP_BASE_URL, IS_SERVER, SDK_VERSION } from "./config";
import { ReflagContext, ReflagDeprecatedContext } from "./context";
import {
CompanyContext,
ContextValue,
ReflagContext,
ReflagDeprecatedContext,
UserContext,
} from "./context";
import {
AutoFeedback,
Feedback,
Expand Down Expand Up @@ -841,7 +847,7 @@ export class ReflagClient {
*
* @param user
*/
async updateUser(user: { [key: string]: string | number | undefined }) {
async updateUser(user: Partial<UserContext>) {
const newUserContext = {
...this.context.user,
...user,
Expand All @@ -865,7 +871,7 @@ export class ReflagClient {
*
* @param company The company details.
*/
async updateCompany(company: { [key: string]: string | number | undefined }) {
async updateCompany(company: Partial<CompanyContext>) {
const newCompanyContext = {
...this.context.company,
...company,
Expand All @@ -889,9 +895,7 @@ export class ReflagClient {
*
* @param otherContext Additional context.
*/
async updateOtherContext(otherContext: {
[key: string]: string | number | undefined;
}) {
async updateOtherContext(otherContext: Record<string, ContextValue>) {
const newOtherContext = {
...this.context.other,
...otherContext,
Expand Down
32 changes: 32 additions & 0 deletions packages/browser-sdk/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { http, HttpResponse } from "msw";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { ReflagClient } from "../src/client";
import { CompanyContext, ContextValue, UserContext } from "../src/context";
import { FlagsClient } from "../src/flag/flags";
import { HttpClient } from "../src/httpClient";
import { deferred } from "./deferred";
Expand Down Expand Up @@ -66,6 +67,37 @@ describe("ReflagClient", () => {
vi.unstubAllGlobals();
});

it("accepts JSON-compatible values in user, company and other context updates", async () => {
const user: UserContext = {
id: "user1",
enabled: true,
tags: ["beta", 1, null],
preferences: { theme: "dark", omitted: undefined },
cleared: null,
};
const company: CompanyContext = {
id: "company1",
enterprise: false,
settings: { regions: ["eu", "us"] },
};
const other: Record<string, ContextValue> = {
preview: true,
settings: { locale: "en" },
};

await client.updateUser(user);
await client.updateCompany(company);
await client.updateOtherContext(other);
await client.updateUser({ enabled: false });

expect(client.getContext()).toEqual({
user: { ...user, enabled: false },
company,
other,
});
expect(flagClientSetContext).toHaveBeenLastCalledWith(client.getContext());
});

describe("updateUser", () => {
it("should update the user context", async () => {
// and send new user data and trigger flag update
Expand Down
8 changes: 4 additions & 4 deletions packages/react-sdk/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ export type ReflagProps = ReflagPropsBase &
* Context which is not related to a user or a company.
* @deprecated Use `context` instead, this property will be removed in the next major version
*/
otherContext?: Record<string, string | number | undefined>;
otherContext?: ReflagContext["other"];
};

/**
Expand Down Expand Up @@ -905,7 +905,7 @@ export function useSendFeedback() {
*/
export function useUpdateUser() {
const client = useClient();
return (opts: { [key: string]: string | number | undefined }) =>
return (opts: Parameters<ReflagClient["updateUser"]>[0]) =>
client.updateUser(opts);
}

Expand All @@ -925,7 +925,7 @@ export function useUpdateUser() {
export function useUpdateCompany() {
const client = useClient();

return (opts: { [key: string]: string | number | undefined }) =>
return (opts: Parameters<ReflagClient["updateCompany"]>[0]) =>
client.updateCompany(opts);
}

Expand All @@ -945,7 +945,7 @@ export function useUpdateCompany() {
*/
export function useUpdateOtherContext() {
const client = useClient();
return (opts: { [key: string]: string | number | undefined }) =>
return (opts: Parameters<ReflagClient["updateOtherContext"]>[0]) =>
client.updateOtherContext(opts);
}

Expand Down
57 changes: 57 additions & 0 deletions packages/react-sdk/test/context-update-types.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type {
CompanyContext,
ContextValue,
UserContext,
} from "@reflag/browser-sdk";
import {
ReflagProps,
useClient,
useUpdateCompany,
useUpdateOtherContext,
useUpdateUser,
} from "@reflag/react-sdk";

export function ContextUpdateTypes() {
const client = useClient();
const updateUser = useUpdateUser();
const updateCompany = useUpdateCompany();
const updateOtherContext = useUpdateOtherContext();
const user: UserContext = {
id: "user-1",
enabled: true,
tags: ["beta", 1, null],
preferences: { theme: "dark", omitted: undefined },
cleared: null,
};
const company: CompanyContext = { ...user, id: "company-1" };
const other: Record<string, ContextValue> = {
preview: true,
settings: { locale: "en" },
};

void client.updateUser(user);
void client.updateCompany(company);
void client.updateOtherContext(other);
void updateUser(user);
void updateCompany(company);
void updateOtherContext(other);
void updateUser({ enabled: false });
void updateCompany({ tags: ["enterprise"] });

const options: ReflagProps = {
publishableKey: "test-key",
otherContext: other,
};
void options;

// @ts-expect-error user IDs must remain strings or numbers
void client.updateUser({ id: true });
// @ts-expect-error company IDs must remain strings or numbers
void updateCompany({ id: { nested: "id" } });
// @ts-expect-error context values must be JSON-compatible
void updateUser({ callback: () => true });
// @ts-expect-error undefined array entries are not JSON context values
void updateOtherContext({ tags: [undefined] });

return null;
}
2 changes: 1 addition & 1 deletion packages/react-sdk/tsconfig.type-tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"@reflag/react-sdk": ["./src/index.tsx"]
}
},
"include": ["src", "test/opt-in-types.tsx"]
"include": ["src", "test/opt-in-types.tsx", "test/context-update-types.tsx"]
}
6 changes: 3 additions & 3 deletions packages/vue-sdk/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ export function useSendFeedback() {
*/
export function useUpdateUser() {
const client = useClient();
return (opts: { [key: string]: string | number | undefined }) =>
return (opts: Parameters<ReflagClient["updateUser"]>[0]) =>
client.updateUser(opts);
}

Expand All @@ -304,7 +304,7 @@ export function useUpdateUser() {
*/
export function useUpdateCompany() {
const client = useClient();
return (opts: { [key: string]: string | number | undefined }) =>
return (opts: Parameters<ReflagClient["updateCompany"]>[0]) =>
client.updateCompany(opts);
}

Expand All @@ -330,7 +330,7 @@ export function useUpdateCompany() {
*/
export function useUpdateOtherContext() {
const client = useClient();
return (opts: { [key: string]: string | number | undefined }) =>
return (opts: Parameters<ReflagClient["updateOtherContext"]>[0]) =>
client.updateOtherContext(opts);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/vue-sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export type ReflagProps = ReflagInitOptionsBase &
* Context which is not related to a user or a company.
* @deprecated Use `context` instead, this property will be removed in the next major version
*/
otherContext?: Record<string, string | number | undefined>;
otherContext?: ReflagContext["other"];
};

/**
Expand Down
57 changes: 57 additions & 0 deletions packages/vue-sdk/test/context-update-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type {
CompanyContext,
ContextValue,
UserContext,
} from "@reflag/browser-sdk";
import {
ReflagProps,
useClient,
useUpdateCompany,
useUpdateOtherContext,
useUpdateUser,
} from "@reflag/vue-sdk";

export function ContextUpdateTypes() {
const client = useClient();
const updateUser = useUpdateUser();
const updateCompany = useUpdateCompany();
const updateOtherContext = useUpdateOtherContext();
const user: UserContext = {
id: "user-1",
enabled: true,
tags: ["beta", 1, null],
preferences: { theme: "dark", omitted: undefined },
cleared: null,
};
const company: CompanyContext = { ...user, id: "company-1" };
const other: Record<string, ContextValue> = {
preview: true,
settings: { locale: "en" },
};

void client.updateUser(user);
void client.updateCompany(company);
void client.updateOtherContext(other);
void updateUser(user);
void updateCompany(company);
void updateOtherContext(other);
void updateUser({ enabled: false });
void updateCompany({ tags: ["enterprise"] });

const options: ReflagProps = {
publishableKey: "test-key",
otherContext: other,
};
void options;

// @ts-expect-error user IDs must remain strings or numbers
void client.updateUser({ id: true });
// @ts-expect-error company IDs must remain strings or numbers
void updateCompany({ id: { nested: "id" } });
// @ts-expect-error context values must be JSON-compatible
void updateUser({ callback: () => true });
// @ts-expect-error undefined array entries are not JSON context values
void updateOtherContext({ tags: [undefined] });

return updateUser;
}
2 changes: 1 addition & 1 deletion packages/vue-sdk/tsconfig.type-tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"@reflag/vue-sdk": ["./src/index.ts"]
}
},
"include": ["src", "test/opt-in-types.ts"]
"include": ["src", "test/opt-in-types.ts", "test/context-update-types.ts"]
}
Loading