Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
3f5c7bc
wip
ameer2468 Oct 12, 2025
2c4c410
onboarding backend
ameer2468 Oct 13, 2025
e68957c
modify stripe webhook to handle checkout from onboarding
ameer2468 Oct 13, 2025
e3b25de
decrease timeout
ameer2468 Oct 13, 2025
ae3ef1d
put deployment url on success for testing
ameer2468 Oct 13, 2025
9b26d12
cancel url
ameer2468 Oct 13, 2025
8d4f42d
fixes
ameer2468 Oct 13, 2025
08ff461
set completion
ameer2468 Oct 13, 2025
6f59fed
attempt fix redirection
ameer2468 Oct 13, 2025
f5e1c65
logging
ameer2468 Oct 13, 2025
609278a
logs
ameer2468 Oct 14, 2025
0f3dda7
restore server env
ameer2468 Oct 14, 2025
d4c17c4
Update _journal.json
ameer2468 Oct 14, 2025
952f019
Merge branch 'main' into onboarding
richiemcilroy Oct 14, 2025
badaa37
cleanup
ameer2468 Oct 14, 2025
7da87fe
Merge branch 'onboarding' of https://github.com/CapSoftware/Cap into …
ameer2468 Oct 14, 2025
73bc732
minor refinements
ameer2468 Oct 14, 2025
a42f264
copy changes
richiemcilroy Oct 14, 2025
cdbeced
Merge branch 'onboarding' of https://github.com/CapSoftware/cap into …
richiemcilroy Oct 14, 2025
c52cf61
use transactions
ameer2468 Oct 14, 2025
b81659a
Merge branch 'onboarding' of https://github.com/CapSoftware/Cap into …
ameer2468 Oct 14, 2025
906d278
scaffold user rpcs
Brendonovich Oct 14, 2025
c70d720
rpc
ameer2468 Oct 14, 2025
2a1b233
Delete ONBOARDING_RPC_MIGRATION.md
ameer2468 Oct 14, 2025
6594be4
use S3 bucket service
ameer2468 Oct 14, 2025
4cfeedd
remove api endpoints
ameer2468 Oct 14, 2025
5ee586f
download page and fixes
ameer2468 Oct 14, 2025
9fb84d0
Merge branch 'main' into onboarding
ameer2468 Oct 14, 2025
44aff44
Update route.ts
ameer2468 Oct 14, 2025
724a038
cleanups
ameer2468 Oct 15, 2025
b54a556
Merge branch 'main' into onboarding
ameer2468 Oct 15, 2025
9088554
Update Items.tsx
ameer2468 Oct 15, 2025
dc24b11
remove dead code
ameer2468 Oct 15, 2025
8a644fe
Merge branch 'main' into onboarding
ameer2468 Oct 15, 2025
0d602f7
remove pushing
ameer2468 Oct 15, 2025
47c0b9f
conditional pushing
ameer2468 Oct 15, 2025
7d3645a
mb
ameer2468 Oct 15, 2025
f9e1710
prefill org name
ameer2468 Oct 15, 2025
c56f999
Merge branch 'main' into staging
ameer2468 Oct 15, 2025
a8ab75b
Update CustomDomainPage.tsx
ameer2468 Oct 15, 2025
63d06c2
Merge branch 'main' into staging
ameer2468 Oct 15, 2025
6c56457
Merge branch 'main' into onboarding
Brendonovich Oct 15, 2025
3004f01
Merge branch 'onboarding' of https://github.com/CapSoftware/Cap into …
Brendonovich Oct 15, 2025
60ec6de
use stripe context
ameer2468 Oct 15, 2025
6facdf6
Merge branch 'onboarding' of https://github.com/CapSoftware/Cap into …
ameer2468 Oct 15, 2025
a6f3843
Update CustomDomainPage.tsx
ameer2468 Oct 15, 2025
8e7372a
ts
ameer2468 Oct 15, 2025
16c5b6b
move user call into org-setup block
ameer2468 Oct 15, 2025
15a0935
redirect to org settings when checking out during onboarding
ameer2468 Oct 15, 2025
9858c08
add signout and skip
ameer2468 Oct 15, 2025
a2a136c
Update DownloadPage.tsx
ameer2468 Oct 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions apps/web/actions/organization/upload-organization-icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ import { organizations } from "@cap/database/schema";
import { serverEnv } from "@cap/env";
import { S3Buckets } from "@cap/web-backend";
import type { Organisation } from "@cap/web-domain";
import DOMPurify from "dompurify";
import { eq } from "drizzle-orm";
import { Effect, Option } from "effect";
import { JSDOM } from "jsdom";
import { revalidatePath } from "next/cache";
import { sanitizeFile } from "@/lib/sanitizeFile";
import { runPromise } from "@/lib/server";
Expand Down Expand Up @@ -37,7 +35,7 @@ export async function uploadOrganizationIcon(
throw new Error("Only the owner can update organization icon");
}

