diff --git a/.server-changes/fix-managed-cloud-org-activation.md b/.server-changes/fix-managed-cloud-org-activation.md new file mode 100644 index 00000000000..e9f395ffc66 --- /dev/null +++ b/.server-changes/fix-managed-cloud-org-activation.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Select plan flow when creating an org. Don't require GitHub verification to access the free plan. diff --git a/apps/webapp/app/models/admin.server.ts b/apps/webapp/app/models/admin.server.ts index 4963a0befc2..406845e5859 100644 --- a/apps/webapp/app/models/admin.server.ts +++ b/apps/webapp/app/models/admin.server.ts @@ -131,7 +131,7 @@ export async function adminGetOrganizations(userId: string, { page, search }: Se slug: true, title: true, v2Enabled: true, - v3Enabled: true, + isActivated: true, deletedAt: true, members: { select: { diff --git a/apps/webapp/app/models/organization.server.ts b/apps/webapp/app/models/organization.server.ts index 79c1ef1b261..a4b8a8ab5f5 100644 --- a/apps/webapp/app/models/organization.server.ts +++ b/apps/webapp/app/models/organization.server.ts @@ -86,7 +86,7 @@ export async function createOrganization( ); } - const _features = featuresForUrl(new URL(env.APP_ORIGIN)); + const features = featuresForUrl(new URL(env.APP_ORIGIN)); const organization = await prisma.organization.create({ data: { @@ -102,7 +102,10 @@ export async function createOrganization( role: "ADMIN", }, }, - v3Enabled: true, + // Managed-cloud orgs start deactivated so they're routed through + // select-plan, which provisions their billing entitlement and activates + // them. Self-hosters have no billing gate, so they're active immediately. + isActivated: !features.isManagedCloud, }, include: { members: true, diff --git a/apps/webapp/app/models/project.server.ts b/apps/webapp/app/models/project.server.ts index 5f07d4d28cf..a9feb394754 100644 --- a/apps/webapp/app/models/project.server.ts +++ b/apps/webapp/app/models/project.server.ts @@ -32,7 +32,7 @@ export async function createProject( select: { id: true, slug: true, - v3Enabled: true, + isActivated: true, maximumConcurrencyLimit: true, maximumProjectCount: true, }, @@ -49,7 +49,7 @@ export async function createProject( } if (version === "v3") { - if (!organization.v3Enabled) { + if (!organization.isActivated) { throw new Error(`Organization can't create v3 projects.`); } } diff --git a/apps/webapp/app/routes/_app.orgs.$organizationId.subscription.v3.free_connect_failed/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationId.subscription.v3.free_connect_failed/route.tsx deleted file mode 100644 index 941f7e4d9c5..00000000000 --- a/apps/webapp/app/routes/_app.orgs.$organizationId.subscription.v3.free_connect_failed/route.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { z } from "zod"; -import { prisma } from "~/db.server"; -import { redirectWithErrorMessage } from "~/models/message.server"; -import { newProjectPath } from "~/utils/pathBuilder"; - -const ParamsSchema = z.object({ - organizationId: z.string(), -}); - -export const loader = async ({ request, params }: LoaderFunctionArgs) => { - const { organizationId } = ParamsSchema.parse(params); - - const org = await prisma.organization.findFirst({ - select: { - slug: true, - _count: { - select: { - projects: true, - }, - }, - }, - where: { - id: organizationId, - }, - }); - - if (!org) { - throw new Response(null, { status: 404 }); - } - - const url = new URL(request.url); - const searchParams = new URLSearchParams(url.search); - const reason = searchParams.get("reason"); - - let errorMessage = reason ? decodeURIComponent(reason) : "Failed to verify your GitHub account"; - - return redirectWithErrorMessage(newProjectPath({ slug: org.slug }), request, errorMessage); -}; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationId.subscription.v3.free_connect_success/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationId.subscription.v3.free_connect_success/route.tsx deleted file mode 100644 index fb7a005061a..00000000000 --- a/apps/webapp/app/routes/_app.orgs.$organizationId.subscription.v3.free_connect_success/route.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; -import { redirect } from "remix-typedjson"; -import { z } from "zod"; -import { prisma } from "~/db.server"; -import { redirectWithSuccessMessage } from "~/models/message.server"; -import { newProjectPath, v3BillingPath } from "~/utils/pathBuilder"; - -const ParamsSchema = z.object({ - organizationId: z.string(), -}); - -export const loader = async ({ request, params }: LoaderFunctionArgs) => { - const { organizationId } = ParamsSchema.parse(params); - - const org = await prisma.organization.findFirst({ - select: { - slug: true, - _count: { - select: { - projects: true, - }, - }, - }, - where: { - id: organizationId, - }, - }); - - if (!org) { - throw new Response(null, { status: 404 }); - } - - const hasProject = org._count.projects > 0; - - if (hasProject) { - return redirectWithSuccessMessage( - v3BillingPath({ slug: org.slug }), - request, - "Free tier unlocked successfully." - ); - } - - return redirect(newProjectPath({ slug: org.slug }, "You're on the Free plan.")); -}; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx index edc625f128e..b5b97a0bb69 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx @@ -118,7 +118,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { select: { id: true, title: true, - v3Enabled: true, + isActivated: true, v2Enabled: true, hasRequestedV3: true, _count: { @@ -138,7 +138,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { } const { isManagedCloud } = featuresForRequest(request); - if (isManagedCloud && !organization.v3Enabled) { + if (isManagedCloud && !organization.isActivated) { return redirect(selectPlanPath({ slug: organizationSlug })); } @@ -151,7 +151,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) { title: organization.title, slug: organizationSlug, projectsCount: organization._count.projects, - v3Enabled: organization.v3Enabled, + isActivated: organization.isActivated, v2Enabled: organization.v2Enabled, hasRequestedV3: organization.hasRequestedV3, }, @@ -324,7 +324,7 @@ export default function Page() { const { organization, message } = useTypedLoaderData(); const lastSubmission = useActionData(); - const canCreateV3Projects = organization.v3Enabled; + const canCreateV3Projects = organization.isActivated; const [form, { projectName, projectVersion }] = useForm({ id: "create-project", diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.select-plan/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.select-plan/route.tsx index cc93b7bc2df..f5658bc4d8b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.select-plan/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.select-plan/route.tsx @@ -43,7 +43,7 @@ export const loader = dashboardLoader( throw new Response(null, { status: 404, statusText: "Organization not found" }); } - if (organization.v3Enabled) { + if (organization.isActivated) { return redirect(organizationPath({ slug: organizationSlug })); } @@ -73,7 +73,6 @@ export default function ChoosePlanPage() { subscription={v3Subscription} organizationSlug={organizationSlug} hasPromotedPlan - showGithubVerificationBadge periodEnd={periodEnd} /> diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx index b87c1bfc4c1..02bd91cb9d5 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx @@ -1,13 +1,7 @@ -import { - CheckIcon, - ExclamationTriangleIcon, - ShieldCheckIcon, - XMarkIcon, -} from "@heroicons/react/20/solid"; +import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid"; import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/24/outline"; import { Form, useLocation, useNavigation } from "@remix-run/react"; import { uiComponent } from "@team-plain/typescript-sdk"; -import { GitHubLightIcon } from "@trigger.dev/companyicons"; import { type AddOnPricing, type FreePlanDefinition, @@ -36,7 +30,6 @@ import { Paragraph } from "~/components/primitives/Paragraph"; import { Spinner } from "~/components/primitives/Spinner"; import { TextArea } from "~/components/primitives/TextArea"; import { TextLink } from "~/components/primitives/TextLink"; -import { SimpleTooltip } from "~/components/primitives/Tooltip"; import { prisma } from "~/db.server"; import { redirectWithErrorMessage } from "~/models/message.server"; import { resolveOrgIdFromSlug } from "~/models/organization.server"; @@ -171,10 +164,6 @@ const pricingDefinitions = { title: "Usage", content: "The compute cost when tasks are executing.", }, - freeUsage: { - title: "Free usage", - content: "Requires a verified GitHub account.", - }, concurrentRuns: { title: "Concurrent runs", content: "The number of runs that can be executed at the same time.", @@ -255,7 +244,6 @@ type PricingPlansProps = { subscription?: SubscriptionResult; organizationSlug: string; hasPromotedPlan: boolean; - showGithubVerificationBadge?: boolean; periodEnd: Date; }; @@ -265,7 +253,6 @@ export function PricingPlans({ subscription, organizationSlug, hasPromotedPlan, - showGithubVerificationBadge, periodEnd, }: PricingPlansProps) { return ( @@ -275,7 +262,6 @@ export function PricingPlans({ plan={plans.free} subscription={subscription} organizationSlug={organizationSlug} - showGithubVerificationBadge={showGithubVerificationBadge} periodEnd={periodEnd} /> { setIsDialogOpen(false); @@ -325,253 +308,145 @@ export function TierFree({ return ( -
- - {showGithubVerificationBadge && status === "approved" && ( - - - GitHub verified -
- } - content={ -
-
- - - verified - -
- - You have connected a verified GitHub account. This is required for the Free plan - to prevent malicious use of our platform. + + {subscription?.plan !== undefined && + subscription.plan.type !== "free" && + subscription.canceledAt === undefined ? ( + + +
+ +
+
+ +
+ + + Downgrade plan? +
+ + + Are you sure you want to downgrade? You will lose access to your current plan's + features on{" "} + .
- } - /> - )} -
- {status === "rejected" ? ( -
-
- - - Your Trigger.dev account failed to be verified for the Free plan because your GitHub - account is too new. We require verification to prevent malicious use of our platform. - - - You can still select a paid plan to continue or if you think this is a mistake,{" "} - - get in touch - - } - /> - . - -
-
- ) : ( - <> - {status === "requires_connect" ? ( - - -
- +
+
+ Why are you thinking of downgrading? +
    + {[ + "The Free plan is all I need", + "Subscription or usage costs too expensive", + "Bugs or technical issues", + "No longer need the service", + "Found a better alternative", + "Lacking features I need", + ].map((label, index) => ( +
  • + { + if (label === "Lacking features I need") { + setIsLackingFeaturesChecked(isChecked); + } + }} + /> +
  • + ))} +
- - - - - - Unlock the Free plan -
- - - To unlock the Free plan, we need to verify that you have an active GitHub - account. - - - We do this to prevent malicious use of our platform. We only ask for the - minimum permissions to verify your account. - -
- - - - -
-
- ) : subscription?.plan !== undefined && - subscription.plan.type !== "free" && - subscription.canceledAt === undefined ? ( - - -
- +
+ + {isLackingFeaturesChecked + ? "What features do you need? Or how can we improve?" + : "What can we do to improve?"} + +