diff --git a/apps/web/app/(org)/login/form.tsx b/apps/web/app/(org)/login/form.tsx index 70b375e4b8..0601ded15d 100644 --- a/apps/web/app/(org)/login/form.tsx +++ b/apps/web/app/(org)/login/form.tsx @@ -437,7 +437,19 @@ const NormalLogin = ({ )} */} -
+ + Don't have an account?{" "} + + Sign up here + + +

OR

diff --git a/apps/web/app/(org)/signup/form.tsx b/apps/web/app/(org)/signup/form.tsx new file mode 100644 index 0000000000..d5817ac073 --- /dev/null +++ b/apps/web/app/(org)/signup/form.tsx @@ -0,0 +1,494 @@ +"use client"; + +import { Button, Input, LogoBadge } from "@cap/ui"; +import { + faArrowLeft, + faEnvelope, + faExclamationCircle, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { AnimatePresence, motion } from "framer-motion"; +import Cookies from "js-cookie"; +import { LucideArrowUpRight } from "lucide-react"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter, useSearchParams } from "next/navigation"; +import { signIn } from "next-auth/react"; +import { Suspense, useEffect, useState } from "react"; +import { toast } from "sonner"; +import { getOrganizationSSOData } from "@/actions/organization/get-organization-sso-data"; +import { trackEvent } from "@/app/utils/analytics"; + +const MotionInput = motion(Input); +const MotionLogoBadge = motion(LogoBadge); +const MotionLink = motion(Link); +const MotionButton = motion(Button); + +export function SignupForm() { + const searchParams = useSearchParams(); + const router = useRouter(); + const next = searchParams?.get("next"); + const [email, setEmail] = useState(""); + const [loading, setLoading] = useState(false); + const [emailSent, setEmailSent] = useState(false); + const [oauthError, setOauthError] = useState(false); + const [showOrgInput, setShowOrgInput] = useState(false); + const [organizationId, setOrganizationId] = useState(""); + const [organizationName, setOrganizationName] = useState(null); + const [lastEmailSentTime, setLastEmailSentTime] = useState( + null, + ); + const theme = Cookies.get("theme") || "light"; + + useEffect(() => { + theme === "dark" + ? (document.body.className = "dark") + : (document.body.className = "light"); + //remove the dark mode when we leave the dashboard + return () => { + document.body.className = "light"; + }; + }, [theme]); + + useEffect(() => { + const error = searchParams?.get("error"); + const errorDesc = searchParams?.get("error_description"); + + const handleErrors = () => { + if (error === "OAuthAccountNotLinked" && !errorDesc) { + setOauthError(true); + return toast.error( + "This email is already associated with a different sign-in method", + ); + } else if ( + error === "profile_not_allowed_outside_organization" && + !errorDesc + ) { + return toast.error( + "Your email domain is not authorized for SSO access. Please use your work email or contact your administrator.", + ); + } else if (error && errorDesc) { + return toast.error(errorDesc); + } + }; + handleErrors(); + }, [searchParams]); + + useEffect(() => { + const pendingPriceId = localStorage.getItem("pendingPriceId"); + const pendingQuantity = localStorage.getItem("pendingQuantity") ?? "1"; + if (emailSent && pendingPriceId) { + localStorage.removeItem("pendingPriceId"); + localStorage.removeItem("pendingQuantity"); + + // Wait a bit to ensure the user is created + setTimeout(async () => { + const response = await fetch(`/api/settings/billing/subscribe`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + priceId: pendingPriceId, + quantity: parseInt(pendingQuantity), + }), + }); + const data = await response.json(); + + if (data.url) { + window.location.href = data.url; + } + }, 2000); + } + }, [emailSent]); + + const handleGoogleSignIn = () => { + trackEvent("auth_started", { method: "google", is_signup: true }); + signIn("google", { + ...(next && next.length > 0 ? { callbackUrl: next } : {}), + }); + }; + + const handleOrganizationLookup = async (e: React.FormEvent) => { + e.preventDefault(); + if (!organizationId) { + toast.error("Please enter an organization ID"); + return; + } + + try { + const data = await getOrganizationSSOData(organizationId); + setOrganizationName(data.name); + + signIn("workos", undefined, { + organization: data.organizationId, + connection: data.connectionId, + }); + } catch (error) { + console.error("Lookup Error:", error); + toast.error("Organization not found or SSO not configured"); + } + }; + + return ( + + setShowOrgInput(false)} + className="absolute overflow-hidden 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" + > + + + Back + + + + + + + + Sign up to Cap + + + Beautiful screen recordings, owned by you. + + + + + +
+ + ); +}; + +const NormalSignup = ({ + setShowOrgInput, + email, + emailSent, + setEmail, + loading, + oauthError, + handleGoogleSignIn, +}: { + setShowOrgInput: (show: boolean) => void; + email: string; + emailSent: boolean; + setEmail: (email: string) => void; + loading: boolean; + oauthError: boolean; + handleGoogleSignIn: () => void; +}) => { + return ( + + + { + setEmail(e.target.value); + }} + /> + } + > + Sign up with email + + +
+ +

OR

+ +
+ + {!oauthError && ( + <> + + Google + Sign up with Google + + + )} + + {oauthError && ( +
+ +

+ It looks like you've previously used this email to sign up via + email. Please enter your email below to receive a sign up link. +

+
+ )} + setShowOrgInput(true)} + disabled={loading} + > + + Sign up with SAML SSO + +
+
+ ); +}; diff --git a/apps/web/app/(org)/signup/page.tsx b/apps/web/app/(org)/signup/page.tsx new file mode 100644 index 0000000000..de68b63d08 --- /dev/null +++ b/apps/web/app/(org)/signup/page.tsx @@ -0,0 +1,29 @@ +import { getCurrentUser } from "@cap/database/auth/session"; +import { faArrowLeft } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import Link from "next/link"; +import { redirect } from "next/navigation"; +import { SignupForm } from "./form"; + +export const dynamic = "force-dynamic"; + +export default async function SignupPage() { + const session = await getCurrentUser(); + if (session) { + redirect("/dashboard"); + } + return ( +
+
+ + + Home + +
+ +
+ ); +} diff --git a/apps/web/app/(site)/Navbar.tsx b/apps/web/app/(site)/Navbar.tsx index 34ea845f17..f16dd135c2 100644 --- a/apps/web/app/(site)/Navbar.tsx +++ b/apps/web/app/(site)/Navbar.tsx @@ -13,8 +13,6 @@ import { navigationMenuTriggerStyle, } from "@cap/ui"; import { classNames } from "@cap/utils"; -import { faGithub } from "@fortawesome/free-brands-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { motion } from "framer-motion"; import Link from "next/link"; import { usePathname } from "next/navigation"; @@ -95,6 +93,13 @@ const Links = [ }, ]; +const AuthLinks = [ + { + label: "Log In", + href: "/login", + }, +]; + export const Navbar = () => { const pathname = usePathname(); const [showMobileMenu, setShowMobileMenu] = useState(false); @@ -154,20 +159,29 @@ export const Navbar = () => { )} ))} + {!auth && + AuthLinks.map((link) => ( + + + + {link.label} + + + + ))}
- { function LoginOrDashboard() { const auth = use(useAuthContext().user); + return ( ); } diff --git a/apps/web/components/ReadyToGetStarted.tsx b/apps/web/components/ReadyToGetStarted.tsx index 822ac0723c..0d4cecd06d 100644 --- a/apps/web/components/ReadyToGetStarted.tsx +++ b/apps/web/components/ReadyToGetStarted.tsx @@ -1,55 +1,55 @@ "use client"; import { Button } from "@cap/ui"; -import { homepageCopy } from "../data/homepage-copy"; import Link from "next/link"; +import { homepageCopy } from "../data/homepage-copy"; export function ReadyToGetStarted() { - return ( -
-
-
-

- {homepageCopy.readyToGetStarted.title} -

-
-
- - -
-
-

- or,{" "} - - Switch from Loom - -

-
-
-
- ); + return ( +
+
+
+

+ {homepageCopy.readyToGetStarted.title} +

+
+
+ + +
+
+

+ or,{" "} + + Switch from Loom + +

+
+
+
+ ); }