-
Notifications
You must be signed in to change notification settings - Fork 50
feat(web): streamline login for returning users #1167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 10 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
abe92dd
feat(web): add HAS_COMPLETED_SIGNUP key to localStorage and update Lo…
tyler-dane 21de560
feat(web): implement useHasCompletedSignup hook and integrate into Lo…
tyler-dane 71bb28e
refactor(web): clean up imports in OnboardingStep component
tyler-dane e937d3a
test(web): add unit tests for OnboardingFlow component
tyler-dane b35b310
refactor(web): simplify Onboarding component structure and improve re…
tyler-dane 73c8f35
refactor(web): rename withProvider to withOnboardingProvider for clarity
tyler-dane ac80173
feat(web): enhance onboarding flow for returning users and add loadin…
tyler-dane 10b6375
feat(backend): include user email in session creation and update auth…
tyler-dane ecedeab
test(web): remove redundant test for new user signup in LoginView
tyler-dane 4248916
test(web): add unit tests for UserProvider component with PostHog int…
tyler-dane 917044f
refactor(web): remove unnecessary comments in OnboardingFlow component
tyler-dane File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,246 @@ | ||
| import { usePostHog } from "posthog-js/react"; | ||
| import Session from "supertokens-auth-react/recipe/session"; | ||
| import "@testing-library/jest-dom"; | ||
| import { render, waitFor } from "@testing-library/react"; | ||
| import { UserProvider } from "./UserProvider"; | ||
|
|
||
| // Mock PostHog | ||
| jest.mock("posthog-js/react"); | ||
| const mockUsePostHog = usePostHog as jest.MockedFunction<typeof usePostHog>; | ||
|
|
||
| // Mock SuperTokens Session | ||
| jest.mock("supertokens-auth-react/recipe/session"); | ||
| const mockSession = Session as jest.Mocked<typeof Session>; | ||
|
|
||
| // Mock AbsoluteOverflowLoader | ||
| jest.mock("@web/components/AbsoluteOverflowLoader", () => ({ | ||
| AbsoluteOverflowLoader: () => <div>Loading...</div>, | ||
| })); | ||
|
|
||
| describe("UserProvider", () => { | ||
| const mockIdentify = jest.fn(); | ||
| const mockGetAccessTokenPayloadSecurely = jest.fn(); | ||
|
|
||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
|
|
||
| // Default mock implementation | ||
| mockSession.getAccessTokenPayloadSecurely = | ||
| mockGetAccessTokenPayloadSecurely; | ||
| }); | ||
|
|
||
| describe("PostHog Integration", () => { | ||
| it("should call posthog.identify when PostHog is enabled and user data is available", async () => { | ||
| const testUserId = "test-user-123"; | ||
| const testEmail = "test@example.com"; | ||
|
|
||
| // Mock session with userId and email | ||
| mockGetAccessTokenPayloadSecurely.mockResolvedValue({ | ||
| sub: testUserId, | ||
| email: testEmail, | ||
| }); | ||
|
|
||
| // Mock PostHog as enabled | ||
| mockUsePostHog.mockReturnValue({ | ||
| identify: mockIdentify, | ||
| } as any); | ||
|
|
||
| render( | ||
| <UserProvider> | ||
| <div>Test Child</div> | ||
| </UserProvider>, | ||
| ); | ||
|
|
||
| // Wait for async data fetch and PostHog identify to be called | ||
| await waitFor(() => { | ||
| expect(mockIdentify).toHaveBeenCalledWith(testEmail, { | ||
| email: testEmail, | ||
| userId: testUserId, | ||
| }); | ||
| }); | ||
|
|
||
| // Verify it was called exactly once | ||
| expect(mockIdentify).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it("should NOT call posthog.identify when PostHog is disabled", async () => { | ||
| const testUserId = "test-user-123"; | ||
| const testEmail = "test@example.com"; | ||
|
|
||
| // Mock session with userId and email | ||
| mockGetAccessTokenPayloadSecurely.mockResolvedValue({ | ||
| sub: testUserId, | ||
| email: testEmail, | ||
| }); | ||
|
|
||
| // Mock PostHog as disabled (returns undefined/null) | ||
| mockUsePostHog.mockReturnValue(undefined as any); | ||
|
|
||
| render( | ||
| <UserProvider> | ||
| <div>Test Child</div> | ||
| </UserProvider>, | ||
| ); | ||
|
|
||
| // Wait a bit to ensure no identify call happens | ||
| await waitFor(() => { | ||
| expect(mockGetAccessTokenPayloadSecurely).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| // Verify identify was never called | ||
| expect(mockIdentify).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("should NOT call posthog.identify when email is missing from session", async () => { | ||
| const testUserId = "test-user-123"; | ||
|
|
||
| // Mock session with userId but NO email | ||
| mockGetAccessTokenPayloadSecurely.mockResolvedValue({ | ||
| sub: testUserId, | ||
| // email is missing | ||
| }); | ||
|
|
||
| // Mock PostHog as enabled | ||
| mockUsePostHog.mockReturnValue({ | ||
| identify: mockIdentify, | ||
| } as any); | ||
|
|
||
| render( | ||
| <UserProvider> | ||
| <div>Test Child</div> | ||
| </UserProvider>, | ||
| ); | ||
|
|
||
| // Wait for data fetch | ||
| await waitFor(() => { | ||
| expect(mockGetAccessTokenPayloadSecurely).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| // Verify identify was not called because email is missing | ||
| expect(mockIdentify).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("should NOT call posthog.identify when userId is missing", async () => { | ||
| const testEmail = "test@example.com"; | ||
|
|
||
| // Mock session with email but NO userId (sub) | ||
| mockGetAccessTokenPayloadSecurely.mockResolvedValue({ | ||
| email: testEmail, | ||
| // sub is missing | ||
| }); | ||
|
|
||
| // Mock PostHog as enabled | ||
| mockUsePostHog.mockReturnValue({ | ||
| identify: mockIdentify, | ||
| } as any); | ||
|
|
||
| render( | ||
| <UserProvider> | ||
| <div>Test Child</div> | ||
| </UserProvider>, | ||
| ); | ||
|
|
||
| // The component should show loading state because userId is null | ||
| // and identify should never be called | ||
| await waitFor(() => { | ||
| expect(mockGetAccessTokenPayloadSecurely).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| expect(mockIdentify).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("should handle posthog.identify not being a function gracefully", async () => { | ||
| const testUserId = "test-user-123"; | ||
| const testEmail = "test@example.com"; | ||
|
|
||
| // Mock session with userId and email | ||
| mockGetAccessTokenPayloadSecurely.mockResolvedValue({ | ||
| sub: testUserId, | ||
| email: testEmail, | ||
| }); | ||
|
|
||
| // Mock PostHog with identify not being a function | ||
| mockUsePostHog.mockReturnValue({ | ||
| identify: null, | ||
| } as any); | ||
|
|
||
| // Should not throw an error | ||
| expect(() => { | ||
| render( | ||
| <UserProvider> | ||
| <div>Test Child</div> | ||
| </UserProvider>, | ||
| ); | ||
| }).not.toThrow(); | ||
|
|
||
| await waitFor(() => { | ||
| expect(mockGetAccessTokenPayloadSecurely).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| // Verify identify was not called | ||
| expect(mockIdentify).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("should render children after user data is loaded", async () => { | ||
| const testUserId = "test-user-123"; | ||
| const testEmail = "test@example.com"; | ||
|
|
||
| mockGetAccessTokenPayloadSecurely.mockResolvedValue({ | ||
| sub: testUserId, | ||
| email: testEmail, | ||
| }); | ||
|
|
||
| mockUsePostHog.mockReturnValue({ | ||
| identify: mockIdentify, | ||
| } as any); | ||
|
|
||
| const { getByText } = render( | ||
| <UserProvider> | ||
| <div>Test Child Content</div> | ||
| </UserProvider>, | ||
| ); | ||
|
|
||
| // Initially should show loading | ||
| expect(getByText("Loading...")).toBeInTheDocument(); | ||
|
|
||
| // After data loads, should show children | ||
| await waitFor(() => { | ||
| expect(getByText("Test Child Content")).toBeInTheDocument(); | ||
| }); | ||
| }); | ||
|
|
||
| it("should handle session fetch errors gracefully", async () => { | ||
| const consoleErrorSpy = jest | ||
| .spyOn(console, "error") | ||
| .mockImplementation(() => {}); | ||
|
|
||
| // Mock session to throw an error | ||
| mockGetAccessTokenPayloadSecurely.mockRejectedValue( | ||
| new Error("Session error"), | ||
| ); | ||
|
|
||
| mockUsePostHog.mockReturnValue({ | ||
| identify: mockIdentify, | ||
| } as any); | ||
|
|
||
| render( | ||
| <UserProvider> | ||
| <div>Test Child</div> | ||
| </UserProvider>, | ||
| ); | ||
|
|
||
| // Should log error | ||
| await waitFor(() => { | ||
| expect(consoleErrorSpy).toHaveBeenCalledWith( | ||
| "Failed to get user because:", | ||
| expect.any(Error), | ||
| ); | ||
| }); | ||
|
|
||
| // Should not call identify | ||
| expect(mockIdentify).not.toHaveBeenCalled(); | ||
|
|
||
| consoleErrorSpy.mockRestore(); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.