=
return !!org;
}
-export const createOrg = async (name: string, domain: string, stripeSessionId?: string): Promise<{ id: number } | ServiceError> => {
+export const createOrg = async (name: string, domain: string, stripeCustomerId?: string): Promise<{ id: number } | ServiceError> => {
const session = await auth();
if (!session) {
return notAuthenticated();
@@ -119,7 +119,7 @@ export const createOrg = async (name: string, domain: string, stripeSessionId?:
data: {
name,
domain,
- stripeSessionId,
+ stripeCustomerId,
members: {
create: {
userId: session.user.id,
@@ -393,14 +393,23 @@ export async function fetchStripeClientSecret(name: string, domain: string) {
const origin = (await headers()).get('origin')
- // Create Checkout Sessions from body params.
+ const test_clock = await stripe.testHelpers.testClocks.create({
+ frozen_time: Math.floor(Date.now() / 1000)
+ })
+
+ const customer = await stripe.customers.create({
+ name: user.name!,
+ email: user.email!,
+ test_clock: test_clock.id
+ })
+
const prices = await stripe.prices.list({
product: 'prod_RkeYDKNFsZJROd',
expand: ['data.product'],
});
const stripeSession = await stripe.checkout.sessions.create({
ui_mode: 'embedded',
-
+ customer: customer.id,
line_items: [
{
price: prices.data[0].id,
@@ -411,7 +420,6 @@ export async function fetchStripeClientSecret(name: string, domain: string) {
subscription_data: {
trial_period_days: 7,
},
- customer_email: user.email!,
payment_method_collection: 'if_required',
return_url: `${origin}/onboard/complete?session_id={CHECKOUT_SESSION_ID}&org_name=${name}&org_domain=${domain}`,
})
@@ -419,6 +427,64 @@ export async function fetchStripeClientSecret(name: string, domain: string) {
return stripeSession.client_secret!;
}
+export async function getSubscriptionCheckoutRedirect(orgId: number) {
+ const org = await prisma.org.findUnique({
+ where: {
+ id: orgId,
+ },
+ });
+
+ if (!org || !org.stripeCustomerId) {
+ return notFound();
+ }
+
+ const existingStripeSubscription = await fetchSubscription(orgId);
+
+ const origin = (await headers()).get('origin')
+ const prices = await stripe.prices.list({
+ product: 'prod_RkeYDKNFsZJROd',
+ expand: ['data.product'],
+ });
+
+ const createNewSubscription = async () => {
+ const stripeSession = await stripe.checkout.sessions.create({
+ customer: org.stripeCustomerId as string,
+ payment_method_types: ['card'],
+ line_items: [
+ {
+ price: prices.data[0].id,
+ quantity: 1
+ }
+ ],
+ mode: 'subscription',
+ payment_method_collection: 'always',
+ success_url: `${origin}/settings/billing`,
+ cancel_url: `${origin}`,
+ });
+
+ return stripeSession.url;
+ }
+
+
+ // If we don't have an existing stripe subscription
+ if (isServiceError(existingStripeSubscription)) {
+ const checkoutUrl = await createNewSubscription();
+ return checkoutUrl;
+ } else {
+ if (existingStripeSubscription.status === "cancelled") {
+ const checkoutUrl = await createNewSubscription();
+ return checkoutUrl;
+ }
+
+ const portalSession = await stripe.billingPortal.sessions.create({
+ customer: org.stripeCustomerId as string,
+ return_url: `${origin}/settings/billing`,
+ });
+
+ return portalSession.url;
+ }
+}
+
export async function fetchStripeSession(sessionId: string) {
const stripeSession = await stripe.checkout.sessions.retrieve(sessionId);
return stripeSession;
@@ -436,16 +502,39 @@ export async function createCustomerPortalSession() {
},
});
- if (!org || !org.stripeSessionId) {
+ if (!org || !org.stripeCustomerId) {
return notFound();
}
const origin = (await headers()).get('origin')
- const stripeSession = await fetchStripeSession(org.stripeSessionId);
const portalSession = await stripe.billingPortal.sessions.create({
- customer: stripeSession.customer as string,
+ customer: org.stripeCustomerId as string,
return_url: `${origin}/settings/billing`,
});
return portalSession;
+}
+
+export async function fetchSubscription(orgId: number) {
+ const org = await prisma.org.findUnique({
+ where: {
+ id: orgId,
+ },
+ });
+
+ if (!org || !org.stripeCustomerId) {
+ return notFound();
+ }
+
+ const subscriptions = await stripe.subscriptions.list({
+ customer: org.stripeCustomerId!
+ })
+
+ if (subscriptions.data.length === 0) {
+ return {
+ status: "no_subscription",
+ message: "No subscription found for this organization"
+ }
+ }
+ return subscriptions.data[0];
}
\ No newline at end of file
diff --git a/packages/web/src/app/components/payWall/checkoutButton.tsx b/packages/web/src/app/components/payWall/checkoutButton.tsx
new file mode 100644
index 00000000..db343f23
--- /dev/null
+++ b/packages/web/src/app/components/payWall/checkoutButton.tsx
@@ -0,0 +1,25 @@
+"use client"
+
+import { Button } from "@/components/ui/button"
+import { getSubscriptionCheckoutRedirect} from "@/actions"
+import { isServiceError } from "@/lib/utils"
+
+
+export function CheckoutButton({ orgId }: { orgId: number }) {
+ const redirectToCheckout = async () => {
+ const redirectUrl = await getSubscriptionCheckoutRedirect(orgId)
+
+ if(isServiceError(redirectUrl)) {
+ console.error("Failed to create checkout session")
+ return
+ }
+
+ window.location.href = redirectUrl!;
+ }
+
+ return (
+
+ Choose Pqweqwro
+
+ )
+}
\ No newline at end of file
diff --git a/packages/web/src/app/components/payWall/enterpriseContactUsButton.tsx b/packages/web/src/app/components/payWall/enterpriseContactUsButton.tsx
new file mode 100644
index 00000000..1fd6bf08
--- /dev/null
+++ b/packages/web/src/app/components/payWall/enterpriseContactUsButton.tsx
@@ -0,0 +1,15 @@
+"use client"
+
+import { Button } from "@/components/ui/button"
+
+export function EnterpriseContactUsButton() {
+ const handleContactUs = () => {
+ window.location.href = "mailto:team@sourcebot.dev?subject=Enterprise%20Pricing%20Inquiry"
+ }
+
+ return (
+
+ Contact Us
+
+ )
+}
\ No newline at end of file
diff --git a/packages/web/src/app/components/payWall/paywallCard.tsx b/packages/web/src/app/components/payWall/paywallCard.tsx
new file mode 100644
index 00000000..c222f3a4
--- /dev/null
+++ b/packages/web/src/app/components/payWall/paywallCard.tsx
@@ -0,0 +1,71 @@
+import { Button } from "@/components/ui/button"
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
+import { Check } from "lucide-react"
+import { EnterpriseContactUsButton } from "./enterpriseContactUsButton"
+import { CheckoutButton } from "./checkoutButton"
+
+const proFeatures = [
+ "Unlimited projects",
+ "Priority support",
+ "Advanced analytics",
+ "Custom integrations",
+ "Team collaboration tools",
+ ]
+
+ const enterpriseFeatures = [
+ "All Pro features",
+ "Dedicated account manager",
+ "Custom SLA",
+ "On-premise deployment option",
+ "Advanced security features",
+ ]
+
+export function PaywallCard({ orgId }: { orgId: number }) {
+ return (
+
+
+
+ Team
+ For professional developers and small teams
+
+
+ $10
+ per user / month
+
+ {proFeatures.map((feature, index) => (
+
+
+ {feature}
+
+ ))}
+
+
+
+
+
+
+
+
+ Enterprise
+ For large organizations with custom needs
+
+
+ Custom
+ tailored to your needs
+
+ {enterpriseFeatures.map((feature, index) => (
+
+
+ {feature}
+
+ ))}
+
+
+
+
+
+
+
+ )
+}
+
diff --git a/packages/web/src/app/layout.tsx b/packages/web/src/app/layout.tsx
index 886aaa7f..c9c82221 100644
--- a/packages/web/src/app/layout.tsx
+++ b/packages/web/src/app/layout.tsx
@@ -10,8 +10,10 @@ import { getCurrentUserOrg } from "@/auth";
import { isServiceError } from "@/lib/utils";
import { NavigationMenu } from "./components/navigationMenu";
import { NoOrganizationCard } from "./components/noOrganizationCard";
+import { PaywallCard } from "./components/payWall/paywallCard";
import { Footer } from "./components/footer";
import { headers } from "next/headers";
+import { fetchSubscription } from "@/actions";
export const metadata: Metadata = {
title: "Sourcebot",
@@ -24,7 +26,10 @@ export default async function RootLayout({
children: React.ReactNode;
}>) {
const orgId = await getCurrentUserOrg();
+ console.log(`orgId: ${orgId}`);
+
const byPassOrgCheck = (await headers()).get("x-bypass-org-check")! == "true";
+ console.log(`bypassOrgCheck: ${byPassOrgCheck}`);
if (isServiceError(orgId) && !byPassOrgCheck) {
return (
-
-
-
-
-
-
-
- )
+
+
+
+
+
+
+
+ )
)
}
+ const bypassPaywall = (await headers()).get("x-bypass-paywall")! == "true";
+ console.log(bypassPaywall);
+ if (!isServiceError(orgId) && !bypassPaywall) {
+ const subscription = await fetchSubscription(orgId as number);
+ if (isServiceError(subscription)) {
+ // TODO: display something better here
+ return (
+
+ Error: {subscription.message}
+
+ )
+ }
+ console.log(subscription.status);
+
+ if(subscription.status !== "active" && subscription.status !== "trialing") {
+ return (
+
+
+
+
+
+ )
+ }
+ }
+
return (
;
}
- const res = await createOrg(orgName, orgDomain, stripeSession.id);
+ const stripeCustomerId = stripeSession.customer as string;
+ const res = await createOrg(orgName, orgDomain, stripeCustomerId);
if (isServiceError(res)) {
console.error("Failed to create org");
return