diff --git a/apps/web/app/(use-page-wrapper)/onboarding/teams/invite/page.tsx b/apps/web/app/(use-page-wrapper)/onboarding/teams/invite/page.tsx index 3bde80e55e..a6c1e4b452 100644 --- a/apps/web/app/(use-page-wrapper)/onboarding/teams/invite/page.tsx +++ b/apps/web/app/(use-page-wrapper)/onboarding/teams/invite/page.tsx @@ -34,7 +34,7 @@ const ServerPage = async () => { return ; } - return ; + return ; }; export default ServerPage; diff --git a/apps/web/modules/onboarding/components/EmailInviteForm.tsx b/apps/web/modules/onboarding/components/EmailInviteForm.tsx index 54dd2ed153..6e0344349f 100644 --- a/apps/web/modules/onboarding/components/EmailInviteForm.tsx +++ b/apps/web/modules/onboarding/components/EmailInviteForm.tsx @@ -50,7 +50,7 @@ export function EmailInviteForm({
{showTeamSelect ? ( -
+
diff --git a/apps/web/modules/onboarding/components/OnboardingLayout.tsx b/apps/web/modules/onboarding/components/OnboardingLayout.tsx index eb670b0902..640de2f14c 100644 --- a/apps/web/modules/onboarding/components/OnboardingLayout.tsx +++ b/apps/web/modules/onboarding/components/OnboardingLayout.tsx @@ -32,7 +32,7 @@ export const OnboardingLayout = ({ userEmail, currentStep, totalSteps, children {/* Logo and container - centered */}
-
+
{/* Column 1 - Always visible, 40% on xl+ */}
{column1}
{/* Column 2 - Hidden on mobile, visible on xl+, 60% on xl+ */} diff --git a/apps/web/modules/onboarding/components/onboarding-browser-view.tsx b/apps/web/modules/onboarding/components/onboarding-browser-view.tsx index 9e4668baee..237936849d 100644 --- a/apps/web/modules/onboarding/components/onboarding-browser-view.tsx +++ b/apps/web/modules/onboarding/components/onboarding-browser-view.tsx @@ -4,6 +4,8 @@ import classNames from "classnames"; import { AnimatePresence, motion } from "framer-motion"; import { usePathname } from "next/navigation"; +import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; +import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Avatar } from "@calcom/ui/components/avatar"; @@ -18,6 +20,21 @@ type OnboardingBrowserViewProps = { teamSlug?: string; }; +const getDisplayUrl = ( + orgSlug: string | null | undefined, + username: string | null | undefined, + teamSlug: string | undefined +): string => { + if (orgSlug) { + return teamSlug !== undefined + ? `${orgSlug}.${subdomainSuffix()}/team/${teamSlug || ""}` + : `${orgSlug}.${subdomainSuffix()}/${username || ""}`; + } + + const webappUrl = WEBAPP_URL.replace(/^https?:\/\//, ""); + return teamSlug !== undefined ? `${webappUrl}/team/${teamSlug || ""}` : `${webappUrl}/${username || ""}`; +}; + export const OnboardingBrowserView = ({ avatar, name, @@ -27,9 +44,9 @@ export const OnboardingBrowserView = ({ }: OnboardingBrowserViewProps) => { const { t } = useLocale(); const pathname = usePathname(); - const webappUrl = WEBAPP_URL.replace(/^https?:\/\//, ""); - const displayUrl = - teamSlug !== undefined ? `${webappUrl}/team/${teamSlug || ""}` : `${webappUrl}/${username || ""}`; + const orgBranding = useOrgBranding(); + + const displayUrl = getDisplayUrl(orgBranding?.slug, username, teamSlug); // Animation variants for entry and exit const containerVariants = { diff --git a/apps/web/modules/onboarding/components/onboarding-invite-browser-view.tsx b/apps/web/modules/onboarding/components/onboarding-invite-browser-view.tsx index 201bff02d7..79e4dc5d84 100644 --- a/apps/web/modules/onboarding/components/onboarding-invite-browser-view.tsx +++ b/apps/web/modules/onboarding/components/onboarding-invite-browser-view.tsx @@ -133,22 +133,44 @@ export const OnboardingInviteBrowserView = ({ duration: 0.5, ease: "backOut", }}> -
-
- -
-

- {displayName} -

-

- {displayBio || "We're emailing you all the details"} -

-
+
+
+ {/* Banner Image */} + {organizationBrand.banner && ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {displayName} +
+ )} + + {/* Organization Avatar - Overlaying the banner */} + {organizationBrand.banner && avatar && ( +
+ +
+ )} +
+ + {/* Organization Info */} +
+ {!organizationBrand.banner && avatar && ( + + )} +

+ {displayName} +

+

+ {displayBio || "We're emailing you all the details"} +

diff --git a/apps/web/modules/onboarding/components/onboarding-organization-browser-view.tsx b/apps/web/modules/onboarding/components/onboarding-organization-browser-view.tsx index 1e3d73d71d..09b4d5e926 100644 --- a/apps/web/modules/onboarding/components/onboarding-organization-browser-view.tsx +++ b/apps/web/modules/onboarding/components/onboarding-organization-browser-view.tsx @@ -3,19 +3,30 @@ import classNames from "classnames"; import { AnimatePresence, motion } from "framer-motion"; import { usePathname } from "next/navigation"; +import { useEffect, useRef } from "react"; -import { WEBAPP_URL } from "@calcom/lib/constants"; +import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { Avatar } from "@calcom/ui/components/avatar"; import { Button } from "@calcom/ui/components/button"; import { Icon, type IconName } from "@calcom/ui/components/icon"; +// Helper function to darken a hex color +const darkenColor = (hex: string, amount: number): string => { + const num = parseInt(hex.replace("#", ""), 16); + const r = Math.max(0, Math.min(255, ((num >> 16) & 0xff) * (1 - amount))); + const g = Math.max(0, Math.min(255, ((num >> 8) & 0xff) * (1 - amount))); + const b = Math.max(0, Math.min(255, (num & 0xff) * (1 - amount))); + return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, "0")}`; +}; + type OnboardingOrganizationBrowserViewProps = { avatar?: string | null; name?: string; bio?: string; slug?: string; bannerUrl?: string | null; + brandColor?: string; }; export const OnboardingOrganizationBrowserView = ({ @@ -24,11 +35,22 @@ export const OnboardingOrganizationBrowserView = ({ bio, slug, bannerUrl, + brandColor, }: OnboardingOrganizationBrowserViewProps) => { const { t } = useLocale(); const pathname = usePathname(); - const webappUrl = WEBAPP_URL.replace(/^https?:\/\//, ""); - const displayUrl = `${webappUrl}/${slug || ""}`; + const containerRef = useRef(null); + const displayUrl = slug ? `${slug}.${subdomainSuffix()}` : subdomainSuffix(); + + // Update CSS variables when brandColor changes + useEffect(() => { + if (brandColor && containerRef.current) { + containerRef.current.style.setProperty("--cal-brand", brandColor); + // Set brand-emphasis as a slightly darker version for hover states + const darkerColor = darkenColor(brandColor, 0.1); + containerRef.current.style.setProperty("--cal-brand-emphasis", darkerColor); + } + }, [brandColor]); // Animation variants for entry and exit const containerVariants = { @@ -79,7 +101,9 @@ export const OnboardingOrganizationBrowserView = ({ ]; return ( -
+
{/* Browser header */}
{/* Navigation buttons */} @@ -164,7 +188,7 @@ export const OnboardingOrganizationBrowserView = ({

{event.description}

-
diff --git a/apps/web/modules/onboarding/components/onboarding-teams-browser-view.tsx b/apps/web/modules/onboarding/components/onboarding-teams-browser-view.tsx index 5597e35a1f..38f92e0dfb 100644 --- a/apps/web/modules/onboarding/components/onboarding-teams-browser-view.tsx +++ b/apps/web/modules/onboarding/components/onboarding-teams-browser-view.tsx @@ -108,11 +108,6 @@ export const OnboardingTeamsBrowserView = ({ {/* Teams Info */}

{t("teams")}

-

- {hasValidTeams - ? t("onboarding_teams_browser_view_subtitle") - : t("onboarding_teams_browser_view_placeholder_subtitle")} -

diff --git a/apps/web/modules/onboarding/hooks/useCreateTeam.ts b/apps/web/modules/onboarding/hooks/useCreateTeam.ts index 52fb4467ea..bc7f09bb6a 100644 --- a/apps/web/modules/onboarding/hooks/useCreateTeam.ts +++ b/apps/web/modules/onboarding/hooks/useCreateTeam.ts @@ -18,11 +18,13 @@ export function useCreateTeam() { const createTeamMutation = trpc.viewer.teams.create.useMutation(); const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation(); - const createTeam = async (store: OnboardingState) => { + const createTeam = async (store?: OnboardingState) => { setIsSubmitting(true); try { - const { teamDetails, teamBrand } = store; + // Get the latest state from the store to ensure we have the most up-to-date values + const currentStore = store || useOnboardingStore.getState(); + const { teamDetails, teamBrand } = currentStore; // Validate team details - if empty, redirect back to team details step if (!teamDetails.name || !teamDetails.name.trim() || !teamDetails.slug || !teamDetails.slug.trim()) { diff --git a/apps/web/modules/onboarding/hooks/useSubmitOnboarding.ts b/apps/web/modules/onboarding/hooks/useSubmitOnboarding.ts index d26ae08f29..f98126835a 100644 --- a/apps/web/modules/onboarding/hooks/useSubmitOnboarding.ts +++ b/apps/web/modules/onboarding/hooks/useSubmitOnboarding.ts @@ -1,3 +1,4 @@ +import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import { useState } from "react"; @@ -11,6 +12,7 @@ import type { OnboardingState } from "../store/onboarding-store"; export const useSubmitOnboarding = () => { const router = useRouter(); + const { update: updateSession } = useSession(); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); const flags = useFlagMap(); @@ -78,6 +80,8 @@ export const useSubmitOnboarding = () => { // No checkout URL means billing is disabled (self-hosted flow) // Organization has already been created by the backend showToast("Organization created successfully!", "success"); + // Refresh session to get updated organization data + await updateSession(); // Set flag to show welcome modal after personal onboarding redirect setShowNewOrgModalFlag(); skipToPersonal(resetOnboarding); diff --git a/apps/web/modules/onboarding/organization/brand/organization-brand-view.tsx b/apps/web/modules/onboarding/organization/brand/organization-brand-view.tsx index 83cd6271c1..d0c7744635 100644 --- a/apps/web/modules/onboarding/organization/brand/organization-brand-view.tsx +++ b/apps/web/modules/onboarding/organization/brand/organization-brand-view.tsx @@ -189,6 +189,7 @@ export const OrganizationBrandView = ({ userEmail }: OrganizationBrandViewProps) bio={organizationDetails.bio} slug={organizationDetails.link} bannerUrl={bannerPreview} + brandColor={brandColor} /> ); diff --git a/apps/web/modules/onboarding/personal/_components/InstallableAppCard.tsx b/apps/web/modules/onboarding/personal/_components/InstallableAppCard.tsx index 92d46d902c..9681faade5 100644 --- a/apps/web/modules/onboarding/personal/_components/InstallableAppCard.tsx +++ b/apps/web/modules/onboarding/personal/_components/InstallableAppCard.tsx @@ -42,7 +42,6 @@ export const InstallableAppCard = ({ title={app.name}> {app.name}

-

{app.description}

const formSchema = z.object({ invites: z.array( z.object({ - email: z.string().email(t("invalid_email_address")), + email: z.union([z.literal(""), z.string().email(t("invalid_email_address"))]), role: z.enum(["MEMBER", "ADMIN"]), }) ), @@ -151,7 +151,7 @@ export const TeamInviteEmailView = ({ userEmail }: TeamInviteEmailViewProps) => {t("onboarding_skip_for_now")}