Stripe portal links are generated when the user clicks through

This commit is contained in:
Matt Aitken
2023-12-05 12:43:55 +00:00
parent 5a63101d76
commit d202705e37
3 changed files with 44 additions and 78 deletions
@@ -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);
}
+4
View File
@@ -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`;
}