diff --git a/packages/web/src/actions.ts b/packages/web/src/actions.ts
index 41583ab6..3edfa4c2 100644
--- a/packages/web/src/actions.ts
+++ b/packages/web/src/actions.ts
@@ -1,8 +1,8 @@
'use server';
import Ajv from "ajv";
-import { auth, getCurrentUserOrg } from "./auth";
-import { notAuthenticated, notFound, ServiceError, unexpectedError, orgDomainExists } from "@/lib/serviceError";
+import { auth } from "./auth";
+import { notAuthenticated, notFound, ServiceError, unexpectedError, orgInvalidSubscription } from "@/lib/serviceError";
import { prisma } from "@/prisma";
import { StatusCodes } from "http-status-codes";
import { ErrorCode } from "@/lib/errorCodes";
@@ -17,6 +17,7 @@ import { headers } from "next/headers"
import { stripe } from "@/lib/stripe"
import { getUser } from "@/data/user";
import { Session } from "next-auth";
+import Stripe from "stripe";
const ajv = new Ajv({
validateFormats: false,
@@ -309,6 +310,35 @@ export const redeemInvite = async (invite: Invite, userId: string): Promise<{ su
withAuth(async () => {
try {
await prisma.$transaction(async (tx) => {
+ const org = await tx.org.findUnique({
+ where: {
+ id: invite.orgId,
+ }
+ });
+
+ if (!org) {
+ return notFound();
+ }
+
+ // Incrememnt the seat count. We check if the subscription is valid in the redeem page so we return an error if that's not the case here
+ if (org.stripeCustomerId) {
+ const subscription = await fetchSubscription(org.id);
+ if (isServiceError(subscription)) {
+ return orgInvalidSubscription();
+ }
+
+ const existingSeatCount = subscription.items.data[0].quantity;
+ const newSeatCount = (existingSeatCount || 1) + 1
+
+ await stripe.subscriptionItems.update(
+ subscription.items.data[0].id,
+ {
+ quantity: newSeatCount,
+ proration_behavior: 'create_prorations',
+ }
+ )
+ }
+
await tx.userToOrg.create({
data: {
userId,
@@ -412,6 +442,11 @@ export async function fetchStripeClientSecret(name: string, domain: string) {
mode: 'subscription',
subscription_data: {
trial_period_days: 7,
+ trial_settings: {
+ end_behavior: {
+ missing_payment_method: 'cancel',
+ },
+ },
},
payment_method_collection: 'if_required',
return_url: `${origin}/onboard/complete?session_id={CHECKOUT_SESSION_ID}&org_name=${name}&org_domain=${domain}`,
@@ -420,63 +455,58 @@ 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,
- },
- });
+export const getSubscriptionCheckoutRedirect = async (domain: string) =>
+ withAuth((session) =>
+ withOrgMembership(session, domain, async (orgId) => {
+ const org = await prisma.org.findUnique({
+ where: {
+ id: orgId,
+ },
+ });
- if (!org || !org.stripeCustomerId) {
- return notFound();
- }
+ 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
+ const orgMembers = await prisma.userToOrg.findMany({
+ where: {
+ orgId,
+ },
+ select: {
+ userId: true,
}
- ],
- mode: 'subscription',
- payment_method_collection: 'always',
- success_url: `${origin}/settings/billing`,
- cancel_url: `${origin}`,
- });
+ });
+ const numOrgMembers = orgMembers.length;
- return stripeSession.url;
- }
+ 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: numOrgMembers
+ }
+ ],
+ mode: 'subscription',
+ payment_method_collection: 'always',
+ success_url: `${origin}/${domain}/settings/billing`,
+ cancel_url: `${origin}/${domain}`,
+ });
- // 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;
- }
+ return stripeSession.url;
+ }
- const portalSession = await stripe.billingPortal.sessions.create({
- customer: org.stripeCustomerId as string,
- return_url: `${origin}/settings/billing`,
- });
-
- return portalSession.url;
- }
-}
+ const newSubscriptionUrl = await createNewSubscription();
+ return newSubscriptionUrl;
+ })
+ )
export async function fetchStripeSession(sessionId: string) {
const stripeSession = await stripe.checkout.sessions.retrieve(sessionId);
@@ -521,10 +551,7 @@ export async function fetchSubscription(orgId: number) {
})
if (subscriptions.data.length === 0) {
- return {
- status: "no_subscription",
- message: "No subscription found for this organization"
- }
+ return notFound();
}
return subscriptions.data[0];
}
diff --git a/packages/web/src/app/[domain]/components/payWall/checkoutButton.tsx b/packages/web/src/app/[domain]/components/payWall/checkoutButton.tsx
index db343f23..00e49f7e 100644
--- a/packages/web/src/app/[domain]/components/payWall/checkoutButton.tsx
+++ b/packages/web/src/app/[domain]/components/payWall/checkoutButton.tsx
@@ -1,25 +1,23 @@
"use client"
import { Button } from "@/components/ui/button"
-import { getSubscriptionCheckoutRedirect} from "@/actions"
+import { getSubscriptionCheckoutRedirect } from "@/actions"
import { isServiceError } from "@/lib/utils"
-export function CheckoutButton({ orgId }: { orgId: number }) {
+export function CheckoutButton({ domain }: { domain: string }) {
const redirectToCheckout = async () => {
- const redirectUrl = await getSubscriptionCheckoutRedirect(orgId)
-
- if(isServiceError(redirectUrl)) {
+ const redirectUrl = await getSubscriptionCheckoutRedirect(domain)
+
+ if (isServiceError(redirectUrl)) {
console.error("Failed to create checkout session")
return
}
-
+
window.location.href = redirectUrl!;
}
return (
-
-
-
+
)
}
\ No newline at end of file
diff --git a/packages/web/src/app/[domain]/components/payWall/paywallCard.tsx b/packages/web/src/app/[domain]/components/payWall/paywallCard.tsx
index c222f3a4..3e1f31df 100644
--- a/packages/web/src/app/[domain]/components/payWall/paywallCard.tsx
+++ b/packages/web/src/app/[domain]/components/payWall/paywallCard.tsx
@@ -1,71 +1,93 @@
-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"
+import Image from "next/image";
+import logoDark from "@/public/sb_logo_dark_large.png";
+import logoLight from "@/public/sb_logo_light_large.png";
-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",
- ]
+const teamFeatures = [
+ "Index hundreds of repos from multiple code hosts (GitHub, GitLab, Gerrit, Gitea, etc.). Self-hosted code sources supported",
+ "Public and private repos supported",
+ "Create sharable links to code snippets",
+ "9x5 email support team@sourcebot.dev",
+]
-export function PaywallCard({ orgId }: { orgId: number }) {
+const enterpriseFeatures = [
+ "All Team features",
+ "Dedicated Slack support channel",
+ "Single tenant deployment",
+ "Advanced security features",
+]
+
+export async function PaywallCard({ domain }: { domain: string }) {
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}
-
- ))}
-
-
-
-
-
-
+
+
+
+
+
+
+ Your subscription has expired.
+
+
+
+
+ Team
+ For professional developers and small teams
+
+
+
+
$10
+
per user / month
+
+
+ {teamFeatures.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/[domain]/layout.tsx b/packages/web/src/app/[domain]/layout.tsx
index 25f7f97c..740a640d 100644
--- a/packages/web/src/app/[domain]/layout.tsx
+++ b/packages/web/src/app/[domain]/layout.tsx
@@ -2,6 +2,11 @@ import { prisma } from "@/prisma";
import { PageNotFound } from "./components/pageNotFound";
import { auth } from "@/auth";
import { getOrgFromDomain } from "@/data/org";
+import { fetchSubscription } from "@/actions";
+import { isServiceError } from "@/lib/utils";
+import { PaywallCard } from "./components/payWall/paywallCard";
+import { NavigationMenu } from "./components/navigationMenu";
+import { Footer } from "./components/footer";
interface LayoutProps {
children: React.ReactNode,
@@ -38,5 +43,18 @@ export default async function Layout({
return
}
+ const subscription = await fetchSubscription(org.id);
+ if (isServiceError(subscription) || (subscription.status !== "active" && subscription.status !== "trialing")) {
+ return (
+
+ )
+ }
+
return children;
}
\ No newline at end of file
diff --git a/packages/web/src/app/[domain]/page.tsx b/packages/web/src/app/[domain]/page.tsx
index 0d0244c5..127a2329 100644
--- a/packages/web/src/app/[domain]/page.tsx
+++ b/packages/web/src/app/[domain]/page.tsx
@@ -13,6 +13,7 @@ import { UpgradeToast } from "./components/upgradeToast";
import Link from "next/link";
import { getOrgFromDomain } from "@/data/org";
import { PageNotFound } from "./components/pageNotFound";
+import { Footer } from "./components/footer";
export default async function Home({ params: { domain } }: { params: { domain: string } }) {
@@ -109,13 +110,7 @@ export default async function Home({ params: { domain } }: { params: { domain: s
-
+
)
}
diff --git a/packages/web/src/app/redeem/page.tsx b/packages/web/src/app/redeem/page.tsx
index e3eb2c97..ce8f27fc 100644
--- a/packages/web/src/app/redeem/page.tsx
+++ b/packages/web/src/app/redeem/page.tsx
@@ -6,6 +6,8 @@ import { AcceptInviteButton } from "./components/acceptInviteButton"
import Image from "next/image";
import logoDark from "@/public/sb_logo_dark_large.png";
import logoLight from "@/public/sb_logo_light_large.png";
+import { fetchSubscription } from "@/actions";
+import { isServiceError } from "@/lib/utils";
interface RedeemPageProps {
searchParams?: {
@@ -60,34 +62,6 @@ export default async function RedeemPage({ searchParams }: RedeemPageProps) {
if (user.email !== invite.recipientEmail) {
return (
-
-
-
-
-
-
This invite doesn't belong to you. You're currenly signed in with ${user.email}
-
-
- )
- } else {
- const orgName = await prisma.org.findUnique({
- where: { id: invite.orgId },
- select: { name: true },
- });
-
- if (!orgName) {
- return (
-
-
This organization wasn't found. Please contact your organization owner.
+ This invite doesn't belong to you. You're currenly signed in with ${user.email}
+ )
+ } else {
+ const org = await prisma.org.findUnique({
+ where: { id: invite.orgId },
+ });
+
+ if (!org) {
+ return (
+
+
+
+
+
+
+
This organization wasn't found. Please contact your organization owner.
+
+
)
}
+ const stripeCustomerId = org.stripeCustomerId;
+ if (stripeCustomerId) {
+ const subscription = await fetchSubscription(org.id);
+ console.log(org);
+ console.log(subscription);
+ if (isServiceError(subscription)) {
+ return (
+
+
+
+
+
+
+
This organization's subscription has expired. Please renew the subscription and try again.
+
+
+ )
+ }
+ }
+
return (
-
-
-
You have been invited to org {orgName.name}
+
+
+
+
+
+
+
You have been invited to org {org.name}
diff --git a/packages/web/src/lib/errorCodes.ts b/packages/web/src/lib/errorCodes.ts
index 20080947..6b8c6eda 100644
--- a/packages/web/src/lib/errorCodes.ts
+++ b/packages/web/src/lib/errorCodes.ts
@@ -9,4 +9,5 @@ export enum ErrorCode {
NOT_FOUND = 'NOT_FOUND',
CONNECTION_SYNC_ALREADY_SCHEDULED = 'CONNECTION_SYNC_ALREADY_SCHEDULED',
ORG_DOMAIN_ALREADY_EXISTS = 'ORG_DOMAIN_ALREADY_EXISTS',
+ ORG_INVALID_SUBSCRIPTION = 'ORG_INVALID_SUBSCRIPTION',
}
diff --git a/packages/web/src/lib/serviceError.ts b/packages/web/src/lib/serviceError.ts
index e8eb6876..cbc2f318 100644
--- a/packages/web/src/lib/serviceError.ts
+++ b/packages/web/src/lib/serviceError.ts
@@ -91,4 +91,12 @@ export const orgDomainExists = (): ServiceError => {
errorCode: ErrorCode.ORG_DOMAIN_ALREADY_EXISTS,
message: "Organization domain already exists, please try a different one.",
}
+}
+
+export const orgInvalidSubscription = (): ServiceError => {
+ return {
+ statusCode: StatusCodes.BAD_REQUEST,
+ errorCode: ErrorCode.ORG_INVALID_SUBSCRIPTION,
+ message: "Invalid subscription",
+ }
}
\ No newline at end of file