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({