Skip to content

Commit 1f76cf4

Browse files
authored
Merge pull request #1101 from trycompai/main
[comp] Production Deploy
2 parents bc84e9a + 5c7c5d0 commit 1f76cf4

11 files changed

Lines changed: 193 additions & 61 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { expect, test } from '@playwright/test';
2+
import { authenticateTestUser, clearAuth } from '../utils/auth-helpers';
3+
import { generateTestData } from '../utils/helpers';
4+
5+
test.describe('Middleware Onboarding Behavior', () => {
6+
test.beforeEach(async ({ page }) => {
7+
await clearAuth(page);
8+
});
9+
10+
test('user with null onboardingCompleted is NOT redirected to onboarding', async ({ page }) => {
11+
const testData = generateTestData();
12+
13+
// Create user with org (onboardingCompleted will be null by default)
14+
await authenticateTestUser(page, {
15+
email: testData.email,
16+
name: testData.userName,
17+
skipOrg: false, // Creates org with null onboardingCompleted
18+
});
19+
20+
// Try to access organization page
21+
await page.goto('/');
22+
23+
// Should NOT be redirected to onboarding
24+
await page.waitForTimeout(2000); // Give time for any redirects
25+
expect(page.url()).not.toContain('/onboarding');
26+
27+
// Should be on an authenticated page (org page, frameworks, etc)
28+
const isOnOrgPage =
29+
page.url().includes('/org_') ||
30+
page.url().includes('/frameworks') ||
31+
page.url().includes('/setup');
32+
expect(isOnOrgPage).toBeTruthy();
33+
});
34+
35+
test('user without org is redirected to setup', async ({ page }) => {
36+
const testData = generateTestData();
37+
38+
// Create user without org
39+
await authenticateTestUser(page, {
40+
email: testData.email,
41+
name: testData.userName,
42+
skipOrg: true,
43+
});
44+
45+
// Navigate to root
46+
await page.goto('/');
47+
48+
// Should be redirected to setup
49+
await page.waitForURL(/\/setup\/[a-zA-Z0-9]+/, { timeout: 10000 });
50+
expect(page.url()).toContain('/setup/');
51+
});
52+
53+
test('unauthenticated user is redirected to auth', async ({ page }) => {
54+
// Try to access protected route without auth
55+
await page.goto('/org_123/frameworks');
56+
57+
// Should be redirected to auth
58+
await page.waitForURL(/\/auth/, { timeout: 5000 });
59+
expect(page.url()).toContain('/auth');
60+
});
61+
});

