"use client"; import { Button } from "@app/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@app/components/ui/form"; import { Input } from "@app/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@app/components/ui/select"; import { Checkbox } from "@app/components/ui/checkbox"; import { toast } from "@app/hooks/useToast"; import { zodResolver } from "@hookform/resolvers/zod"; import { AxiosResponse } from "axios"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import CopyTextBox from "@app/components/CopyTextBox"; import { Credenza, CredenzaBody, CredenzaClose, CredenzaContent, CredenzaDescription, CredenzaFooter, CredenzaHeader, CredenzaTitle } from "@app/components/Credenza"; import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { GenerateNewLicenseResponse } from "@server/private/routers/generatedLicense/generateNewLicense"; import { useTranslations } from "next-intl"; import React from "react"; import { StrategySelect, StrategyOption } from "./StrategySelect"; import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; import { InfoIcon, Check } from "lucide-react"; import { useUserContext } from "@app/hooks/useUserContext"; type FormProps = { open: boolean; setOpen: (open: boolean) => void; orgId: string; onGenerated?: () => void; }; export default function GenerateLicenseKeyForm({ open, setOpen, orgId, onGenerated }: FormProps) { const t = useTranslations(); const { env } = useEnvContext(); const api = createApiClient({ env }); const { user } = useUserContext(); const [currentStep, setCurrentStep] = useState(1); const [loading, setLoading] = useState(false); const [generatedKey, setGeneratedKey] = useState(null); const [formKey, setFormKey] = useState(0); // Step 1: Email & License Type const step1Schema = z.object({ email: z .string() .email(t("generateLicenseKeyForm.validation.emailRequired")), useCaseType: z.enum(["personal", "business"], { required_error: t( "generateLicenseKeyForm.validation.useCaseTypeRequired" ) }) }); // Step 2: Personal Information const createStep2Schema = (useCaseType: string | undefined) => z .object({ firstName: z .string() .min( 1, t("generateLicenseKeyForm.validation.firstNameRequired") ), lastName: z .string() .min( 1, t("generateLicenseKeyForm.validation.lastNameRequired") ), jobTitle: z.string().optional(), primaryUse: z .string() .min( 1, t( "generateLicenseKeyForm.validation.primaryUseRequired" ) ), industry: z.string().optional(), prospectiveUsers: z.coerce.number().optional(), prospectiveSites: z.coerce.number().optional() }) .refine( (data) => { // If business use case, job title is required if (useCaseType === "business") { return data.jobTitle; } return true; }, { message: t( "generateLicenseKeyForm.validation.jobTitleRequiredBusiness" ), path: ["jobTitle"] } ) .refine( (data) => { // If business use case, industry is required if (useCaseType === "business") { return data.industry; } return true; }, { message: t( "generateLicenseKeyForm.validation.industryRequiredBusiness" ), path: ["industry"] } ); // Step 3: Contact Information const createStep3Schema = (useCaseType: string | undefined) => z .object({ stateProvinceRegion: z .string() .min( 1, t( "generateLicenseKeyForm.validation.stateProvinceRegionRequired" ) ), postalZipCode: z .string() .min( 1, t( "generateLicenseKeyForm.validation.postalZipCodeRequired" ) ), country: z.string().optional(), phoneNumber: z.string().optional(), companyName: z.string().optional(), countryOfResidence: z.string().optional(), companyWebsite: z.string().optional(), companyPhoneNumber: z.string().optional() }) .refine( (data) => { // If business use case, company name is required if (useCaseType === "business") { return data.companyName; } return true; }, { message: t( "generateLicenseKeyForm.validation.companyNameRequiredBusiness" ), path: ["companyName"] } ) .refine( (data) => { // If business use case, country of residence is required if (useCaseType === "business") { return data.countryOfResidence; } return true; }, { message: t( "generateLicenseKeyForm.validation.countryOfResidenceRequiredBusiness" ), path: ["countryOfResidence"] } ) .refine( (data) => { // If personal use case, country is required if (useCaseType === "personal" && !data.country) { return false; } return true; }, { message: t( "generateLicenseKeyForm.validation.countryRequiredPersonal" ), path: ["country"] } ); // Step 4: Terms & Generate const step4Schema = z.object({ agreedToTerms: z .boolean() .refine( (val) => val === true, t("generateLicenseKeyForm.validation.agreeToTermsRequired") ), complianceConfirmed: z .boolean() .refine( (val) => val === true, t("generateLicenseKeyForm.validation.complianceConfirmationRequired") ) }); // Complete form schema for final submission with conditional validation const createFormSchema = (useCaseType: string | undefined) => z .object({ email: z.string().email("Please enter a valid email address"), useCaseType: z.enum(["personal", "business"]), firstName: z.string().min(1, "First name is required"), lastName: z.string().min(1, "Last name is required"), jobTitle: z.string().optional(), primaryUse: z .string() .min(1, "Please describe your primary use"), industry: z.string().optional(), prospectiveUsers: z.coerce.number().optional(), prospectiveSites: z.coerce.number().optional(), stateProvinceRegion: z .string() .min( 1, t( "generateLicenseKeyForm.validation.stateProvinceRegionRequired" ) ), postalZipCode: z .string() .min( 1, t( "generateLicenseKeyForm.validation.postalZipCodeRequired" ) ), country: z.string().optional(), phoneNumber: z.string().optional(), companyName: z.string().optional(), countryOfResidence: z.string().optional(), companyWebsite: z.string().optional(), companyPhoneNumber: z.string().optional(), agreedToTerms: z .boolean() .refine( (val) => val === true, t( "generateLicenseKeyForm.validation.agreeToTermsRequired" ) ), complianceConfirmed: z .boolean() .refine( (val) => val === true, t("generateLicenseKeyForm.validation.complianceConfirmationRequired") ) }) .refine( (data) => { // If business use case, job title is required if (useCaseType === "business") { return data.jobTitle; } return true; }, { message: t( "generateLicenseKeyForm.validation.jobTitleRequiredBusiness" ), path: ["jobTitle"] } ) .refine( (data) => { // If business use case, industry is required if (useCaseType === "business") { return data.industry; } return true; }, { message: t( "generateLicenseKeyForm.validation.industryRequiredBusiness" ), path: ["industry"] } ) .refine( (data) => { // If business use case, company name is required if (useCaseType === "business") { return data.companyName; } return true; }, { message: t( "generateLicenseKeyForm.validation.companyNameRequiredBusiness" ), path: ["companyName"] } ) .refine( (data) => { // If business use case, country of residence is required if (useCaseType === "business") { return data.countryOfResidence; } return true; }, { message: t( "generateLicenseKeyForm.validation.countryOfResidenceRequiredBusiness" ), path: ["countryOfResidence"] } ) .refine( (data) => { // If personal use case, country is required if (useCaseType === "personal") { return data.country; } return true; }, { message: t( "generateLicenseKeyForm.validation.countryRequiredPersonal" ), path: ["country"] } ); type FormData = z.infer>; // Base schema for form initialization (without conditional validation) const baseFormSchema = z.object({ email: z.string().email("Please enter a valid email address"), useCaseType: z.enum(["personal", "business"]), firstName: z.string().min(1, "First name is required"), lastName: z.string().min(1, "Last name is required"), jobTitle: z.string().optional(), primaryUse: z.string().min(1, "Please describe your primary use"), industry: z.string().optional(), prospectiveUsers: z.coerce.number().optional(), prospectiveSites: z.coerce.number().optional(), stateProvinceRegion: z .string() .min(1, "State/Province/Region is required"), postalZipCode: z.string().min(1, "Postal/ZIP Code is required"), country: z.string().optional(), phoneNumber: z.string().optional(), companyName: z.string().optional(), countryOfResidence: z.string().optional(), companyWebsite: z.string().optional(), companyPhoneNumber: z.string().optional(), agreedToTerms: z .boolean() .refine( (val) => val === true, t("generateLicenseKeyForm.validation.agreeToTermsRequired") ), complianceConfirmed: z .boolean() .refine( (val) => val === true, t("generateLicenseKeyForm.validation.complianceConfirmationRequired") ) }); const form = useForm({ resolver: zodResolver(baseFormSchema), defaultValues: { email: user?.email || "", useCaseType: undefined, firstName: "", lastName: "", jobTitle: "", primaryUse: "", industry: "", prospectiveUsers: undefined, prospectiveSites: undefined, stateProvinceRegion: "", postalZipCode: "", country: "", phoneNumber: "", companyName: "", countryOfResidence: "", companyWebsite: "", companyPhoneNumber: "", agreedToTerms: false, complianceConfirmed: false } }); const useCaseType = form.watch("useCaseType"); const [previousUseCaseType, setPreviousUseCaseType] = useState< string | undefined >(undefined); // Reset form when use case type changes React.useEffect(() => { if ( useCaseType !== previousUseCaseType && useCaseType && previousUseCaseType ) { // Reset fields that are specific to use case type form.setValue("jobTitle", ""); form.setValue("prospectiveUsers", undefined); form.setValue("prospectiveSites", undefined); form.setValue("companyName", ""); form.setValue("countryOfResidence", ""); form.setValue("companyWebsite", ""); form.setValue("companyPhoneNumber", ""); form.setValue("phoneNumber", ""); form.setValue("country", ""); setPreviousUseCaseType(useCaseType); } }, [useCaseType, previousUseCaseType, form]); // Reset form when dialog opens React.useEffect(() => { if (open) { form.reset({ email: user?.email || "", useCaseType: undefined, firstName: "", lastName: "", jobTitle: "", primaryUse: "", industry: "", prospectiveUsers: undefined, prospectiveSites: undefined, stateProvinceRegion: "", postalZipCode: "", country: "", phoneNumber: "", companyName: "", countryOfResidence: "", companyWebsite: "", companyPhoneNumber: "", agreedToTerms: false, complianceConfirmed: false }); setCurrentStep(1); setGeneratedKey(null); setPreviousUseCaseType(undefined); } }, [open, form, user?.email]); const useCaseOptions: StrategyOption<"personal" | "business">[] = [ { id: "personal", title: t("generateLicenseKeyForm.useCaseOptions.personal.title"), description: (

{t( "generateLicenseKeyForm.useCaseOptions.personal.description" )}

  • Home-lab enthusiasts and self-hosting hobbyists
  • Personal projects, learning, and experimentation
  • Individual developers and tech enthusiasts
) }, { id: "business", title: t("generateLicenseKeyForm.useCaseOptions.business.title"), description: (

{t( "generateLicenseKeyForm.useCaseOptions.business.description" )}

  • Companies, startups, and organizations
  • Professional services and client work
  • Revenue-generating or commercial use cases
) } ]; const steps = [ { title: t("generateLicenseKeyForm.steps.emailLicenseType.title"), description: t( "generateLicenseKeyForm.steps.emailLicenseType.description" ) }, { title: t("generateLicenseKeyForm.steps.personalInformation.title"), description: t( "generateLicenseKeyForm.steps.personalInformation.description" ) }, { title: t("generateLicenseKeyForm.steps.contactInformation.title"), description: t( "generateLicenseKeyForm.steps.contactInformation.description" ) }, { title: t("generateLicenseKeyForm.steps.termsGenerate.title"), description: t( "generateLicenseKeyForm.steps.termsGenerate.description" ) } ]; const nextStep = async () => { let isValid = false; try { // Validate current step based on step number switch (currentStep) { case 1: await step1Schema.parseAsync(form.getValues()); isValid = true; break; case 2: await createStep2Schema( form.getValues("useCaseType") ).parseAsync(form.getValues()); isValid = true; break; case 3: await createStep3Schema( form.getValues("useCaseType") ).parseAsync(form.getValues()); isValid = true; break; case 4: await step4Schema.parseAsync(form.getValues()); isValid = true; break; default: isValid = false; } } catch (error) { if (error instanceof z.ZodError) { // Set form errors for the current step fields error.errors.forEach((err) => { const fieldName = err.path[0] as keyof FormData; form.setError(fieldName, { type: "manual", message: err.message }); }); } return; } if (isValid && currentStep < steps.length) { setCurrentStep(currentStep + 1); } }; const prevStep = () => { if (currentStep > 1) { setCurrentStep(currentStep - 1); } }; const onSubmit = async (values: FormData) => { // Validate with the dynamic schema before submission try { await createFormSchema(values.useCaseType).parseAsync(values); } catch (error) { if (error instanceof z.ZodError) { // Set form errors for any validation failures error.errors.forEach((err) => { const fieldName = err.path[0] as keyof FormData; form.setError(fieldName, { type: "manual", message: err.message }); }); return; } } setLoading(true); try { const payload = { email: values.email, useCaseType: values.useCaseType, personal: values.useCaseType === "personal" ? { firstName: values.firstName, lastName: values.lastName, aboutYou: { primaryUse: values.primaryUse }, personalInfo: { stateProvinceRegion: values.stateProvinceRegion, postalZipCode: values.postalZipCode, country: values.country, phoneNumber: values.phoneNumber || "" } } : undefined, business: values.useCaseType === "business" ? { firstName: values.firstName, lastName: values.lastName, jobTitle: values.jobTitle || "", aboutYou: { primaryUse: values.primaryUse, industry: values.industry, prospectiveUsers: values.prospectiveUsers || undefined, prospectiveSites: values.prospectiveSites || undefined }, companyInfo: { companyName: values.companyName || "", countryOfResidence: values.countryOfResidence || "", stateProvinceRegion: values.stateProvinceRegion, postalZipCode: values.postalZipCode, companyWebsite: values.companyWebsite || "", companyPhoneNumber: values.companyPhoneNumber || "" } } : undefined, consent: { agreedToTerms: values.agreedToTerms, acknowledgedPrivacyPolicy: values.agreedToTerms, complianceConfirmed: values.complianceConfirmed } }; const response = await api.put< AxiosResponse >(`/org/${orgId}/license`, payload); if (response.data.data?.licenseKey?.licenseKey) { setGeneratedKey(response.data.data.licenseKey.licenseKey); onGenerated?.(); toast({ title: t("generateLicenseKeyForm.toasts.success.title"), description: t( "generateLicenseKeyForm.toasts.success.description" ), variant: "default" }); } } catch (e) { console.error(e); toast({ title: t("generateLicenseKeyForm.toasts.error.title"), description: formatAxiosError( e, t("generateLicenseKeyForm.toasts.error.description") ), variant: "destructive" }); } setLoading(false); }; const handleClose = () => { setOpen(false); setCurrentStep(1); setGeneratedKey(null); setFormKey((prev) => prev + 1); // Force form reset by changing key form.reset({ email: user?.email || "", useCaseType: undefined, firstName: "", lastName: "", jobTitle: "", primaryUse: "", industry: "", prospectiveUsers: undefined, prospectiveSites: undefined, stateProvinceRegion: "", postalZipCode: "", country: "", phoneNumber: "", companyName: "", countryOfResidence: "", companyWebsite: "", companyPhoneNumber: "", agreedToTerms: false, complianceConfirmed: false }); }; const renderStepContent = () => { switch (currentStep) { case 1: return (
{t( "generateLicenseKeyForm.alerts.commercialUseDisclosure.title" )} {t( "generateLicenseKeyForm.alerts.commercialUseDisclosure.description" )} ( {t( "generateLicenseKeyForm.form.useCaseQuestion" )} { field.onChange(value); // Reset form when use case type changes form.reset({ email: user?.email || "", useCaseType: value, firstName: "", lastName: "", jobTitle: "", primaryUse: "", industry: "", prospectiveUsers: undefined, prospectiveSites: undefined, stateProvinceRegion: "", postalZipCode: "", country: "", phoneNumber: "", companyName: "", countryOfResidence: "", companyWebsite: "", companyPhoneNumber: "", agreedToTerms: false, complianceConfirmed: false }); }} cols={2} /> )} />
); case 2: return (
( {t( "generateLicenseKeyForm.form.firstName" )} )} /> ( {t( "generateLicenseKeyForm.form.lastName" )} )} />
{useCaseType === "business" && ( ( {t( "generateLicenseKeyForm.form.jobTitle" )} )} /> )}
( {t( "generateLicenseKeyForm.form.primaryUseQuestion" )} )} /> {useCaseType === "business" && ( <> ( {t( "generateLicenseKeyForm.form.industryQuestion" )} )} /> ( {t( "generateLicenseKeyForm.form.prospectiveUsersQuestion" )} )} /> ( {t( "generateLicenseKeyForm.form.prospectiveSitesQuestion" )} )} /> )}
); case 3: return (
{useCaseType === "business" && (
( {t( "generateLicenseKeyForm.form.companyName" )} )} /> ( {t( "generateLicenseKeyForm.form.countryOfResidence" )} )} />
( {t( "generateLicenseKeyForm.form.stateProvinceRegion" )} )} /> ( {t( "generateLicenseKeyForm.form.postalZipCode" )} )} />
( {t( "generateLicenseKeyForm.form.companyWebsite" )} )} /> ( {t( "generateLicenseKeyForm.form.companyPhoneNumber" )} )} />
)} {useCaseType === "personal" && (
( {t( "generateLicenseKeyForm.form.stateProvinceRegion" )} )} /> ( {t( "generateLicenseKeyForm.form.postalZipCode" )} )} />
( {t( "generateLicenseKeyForm.form.country" )} )} /> ( {t( "generateLicenseKeyForm.form.phoneNumberOptional" )} )} />
)}
); case 4: return (
(
{t("signUpTerms.IAgreeToThe")}{" "} {t( "signUpTerms.termsOfService" )}{" "} {t("signUpTerms.and")}{" "} {t( "signUpTerms.privacyPolicy" )}
)} /> (
I confirm that I am in compliance with the{" "} Fossorial Commercial License {" "} and that reporting inaccurate information or misidentifying use of the product is a violation of the license.
)} />
); default: return null; } }; return ( {t("generateLicenseKey")} {steps[currentStep - 1]?.description}
{/* Progress indicator */}
{steps.map((step, index) => (
{index + 1}
{step.title}
))}
{generatedKey ? (
{useCaseType === "business" && ( {t( "generateLicenseKeyForm.alerts.trialPeriodInformation.title" )} {t( "generateLicenseKeyForm.alerts.trialPeriodInformation.description" )} )}
) : (
{renderStepContent()}
)}
{!generatedKey && ( <> {currentStep > 1 && ( )} {currentStep < steps.length ? ( ) : ( )} )}
); }