From 49a2d30d7c69f1bec1f1ebeb10644dc29cc89b21 Mon Sep 17 00:00:00 2001 From: Tim Wilson Date: Fri, 21 Nov 2025 16:05:49 -0500 Subject: [PATCH 1/3] Program onboarding domains updates --- apps/web/ui/domains/add-edit-domain-form.tsx | 12 +- apps/web/ui/modals/register-domain-modal.tsx | 21 +- .../partners/program-link-configuration.tsx | 261 ++++++++++++++---- 3 files changed, 241 insertions(+), 53 deletions(-) diff --git a/apps/web/ui/domains/add-edit-domain-form.tsx b/apps/web/ui/domains/add-edit-domain-form.tsx index 3f8da5e9687..50de3351dcf 100644 --- a/apps/web/ui/domains/add-edit-domain-form.tsx +++ b/apps/web/ui/domains/add-edit-domain-form.tsx @@ -31,7 +31,7 @@ import { } from "lucide-react"; import { motion } from "motion/react"; import posthog from "posthog-js"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { FormEvent, useEffect, useMemo, useRef, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { toast } from "sonner"; import { useDebouncedCallback } from "use-debounce"; @@ -276,7 +276,15 @@ export function AddEditDomainForm({ const currentStatusProps = STATUS_CONFIG[domainStatus]; return ( -
+ ) => { + // prevent the submission event from propagating to the parent form (in the link builder) + e.preventDefault(); + e.stopPropagation(); + handleSubmit(onSubmit)(e); + }} + >
void; + onSuccess?: (domain: string) => void; + setRegisteredParam?: boolean; } -const RegisterDomain = ({ showModal, setShowModal }: RegisterDomainProps) => { +const RegisterDomain = ({ + showModal, + setShowModal, + onSuccess, + setRegisteredParam, +}: RegisterDomainProps) => { const { queryParams } = useRouterStuff(); return ( @@ -23,8 +30,11 @@ const RegisterDomain = ({ showModal, setShowModal }: RegisterDomainProps) => { { + onSuccess?.(domain); setShowModal(false); - queryParams({ set: { registered: domain.toLowerCase() } }); + + if (setRegisteredParam !== false) + queryParams({ set: { registered: domain.toLowerCase() } }); }} onCancel={() => setShowModal(false)} /> @@ -33,7 +43,9 @@ const RegisterDomain = ({ showModal, setShowModal }: RegisterDomainProps) => { ); }; -export function useRegisterDomainModal() { +export function useRegisterDomainModal( + props: Omit = {}, +) { const [showRegisterDomainModal, setShowRegisterDomainModal] = useState(false); const RegisterDomainModal = useCallback(() => { @@ -41,9 +53,10 @@ export function useRegisterDomainModal() { ); - }, [showRegisterDomainModal, setShowRegisterDomainModal]); + }, [showRegisterDomainModal, setShowRegisterDomainModal, props]); return useMemo( () => ({ setShowRegisterDomainModal, RegisterDomainModal }), diff --git a/apps/web/ui/partners/program-link-configuration.tsx b/apps/web/ui/partners/program-link-configuration.tsx index e231175da20..dad6859c67c 100644 --- a/apps/web/ui/partners/program-link-configuration.tsx +++ b/apps/web/ui/partners/program-link-configuration.tsx @@ -1,21 +1,28 @@ import { getLinkStructureOptions } from "@/lib/partners/get-link-structure-options"; +import useDomains from "@/lib/swr/use-domains"; import useWorkspace from "@/lib/swr/use-workspace"; import { DomainVerificationStatusProps } from "@/lib/types"; import DomainConfiguration from "@/ui/domains/domain-configuration"; import { DomainSelector } from "@/ui/domains/domain-selector"; -import { InfoTooltip, Input, LinkLogo } from "@dub/ui"; -import { ArrowTurnRight2 } from "@dub/ui/icons"; -import { fetcher, getApexDomain, getPrettyUrl } from "@dub/utils"; +import { AnimatedSizeContainer, InfoTooltip, Input, LinkLogo } from "@dub/ui"; +import { ArrowTurnRight2, ChevronRight, LoadingSpinner } from "@dub/ui/icons"; +import { cn, fetcher, getApexDomain, getPrettyUrl } from "@dub/utils"; import { AnimatePresence, motion } from "motion/react"; +import { useMemo, useState } from "react"; import useSWRImmutable from "swr/immutable"; +import { useAddEditDomainModal } from "../modals/add-edit-domain-modal"; +import { useRegisterDomainModal } from "../modals/register-domain-modal"; -interface ProgramLinkConfigurationProps { +type DomainProps = { domain: string | null; - url: string | null; onDomainChange: (domain: string) => void; +}; + +type ProgramLinkConfigurationProps = { + url: string | null; onUrlChange: (url: string) => void; hideLinkPreview?: boolean; -} +} & DomainProps; export function ProgramLinkConfiguration({ domain, @@ -24,18 +31,6 @@ export function ProgramLinkConfiguration({ onUrlChange, hideLinkPreview, }: ProgramLinkConfigurationProps) { - const { id: workspaceId } = useWorkspace(); - - const { data: verificationData } = useSWRImmutable<{ - status: DomainVerificationStatusProps; - response: any; - }>( - workspaceId && domain - ? `/api/domains/${domain}/verify?workspaceId=${workspaceId}` - : null, - fetcher, - ); - const linkStructureOptions = getLinkStructureOptions({ domain, url, @@ -43,42 +38,17 @@ export function ProgramLinkConfiguration({ return (
-
-
+
+
- - - -

- Custom domain that will be used for your program's referral links -

+
- - {domain && - verificationData && - verificationData.status !== "Valid Configuration" && ( - - - - )} - -
); } + +function DomainOnboarding({ domain, onDomainChange }: DomainProps) { + const { allWorkspaceDomains: domains, loading: isLoadingDomains } = + useDomains(); + + const [state, setState] = useState<"idle" | "select">( + domain ? "select" : "idle", + ); + + const { RegisterDomainModal, setShowRegisterDomainModal } = + useRegisterDomainModal({ + onSuccess: (domain) => { + onDomainChange(domain); + setState("select"); + }, + setRegisteredParam: false, + }); + + const { AddEditDomainModal, setShowAddEditDomainModal } = + useAddEditDomainModal({ + onSuccess: (domain) => { + onDomainChange(domain.slug); + setState("select"); + }, + }); + + const idleOptions = useMemo( + () => [ + { + icon: "https://assets.dub.co/icons/crown.webp", + title: "Claim a free .link domain", + badge: "No setup", + badgeClassName: "bg-green-100 text-green-800", + description: "Free for one year with your paid account.", + onSelect: () => setShowRegisterDomainModal(true), + }, + { + icon: "https://assets.dub.co/icons/link.webp", + title: "Connect a domain you own", + badge: "DNS setup required", + badgeClassName: "bg-bg-inverted/10 text-neutral-800", + description: + "Dedicate a domain exclusively for your short links and program.", + onSelect: () => { + if (!domains?.length) setShowAddEditDomainModal(true); + else setState("select"); + }, + loading: isLoadingDomains, + }, + ], + [ + domains, + isLoadingDomains, + setShowAddEditDomainModal, + setShowRegisterDomainModal, + ], + ); + + return ( + <> + + + +
+ + {state === "idle" && ( + +
+ {idleOptions.map((option) => ( + + ))} +
+ +

+ This domain will be used for your program’s referral links +

+
+ )} + + {state === "select" && ( + + + + )} +
+
+
+ + ); +} + +function DomainOnboardingSelection({ domain, onDomainChange }: DomainProps) { + const { id: workspaceId } = useWorkspace(); + + const { data: verificationData } = useSWRImmutable<{ + status: DomainVerificationStatusProps; + response: any; + }>( + workspaceId && domain + ? `/api/domains/${domain}/verify?workspaceId=${workspaceId}` + : null, + fetcher, + ); + + return ( +
+ + +

+ This domain will be used for your program’s referral links +

+ + + {domain && + verificationData && + verificationData.status !== "Valid Configuration" && ( + + + + )} + +
+ ); +} From 12b62431ec181f53835e1122ea737d370a1bf031 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Fri, 21 Nov 2025 16:54:12 -0800 Subject: [PATCH 2/3] Select a different option --- .../partners/program-link-configuration.tsx | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/apps/web/ui/partners/program-link-configuration.tsx b/apps/web/ui/partners/program-link-configuration.tsx index dad6859c67c..c1a099732e3 100644 --- a/apps/web/ui/partners/program-link-configuration.tsx +++ b/apps/web/ui/partners/program-link-configuration.tsx @@ -4,8 +4,19 @@ import useWorkspace from "@/lib/swr/use-workspace"; import { DomainVerificationStatusProps } from "@/lib/types"; import DomainConfiguration from "@/ui/domains/domain-configuration"; import { DomainSelector } from "@/ui/domains/domain-selector"; -import { AnimatedSizeContainer, InfoTooltip, Input, LinkLogo } from "@dub/ui"; -import { ArrowTurnRight2, ChevronRight, LoadingSpinner } from "@dub/ui/icons"; +import { + AnimatedSizeContainer, + Button, + InfoTooltip, + Input, + LinkLogo, +} from "@dub/ui"; +import { + ArrowTurnRight2, + ChevronLeft, + ChevronRight, + LoadingSpinner, +} from "@dub/ui/icons"; import { cn, fetcher, getApexDomain, getPrettyUrl } from "@dub/utils"; import { AnimatePresence, motion } from "motion/react"; import { useMemo, useState } from "react"; @@ -269,6 +280,7 @@ function DomainOnboarding({ domain, onDomainChange }: DomainProps) { setState("idle")} /> )} @@ -279,7 +291,11 @@ function DomainOnboarding({ domain, onDomainChange }: DomainProps) { ); } -function DomainOnboardingSelection({ domain, onDomainChange }: DomainProps) { +function DomainOnboardingSelection({ + domain, + onDomainChange, + onBack, +}: DomainProps & { onBack: () => void }) { const { id: workspaceId } = useWorkspace(); const { data: verificationData } = useSWRImmutable<{ @@ -294,6 +310,15 @@ function DomainOnboardingSelection({ domain, onDomainChange }: DomainProps) { return (
+ + )} +
+ +
+ + {state === "idle" && ( + +
+ {idleOptions.map((option) => ( +
- {option.loading ? ( - - ) : ( - - )} - - ))} -
+ {option.loading ? ( + + ) : ( + + )} + + ))} +
-

- This domain will be used for your program’s referral links -

- - )} +

+ This domain will be used for your program’s referral links +

+ + )} - {state === "select" && ( - - setState("idle")} - /> - - )} - -
- + {state === "select" && ( + + setState("idle")} + /> + + )} + +
+ +
); } @@ -310,15 +309,6 @@ function DomainOnboardingSelection({ return (
-