Skip to content

Commit d58341b

Browse files
ItsnotakaMarfuen
andauthored
feat: Added logout function to onboarding/setup (#1914)
* chore(bun.lock): downgrade configVersion from 1 to 0 * chore(api): update tsconfig path mappings and output directory * feat(layout): add variant prop to MinimalHeader for onboarding and setup --------- Co-authored-by: Mariano Fuentes <[email protected]>
1 parent 10cf6b1 commit d58341b

File tree

7 files changed

+225
-12
lines changed

7 files changed

+225
-12
lines changed

apps/api/tsconfig.json

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
"allowSyntheticDefaultImports": true,
1313
"target": "esnext",
1414
"sourceMap": true,
15-
"outDir": "./dist",
16-
"baseUrl": "./",
15+
"outDir": "dist",
16+
"baseUrl": ".",
1717
"incremental": true,
1818
"skipLibCheck": true,
1919
"strictNullChecks": true,
@@ -23,13 +23,7 @@
2323
"noFallthroughCasesInSwitch": false,
2424
"paths": {
2525
"@/*": ["./src/*"],
26-
"@db": ["./prisma/index"],
27-
"@comp/integration-platform": [
28-
"../../packages/integration-platform/src/index.ts"
29-
],
30-
"@comp/integration-platform/*": [
31-
"../../packages/integration-platform/src/*"
32-
]
26+
"@db": ["./prisma/index"]
3327
},
3428
"jsx": "react-jsx"
3529
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { expect, test } from '@playwright/test';
2+
import { authenticateTestUser, clearAuth, grantAccess } from '../utils/auth-helpers';
3+
import { generateTestData } from '../utils/helpers';
4+
5+
test.describe('Onboarding Sign Out Flow', () => {
6+
test.setTimeout(60000);
7+
8+
test.beforeEach(async ({ page }) => {
9+
await clearAuth(page);
10+
});
11+
12+
test('user can sign out from onboarding to switch email', async ({ page }) => {
13+
const testData = generateTestData();
14+
const website = `example${Date.now()}.com`;
15+
16+
await authenticateTestUser(page, {
17+
email: testData.email,
18+
name: testData.userName,
19+
skipOrg: true,
20+
hasAccess: true,
21+
});
22+
23+
await page.goto('/setup');
24+
await page.waitForURL(/\/setup\/[a-zA-Z0-9]+/);
25+
26+
await page.waitForSelector('input[type="checkbox"]:checked');
27+
await page.waitForFunction(
28+
() => {
29+
const button = document.querySelector(
30+
'[data-testid="setup-next-button"]',
31+
) as HTMLButtonElement;
32+
return button && !button.disabled && button.offsetParent !== null;
33+
},
34+
{ timeout: 10000 },
35+
);
36+
await page.waitForTimeout(500);
37+
await page.getByTestId('setup-next-button').click();
38+
39+
await page.getByPlaceholder('e.g., Acme Inc.').fill(testData.organizationName);
40+
await page.waitForFunction(
41+
() => {
42+
const button = document.querySelector(
43+
'[data-testid="setup-next-button"]',
44+
) as HTMLButtonElement;
45+
return button && !button.disabled && button.offsetParent !== null;
46+
},
47+
{ timeout: 5000 },
48+
);
49+
await page.waitForTimeout(300);
50+
await page.getByTestId('setup-next-button').click();
51+
52+
await page.getByPlaceholder('example.com').fill(website);
53+
await Promise.all([
54+
page.waitForURL(/\/upgrade\/org_/),
55+
page.getByTestId('setup-finish-button').click(),
56+
]);
57+
58+
const orgId = page.url().match(/org_[a-zA-Z0-9]+/)?.[0];
59+
expect(orgId).toBeTruthy();
60+
61+
await grantAccess(page, orgId!, true);
62+
await page.reload();
63+
64+
await expect(page).toHaveURL(`/onboarding/${orgId!}`);
65+
await expect(page.getByText('Step 1 of 9')).toBeVisible();
66+
67+
const avatarTrigger = page.getByTestId('onboarding-user-menu-trigger');
68+
await expect(avatarTrigger).toBeVisible();
69+
await avatarTrigger.click();
70+
71+
const signOutButton = page.getByTestId('onboarding-sign-out');
72+
await expect(signOutButton).toBeVisible();
73+
await signOutButton.click();
74+
75+
await expect(page).toHaveURL(/\/auth/);
76+
77+
await page.goto(`/onboarding/${orgId!}`);
78+
await expect(page).toHaveURL(/\/auth/);
79+
});
80+
81+
test('onboarding avatar menu displays user info', async ({ page }) => {
82+
const testData = generateTestData();
83+
const website = `example${Date.now()}.com`;
84+
85+
await authenticateTestUser(page, {
86+
email: testData.email,
87+
name: testData.userName,
88+
skipOrg: true,
89+
hasAccess: true,
90+
});
91+
92+
await page.goto('/setup');
93+
await page.waitForURL(/\/setup\/[a-zA-Z0-9]+/);
94+
95+
await page.waitForSelector('input[type="checkbox"]:checked');
96+
await page.waitForFunction(
97+
() => {
98+
const button = document.querySelector(
99+
'[data-testid="setup-next-button"]',
100+
) as HTMLButtonElement;
101+
return button && !button.disabled && button.offsetParent !== null;
102+
},
103+
{ timeout: 10000 },
104+
);
105+
await page.waitForTimeout(500);
106+
await page.getByTestId('setup-next-button').click();
107+
108+
await page.getByPlaceholder('e.g., Acme Inc.').fill(testData.organizationName);
109+
await page.waitForFunction(
110+
() => {
111+
const button = document.querySelector(
112+
'[data-testid="setup-next-button"]',
113+
) as HTMLButtonElement;
114+
return button && !button.disabled && button.offsetParent !== null;
115+
},
116+
{ timeout: 5000 },
117+
);
118+
await page.waitForTimeout(300);
119+
await page.getByTestId('setup-next-button').click();
120+
121+
await page.getByPlaceholder('example.com').fill(website);
122+
await Promise.all([
123+
page.waitForURL(/\/upgrade\/org_/),
124+
page.getByTestId('setup-finish-button').click(),
125+
]);
126+
127+
const orgId = page.url().match(/org_[a-zA-Z0-9]+/)?.[0];
128+
expect(orgId).toBeTruthy();
129+
130+
await grantAccess(page, orgId!, true);
131+
await page.reload();
132+
133+
await expect(page).toHaveURL(`/onboarding/${orgId!}`);
134+
135+
const avatarTrigger = page.getByTestId('onboarding-user-menu-trigger');
136+
await expect(avatarTrigger).toBeVisible();
137+
await avatarTrigger.click();
138+
139+
await expect(page.getByText(testData.userName)).toBeVisible();
140+
await expect(page.getByText(testData.email)).toBeVisible();
141+
});
142+
});

apps/app/src/app/(app)/onboarding/[orgId]/layout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export default async function OnboardingRouteLayout({
5151
user={session.user}
5252
organizations={[]}
5353
currentOrganization={organization}
54+
variant="onboarding"
5455
/>
5556
{children}
5657
</div>

apps/app/src/app/(app)/setup/[setupId]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export default async function SetupWithIdPage({ params, searchParams }: SetupPag
4545
<div className="flex flex-1 min-h-0">
4646
{/* Form Section - Left Side */}
4747
<div className="flex-1 flex flex-col">
48-
<MinimalHeader user={user} organizations={[]} currentOrganization={null} />
48+
<MinimalHeader user={user} organizations={[]} currentOrganization={null} variant="setup" />
4949

5050
<OrganizationSetupForm
5151
setupId={setupId}

apps/app/src/components/layout/MinimalHeader.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { User } from 'better-auth';
77
import { useAction } from 'next-safe-action/hooks';
88
import Link from 'next/link';
99
import { useRouter } from 'next/navigation';
10+
import { OnboardingUserMenu } from './OnboardingUserMenu';
1011

1112
interface MinimalHeaderProps {
1213
user: User;
@@ -35,10 +36,13 @@ export function MinimalHeader({
3536
const hasExistingOrgs = organizations.length > 0;
3637

3738
return (
38-
<header className="sticky top-0 z-10 bg-background flex items-center h-[90px] w-full px-4 md:px-18">
39+
<header className="sticky top-0 z-10 bg-background flex items-center justify-between h-[90px] w-full px-4 md:px-18">
3940
<Link href="/" className="flex items-center">
4041
<Logo />
4142
</Link>
43+
{(variant === 'onboarding' || variant === 'setup') && (
44+
<OnboardingUserMenu user={user} />
45+
)}
4246
</header>
4347
);
4448
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
'use client';
2+
3+
import { authClient } from '@/utils/auth-client';
4+
import { Avatar, AvatarFallback, AvatarImageNext } from '@comp/ui/avatar';
5+
import {
6+
DropdownMenu,
7+
DropdownMenuContent,
8+
DropdownMenuItem,
9+
DropdownMenuLabel,
10+
DropdownMenuSeparator,
11+
DropdownMenuTrigger,
12+
} from '@comp/ui/dropdown-menu';
13+
import type { User } from 'better-auth';
14+
import { useRouter } from 'next/navigation';
15+
import { useState } from 'react';
16+
17+
interface OnboardingUserMenuProps {
18+
user: User;
19+
}
20+
21+
export function OnboardingUserMenu({ user }: OnboardingUserMenuProps) {
22+
const router = useRouter();
23+
const [isSigningOut, setIsSigningOut] = useState(false);
24+
25+
const handleSignOut = async () => {
26+
setIsSigningOut(true);
27+
await authClient.signOut({
28+
fetchOptions: {
29+
onSuccess: () => {
30+
router.push('/auth');
31+
},
32+
},
33+
});
34+
};
35+
36+
return (
37+
<DropdownMenu>
38+
<DropdownMenuTrigger asChild data-testid="onboarding-user-menu-trigger">
39+
<Avatar className="h-8 w-8 cursor-pointer rounded-full">
40+
{user.image && (
41+
<AvatarImageNext
42+
src={user.image}
43+
alt={user.name ?? user.email ?? ''}
44+
width={32}
45+
height={32}
46+
quality={100}
47+
/>
48+
)}
49+
<AvatarFallback>
50+
<span className="text-xs">
51+
{user.name?.charAt(0)?.toUpperCase() || user.email?.charAt(0)?.toUpperCase()}
52+
</span>
53+
</AvatarFallback>
54+
</Avatar>
55+
</DropdownMenuTrigger>
56+
<DropdownMenuContent className="w-[240px]" sideOffset={10} align="end">
57+
<DropdownMenuLabel>
58+
<div className="flex items-center justify-between">
59+
<div className="flex flex-col">
60+
<span className="line-clamp-1 block max-w-[155px] truncate">{user.name}</span>
61+
<span className="truncate text-xs font-normal text-[#606060]">{user.email}</span>
62+
</div>
63+
</div>
64+
</DropdownMenuLabel>
65+
<DropdownMenuSeparator />
66+
<DropdownMenuItem onClick={handleSignOut} disabled={isSigningOut} data-testid="onboarding-sign-out">
67+
{isSigningOut ? 'Signing out...' : 'Sign out'}
68+
</DropdownMenuItem>
69+
</DropdownMenuContent>
70+
</DropdownMenu>
71+
);
72+
}

bun.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"lockfileVersion": 1,
3-
"configVersion": 1,
3+
"configVersion": 0,
44
"workspaces": {
55
"": {
66
"name": "comp",

0 commit comments

Comments
 (0)