Skip to content

Commit 97bb793

Browse files
add manage subscription button
1 parent 65a016c commit 97bb793

File tree

4 files changed

+104
-3
lines changed

4 files changed

+104
-3
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"use client";
2+
3+
import { useState, useCallback } from "react";
4+
import { LoadingButton } from "@/components/ui/loading-button";
5+
import { createPortalSession } from "@/ee/features/lighthouse/actions";
6+
import { isServiceError } from "@/lib/utils";
7+
import { useToast } from "@/components/hooks/use-toast";
8+
import { useRouter } from "next/navigation";
9+
10+
export function ManageSubscriptionButton() {
11+
const [isLoading, setIsLoading] = useState(false);
12+
const { toast } = useToast();
13+
const router = useRouter();
14+
15+
const handleClick = useCallback(() => {
16+
setIsLoading(true);
17+
18+
const returnUrl = `${window.location.origin}/settings/license`;
19+
20+
createPortalSession(returnUrl)
21+
.then((response) => {
22+
if (isServiceError(response)) {
23+
toast({
24+
description: `Failed to open subscription portal: ${response.message}`,
25+
variant: "destructive",
26+
});
27+
setIsLoading(false);
28+
} else {
29+
router.push(response.url);
30+
}
31+
});
32+
}, [router, toast]);
33+
34+
return (
35+
<LoadingButton
36+
variant="outline"
37+
onClick={handleClick}
38+
loading={isLoading}
39+
>
40+
Manage subscription
41+
</LoadingButton>
42+
);
43+
}

packages/web/src/app/(app)/settings/license/page.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { authenticatedPage } from "@/middleware/authenticatedPage";
22
import { OrgRole } from "@sourcebot/db";
33
import { ActivationCodeCard } from "./activationCodeCard";
44
import { PurchaseButton } from "./purchaseButton";
5+
import { ManageSubscriptionButton } from "./manageSubscriptionButton";
56
import { BasicSettingsCard } from "../components/settingsCard";
67
import { getPlan } from "@/lib/entitlements";
78

@@ -25,7 +26,10 @@ export default authenticatedPage(async ({ prisma, org }) => {
2526
<span className="text-sm font-medium">{plan}</span>
2627
</BasicSettingsCard>
2728
<ActivationCodeCard isActivated={!!license} />
28-
<PurchaseButton />
29+
<div className="flex gap-3">
30+
<PurchaseButton />
31+
{license && <ManageSubscriptionButton />}
32+
</div>
2933
</div>
3034
);
3135
}, {

packages/web/src/ee/features/lighthouse/actions.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import { OrgRole } from "@sourcebot/db";
77
import { ServiceError } from "@/lib/serviceError";
88
import { StatusCodes } from "http-status-codes";
99
import { ErrorCode } from "@/lib/errorCodes";
10-
import { env, encryptActivationCode } from "@sourcebot/shared";
10+
import { env, encryptActivationCode, decryptActivationCode } from "@sourcebot/shared";
1111
import { sendServicePing } from "@/ee/features/lighthouse/servicePing";
1212
import { fetchWithRetry } from "@/lib/utils";
13-
import { checkoutResponseSchema } from "./types";
13+
import { checkoutResponseSchema, portalResponseSchema } from "./types";
1414

1515
export const activateLicense = async (activationCode: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
1616
withAuth(async ({ org, role, prisma }) =>
@@ -124,3 +124,53 @@ export const deactivateLicense = async (): Promise<{ success: boolean } | Servic
124124
})
125125
)
126126
);
127+
128+
export const createPortalSession = async (returnUrl: string): Promise<{ url: string } | ServiceError> => sew(() =>
129+
withAuth(async ({ org, role, prisma }) =>
130+
withMinimumOrgRole(role, OrgRole.OWNER, async () => {
131+
const license = await prisma.license.findUnique({
132+
where: { orgId: org.id },
133+
});
134+
135+
if (!license) {
136+
return {
137+
statusCode: StatusCodes.NOT_FOUND,
138+
errorCode: ErrorCode.NOT_FOUND,
139+
message: "No license found.",
140+
} satisfies ServiceError;
141+
}
142+
143+
const activationCode = decryptActivationCode(license.activationCode);
144+
145+
const response = await fetchWithRetry(`${env.SOURCEBOT_LIGHTHOUSE_URL}/portal`, {
146+
method: 'POST',
147+
headers: { 'Content-Type': 'application/json' },
148+
body: JSON.stringify({
149+
activationCode,
150+
returnUrl,
151+
}),
152+
});
153+
154+
if (!response.ok) {
155+
return {
156+
statusCode: StatusCodes.BAD_GATEWAY,
157+
errorCode: ErrorCode.UNEXPECTED_ERROR,
158+
message: "Failed to create portal session.",
159+
} satisfies ServiceError;
160+
}
161+
162+
const body = await response.json();
163+
const result = portalResponseSchema.safeParse(body);
164+
165+
if (!result.success) {
166+
return {
167+
statusCode: StatusCodes.BAD_GATEWAY,
168+
errorCode: ErrorCode.UNEXPECTED_ERROR,
169+
message: "Invalid response from Lighthouse.",
170+
} satisfies ServiceError;
171+
}
172+
173+
return { url: result.data.url };
174+
})
175+
)
176+
);

packages/web/src/ee/features/lighthouse/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ export const lighthouseResponseSchema = z.object({
99
export const checkoutResponseSchema = z.object({
1010
url: z.string(),
1111
});
12+
13+
export const portalResponseSchema = z.object({
14+
url: z.string(),
15+
});

0 commit comments

Comments
 (0)