diff --git a/.changeset/expo-push-reregister-on-user-switch.md b/.changeset/expo-push-reregister-on-user-switch.md new file mode 100644 index 000000000..b609a9ba5 --- /dev/null +++ b/.changeset/expo-push-reregister-on-user-switch.md @@ -0,0 +1,5 @@ +--- +"@knocklabs/expo": patch +--- + +Fix Expo push auto-registration not re-running when the signed-in user changes in place. The auto-register effect now depends on the authenticated `userId`, so switching users on the same `KnockProvider` (where `isAuthenticated` never flips) re-registers the device token against the new user's channel data. diff --git a/packages/expo/src/modules/push/KnockExpoPushNotificationProvider.tsx b/packages/expo/src/modules/push/KnockExpoPushNotificationProvider.tsx index 9b75d5d3c..07d2c3793 100644 --- a/packages/expo/src/modules/push/KnockExpoPushNotificationProvider.tsx +++ b/packages/expo/src/modules/push/KnockExpoPushNotificationProvider.tsx @@ -67,7 +67,7 @@ const InternalExpoPushNotificationProvider: React.FC< autoRegister = true, }) => { const knockClient = useKnockClient(); - const { status: authStatus } = useKnockAuthState(knockClient); + const { status: authStatus, userId } = useKnockAuthState(knockClient); const isAuthenticated = authStatus === "authenticated"; const { registerPushTokenToChannel, unregisterPushTokenFromChannel } = usePushNotifications(); @@ -182,6 +182,9 @@ const InternalExpoPushNotificationProvider: React.FC< // Gating on auth defers the OS permission prompt (prompting a logged-out user // is hostile) and avoids registering a token against no user. Because // `isAuthenticated` is a dependency, enabling auth later re-runs registration. + // `userId` is also a dependency so that an in-place user switch (same Knock + // instance re-authenticated as a different user, where `isAuthenticated` never + // flips) re-registers the device token against the new user's channel data. useEffect(() => { if (!autoRegister || !isAuthenticated) { return; @@ -210,6 +213,7 @@ const InternalExpoPushNotificationProvider: React.FC< }, [ autoRegister, isAuthenticated, + userId, knockExpoChannelId, registerForPushNotifications, registerPushTokenToChannel, diff --git a/packages/expo/test/modules/push/KnockExpoPushNotificationProvider.test.tsx b/packages/expo/test/modules/push/KnockExpoPushNotificationProvider.test.tsx index c31b8d5c6..4938e0448 100644 --- a/packages/expo/test/modules/push/KnockExpoPushNotificationProvider.test.tsx +++ b/packages/expo/test/modules/push/KnockExpoPushNotificationProvider.test.tsx @@ -79,15 +79,20 @@ const mockKnockClient = { }, }; +// Controllable signed-in user id so a test can simulate an in-place user switch +// (same Knock instance re-authenticated as a different user). +let mockUserId: string | undefined = "user_1"; + vi.mock("@knocklabs/react-core", () => ({ useKnockClient: () => mockKnockClient, // Derive auth state from the same `isAuthenticated` mock so tests only have to - // toggle it in one place. + // toggle it in one place; `mockUserId` lets a test change the signed-in user + // while staying authenticated. useKnockAuthState: () => ({ status: mockKnockClient.isAuthenticated() ? "authenticated" : "unauthenticated", - userId: mockKnockClient.isAuthenticated() ? "user_1" : undefined, + userId: mockKnockClient.isAuthenticated() ? mockUserId : undefined, userToken: undefined, }), })); @@ -669,4 +674,50 @@ describe("KnockExpoPushNotificationProvider", () => { // Should still only be called once expect(getTokenSpy).toHaveBeenCalledTimes(1); }); + + test("re-registers the push token when the authenticated user changes in place", async () => { + const NotificationsMock = await import("expo-notifications"); + const getTokenSpy = vi.mocked(NotificationsMock.getExpoPushTokenAsync); + getTokenSpy.mockClear(); + getTokenSpy.mockResolvedValue({ data: "test-token" }); + mockRegisterPushTokenToChannel.mockClear(); + + const TestChild = () =>
Test Child
; + + try { + const { rerender } = render( + + + , + ); + + // Registers once for the initial user. + await waitFor(() => { + expect(mockRegisterPushTokenToChannel).toHaveBeenCalledTimes(1); + }); + + // Switch to a different user in place: same Knock instance, still + // authenticated, only the signed-in userId changes. `isAuthenticated` + // never flips, so the userId dependency is what drives re-registration. + mockUserId = "user_2"; + rerender( + + + , + ); + + // The device token must be registered again for the new user. + await waitFor(() => { + expect(mockRegisterPushTokenToChannel).toHaveBeenCalledTimes(2); + }); + } finally { + mockUserId = "user_1"; + } + }); });