Skip to content

Commit 5fc0af9

Browse files
authored
feat: adds option for user identification control (#744)
1 parent 54fdf51 commit 5fc0af9

File tree

8 files changed

+484
-2
lines changed

8 files changed

+484
-2
lines changed

.changeset/cool-shrimps-cough.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@knocklabs/react-core": minor
3+
"@knocklabs/client": minor
4+
---
5+
6+
feat: adds `identificationStrategy` option for user identification control

packages/client/src/interfaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export type UserTokenExpiringCallback = (
5454
export interface AuthenticateOptions {
5555
onUserTokenExpiring?: UserTokenExpiringCallback;
5656
timeBeforeExpirationInMs?: number;
57+
identificationStrategy?: "inline" | "skip";
5758
}
5859

5960
export interface BulkOperation {

packages/client/src/knock.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ class Knock {
8686
let reinitializeApi = false;
8787
const currentApiClient = this.apiClient;
8888
const userId = this.getUserId(userIdOrUserWithProperties);
89+
const identificationStrategy = options?.identificationStrategy || "inline";
8990

9091
// If we've previously been initialized and the values have now changed, then we
9192
// need to reinitialize any stateful connections we have
@@ -120,11 +121,20 @@ class Knock {
120121
this.log("Reinitialized real-time connections");
121122
}
122123

123-
// Inline identify the user if we've been given an object with an id.
124+
// We explicitly skip the inline identification if the strategy is set to "skip"
125+
if (identificationStrategy === "skip") {
126+
this.log("Skipping inline user identification");
127+
return;
128+
}
129+
130+
// Inline identify the user if we've been given an object with an id
131+
// and the strategy is set to "inline".
124132
if (
133+
identificationStrategy === "inline" &&
125134
typeof userIdOrUserWithProperties === "object" &&
126135
userIdOrUserWithProperties?.id
127136
) {
137+
this.log(`Identifying user ${userIdOrUserWithProperties.id} inline`);
128138
const { id, ...properties } = userIdOrUserWithProperties;
129139
this.user.identify(properties);
130140
}

packages/client/test/knock.test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,118 @@ describe("Knock Client", () => {
195195
});
196196
});
197197

198+
describe("Inline identification strategy", () => {
199+
test("defaults to inline identification when no strategy is specified", () => {
200+
const knock = new Knock("pk_test_12345");
201+
const identify = vi.spyOn(knock.user, "identify");
202+
203+
knock.authenticate(
204+
{ id: "user_123", name: "John Doe", email: "john@example.com" },
205+
"token_456",
206+
);
207+
208+
expect(identify).toHaveBeenCalledWith({
209+
name: "John Doe",
210+
email: "john@example.com",
211+
});
212+
});
213+
214+
test("performs inline identification when strategy is explicitly set to 'inline'", () => {
215+
const knock = new Knock("pk_test_12345");
216+
const identify = vi.spyOn(knock.user, "identify");
217+
218+
knock.authenticate(
219+
{ id: "user_123", name: "John Doe", email: "john@example.com" },
220+
"token_456",
221+
{
222+
identificationStrategy: "inline",
223+
},
224+
);
225+
226+
expect(identify).toHaveBeenCalledWith({
227+
name: "John Doe",
228+
email: "john@example.com",
229+
});
230+
});
231+
232+
test("skips inline identification when strategy is set to 'skip'", () => {
233+
const knock = new Knock("pk_test_12345");
234+
const identify = vi.spyOn(knock.user, "identify");
235+
236+
knock.authenticate(
237+
{ id: "user_123", name: "John Doe", email: "john@example.com" },
238+
"token_456",
239+
{
240+
identificationStrategy: "skip",
241+
},
242+
);
243+
244+
expect(identify).not.toHaveBeenCalled();
245+
});
246+
247+
test("does not identify when authenticating with string userId regardless of strategy", () => {
248+
const knock = new Knock("pk_test_12345");
249+
const identify = vi.spyOn(knock.user, "identify");
250+
251+
knock.authenticate("user_123", "token_456", {
252+
identificationStrategy: "inline",
253+
});
254+
255+
expect(identify).not.toHaveBeenCalled();
256+
});
257+
258+
test("does not identify with string userId when strategy is 'skip'", () => {
259+
const knock = new Knock("pk_test_12345");
260+
const identify = vi.spyOn(knock.user, "identify");
261+
262+
knock.authenticate("user_123", "token_456", {
263+
identificationStrategy: "skip",
264+
});
265+
266+
expect(identify).not.toHaveBeenCalled();
267+
});
268+
269+
test("logs appropriate message when skipping identification", () => {
270+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
271+
272+
try {
273+
const knock = new Knock("pk_test_12345", { logLevel: "debug" });
274+
const identify = vi.spyOn(knock.user, "identify");
275+
276+
knock.authenticate({ id: "user_123", name: "John Doe" }, "token_456", {
277+
identificationStrategy: "skip",
278+
});
279+
280+
expect(consoleSpy).toHaveBeenCalledWith(
281+
"[Knock] Skipping inline user identification",
282+
);
283+
expect(identify).not.toHaveBeenCalled();
284+
} finally {
285+
consoleSpy.mockRestore();
286+
}
287+
});
288+
289+
test("logs appropriate message when performing inline identification", () => {
290+
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
291+
292+
try {
293+
const knock = new Knock("pk_test_12345", { logLevel: "debug" });
294+
const identify = vi.spyOn(knock.user, "identify");
295+
296+
knock.authenticate({ id: "user_123", name: "John Doe" }, "token_456", {
297+
identificationStrategy: "inline",
298+
});
299+
300+
expect(consoleSpy).toHaveBeenCalledWith(
301+
"[Knock] Identifying user user_123 inline",
302+
);
303+
expect(identify).toHaveBeenCalled();
304+
} finally {
305+
consoleSpy.mockRestore();
306+
}
307+
});
308+
});
309+
198310
describe("Client Management", () => {
199311
test("provides API client after setup", () => {
200312
const { knock, mockApiClient } = createMockKnock();

packages/react-core/src/modules/core/context/KnockProvider.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@ export type KnockProviderProps = {
3737
*/
3838
userId: Knock["userId"];
3939
user?: never;
40+
identificationStrategy?: never;
4041
}
4142
| {
4243
user: UserWithProperties;
44+
identificationStrategy?: AuthenticateOptions["identificationStrategy"];
4345
/**
4446
* @deprecated The `userId` prop is deprecated and will be removed in a future version.
4547
* Please pass the `user` prop instead containing an `id` value.
@@ -61,6 +63,7 @@ export const KnockProvider: React.FC<PropsWithChildren<KnockProviderProps>> = ({
6163
timeBeforeExpirationInMs,
6264
children,
6365
i18n,
66+
identificationStrategy,
6467
...props
6568
}) => {
6669
const userIdOrUserWithProperties = props?.user || props?.userId;
@@ -72,8 +75,15 @@ export const KnockProvider: React.FC<PropsWithChildren<KnockProviderProps>> = ({
7275
onUserTokenExpiring,
7376
timeBeforeExpirationInMs,
7477
logLevel,
78+
identificationStrategy,
7579
}),
76-
[host, onUserTokenExpiring, timeBeforeExpirationInMs, logLevel],
80+
[
81+
host,
82+
onUserTokenExpiring,
83+
timeBeforeExpirationInMs,
84+
logLevel,
85+
identificationStrategy,
86+
],
7787
);
7888

7989
const knock = useAuthenticatedKnockClient(

packages/react-core/src/modules/core/hooks/useAuthenticatedKnockClient.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ function authenticateWithOptions(
1717
knock.authenticate(userIdOrUserWithProperties, userToken, {
1818
onUserTokenExpiring: options?.onUserTokenExpiring,
1919
timeBeforeExpirationInMs: options?.timeBeforeExpirationInMs,
20+
identificationStrategy: options?.identificationStrategy,
2021
});
2122
}
2223

0 commit comments

Comments
 (0)