From b5d76f73e869ac1036b47157cacba0deb925f75a Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 4 Feb 2026 16:08:37 -0800 Subject: [PATCH] add new tier select form --- messages/en-US.json | 26 + src/components/GenerateLicenseKeysTable.tsx | 4 +- src/components/NewPricingLicenseForm.tsx | 893 ++++++++++++++++++++ 3 files changed, 921 insertions(+), 2 deletions(-) create mode 100644 src/components/NewPricingLicenseForm.tsx diff --git a/messages/en-US.json b/messages/en-US.json index f2affe11..eb01bdee 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2113,6 +2113,32 @@ } } }, + "newPricingLicenseForm": { + "title": "Get a license", + "description": "Choose a plan and tell us how you plan to use Pangolin.", + "chooseTier": "Choose your plan", + "viewPricingLink": "See pricing, features, and limits", + "tiers": { + "starter": { + "title": "Starter", + "description": "Ideal for small teams and getting started. Includes core features and support." + }, + "scale": { + "title": "Scale", + "description": "For growing teams and production use. Higher limits and priority support." + } + }, + "personalUseOnly": "Personal use only (free license — no checkout)", + "buttons": { + "continueToCheckout": "Continue to checkout" + }, + "toasts": { + "checkoutError": { + "title": "Checkout error", + "description": "Could not start checkout. Please try again." + } + } + }, "priority": "Priority", "priorityDescription": "Higher priority routes are evaluated first. Priority = 100 means automatic ordering (system decides). Use another number to enforce manual priority.", "instanceName": "Instance Name", diff --git a/src/components/GenerateLicenseKeysTable.tsx b/src/components/GenerateLicenseKeysTable.tsx index 2be17e89..c6db4e1d 100644 --- a/src/components/GenerateLicenseKeysTable.tsx +++ b/src/components/GenerateLicenseKeysTable.tsx @@ -15,7 +15,7 @@ import { useRouter } from "next/navigation"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import GenerateLicenseKeyForm from "./GenerateLicenseKeyForm"; +import NewPricingLicenseForm from "./NewPricingLicenseForm"; type GnerateLicenseKeysTableProps = { licenseKeys: GeneratedLicenseKey[]; @@ -198,7 +198,7 @@ export default function GenerateLicenseKeysTable({ }} /> - void; + orgId: string; + onGenerated?: () => void; +}; + +export default function NewPricingLicenseForm({ + open, + setOpen, + orgId, + onGenerated +}: FormProps) { + const t = useTranslations(); + const { env } = useEnvContext(); + const api = createApiClient({ env }); + const { user } = useUserContext(); + + const [loading, setLoading] = useState(false); + const [generatedKey, setGeneratedKey] = useState(null); + const [personalUseOnly, setPersonalUseOnly] = useState(false); + const [selectedTier, setSelectedTier] = useState<"starter" | "scale">( + "starter" + ); + + const personalFormSchema = z.object({ + email: z.email(), + firstName: z.string().min(1), + lastName: z.string().min(1), + primaryUse: z.string().min(1), + country: z.string().min(1), + phoneNumber: z.string().optional(), + agreedToTerms: z.boolean().refine((val) => val === true), + complianceConfirmed: z.boolean().refine((val) => val === true) + }); + + const businessFormSchema = z.object({ + email: z.email(), + firstName: z.string().min(1), + lastName: z.string().min(1), + primaryUse: z.string().min(1), + industry: z.string().min(1), + companyName: z.string().min(1), + companyWebsite: z.string().optional(), + companyPhoneNumber: z.string().optional(), + agreedToTerms: z.boolean().refine((val) => val === true), + complianceConfirmed: z.boolean().refine((val) => val === true) + }); + + type PersonalFormData = z.infer; + type BusinessFormData = z.infer; + + const personalForm = useForm({ + resolver: zodResolver(personalFormSchema) as Resolver, + defaultValues: { + email: user?.email || "", + firstName: "", + lastName: "", + primaryUse: "", + country: "", + phoneNumber: "", + agreedToTerms: false, + complianceConfirmed: false + } + }); + + const businessForm = useForm({ + resolver: zodResolver(businessFormSchema) as Resolver, + defaultValues: { + email: user?.email || "", + firstName: "", + lastName: "", + primaryUse: "", + industry: "", + companyName: "", + companyWebsite: "", + companyPhoneNumber: "", + agreedToTerms: false, + complianceConfirmed: false + } + }); + + React.useEffect(() => { + if (open) { + resetForm(); + setGeneratedKey(null); + setPersonalUseOnly(false); + setSelectedTier("starter"); + } + }, [open]); + + function resetForm() { + personalForm.reset({ + email: user?.email || "", + firstName: "", + lastName: "", + primaryUse: "", + country: "", + phoneNumber: "", + agreedToTerms: false, + complianceConfirmed: false + }); + businessForm.reset({ + email: user?.email || "", + firstName: "", + lastName: "", + primaryUse: "", + industry: "", + companyName: "", + companyWebsite: "", + companyPhoneNumber: "", + agreedToTerms: false, + complianceConfirmed: false + }); + } + + const tierOptions: StrategyOption<"starter" | "scale">[] = [ + { + id: "starter", + title: t("newPricingLicenseForm.tiers.starter.title"), + description: t("newPricingLicenseForm.tiers.starter.description") + }, + { + id: "scale", + title: t("newPricingLicenseForm.tiers.scale.title"), + description: t("newPricingLicenseForm.tiers.scale.description") + } + ]; + + const submitLicenseRequest = async ( + payload: Record + ): Promise => { + setLoading(true); + try { + 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 onSubmitPersonal = async (values: PersonalFormData) => { + await submitLicenseRequest({ + email: values.email, + useCaseType: "personal", + personal: { + firstName: values.firstName, + lastName: values.lastName, + aboutYou: { primaryUse: values.primaryUse }, + personalInfo: { + country: values.country, + phoneNumber: values.phoneNumber || "" + } + }, + business: undefined, + consent: { + agreedToTerms: values.agreedToTerms, + acknowledgedPrivacyPolicy: values.agreedToTerms, + complianceConfirmed: values.complianceConfirmed + } + }); + }; + + const handleContinueToCheckout = async () => { + const valid = await businessForm.trigger(); + if (!valid) return; + + const values = businessForm.getValues(); + setLoading(true); + try { + const tier = TIER_TO_LICENSE_ID[selectedTier]; + const response = await api.post>( + `/org/${orgId}/billing/create-checkout-session-license`, + { tier } + ); + const checkoutUrl = response.data.data; + if (checkoutUrl) { + window.location.href = checkoutUrl; + } else { + toast({ + title: t( + "newPricingLicenseForm.toasts.checkoutError.title" + ), + description: t( + "newPricingLicenseForm.toasts.checkoutError.description" + ), + variant: "destructive" + }); + setLoading(false); + } + } catch (error) { + toast({ + title: t("newPricingLicenseForm.toasts.checkoutError.title"), + description: formatAxiosError( + error, + t("newPricingLicenseForm.toasts.checkoutError.description") + ), + variant: "destructive" + }); + setLoading(false); + } + }; + + const handleClose = () => { + setOpen(false); + setGeneratedKey(null); + resetForm(); + }; + + return ( + + + + + {t("newPricingLicenseForm.title")} + + + {t("newPricingLicenseForm.description")} + + + +
+ {generatedKey ? ( +
+ +
+ ) : ( + <> + {/* Tier selection - required when not personal use */} + {!personalUseOnly && ( +
+ + + setSelectedTier(value) + } + cols={2} + /> + + {t( + "newPricingLicenseForm.viewPricingLink" + )} + +
+ )} + + {/* Personal use only checkbox at the bottom of options */} +
+ { + setPersonalUseOnly( + checked === true + ); + if (checked) { + businessForm.reset(); + } else { + personalForm.reset(); + } + }} + /> + +
+ + {/* License disclosure - only when personal use */} + {personalUseOnly && ( + + + + {t( + "generateLicenseKeyForm.alerts.commercialUseDisclosure.title" + )} + + + {t( + "generateLicenseKeyForm.alerts.commercialUseDisclosure.description" + ) + .split( + "Fossorial Commercial License Terms" + ) + .map((part, index) => ( + + {part} + {index === 0 && ( + + Fossorial + Commercial + License Terms + + )} + + ))} + + + )} + + {/* Personal form: only when personal use only is checked */} + {personalUseOnly && ( +
+ +
+ ( + + + {t( + "generateLicenseKeyForm.form.firstName" + )} + + + + + + + )} + /> + ( + + + {t( + "generateLicenseKeyForm.form.lastName" + )} + + + + + + + )} + /> +
+ + ( + + + {t( + "generateLicenseKeyForm.form.primaryUseQuestion" + )} + + + + + + + )} + /> + +
+ ( + + + {t( + "generateLicenseKeyForm.form.country" + )} + + + + + + + )} + /> + ( + + + {t( + "generateLicenseKeyForm.form.phoneNumberOptional" + )} + + + + + + + )} + /> +
+ +
+ ( + + + + +
+ +
+ {t( + "signUpTerms.IAgreeToThe" + )}{" "} + + {t( + "signUpTerms.termsOfService" + )}{" "} + + {t( + "signUpTerms.and" + )}{" "} + + {t( + "signUpTerms.privacyPolicy" + )} + +
+
+ +
+
+ )} + /> + ( + + + + +
+ +
+ {t( + "generateLicenseKeyForm.form.complianceConfirmation" + )}{" "} + + https://pangolin.net/fcl.html + +
+
+ +
+
+ )} + /> +
+ + + )} + + {/* Business form: when not personal use - enter business info then continue to checkout */} + {!personalUseOnly && ( +
+ +
+ ( + + + {t( + "generateLicenseKeyForm.form.firstName" + )} + + + + + + + )} + /> + ( + + + {t( + "generateLicenseKeyForm.form.lastName" + )} + + + + + + + )} + /> +
+ + ( + + + {t( + "generateLicenseKeyForm.form.primaryUseQuestion" + )} + + + + + + + )} + /> + + ( + + + {t( + "generateLicenseKeyForm.form.industryQuestion" + )} + + + + + + + )} + /> + + ( + + + {t( + "generateLicenseKeyForm.form.companyName" + )} + + + + + + + )} + /> + +
+ ( + + + {t( + "generateLicenseKeyForm.form.companyWebsite" + )} + + + + + + + )} + /> + ( + + + {t( + "generateLicenseKeyForm.form.companyPhoneNumber" + )} + + + + + + + )} + /> +
+ +
+ ( + + + + +
+ +
+ {t( + "signUpTerms.IAgreeToThe" + )}{" "} + + {t( + "signUpTerms.termsOfService" + )}{" "} + + {t( + "signUpTerms.and" + )}{" "} + + {t( + "signUpTerms.privacyPolicy" + )} + +
+
+ +
+
+ )} + /> + ( + + + + +
+ +
+ {t( + "generateLicenseKeyForm.form.complianceConfirmation" + )}{" "} + + https://pangolin.net/fcl.html + +
+
+ +
+
+ )} + /> +
+ + + )} + + )} +
+
+ + + + + + {!generatedKey && personalUseOnly && ( + + )} + + {!generatedKey && !personalUseOnly && ( + + )} + +
+
+ ); +}