Stripe portal links are generated when the user clicks through
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
import { ArrowUpCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { CalendarDaysIcon, ReceiptRefundIcon } from "@heroicons/react/20/solid";
|
||||
import { ArrowUpCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { Outlet } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { ActiveSubscription } from "@trigger.dev/billing";
|
||||
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { BreadcrumbLink } from "~/components/navigation/Breadcrumb";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
@@ -17,17 +16,12 @@ import {
|
||||
PageTitle,
|
||||
PageTitleRow,
|
||||
} from "~/components/primitives/PageHeader";
|
||||
import { featuresForRequest } from "~/features.server";
|
||||
import { useFeatures } from "~/hooks/useFeatures";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { BillingService } from "~/services/billing.server";
|
||||
import { OrgUsagePresenter } from "~/presenters/OrgUsagePresenter.server";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { formatDurationInDays } from "~/utils";
|
||||
import { Handle } from "~/utils/handle";
|
||||
import { OrganizationParamsSchema, plansPath, usagePath } from "~/utils/pathBuilder";
|
||||
import { plansPath, stripePortalPath, usagePath } from "~/utils/pathBuilder";
|
||||
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
|
||||
import { ActiveSubscription } from "@trigger.dev/billing";
|
||||
|
||||
export const handle: Handle = {
|
||||
breadcrumb: (match) => <BreadcrumbLink to={match.pathname} title="Usage & Billing" />,
|
||||
@@ -70,12 +64,12 @@ export default function Page() {
|
||||
<>
|
||||
{currentPlan?.subscription?.isPaying && (
|
||||
<>
|
||||
{/* <LinkButton to={stripePortalLink} variant="secondary/small">
|
||||
<LinkButton to={stripePortalPath(organization)} variant="secondary/small">
|
||||
Invoices
|
||||
</LinkButton>
|
||||
<LinkButton to={stripePortalLink} variant="secondary/small">
|
||||
<LinkButton to={stripePortalPath(organization)} variant="secondary/small">
|
||||
Manage card details
|
||||
</LinkButton> */}
|
||||
</LinkButton>
|
||||
</>
|
||||
)}
|
||||
<LinkButton
|
||||
|
||||
@@ -1,79 +1,47 @@
|
||||
import { parse } from "@conform-to/zod";
|
||||
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { SetPlanBodySchema } from "@trigger.dev/billing";
|
||||
import { ActionFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { redirect } from "remix-typedjson";
|
||||
import { prisma } from "~/db.server";
|
||||
import { redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { redirectBackWithErrorMessage, redirectWithErrorMessage } from "~/models/message.server";
|
||||
import { BillingService } from "~/services/billing.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { requireUser } from "~/services/session.server";
|
||||
import {
|
||||
OrganizationParamsSchema,
|
||||
organizationBillingPath,
|
||||
subscribedPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { OrganizationParamsSchema, usagePath } from "~/utils/pathBuilder";
|
||||
|
||||
export async function action({ request, params }: ActionFunctionArgs) {
|
||||
export async function loader({ request, params }: ActionFunctionArgs) {
|
||||
const user = await requireUser(request);
|
||||
|
||||
const { organizationSlug } = OrganizationParamsSchema.parse(params);
|
||||
|
||||
const formData = await request.formData();
|
||||
const submission = parse(formData, { schema: SetPlanBodySchema });
|
||||
if (!submission.value || submission.intent !== "submit") {
|
||||
return json(submission);
|
||||
}
|
||||
|
||||
try {
|
||||
const org = await prisma.organization.findUnique({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
slug: organizationSlug,
|
||||
members: {
|
||||
some: {
|
||||
userId: user.id,
|
||||
},
|
||||
const org = await prisma.organization.findUnique({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
slug: organizationSlug,
|
||||
members: {
|
||||
some: {
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (!org) {
|
||||
submission.error.message = "Invalid organization";
|
||||
return json(submission);
|
||||
}
|
||||
|
||||
const billingPresenter = new BillingService(true);
|
||||
const result = await billingPresenter.setPlan(org.id, submission.value);
|
||||
if (result === undefined) {
|
||||
submission.error.message = "No billing client";
|
||||
return json(submission);
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
submission.error.message = result.error;
|
||||
return json(submission);
|
||||
}
|
||||
|
||||
switch (result.action) {
|
||||
case "create_subscription_flow_start": {
|
||||
return redirect(result.checkoutUrl);
|
||||
}
|
||||
case "canceled_subscription": {
|
||||
return redirectWithSuccessMessage(
|
||||
organizationBillingPath({ slug: organizationSlug }),
|
||||
request,
|
||||
"Your subscription has been canceled."
|
||||
);
|
||||
}
|
||||
case "updated_subscription": {
|
||||
return redirect(subscribedPath({ slug: organizationSlug }), request);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Error setting plan", { error: e });
|
||||
submission.error.message = e instanceof Error ? e.message : JSON.stringify(e);
|
||||
return json(submission);
|
||||
if (!org) {
|
||||
return redirectWithErrorMessage(
|
||||
usagePath({ slug: organizationSlug }),
|
||||
request,
|
||||
"Something went wrong. Please try again later."
|
||||
);
|
||||
}
|
||||
|
||||
const billingPresenter = new BillingService(true);
|
||||
const result = await billingPresenter.customerPortalUrl(org.id, organizationSlug);
|
||||
|
||||
if (!result || !result.success || !result.customerPortalUrl) {
|
||||
return redirectWithErrorMessage(
|
||||
usagePath({ slug: organizationSlug }),
|
||||
request,
|
||||
"Something went wrong. Please try again later."
|
||||
);
|
||||
}
|
||||
|
||||
return redirect(result.customerPortalUrl);
|
||||
}
|
||||
|
||||
@@ -125,6 +125,10 @@ export function usagePath(organization: OrgForPath) {
|
||||
return `${organizationPath(organization)}/billing`;
|
||||
}
|
||||
|
||||
export function stripePortalPath(organization: OrgForPath) {
|
||||
return `/resources/${organization.slug}/subscription/portal`;
|
||||
}
|
||||
|
||||
export function plansPath(organization: OrgForPath) {
|
||||
return `${organizationPath(organization)}/billing/plans`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user