diff --git a/apps/web/app/(ee)/api/admin/links/count/route.ts b/apps/web/app/(ee)/api/admin/links/count/route.ts
index fca4f33d336..5079ff8e387 100644
--- a/apps/web/app/(ee)/api/admin/links/count/route.ts
+++ b/apps/web/app/(ee)/api/admin/links/count/route.ts
@@ -1,6 +1,6 @@
import { withAdmin } from "@/lib/auth";
import { prisma } from "@dub/prisma";
-import { DUB_DOMAINS_ARRAY, LEGAL_USER_ID } from "@dub/utils";
+import { LEGAL_USER_ID } from "@dub/utils";
import { NextResponse } from "next/server";
// GET /api/admin/links/count
@@ -18,15 +18,10 @@ export const GET = withAdmin(async ({ searchParams }) => {
const linksWhere = {
// when filtering by domain, only filter by domain if the filter group is not "Domains"
- ...(domain && groupBy !== "domain"
- ? {
- domain,
- }
- : {
- domain: {
- in: DUB_DOMAINS_ARRAY,
- },
- }),
+ ...(domain &&
+ groupBy !== "domain" && {
+ domain,
+ }),
userId: {
not: LEGAL_USER_ID,
},
diff --git a/apps/web/app/(ee)/api/admin/links/route.ts b/apps/web/app/(ee)/api/admin/links/route.ts
index b88b0af97b2..4491a643595 100644
--- a/apps/web/app/(ee)/api/admin/links/route.ts
+++ b/apps/web/app/(ee)/api/admin/links/route.ts
@@ -1,7 +1,7 @@
import { transformLink } from "@/lib/api/links";
import { withAdmin } from "@/lib/auth";
import { prisma } from "@dub/prisma";
-import { DUB_DOMAINS_ARRAY, LEGAL_USER_ID } from "@dub/utils";
+import { LEGAL_USER_ID } from "@dub/utils";
import { NextResponse } from "next/server";
// GET /api/admin/links
@@ -20,13 +20,7 @@ export const GET = withAdmin(async ({ searchParams }) => {
const response = await prisma.link.findMany({
where: {
- ...(domain
- ? { domain }
- : {
- domain: {
- in: DUB_DOMAINS_ARRAY,
- },
- }),
+ ...(domain && { domain }),
...(!search && {
createdAt: {
gte: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30), // 30 days ago
diff --git a/apps/web/app/api/workspaces/[idOrSlug]/billing/upgrade/route.ts b/apps/web/app/api/workspaces/[idOrSlug]/billing/upgrade/route.ts
index 65e01ae1c24..584ec383b50 100644
--- a/apps/web/app/api/workspaces/[idOrSlug]/billing/upgrade/route.ts
+++ b/apps/web/app/api/workspaces/[idOrSlug]/billing/upgrade/route.ts
@@ -15,14 +15,14 @@ const upgradePlanSchema = z.object({
message: "Invalid baseUrl.",
}),
onboarding: booleanQuerySchema.nullish(),
- isTrialVariant: booleanQuerySchema.nullish(),
});
// POST /api/workspaces/[idOrSlug]/billing/upgrade
export const POST = withWorkspace(
async ({ req, workspace, session }) => {
- let { plan, period, tier, baseUrl, onboarding, isTrialVariant } =
- upgradePlanSchema.parse(await req.json());
+ let { plan, period, tier, baseUrl, onboarding } = upgradePlanSchema.parse(
+ await req.json(),
+ );
const lookupKey =
tier > 1 ? `${plan}${tier}_${period}` : `${plan}_${period}`;
@@ -105,15 +105,13 @@ export const POST = withWorkspace(
const customer = await getDubCustomer(session.user.id);
// Only apply trial if the customer is a:
+ // - on the free plan
// - new Stripe customer
// - no prior/existing trial on workspace
- // - is coming from onboarding
- // - is trial variant
const shouldApplyCheckoutTrial =
+ workspace.plan === "free" &&
workspace.stripeId == null &&
- workspace.trialEndsAt == null &&
- onboarding &&
- isTrialVariant;
+ workspace.trialEndsAt == null;
const stripeSession = await stripe.checkout.sessions.create({
...(workspace.stripeId
diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/upgrade/adjust-usage-row.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/upgrade/adjust-usage-row.tsx
index 06b4cb953f6..fac36ed1189 100644
--- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/upgrade/adjust-usage-row.tsx
+++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/upgrade/adjust-usage-row.tsx
@@ -1,6 +1,7 @@
import useWorkspace from "@/lib/swr/use-workspace";
import { Button, Grid, Slider } from "@dub/ui";
import {
+ BUSINESS_PLAN,
ENTERPRISE_PLAN,
SELF_SERVE_PAID_PLANS,
cn,
@@ -12,11 +13,11 @@ import { useSearchParams } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
export function AdjustUsageRow({
- onLinksUsageChange,
onEventsUsageChange,
+ onLinksUsageChange,
}: {
- onLinksUsageChange: (value: number) => void;
onEventsUsageChange: (value: number) => void;
+ onLinksUsageChange: (value: number) => void;
}) {
const [isExpanded, setIsExpanded] = useState(false);
@@ -101,6 +102,11 @@ function UsageSlider({
return planDetails.limits[limitKey];
}
}
+
+ if (workspace.plan === "free") {
+ return BUSINESS_PLAN.limits[limitKey];
+ }
+
const currentLimit = workspace[workspaceLimitKey];
return usageSteps.reduce((prev, curr) =>
Math.abs(curr - currentLimit) < Math.abs(prev - currentLimit)
diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/upgrade/page.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/upgrade/page.tsx
index 6ec3ad6fd2d..e5a4c86f62e 100644
--- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/upgrade/page.tsx
+++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/upgrade/page.tsx
@@ -20,7 +20,9 @@ import {
Users2,
} from "@dub/ui";
import {
+ capitalize,
cn,
+ DUB_TRIAL_PERIOD_DAYS,
getSuggestedPlan,
isDowngradePlan,
isLegacyBusinessPlan,
@@ -185,6 +187,11 @@ export default function WorkspaceBillingUpgradePage() {
}),
);
+ const isEligibleForTrial =
+ currentPlan === "free" &&
+ stripeId == null &&
+ trialEndsAt == null;
+
return (
setLinksUsage(value)}
onEventsUsageChange={(value) => setEventsUsage(value)}
+ onLinksUsageChange={(value) => setLinksUsage(value)}
/>
diff --git a/apps/web/app/app.dub.co/(onboarding)/onboarding/use-onboarding-trial-variant.ts b/apps/web/app/app.dub.co/(onboarding)/onboarding/use-onboarding-trial-variant.ts
deleted file mode 100644
index 9f573c46bb3..00000000000
--- a/apps/web/app/app.dub.co/(onboarding)/onboarding/use-onboarding-trial-variant.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { useSyncedLocalStorage } from "@/lib/hooks/use-synced-local-storage";
-import { useEffect } from "react";
-
-export function useOnboardingTrialVariant() {
- const [trialVariant, setTrialVariant] = useSyncedLocalStorage<
- "control" | "trial_d1d6f8671832d7a30e805a7fa01f968b" | undefined
- >("dub_onboarding_trial_variant", undefined);
-
- useEffect(() => {
- if (trialVariant !== undefined) return;
- setTrialVariant(
- Math.random() > 0.5
- ? "trial_d1d6f8671832d7a30e805a7fa01f968b"
- : "control",
- );
- }, [trialVariant]);
-
- return {
- isTrialVariant: trialVariant === "trial_d1d6f8671832d7a30e805a7fa01f968b",
- };
-}
diff --git a/apps/web/app/providers.tsx b/apps/web/app/providers.tsx
index ac1071be3ed..df4ef5ae916 100644
--- a/apps/web/app/providers.tsx
+++ b/apps/web/app/providers.tsx
@@ -4,21 +4,11 @@ import { KeyboardShortcutProvider, TooltipProvider } from "@dub/ui";
import PlausibleProvider from "next-plausible";
import { ReactNode } from "react";
import { Toaster } from "sonner";
-import { useOnboardingTrialVariant } from "./app.dub.co/(onboarding)/onboarding/use-onboarding-trial-variant";
export default function RootProviders({ children }: { children: ReactNode }) {
- const { isTrialVariant } = useOnboardingTrialVariant();
-
return (
-
+
{children}
diff --git a/apps/web/lib/actions/send-otp.ts b/apps/web/lib/actions/send-otp.ts
index ed977276d60..8e73bd4de62 100644
--- a/apps/web/lib/actions/send-otp.ts
+++ b/apps/web/lib/actions/send-otp.ts
@@ -40,23 +40,28 @@ export const sendOtpAction = actionClient
const isGenericEmailWithPlus = email.includes("+") && isGenericEmail(email);
- const emailDomain = email.split("@")[1];
+ const emailDomain = (email.split("@")[1] ?? "").trim().toLowerCase();
const [isDisposable, emailDomainTerms] = await Promise.all([
redis.sismember("disposableEmailDomains", emailDomain),
process.env.EDGE_CONFIG ? get("emailDomainTerms") : [],
]);
- // Only build the regex if we have at least one term; otherwise set to null
- const blacklistedEmailDomainTermsRegex =
+ const escapedDomainTerms =
emailDomainTerms && Array.isArray(emailDomainTerms)
- ? new RegExp(
- emailDomainTerms
- .map((term: string) =>
- term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
- ) // replace special characters with escape sequences
- .join("|"),
- )
+ ? emailDomainTerms
+ .map((term: string) =>
+ String(term)
+ .trim()
+ .toLowerCase()
+ .replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
+ )
+ .filter((term) => term.length > 0)
+ : [];
+
+ const blacklistedEmailDomainTermsRegex =
+ escapedDomainTerms.length > 0
+ ? new RegExp(escapedDomainTerms.join("|"))
: null;
// if any of the flags match, run one final edge case check, before throwing an error
diff --git a/apps/web/lib/zod/schemas/opens.ts b/apps/web/lib/zod/schemas/opens.ts
index 973689af3ff..96f2637bb68 100644
--- a/apps/web/lib/zod/schemas/opens.ts
+++ b/apps/web/lib/zod/schemas/opens.ts
@@ -30,7 +30,7 @@ export const trackOpenResponseSchema = z.object({
.string()
.nullable()
.describe(
- "The click ID of the associated open event (or the prior click that led the user to the app store for probabilistic tracking). This will be `null` if the open event was not associated with a deep link (e.g. a direct download from the app store), or if the open event was performed by a bot (no click recorded). Learn more: https://d.to/ddl",
+ "The click ID of the associated open event (or the prior click that led the user to the app store for probabilistic tracking). Learn more: https://d.to/ddl",
),
link: z
.object({
diff --git a/apps/web/ui/modals/manage-usage-modal.tsx b/apps/web/ui/modals/manage-usage-modal.tsx
index 94904ef5def..5b4f23d0825 100644
--- a/apps/web/ui/modals/manage-usage-modal.tsx
+++ b/apps/web/ui/modals/manage-usage-modal.tsx
@@ -2,8 +2,10 @@ import { clientAccessCheck } from "@/lib/client-access-check";
import useWorkspace from "@/lib/swr/use-workspace";
import { CursorRays, Hyperlink, Modal, Slider, ToggleGroup } from "@dub/ui";
import {
+ DUB_TRIAL_PERIOD_DAYS,
ENTERPRISE_PLAN,
SELF_SERVE_PAID_PLANS,
+ capitalize,
cn,
getSuggestedPlan,
isDowngradePlan,
@@ -27,8 +29,17 @@ type ManageUsageModalProps = {
function ManageUsageModalContent({ type }: ManageUsageModalProps) {
const workspace = useWorkspace();
- const { slug, role, plan, planPeriod, planTier, usageLimit, linksLimit } =
- workspace;
+ const {
+ slug,
+ role,
+ stripeId,
+ plan,
+ planPeriod,
+ planTier,
+ trialEndsAt,
+ usageLimit,
+ linksLimit,
+ } = workspace;
const { error: permissionsError } = clientAccessCheck({
action: "billing.write",
@@ -85,6 +96,9 @@ function ManageUsageModalContent({ type }: ManageUsageModalProps) {
newTier: suggestedPlanTier,
});
+ const isEligibleForTrial =
+ plan === "free" && stripeId == null && trialEndsAt == null;
+
if (usageSteps.length < 2) return null;
return (
@@ -185,9 +199,11 @@ function ManageUsageModalContent({ type }: ManageUsageModalProps) {
? "Current plan"
: isDowngradeSuggested
? "Downgrade"
- : planPeriod !== period
- ? `Switch to ${period}`
- : "Upgrade"
+ : planPeriod && planPeriod !== period
+ ? `Switch to ${suggestedPlan.name} ${capitalize(period)}`
+ : isEligibleForTrial
+ ? `Start ${DUB_TRIAL_PERIOD_DAYS}-day trial`
+ : `Upgrade to ${suggestedPlan.name} ${capitalize(period)}`
}
variant={isDowngradeSuggested ? "secondary" : "primary"}
className="h-8 rounded-lg shadow-sm"
diff --git a/apps/web/ui/partners/partners-upgrade-modal.tsx b/apps/web/ui/partners/partners-upgrade-modal.tsx
index 132809b7d7c..ed20e37452e 100644
--- a/apps/web/ui/partners/partners-upgrade-modal.tsx
+++ b/apps/web/ui/partners/partners-upgrade-modal.tsx
@@ -9,7 +9,7 @@ import {
Tooltip,
useRouterStuff,
} from "@dub/ui";
-import { cn, INFINITY_NUMBER, nFormatter, PLANS } from "@dub/utils";
+import { capitalize, cn, INFINITY_NUMBER, nFormatter, PLANS } from "@dub/utils";
import NumberFlow from "@number-flow/react";
import Link from "next/link";
import { Dispatch, ReactNode, SetStateAction, useMemo, useState } from "react";
@@ -263,7 +263,7 @@ export function PartnersUpgradeModal({
) : (
diff --git a/apps/web/ui/partners/program-card.tsx b/apps/web/ui/partners/program-card.tsx
index 77ac5f3f8c0..126ba052b1b 100644
--- a/apps/web/ui/partners/program-card.tsx
+++ b/apps/web/ui/partners/program-card.tsx
@@ -70,7 +70,7 @@ function rejectedApplicationTooltipContent(
}
return (
-
+
{reviewedAt ? (
}
diff --git a/apps/web/ui/workspaces/upgrade-plan-button.tsx b/apps/web/ui/workspaces/upgrade-plan-button.tsx
index 1ced6e1ffc9..9c079766853 100644
--- a/apps/web/ui/workspaces/upgrade-plan-button.tsx
+++ b/apps/web/ui/workspaces/upgrade-plan-button.tsx
@@ -12,7 +12,6 @@ import {
isWorkspaceBillingTrialActive,
SELF_SERVE_PAID_PLANS,
} from "@dub/utils";
-import { useOnboardingTrialVariant } from "app/app.dub.co/(onboarding)/onboarding/use-onboarding-trial-variant";
import { usePlausible } from "next-plausible";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useMemo, useState } from "react";
@@ -44,7 +43,6 @@ export function UpgradePlanButton({
const plausible = usePlausible();
const product = searchParams.get("product");
- const { isTrialVariant } = useOnboardingTrialVariant();
const isTrialActive = isWorkspaceBillingTrialActive(trialEndsAt);
const selectedPlan =
@@ -96,7 +94,6 @@ export function UpgradePlanButton({
period,
baseUrl: `${APP_DOMAIN}${pathname}${queryString.length > 0 ? `?${queryString}` : ""}`,
onboarding: searchParams.get("workspace") ? "true" : "false",
- isTrialVariant: isTrialVariant ? "true" : "false",
}),
},
);
@@ -178,17 +175,16 @@ export function UpgradePlanButton({