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;
}