diff --git a/messages/en-US.json b/messages/en-US.json index 8218cf8d3..ba0f1c004 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -3175,5 +3175,6 @@ "webhookUrlLabel": "URL", "webhookHeaderKeyPlaceholder": "Key", "webhookHeaderValuePlaceholder": "Value", - "alertLabel": "Alert" + "alertLabel": "Alert", + "domainPickerWildcardSubdomainNotAllowed": "Wildcard subdomains are not allowed." } diff --git a/server/lib/domainUtils.ts b/server/lib/domainUtils.ts index b8dab5e4c..b147e79b8 100644 --- a/server/lib/domainUtils.ts +++ b/server/lib/domainUtils.ts @@ -150,7 +150,7 @@ export async function validateAndConstructDomain( return { success: true, fullDomain, - subdomain: isWildcard ? "*" : (finalSubdomain ?? null), + subdomain: finalSubdomain ?? null, wildcard: isWildcard }; } catch (error) { diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index feb12e90e..7c6a46d8f 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -46,7 +46,7 @@ const updateHttpResourceBodySchema = z "niceId can only contain letters, numbers, and dashes" ) .optional(), - subdomain: subdomainSchema.nullable().optional(), + subdomain: z.string().nullable().optional(), ssl: z.boolean().optional(), sso: z.boolean().optional(), blockAccess: z.boolean().optional(), diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx index 2b87da745..e1c5b6ac2 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx @@ -670,6 +670,7 @@ export default function GeneralForm() {
= 1 diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx index 281248327..73926448b 100644 --- a/src/components/DomainPicker.tsx +++ b/src/components/DomainPicker.tsx @@ -46,6 +46,7 @@ import { Zap } from "lucide-react"; import { useTranslations } from "next-intl"; +import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { usePaidStatus } from "@/hooks/usePaidStatus"; import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix"; import { toUnicode } from "punycode"; @@ -119,11 +120,8 @@ export default function DomainPicker({ orgQueries.domains({ orgId }) ); - // Wildcard mode toggle — only relevant when allowWildcard is true, the - // user has a paid plan, and the selected base domain supports it. - const [wildcardMode, setWildcardMode] = useState( - wildcardAllowed && !!defaultSubdomain && isWildcardSubdomain(defaultSubdomain) - ); + // Wildcard mode is derived from the input itself — if the user types a + // wildcard subdomain (e.g. *.foo) and allowWildcard is enabled, it's active. if (!env.flags.usePangolinDns) { hideFreeDomain = true; @@ -193,7 +191,8 @@ export default function DomainPicker({ firstOrExistingDomain.type !== "cname" ? defaultSubdomain?.trim() || undefined : undefined; - const isWc = allowWildcard && !!sub && isWildcardSubdomain(sub); + const isWc = + allowWildcard && !!sub && isWildcardSubdomain(sub); onDomainChange?.({ domainId: firstOrExistingDomain.domainId, @@ -300,7 +299,8 @@ export default function DomainPicker({ }, [userInput, debouncedCheckAvailability, selectedBaseDomain]); const finalizeSubdomain = (sub: string, base: DomainOption): string => { - const sanitized = finalizeSubdomainSanitize(sub, wildcardAllowed && wildcardMode); + const wildcardMode = wildcardAllowed && isWildcardSubdomain(sub); + const sanitized = finalizeSubdomainSanitize(sub, wildcardMode); if (!sanitized) { toast({ @@ -317,7 +317,7 @@ export default function DomainPicker({ ? "provided-search" : "organization", domainType: base.domainType, - allowWildcard: wildcardAllowed && wildcardMode + allowWildcard: wildcardMode }); if (!ok) { @@ -346,7 +346,7 @@ export default function DomainPicker({ }; const handleSubdomainChange = (value: string) => { - const raw = sanitizeInputRaw(value, wildcardAllowed && wildcardMode); + const raw = sanitizeInputRaw(value, allowWildcard); setSubdomainInput(raw); setSelectedProvidedDomain(null); @@ -354,7 +354,7 @@ export default function DomainPicker({ const fullDomain = raw ? `${raw}.${selectedBaseDomain.domain}` : selectedBaseDomain.domain; - const isWc = wildcardAllowed && wildcardMode && isWildcardSubdomain(raw); + const isWc = wildcardAllowed && isWildcardSubdomain(raw); onDomainChange?.({ domainId: selectedBaseDomain.domainId!, @@ -384,16 +384,15 @@ export default function DomainPicker({ const handleBaseDomainSelect = (option: DomainOption) => { let sub = subdomainInput; - // Wildcard mode is not applicable for cname or provided-search domains, - // or when the user doesn't have a paid plan. - const newWildcardMode = + // If the selected domain doesn't support wildcards, strip any wildcard prefix. + const supportsWildcard = wildcardAllowed && - wildcardMode && option.type === "organization" && option.domainType !== "cname"; - if (newWildcardMode !== wildcardMode) { - setWildcardMode(newWildcardMode); + if (!supportsWildcard && isWildcardSubdomain(sub)) { + sub = sub.replace(/^\*\./, ""); + setSubdomainInput(sub); } if (sub && sub.trim() !== "") { @@ -419,7 +418,7 @@ export default function DomainPicker({ } const fullDomain = sub ? `${sub}.${option.domain}` : option.domain; - const isWc = newWildcardMode && !!sub && isWildcardSubdomain(sub); + const isWc = wildcardAllowed && !!sub && isWildcardSubdomain(sub); if (option.type === "provided-search") { onDomainChange?.(null); // prevent the modal from closing with `.Free Provided domain` @@ -464,7 +463,8 @@ export default function DomainPicker({ ? "provided-search" : "organization", domainType: selectedBaseDomain.domainType, - allowWildcard: wildcardAllowed && wildcardMode + allowWildcard: + wildcardAllowed && isWildcardSubdomain(subdomainInput) }) : true; @@ -473,30 +473,6 @@ export default function DomainPicker({ selectedBaseDomain.type === "organization" && selectedBaseDomain.domainType !== "cname"; - // Wildcard toggle is shown when the caller opts in and the selected domain - // supports it (ns or wildcard type). Shown even when unpaid so we can - // render a disabled state explaining it's a paid feature. - const showWildcardToggle = - allowWildcard && - showSubdomainInput && - (selectedBaseDomain?.domainType === "ns" || - selectedBaseDomain?.domainType === "wildcard"); - - const handleWildcardModeChange = (enabled: boolean) => { - setWildcardMode(enabled); - // Reset subdomain input when toggling modes to avoid invalid state - setSubdomainInput(""); - if (selectedBaseDomain?.type === "organization") { - onDomainChange?.({ - domainId: selectedBaseDomain.domainId!, - type: "organization", - subdomain: undefined, - fullDomain: selectedBaseDomain.domain, - baseDomain: selectedBaseDomain.domain, - wildcard: enabled ? true : false - }); - } - }; const showProvidedDomainSearch = selectedBaseDomain?.type === "provided-search"; @@ -525,30 +501,6 @@ export default function DomainPicker({ - {showWildcardToggle && ( - - )}
{showSubdomainInput && subdomainInput && - !isValidSubdomainStructure(subdomainInput, wildcardAllowed && wildcardMode) && ( + !isValidSubdomainStructure( + subdomainInput, + wildcardAllowed && + isWildcardSubdomain(subdomainInput) + ) && (

{t("domainPickerInvalidSubdomainStructure")}

)} + {allowWildcard && + !wildcardAllowed && + showSubdomainInput && + isWildcardSubdomain(subdomainInput) && ( + <> +

+ {t( + "domainPickerWildcardSubdomainNotAllowed" + )} +

+ + + )}
@@ -678,23 +653,23 @@ export default function DomainPicker({ {orgDomain.type === - "wildcard" - ? t( - "domainPickerManual" - ) - : ( - <> - {orgDomain.type.toUpperCase()}{" "} - •{" "} - {orgDomain.verified - ? t( - "domainPickerVerified" - ) - : t( - "domainPickerUnverified" - )} - - )} + "wildcard" ? ( + t( + "domainPickerManual" + ) + ) : ( + <> + {orgDomain.type.toUpperCase()}{" "} + •{" "} + {orgDomain.verified + ? t( + "domainPickerVerified" + ) + : t( + "domainPickerUnverified" + )} + + )}
{requiresPaywall && !hideFreeDomain && ( - - -
- - - {t("domainPickerFreeDomainsPaidFeature")} - -
-
-
- )} + + +
+ + + {t("domainPickerFreeDomainsPaidFeature")} + +
+
+
+ )} {/*showProvidedDomainSearch && build === "saas" && ( diff --git a/src/components/ShowTrialCard.tsx b/src/components/ShowTrialCard.tsx index 1cc8e79f1..dc58483f8 100644 --- a/src/components/ShowTrialCard.tsx +++ b/src/components/ShowTrialCard.tsx @@ -14,7 +14,7 @@ import { } from "@app/components/ui/tooltip"; import { useTranslations } from "next-intl"; -const TRIAL_DURATION_DAYS = 14; +const TRIAL_DURATION_DAYS = 10; export default function ShowTrialCard({ isCollapsed diff --git a/src/hooks/useSubscriptionStatusContext.ts b/src/hooks/useSubscriptionStatusContext.ts index 59d6a6b9a..2fd61dd2e 100644 --- a/src/hooks/useSubscriptionStatusContext.ts +++ b/src/hooks/useSubscriptionStatusContext.ts @@ -8,9 +8,7 @@ export function useSubscriptionStatusContext() { } const context = useContext(SubscriptionStatusContext); if (context === undefined) { - throw new Error( - "useSubscriptionStatusContext must be used within an SubscriptionStatusProvider" - ); + return null; } return context; }