const file = formData.get("file") as File;
const file = formData.get("icon") as File | null;

if (!file) {
throw new Error("No file provided");
Expand All @@ -64,11 +62,12 @@ export async function uploadOrganizationIcon(
await Effect.gen(function* () {
const [bucket] = yield* S3Buckets.getBucketAccess(Option.none());

yield* bucket.putObject(
fileKey,
yield* Effect.promise(() => sanitizedFile.bytes()),
{ contentType: file.type },
);
const bodyBytes = yield* Effect.promise(async () => {
const buf = await sanitizedFile.arrayBuffer();
return new Uint8Array(buf);
});

yield* bucket.putObject(fileKey, bodyBytes, { contentType: file.type });
// Construct the icon URL
if (serverEnv().CAP_AWS_BUCKET_URL) {
// If a custom bucket URL is defined, use it
Expand Down
20 changes: 1 addition & 19 deletions apps/web/app/(org)/dashboard/_components/Navbar/Items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ interface Props {
const AdminNavItems = ({ toggleMobileNav }: Props) => {
const pathname = usePathname();
const [open, setOpen] = useState(false);
const [hoveredItem, setHoveredItem] = useState<string | null>(null);
const { user, sidebarCollapsed, userCapsCount } = useDashboardContext();

const manageNavigation = [
Expand Down Expand Up @@ -221,7 +220,7 @@ const AdminNavItems = ({ toggleMobileNav }: Props) => {
? "pointer-events-none"
: "text-gray-10 hover:text-gray-12 hover:bg-gray-6",
)}
key={`${organization.organization.name}-organization`}
key={`${organization.organization.name}-organization-${organization.organization.id}`}
onSelect={async () => {
await updateActiveOrganization(
organization.organization.id,
Expand Down Expand Up @@ -323,23 +322,6 @@ const AdminNavItems = ({ toggleMobileNav }: Props) => {
/>
)}

{hoveredItem === item.name && !isPathActive(item.href) && (
<motion.div
layoutId="hoverIndicator"
className={clsx(
"absolute bg-transparent rounded-xl",
sidebarCollapsed ? "inset-0 mx-auto w-9 h-9" : "inset-0",
)}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{
type: "spring",
bounce: 0.2,
duration: 0.2,
}}
/>
)}
<NavItem
name={item.name}
href={item.href}
Expand Down
10 changes: 0 additions & 10 deletions apps/web/app/(org)/dashboard/caps/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,16 +123,6 @@ export default async function CapsPage(props: PageProps<"/dashboard/caps">) {

const totalCount = totalCountResult[0]?.count || 0;

// Get custom domain and verification status for the user's organization
const organizationData = await db()
.select({
customDomain: organizations.customDomain,
domainVerified: organizations.domainVerified,
})
.from(organizations)
.where(eq(organizations.id, user.activeOrganizationId))
.limit(1);

const videoData = await db()
.select({
id: videos.id,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/(org)/dashboard/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default async function DashboardLayout({
}

if (!user.name || user.name.length === 0) {
redirect("/onboarding");
redirect("/onboarding/welcome");
}
Comment on lines 31 to 33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify onboarding step enforcement is handled elsewhere.

The current code only checks user.name before allowing dashboard access. Given the PR adds multi-step onboarding with tracked steps (welcome, organizationSetup, customDomain, inviteTeam, download as mentioned in the AI summary), verify that:

  1. Per-step redirects are enforced by the onboarding layout/pages themselves
  2. Users cannot bypass onboarding steps by manually navigating to /dashboard after only completing the welcome step

The previous version (per past review comments) had checks for individual onboardingSteps properties here that were problematic for existing users. If those checks were intentionally moved to the onboarding layout, that's correct. If they were removed without being relocated, incomplete onboarding users could access the dashboard.

Run the following script to verify where onboarding step checks are enforced:


🏁 Script executed:

#!/bin/bash
# Search for onboarding step checks in layouts and middleware

# Check onboarding layout for step enforcement
echo "=== Onboarding layout files ==="
fd -e tsx -e ts "layout" apps/web/app | rg "onboarding"

# Search for onboardingSteps checks across the codebase
echo -e "\n=== OnboardingSteps property checks ==="
rg -nP --type=ts -C3 'onboardingSteps\.(welcome|organizationSetup|customDomain|inviteTeam)' apps/web/app

# Search for onboarding completion checks
echo -e "\n=== Onboarding completion checks ==="
rg -nP --type=ts -C3 'onboarding_completed_at|onboardingCompleted' apps/web/app

Length of output: 2510


🏁 Script executed:

#!/bin/bash
# Search for redirect calls in each onboarding layout
rg -nP --type=tsx -n 'redirect\(' apps/web/app/\(org\)/onboarding

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Search for redirect calls in onboarding layout files
fd -e tsx apps/web/app/\(org\)/onboarding | xargs rg -n 'redirect\('

Length of output: 4530


Enforce full onboarding in dashboard layout.
In apps/web/app/(org)/dashboard/layout.tsx, add checks for user.onboardingSteps.organizationSetup, .customDomain, and .inviteTeam (beyond just user.name) and redirect to the respective onboarding step if any are incomplete; currently only user.name is gated, allowing dashboard access after only the welcome step.

🤖 Prompt for AI Agents
In apps/web/app/(org)/dashboard/layout.tsx around lines 32 to 34, the layout
currently only checks user.name before redirecting to onboarding; add checks for
user.onboardingSteps.organizationSetup, user.onboardingSteps.customDomain, and
user.onboardingSteps.inviteTeam and redirect to their respective onboarding
routes if they are falsey or incomplete; implement safe null/undefined checks
for user.onboardingSteps before accessing properties, and ensure the redirects
follow the correct route order (organization setup -> custom domain -> invite
team) so users cannot access the dashboard until all onboarding steps are
complete.


let organizationSelect: Organization[] = [];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
"use client";

import { CardDescription, Label } from "@cap/ui";
import { useState } from "react";
import { useId, useState } from "react";
import { toast } from "sonner";
import { removeOrganizationIcon } from "@/actions/organization/remove-icon";
import { uploadOrganizationIcon } from "@/actions/organization/upload-organization-icon";
import { FileInput } from "@/components/FileInput";
import { useDashboardContext } from "../../../Contexts";

export const OrganizationIcon = () => {
const iconInputId = useId();
const { activeOrganization } = useDashboardContext();
const organizationId = activeOrganization?.organization.id;
const existingIconUrl = activeOrganization?.organization.iconUrl;
Expand All @@ -22,10 +23,9 @@ export const OrganizationIcon = () => {
// Upload the file to the server immediately
try {
setIsUploading(true);
const formData = new FormData();
formData.append("file", file);

const result = await uploadOrganizationIcon(formData, organizationId);
const fd = new FormData();
fd.append("icon", file);
const result = await uploadOrganizationIcon(fd, organizationId);

if (result.success) {
toast.success("Organization icon updated successfully");
Expand Down Expand Up @@ -68,7 +68,7 @@ export const OrganizationIcon = () => {
<FileInput
height={44}
previewIconSize={20}
id="icon"
id={iconInputId}
name="icon"
onChange={handleFileChange}
disabled={isUploading}
Expand Down
109 changes: 0 additions & 109 deletions apps/web/app/(org)/onboarding/Onboarding.tsx

This file was deleted.

45 changes: 45 additions & 0 deletions apps/web/app/(org)/onboarding/[...steps]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { getCurrentUser } from "@cap/database/auth/session";
import { redirect } from "next/navigation";

export default async function OnboardingStepLayout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ steps: string[] }>;
}) {
const user = await getCurrentUser();

if (!user) {
redirect("/login");
}

const steps = user.onboardingSteps || {};
const currentStep = (await params).steps?.[0] ?? "welcome";

const ordered = [
"welcome",
"organization-setup",
"custom-domain",
"invite-team",
"download",
] as const;
const isComplete = (s: (typeof ordered)[number]) =>
s === "welcome"
? Boolean(steps.welcome && user.name)
: s === "organization-setup"
? Boolean(steps.organizationSetup)
: s === "custom-domain"
? Boolean(steps.customDomain)
: s === "invite-team"
? Boolean(steps.inviteTeam)
: Boolean(steps.download);

const firstIncomplete = ordered.find((s) => !isComplete(s)) ?? "download";

if (currentStep !== firstIncomplete) {
redirect(`/onboarding/${firstIncomplete}`);
}

return children;
}
33 changes: 33 additions & 0 deletions apps/web/app/(org)/onboarding/[...steps]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { getCurrentUser } from "@cap/database/auth/session";
import { CustomDomainPage } from "../components/CustomDomainPage";
import { DownloadPage } from "../components/DownloadPage";
import { InviteTeamPage } from "../components/InviteTeamPage";
import { OrganizationSetupPage } from "../components/OrganizationSetupPage";
import { WelcomePage } from "../components/WelcomePage";

export default async function OnboardingStepPage({
params,
}: {
params: Promise<{
steps: "welcome" | "organization-setup" | "custom-domain" | "invite-team";
}>;
}) {
const step = (await params).steps[0];

switch (step) {
case "welcome":
return <WelcomePage />;
case "organization-setup": {
const user = await getCurrentUser();
return <OrganizationSetupPage firstName={user?.name} />;
}
case "custom-domain":
return <CustomDomainPage />;
case "invite-team":
return <InviteTeamPage />;
case "download":
return <DownloadPage />;
default:
return null;
}
}
55 changes: 55 additions & 0 deletions apps/web/app/(org)/onboarding/components/Base.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client";

import { LogoBadge } from "@cap/ui";
import { faArrowLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import clsx from "clsx";
import { useRouter } from "next/navigation";

export const Base = ({
children,
title,
description,
descriptionClassName,
hideBackButton = true,
}: {
children: React.ReactNode;
title: string;
description: string | React.ReactNode;
descriptionClassName?: string;
hideBackButton?: boolean;
}) => {
const router = useRouter();
return (
<div className="relative w-[calc(100%-2%)] space-y-7 p-7 max-w-[472px] bg-gray-2 border border-gray-4 rounded-2xl">
{!hideBackButton && (
<div
onClick={() => router.back()}
className="absolute overflow-hidden flex top-5 rounded-full left-5 z-20 hover:bg-gray-1 gap-2 items-center py-1.5 px-3 text-gray-12 bg-transparent border border-gray-4 transition-colors duration-300 cursor-pointer"
>
<FontAwesomeIcon className="w-2" icon={faArrowLeft} />
<p className="text-xs text-inherit">Back</p>
</div>
)}
<a href="/">
<LogoBadge className="mx-auto w-auto h-12" />
</a>
<div className="flex flex-col justify-center items-center space-y-1 text-center">
<h2 className="text-2xl font-semibold text-gray-12">{title}</h2>
{typeof description === "string" ? (
<p
className={clsx(
"w-full text-base max-w-[260px] text-gray-10",
descriptionClassName,
)}
>
{description}
</p>
) : (
description
)}
</div>
{children}
</div>
);
};
Loading