diff --git a/ee/apps/den-web/app/(den)/_components/auth-panel.tsx b/ee/apps/den-web/app/(den)/_components/auth-panel.tsx new file mode 100644 index 000000000..b775c113b --- /dev/null +++ b/ee/apps/den-web/app/(den)/_components/auth-panel.tsx @@ -0,0 +1,370 @@ +"use client"; + +import { ArrowRight, CheckCircle2 } from "lucide-react"; +import { usePathname, useRouter } from "next/navigation"; +import { useEffect, useRef, useState, type ReactNode } from "react"; +import { isSamePathname } from "../_lib/client-route"; +import type { AuthMode } from "../_lib/den-flow"; +import { useDenFlow } from "../_providers/den-flow-provider"; + +function getDesktopGrant(url: string | null) { + if (!url) return null; + try { + const parsed = new URL(url); + const grant = parsed.searchParams.get("grant")?.trim() ?? ""; + return grant || null; + } catch { + return null; + } +} + +function GitHubLogo() { + return ( + + ); +} + +function GoogleLogo() { + return ( + + ); +} + +function SocialButton({ + children, + onClick, + disabled, +}: { + children: ReactNode; + onClick: () => void; + disabled: boolean; +}) { + return ( + + ); +} + +export function AuthPanel({ + panelTitle, + panelCopy, + prefilledEmail, + prefillKey, + initialMode = "sign-up", + lockEmail = false, + hideSocialAuth = false, + hideEmailField = false, + eyebrow = "Account", +}: { + panelTitle?: string; + panelCopy?: string; + prefilledEmail?: string; + prefillKey?: string; + initialMode?: AuthMode; + lockEmail?: boolean; + hideSocialAuth?: boolean; + hideEmailField?: boolean; + eyebrow?: string; +}) { + const router = useRouter(); + const pathname = usePathname(); + const prefillRef = useRef(null); + const [copiedDesktopField, setCopiedDesktopField] = useState<"link" | "code" | null>(null); + const { + authMode, + setAuthMode, + email, + setEmail, + password, + setPassword, + verificationCode, + setVerificationCode, + verificationRequired, + authBusy, + authInfo, + authError, + desktopAuthRequested, + desktopRedirectUrl, + desktopRedirectBusy, + showAuthFeedback, + submitAuth, + submitVerificationCode, + resendVerificationCode, + cancelVerification, + beginSocialAuth, + resolveUserLandingRoute, + } = useDenFlow(); + + const desktopGrant = getDesktopGrant(desktopRedirectUrl); + const resolvedPanelTitle = verificationRequired + ? "Verify your email." + : panelTitle ?? (authMode === "sign-up" ? "Create your Cloud account." : "Sign in to Cloud."); + const resolvedPanelCopy = verificationRequired + ? "Enter the six-digit code from your inbox to finish setup." + : panelCopy ?? ( + authMode === "sign-up" + ? "Start with email, GitHub, or Google." + : "Welcome back. Keep your team setup in sync across Cloud and desktop." + ); + const showLockedEmailSummary = Boolean(prefilledEmail && lockEmail && hideEmailField); + + useEffect(() => { + const key = prefillKey ?? prefilledEmail?.trim() ?? null; + if (!key || prefillRef.current === key) { + return; + } + + prefillRef.current = key; + setAuthMode(initialMode); + setEmail(prefilledEmail?.trim() ?? ""); + setPassword(""); + setVerificationCode(""); + }, [initialMode, prefillKey, prefilledEmail, setAuthMode, setEmail, setPassword, setVerificationCode]); + + const copyDesktopValue = async (field: "link" | "code", value: string | null) => { + if (!value) return; + await navigator.clipboard.writeText(value); + setCopiedDesktopField(field); + window.setTimeout(() => { + setCopiedDesktopField((current) => (current === field ? null : current)); + }, 1800); + }; + + return ( +
+
+

+ {eyebrow} +

+

{resolvedPanelTitle}

+

{resolvedPanelCopy}

+
+ + {desktopAuthRequested ? ( +
+ Finish auth here and we'll send you back into the OpenWork desktop app. + {desktopRedirectUrl ? ( +
+
+ + + {desktopGrant ? ( + + ) : null} +
+

+ If OpenWork does not open automatically, copy the sign-in link or one-time code and paste it into the OpenWork desktop app. +

+
+ ) : null} +
+ ) : null} + +
{ + const next = verificationRequired + ? await submitVerificationCode(event) + : await submitAuth(event); + if (next === "dashboard" || next === "join-org") { + const target = await resolveUserLandingRoute(); + if (target && !isSamePathname(pathname, target)) { + router.replace(target); + } + } else if (next === "checkout" && !isSamePathname(pathname, "/checkout")) { + router.replace("/checkout"); + } + }} + > + {!verificationRequired && !hideSocialAuth ? ( + <> + void beginSocialAuth("github")} + disabled={authBusy || desktopRedirectBusy} + > + + Continue with GitHub + + + void beginSocialAuth("google")} + disabled={authBusy || desktopRedirectBusy} + > + + Continue with Google + + + + + ) : null} + + {showLockedEmailSummary ? ( +
+

+ Invited email +

+

{prefilledEmail}

+
+ ) : null} + + {!hideEmailField ? ( + + ) : null} + + {!verificationRequired ? ( + + ) : ( + + )} + + + + {verificationRequired ? ( +
+ + +
+ ) : null} +
+ + {!verificationRequired ? ( +
+

{authMode === "sign-in" ? "Need an account?" : "Already have an account?"}

+ +
+ ) : null} + + {showAuthFeedback ? ( +
+

{authInfo}

+ {authError ?

{authError}

: null} + {!authError && verificationRequired ? ( +
+ + Waiting for your verification code +
+ ) : null} +
+ ) : null} +
+ ); +} diff --git a/ee/apps/den-web/app/(den)/_components/auth-screen.tsx b/ee/apps/den-web/app/(den)/_components/auth-screen.tsx index b4667e8d1..f4dbd3de4 100644 --- a/ee/apps/den-web/app/(den)/_components/auth-screen.tsx +++ b/ee/apps/den-web/app/(den)/_components/auth-screen.tsx @@ -2,44 +2,11 @@ import { PaperMeshGradient } from "@openwork/ui/react"; import { Dithering } from "@paper-design/shaders-react"; -import { ArrowRight, CheckCircle2 } from "lucide-react"; import { usePathname, useRouter } from "next/navigation"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef } from "react"; import { isSamePathname } from "../_lib/client-route"; import { useDenFlow } from "../_providers/den-flow-provider"; - -function getDesktopGrant(url: string | null) { - if (!url) return null; - try { - const parsed = new URL(url); - const grant = parsed.searchParams.get("grant")?.trim() ?? ""; - return grant || null; - } catch { - return null; - } -} - -function GitHubLogo() { - return ( - - ); -} - -function GoogleLogo() { - return ( - - ); -} +import { AuthPanel } from "./auth-panel"; function FeatureCard({ title, body }: { title: string; body: string }) { return ( @@ -50,27 +17,6 @@ function FeatureCard({ title, body }: { title: string; body: string }) { ); } -function SocialButton({ - children, - onClick, - disabled, -}: { - children: React.ReactNode; - onClick: () => void; - disabled: boolean; -}) { - return ( - - ); -} - function LoadingPanel({ title, body }: { title: string; body: string }) { return (
@@ -92,45 +38,9 @@ export function AuthScreen() { const router = useRouter(); const pathname = usePathname(); const routingRef = useRef(false); - const [copiedDesktopField, setCopiedDesktopField] = useState<"link" | "code" | null>(null); - const { - authMode, - setAuthMode, - email, - setEmail, - password, - setPassword, - verificationCode, - setVerificationCode, - verificationRequired, - authBusy, - authInfo, - authError, - user, - sessionHydrated, - desktopAuthRequested, - desktopRedirectUrl, - desktopRedirectBusy, - showAuthFeedback, - submitAuth, - submitVerificationCode, - resendVerificationCode, - cancelVerification, - beginSocialAuth, - resolveUserLandingRoute, - } = useDenFlow(); - const desktopGrant = getDesktopGrant(desktopRedirectUrl); + const { user, sessionHydrated, desktopAuthRequested, resolveUserLandingRoute } = useDenFlow(); const hasResolvedSession = sessionHydrated && Boolean(user) && !desktopAuthRequested; - const copyDesktopValue = async (field: "link" | "code", value: string | null) => { - if (!value) return; - await navigator.clipboard.writeText(value); - setCopiedDesktopField(field); - window.setTimeout(() => { - setCopiedDesktopField((current) => (current === field ? null : current)); - }, 1800); - }; - useEffect(() => { if (!hasResolvedSession || routingRef.current) { return; @@ -148,18 +58,6 @@ export function AuthScreen() { }); }, [hasResolvedSession, pathname, resolveUserLandingRoute, router]); - const panelTitle = verificationRequired - ? "Verify your email." - : authMode === "sign-up" - ? "Create your Cloud account." - : "Sign in to Cloud."; - - const panelCopy = verificationRequired - ? "Enter the six-digit code from your inbox to finish setup." - : authMode === "sign-up" - ? "Start with email, GitHub, or Google." - : "Welcome back. Keep your team setup in sync across Cloud and desktop."; - if (!sessionHydrated) { return (
@@ -241,212 +139,7 @@ export function AuthScreen() { body="We found your account and are sending you to the right Cloud destination now." /> ) : ( -
-
-

- Account -

-

{panelTitle}

-

{panelCopy}

-
- - {desktopAuthRequested ? ( -
- Finish auth here and we'll send you back into the OpenWork desktop app. - {desktopRedirectUrl ? ( -
-
- - - {desktopGrant ? ( - - ) : null} -
-

- If OpenWork does not open automatically, copy the sign-in link or one-time code and paste it into the OpenWork desktop app. -

-
- ) : null} -
- ) : null} - -
{ - const next = verificationRequired - ? await submitVerificationCode(event) - : await submitAuth(event); - if (next === "dashboard" || next === "join-org") { - const target = await resolveUserLandingRoute(); - if (target && !isSamePathname(pathname, target)) { - router.replace(target); - } - } else if (next === "checkout" && !isSamePathname(pathname, "/checkout")) { - router.replace("/checkout"); - } - }} - > - {!verificationRequired ? ( - <> - void beginSocialAuth("github")} - disabled={authBusy || desktopRedirectBusy} - > - - Continue with GitHub - - - void beginSocialAuth("google")} - disabled={authBusy || desktopRedirectBusy} - > - - Continue with Google - - - - - ) : null} - - - - {!verificationRequired ? ( - - ) : ( - - )} - - - - {verificationRequired ? ( -
- - -
- ) : null} -
- - {!verificationRequired ? ( -
-

{authMode === "sign-in" ? "Need an account?" : "Already have an account?"}

- -
- ) : null} - - {showAuthFeedback ? ( -
-

{authInfo}

- {authError ?

{authError}

: null} - {!authError && verificationRequired ? ( -
- - Waiting for your verification code -
- ) : null} -
- ) : null} -
+ )}
diff --git a/ee/apps/den-web/app/(den)/_components/join-org-screen.tsx b/ee/apps/den-web/app/(den)/_components/join-org-screen.tsx index 34e70e321..e56055add 100644 --- a/ee/apps/den-web/app/(den)/_components/join-org-screen.tsx +++ b/ee/apps/den-web/app/(den)/_components/join-org-screen.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useState } from "react"; import { getErrorMessage, requestJson } from "../_lib/den-flow"; import { PENDING_ORG_INVITATION_STORAGE_KEY, @@ -13,6 +13,7 @@ import { type DenInvitationPreview, } from "../_lib/den-org"; import { useDenFlow } from "../_providers/den-flow-provider"; +import { AuthPanel } from "./auth-panel"; function LoadingCard({ title, body }: { title: string; body: string }) { return ( @@ -51,22 +52,6 @@ export function JoinOrgScreen({ invitationId }: { invitationId: string }) { const [joinBusy, setJoinBusy] = useState(false); const [joinError, setJoinError] = useState(null); - const signUpHref = useMemo(() => { - if (!invitationId) { - return "/?mode=sign-up"; - } - - return `/?mode=sign-up&invite=${encodeURIComponent(invitationId)}`; - }, [invitationId]); - - const signInHref = useMemo(() => { - if (!invitationId) { - return "/?mode=sign-in"; - } - - return `/?mode=sign-in&invite=${encodeURIComponent(invitationId)}`; - }, [invitationId]); - const invitedEmailMatches = preview && user ? preview.invitation.email.trim().toLowerCase() === user.email.trim().toLowerCase() : false; @@ -173,6 +158,9 @@ export function JoinOrgScreen({ invitationId }: { invitationId: string }) { async function handleSwitchAccount() { await signOut(); + if (typeof window !== "undefined" && invitationId) { + window.sessionStorage.setItem(PENDING_ORG_INVITATION_STORAGE_KEY, invitationId); + } router.replace(getJoinOrgRoute(invitationId)); } @@ -197,6 +185,45 @@ export function JoinOrgScreen({ invitationId }: { invitationId: string }) { ); } + if (preview.invitation.status === "pending" && !user) { + return ( +
+
+
+

OpenWork Cloud

+
+

You've been invited to

+

{preview.organization.name}

+
+

Role: {formatRoleLabel(preview.invitation.role)}

+
+ +
+
+

Invited email

+

{preview.invitation.email}

+
+

+ Set a password for this invited email to create your OpenWork Cloud account, or switch to sign in if you already use it. +

+
+
+ + +
+ ); + } + const showAcceptAction = preview.invitation.status === "pending" && Boolean(user) && invitedEmailMatches; return ( @@ -228,18 +255,6 @@ export function JoinOrgScreen({ invitationId }: { invitationId: string }) { - ) : !user ? ( -
-

Create an account or sign in first, then come back here to confirm the invitation.

-
- - Create account to continue - - - Sign in instead - -
-
) : !invitedEmailMatches ? (