apps/app/e2e/tests/onboarding.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,8 @@ test.describe('Onboarding Flow', () => {
269269
await secondNextButton.click();
270270

271271
// Next button should be disabled when website is empty
272-
await expect(page.locator('button:has-text("Next")')).toBeDisabled();
272+
const nextButton = page.locator('button:has-text("Next")');
273+
await expect(nextButton).toBeDisabled();
273274

274275
// Fill website with simple text (will become valid with .com added)
275276
await fillFormField(page, 'input[name="website"]', 'testwebsite');

apps/app/e2e/tests/split-onboarding.spec.ts

Lines changed: 91 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,55 @@
11
import { expect, test } from '@playwright/test';
2+
import { authenticateTestUser, clearAuth } from '../utils/auth-helpers';
23
import { generateTestData } from '../utils/helpers';
34

45
test.describe('Split Onboarding Flow', () => {
6+
test.beforeEach(async ({ page }) => {
7+
// Clear any existing auth state
8+
await clearAuth(page);
9+
});
10+
511
test('new user completes split onboarding: 3 steps → payment → 9 steps → product access', async ({
612
page,
713
}) => {
814
const testData = generateTestData();
915
const website = `example${Date.now()}.com`;
1016

11-
// Start at setup
17+
// Authenticate user first
18+
await authenticateTestUser(page, {
19+
email: testData.email,
20+
name: testData.userName,
21+
skipOrg: true, // Don't create org, user will go through setup
22+
});
23+
24+
// Navigate to setup
1225
await page.goto('/setup');
1326

27+
// Should redirect to /setup/[setupId]
28+
await page.waitForURL(/\/setup\/[a-zA-Z0-9]+/, { timeout: 10000 });
29+
30+
// Wait for content to load
31+
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
32+
1433
// Step 1: Select framework
15-
await expect(page.getByText('Which compliance frameworks do you need?')).toBeVisible();
16-
await page.getByRole('checkbox', { name: 'SOC 2' }).click();
34+
await expect(page.locator('text=/compliance frameworks/i').first()).toBeVisible({
35+
timeout: 10000,
36+
});
37+
38+
// Check if framework is already selected, if not select one
39+
const checkedFrameworks = await page.locator('input[type="checkbox"]:checked').count();
40+
if (checkedFrameworks === 0) {
41+
await page.locator('label:has-text("SOC 2")').click();
42+
}
1743
await page.getByRole('button', { name: 'Next' }).click();
1844

1945
// Step 2: Organization name
20-
await expect(page.getByText("What's your organization's name?")).toBeVisible();
21-
await page.getByPlaceholder("Enter your organization's name").fill(testData.organizationName);
46+
await page.waitForSelector('input[name="organizationName"]', { timeout: 10000 });
47+
await page.locator('input[name="organizationName"]').fill(testData.organizationName);
2248
await page.getByRole('button', { name: 'Next' }).click();
2349

2450
// Step 3: Website
25-
await expect(page.getByText("What's your organization's domain?")).toBeVisible();
26-
await page.getByPlaceholder('example.com').fill(website);
51+
await page.waitForSelector('input[name="website"]', { timeout: 10000 });
52+
await page.locator('input[name="website"]').fill(website);
2753
await page.getByRole('button', { name: 'Next' }).click();
2854

2955
// Should redirect to upgrade page
@@ -92,13 +118,32 @@ test.describe('Split Onboarding Flow', () => {
92118
const testData = generateTestData();
93119
const website = `example${Date.now()}.com`;
94120

121+
// Authenticate user first
122+
await authenticateTestUser(page, {
123+
email: testData.email,
124+
name: testData.userName,
125+
skipOrg: true,
126+
});
127+
95128
// First create org through minimal flow
96129
await page.goto('/setup');
97-
await page.getByRole('checkbox', { name: 'SOC 2' }).click();
130+
await page.waitForURL(/\/setup\/[a-zA-Z0-9]+/, { timeout: 10000 });
131+
132+
// Select framework
133+
const checkedFrameworks = await page.locator('input[type="checkbox"]:checked').count();
134+
if (checkedFrameworks === 0) {
135+
await page.locator('label:has-text("SOC 2")').click();
136+
}
98137
await page.getByRole('button', { name: 'Next' }).click();
99-
await page.getByPlaceholder("Enter your organization's name").fill(testData.organizationName);
138+
139+
// Fill organization name
140+
await page.waitForSelector('input[name="organizationName"]', { timeout: 10000 });
141+
await page.locator('input[name="organizationName"]').fill(testData.organizationName);
100142
await page.getByRole('button', { name: 'Next' }).click();
101-
await page.getByPlaceholder('example.com').fill(website);
143+
144+
// Fill website
145+
await page.waitForSelector('input[name="website"]', { timeout: 10000 });
146+
await page.locator('input[name="website"]').fill(website);
102147
await page.getByRole('button', { name: 'Next' }).click();
103148

104149
const orgIdMatch = page.url().match(/org_[a-zA-Z0-9]+/);
@@ -129,12 +174,31 @@ test.describe('Split Onboarding Flow', () => {
129174
const firstOrg = generateTestData();
130175
const firstWebsite = `example${Date.now()}.com`;
131176

177+
// Authenticate user first
178+
await authenticateTestUser(page, {
179+
email: firstOrg.email,
180+
name: firstOrg.userName,
181+
skipOrg: true,
182+
});
183+
132184
await page.goto('/setup');
133-
await page.getByRole('checkbox', { name: 'SOC 2' }).click();
185+
await page.waitForURL(/\/setup\/[a-zA-Z0-9]+/, { timeout: 10000 });
186+
187+
// Select framework
188+
const checkedFrameworks = await page.locator('input[type="checkbox"]:checked').count();
189+
if (checkedFrameworks === 0) {
190+
await page.locator('label:has-text("SOC 2")').click();
191+
}
134192
await page.getByRole('button', { name: 'Next' }).click();
135-
await page.getByPlaceholder("Enter your organization's name").fill(firstOrg.organizationName);
193+
194+
// Fill organization name
195+
await page.waitForSelector('input[name="organizationName"]', { timeout: 10000 });
196+
await page.locator('input[name="organizationName"]').fill(firstOrg.organizationName);
136197
await page.getByRole('button', { name: 'Next' }).click();
137-
await page.getByPlaceholder('example.com').fill(firstWebsite);
198+
199+
// Fill website
200+
await page.waitForSelector('input[name="website"]', { timeout: 10000 });
201+
await page.locator('input[name="website"]').fill(firstWebsite);
138202
await page.getByRole('button', { name: 'Next' }).click();
139203

140204
const firstOrgIdMatch = page.url().match(/org_[a-zA-Z0-9]+/);
@@ -144,22 +208,28 @@ test.describe('Split Onboarding Flow', () => {
144208
// Now create additional org using dropdown
145209
await page.goto(`/upgrade/${firstOrgId}`);
146210

147-
// Open org switcher
148-
await page.getByRole('button', { name: firstOrg.organizationName }).click();
149-
await page.getByText('Create Organization').click();
211+
// Try navigating directly to setup with intent
212+
await page.goto('/setup?intent=create-additional');
150213

151-
// Should be at setup with intent
152-
await expect(page).toHaveURL('/setup?intent=create-additional');
214+
// Should redirect to /setup/[setupId] with intent preserved
215+
await page.waitForURL(/\/setup\/[a-zA-Z0-9]+\?intent=create-additional/, { timeout: 10000 });
153216

154217
// Complete setup for second org
155218
const secondOrg = generateTestData();
156219
const secondWebsite = `example${Date.now() + 1}.com`;
157220

158-
await page.getByRole('checkbox', { name: 'ISO 27001' }).click();
221+
// Select a different framework
222+
await page.locator('label:has-text("ISO 27001")').click();
159223
await page.getByRole('button', { name: 'Next' }).click();
160-
await page.getByPlaceholder("Enter your organization's name").fill(secondOrg.organizationName);
224+
225+
// Fill organization name
226+
await page.waitForSelector('input[name="organizationName"]', { timeout: 10000 });
227+
await page.locator('input[name="organizationName"]').fill(secondOrg.organizationName);
161228
await page.getByRole('button', { name: 'Next' }).click();
162-
await page.getByPlaceholder('example.com').fill(secondWebsite);
229+
230+
// Fill website
231+
await page.waitForSelector('input[name="website"]', { timeout: 10000 });
232+
await page.locator('input[name="website"]').fill(secondWebsite);
163233
await page.getByRole('button', { name: 'Next' }).click();
164234

165235
// Should redirect to upgrade for the new org

apps/app/src/app/(app)/onboarding/components/PostPaymentOnboarding.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,20 @@ export function PostPaymentOnboarding({
4646
// Set to max scale when finalizing
4747
window.dispatchEvent(
4848
new CustomEvent('onboarding-step-change', {
49-
detail: { stepIndex: 8, totalSteps: 9, progress: 1 },
49+
detail: { stepIndex: totalSteps - 1, totalSteps, progress: 1 },
5050
}),
5151
);
5252
} else {
53-
const progress = stepIndex / 8; // 8 because we have 9 steps (0-8)
53+
const progress = stepIndex / (totalSteps - 1);
5454
// Dispatch custom event to notify the background wrapper
5555
window.dispatchEvent(
5656
new CustomEvent('onboarding-step-change', {
57-
detail: { stepIndex, totalSteps: 9, progress },
57+
detail: { stepIndex, totalSteps, progress },
5858
}),
5959
);
6060
}
6161
}
62-
}, [stepIndex, isFinalizing]);
62+
}, [stepIndex, isFinalizing, totalSteps]);
6363

6464
return (
6565
<div className="scrollbar-hide flex min-h-[calc(100vh-50px)] flex-col items-center justify-center p-4">

apps/app/src/app/(app)/setup/components/AnimatedGradientBackground.tsx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,12 @@ void main() {
159159
float mouseDistance = length(mouseOffset);
160160
161161
// Dynamic color palette that shifts based on deformation and mouse
162-
vec3 color1 = mix(vec3(0.1, 0.9, 0.4), vec3(0.9, 0.1, 0.2), u_rage); // Green to red
163-
vec3 color2 = mix(vec3(0.0, 0.7, 0.5), vec3(0.7, 0.0, 0.1), u_rage); // Teal to dark red
164-
vec3 color3 = mix(vec3(0.3, 1.0, 0.3), vec3(1.0, 0.3, 0.0), u_rage); // Electric green to orange-red
162+
vec3 color1 = mix(vec3(0.024, 0.306, 0.231), vec3(0.9, 0.1, 0.2), u_rage); // emerald-900 to red
163+
vec3 color2 = mix(vec3(0.024, 0.18, 0.133), vec3(0.7, 0.0, 0.1), u_rage); // emerald-950 to dark red
164+
vec3 color3 = mix(vec3(0.024, 0.373, 0.275), vec3(1.0, 0.3, 0.0), u_rage); // emerald-800 to orange-red
165165
166-
// Keyboard glow effect - more subtle cyan tint
167-
vec3 glowColor = mix(vec3(0.3, 0.9, 0.8), vec3(1.0, 0.3, 0.3), u_rage);
166+
// Keyboard glow effect - emerald tint
167+
vec3 glowColor = mix(vec3(0.016, 0.471, 0.341), vec3(1.0, 0.3, 0.3), u_rage); // emerald-700
168168
float glowIntensity = u_glow * 0.25 * (1.0 + u_rage * 0.5);
169169
170170
// Pulse effect - very subtle white tint
@@ -176,8 +176,8 @@ void main() {
176176
vec3 baseColor = mix(color1, color2, sin(deformIntensity + u_time * 0.5) * 0.5 + 0.5);
177177
baseColor = mix(baseColor, color3, smoothstep(0.1, 0.3, abs(vNoise)));
178178
179-
// Boost base color saturation
180-
baseColor = baseColor * 1.2;
179+
// Subtle boost to maintain deep emerald richness
180+
baseColor = baseColor * 1.08;
181181
182182
// Mix in glow color based on keyboard activity - very subtle to preserve vibrancy
183183
baseColor = mix(baseColor, glowColor, glowIntensity * rimLight * 0.3);
@@ -187,38 +187,38 @@ void main() {
187187
188188
// Extra glow where stretched towards mouse
189189
float stretchGlow = smoothstep(0.3, 0.7, dot(normalize(vPosition), normalize(vec3(mouseOffset, 0.0))));
190-
baseColor += mix(vec3(0.1, 0.4, 0.2), vec3(0.4, 0.1, 0.0), u_rage) * stretchGlow * 0.4;
190+
baseColor += mix(vec3(0.024, 0.373, 0.275), vec3(0.4, 0.1, 0.0), u_rage) * stretchGlow * 0.4; // emerald-800
191191
192192
// Holographic interference patterns enhanced by keyboard glow
193193
float interference = sin(vPosition.x * 15.0 + u_time) * sin(vPosition.y * 15.0 - u_time * 0.7);
194194
interference *= 0.1 * rimLight * (1.0 + u_glow * 1.5 + u_rage * 2.0);
195-
baseColor += vec3(interference * 0.2, interference * 0.5, interference * 0.3);
195+
baseColor += vec3(interference * 0.02, interference * 0.3, interference * 0.2);
196196
197197
// Electric rim effect enhanced by mouse proximity and keyboard glow
198198
float electricRim = pow(rimLight, 0.5);
199-
vec3 electricColor = mix(vec3(0.4, 1.0, 0.6), vec3(1.0, 0.4, 0.2), u_rage);
199+
vec3 electricColor = mix(vec3(0.02, 0.588, 0.412), vec3(1.0, 0.4, 0.2), u_rage); // emerald-600
200200
float electric = sin(atan(vPosition.y, vPosition.x) * 20.0 + u_time * 3.0) * electricRim;
201201
electric = smoothstep(0.6, 1.0, electric) * 0.3;
202202
electric *= 1.0 + (1.0 - mouseDistance) * 0.5 + u_glow * 0.5 + u_rage * 1.0;
203203
204204
// Core energy with color variation
205205
float centerDistance = length(vPosition) / 2.2;
206206
float coreGlow = smoothstep(1.0, 0.0, centerDistance) * 0.5;
207-
vec3 coreColor = mix(vec3(0.5, 1.0, 0.7), vec3(0.3, 0.8, 1.0), sin(u_time * 1.5) * 0.5 + 0.5);
207+
vec3 coreColor = mix(vec3(0.016, 0.471, 0.341), vec3(0.024, 0.306, 0.231), sin(u_time * 1.5) * 0.5 + 0.5); // emerald-700 to emerald-900
208208
coreColor = mix(coreColor, vec3(1.0, 0.5, 0.3), u_rage);
209209
coreColor = mix(coreColor, glowColor, u_glow * 0.3);
210210
211211
// Combine all effects
212212
vec3 finalColor = baseColor;
213213
finalColor += electricColor * electric;
214214
finalColor += coreColor * coreGlow * (1.0 + u_pulse * 0.3);
215-
finalColor += electricColor * electricRim * 0.6;
215+
finalColor += electricColor * electricRim * 0.25;
216216
217217
// Keyboard effect - flowing energy waves instead of dots
218218
float flowingEnergy = sin(vPosition.x * 10.0 - u_time * 5.0 + vPosition.y * 5.0) *
219219
cos(vPosition.y * 8.0 + u_time * 4.0 - vPosition.z * 3.0);
220220
flowingEnergy = smoothstep(0.7, 1.0, abs(flowingEnergy)) * u_glow;
221-
vec3 energyColor = mix(vec3(0.2, 1.0, 0.8), vec3(0.5, 0.8, 1.0), sin(u_time * 2.0) * 0.5 + 0.5);
221+
vec3 energyColor = mix(vec3(0.02, 0.588, 0.412), vec3(0.016, 0.471, 0.341), sin(u_time * 2.0) * 0.5 + 0.5); // emerald-600 to emerald-700
222222
energyColor = mix(energyColor, vec3(1.0, 0.3, 0.2), u_rage);
223223
finalColor += energyColor * flowingEnergy * rimLight * 0.6;
224224
@@ -474,7 +474,7 @@ interface AnimatedGradientBackgroundProps {
474474

475475
export function AnimatedGradientBackground({ scale = 1 }: AnimatedGradientBackgroundProps) {
476476
return (
477-
<div className="fixed inset-0 -z-10 opacity-40">
477+
<div className="fixed inset-0 -z-10 opacity-50">
478478
<Canvas
479479
camera={{
480480
position: [0, 0, 7],

apps/app/src/app/(app)/upgrade/[orgId]/components/AnimatedPricingBanner.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function AnimatedPricingBanner() {
3838
<div className="absolute inset-0 bg-gradient-to-r from-primary/10 via-primary/5 to-primary/10 backdrop-blur-md" />
3939

4040
{/* Clipped animated background */}
41-
<div className="absolute inset-0 overflow-hidden opacity-60">
41+
<div className="absolute inset-0 overflow-hidden opacity-70">
4242
<div className="absolute inset-0 scale-[3] translate-y-1/2">
4343
<AnimatedGradientBackground scale={2} />
4444
</div>

0 commit comments

Comments
 (0)