mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-14 21:35:25 +00:00
add back paywall and also add support for incrememnting seat count on invite redemption
This commit is contained in:
parent
53dac384af
commit
6caed350d3
8 changed files with 279 additions and 165 deletions
|
|
@ -1,8 +1,8 @@
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import Ajv from "ajv";
|
import Ajv from "ajv";
|
||||||
import { auth, getCurrentUserOrg } from "./auth";
|
import { auth } from "./auth";
|
||||||
import { notAuthenticated, notFound, ServiceError, unexpectedError, orgDomainExists } from "@/lib/serviceError";
|
import { notAuthenticated, notFound, ServiceError, unexpectedError, orgInvalidSubscription } from "@/lib/serviceError";
|
||||||
import { prisma } from "@/prisma";
|
import { prisma } from "@/prisma";
|
||||||
import { StatusCodes } from "http-status-codes";
|
import { StatusCodes } from "http-status-codes";
|
||||||
import { ErrorCode } from "@/lib/errorCodes";
|
import { ErrorCode } from "@/lib/errorCodes";
|
||||||
|
|
@ -17,6 +17,7 @@ import { headers } from "next/headers"
|
||||||
import { stripe } from "@/lib/stripe"
|
import { stripe } from "@/lib/stripe"
|
||||||
import { getUser } from "@/data/user";
|
import { getUser } from "@/data/user";
|
||||||
import { Session } from "next-auth";
|
import { Session } from "next-auth";
|
||||||
|
import Stripe from "stripe";
|
||||||
|
|
||||||
const ajv = new Ajv({
|
const ajv = new Ajv({
|
||||||
validateFormats: false,
|
validateFormats: false,
|
||||||
|
|
@ -309,6 +310,35 @@ export const redeemInvite = async (invite: Invite, userId: string): Promise<{ su
|
||||||
withAuth(async () => {
|
withAuth(async () => {
|
||||||
try {
|
try {
|
||||||
await prisma.$transaction(async (tx) => {
|
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({
|
await tx.userToOrg.create({
|
||||||
data: {
|
data: {
|
||||||
userId,
|
userId,
|
||||||
|
|
@ -412,6 +442,11 @@ export async function fetchStripeClientSecret(name: string, domain: string) {
|
||||||
mode: 'subscription',
|
mode: 'subscription',
|
||||||
subscription_data: {
|
subscription_data: {
|
||||||
trial_period_days: 7,
|
trial_period_days: 7,
|
||||||
|
trial_settings: {
|
||||||
|
end_behavior: {
|
||||||
|
missing_payment_method: 'cancel',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
payment_method_collection: 'if_required',
|
payment_method_collection: 'if_required',
|
||||||
return_url: `${origin}/onboard/complete?session_id={CHECKOUT_SESSION_ID}&org_name=${name}&org_domain=${domain}`,
|
return_url: `${origin}/onboard/complete?session_id={CHECKOUT_SESSION_ID}&org_name=${name}&org_domain=${domain}`,
|
||||||
|
|
@ -420,7 +455,9 @@ export async function fetchStripeClientSecret(name: string, domain: string) {
|
||||||
return stripeSession.client_secret!;
|
return stripeSession.client_secret!;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSubscriptionCheckoutRedirect(orgId: number) {
|
export const getSubscriptionCheckoutRedirect = async (domain: string) =>
|
||||||
|
withAuth((session) =>
|
||||||
|
withOrgMembership(session, domain, async (orgId) => {
|
||||||
const org = await prisma.org.findUnique({
|
const org = await prisma.org.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: orgId,
|
id: orgId,
|
||||||
|
|
@ -431,7 +468,15 @@ export async function getSubscriptionCheckoutRedirect(orgId: number) {
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingStripeSubscription = await fetchSubscription(orgId);
|
const orgMembers = await prisma.userToOrg.findMany({
|
||||||
|
where: {
|
||||||
|
orgId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const numOrgMembers = orgMembers.length;
|
||||||
|
|
||||||
const origin = (await headers()).get('origin')
|
const origin = (await headers()).get('origin')
|
||||||
const prices = await stripe.prices.list({
|
const prices = await stripe.prices.list({
|
||||||
|
|
@ -446,37 +491,22 @@ export async function getSubscriptionCheckoutRedirect(orgId: number) {
|
||||||
line_items: [
|
line_items: [
|
||||||
{
|
{
|
||||||
price: prices.data[0].id,
|
price: prices.data[0].id,
|
||||||
quantity: 1
|
quantity: numOrgMembers
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
mode: 'subscription',
|
mode: 'subscription',
|
||||||
payment_method_collection: 'always',
|
payment_method_collection: 'always',
|
||||||
success_url: `${origin}/settings/billing`,
|
success_url: `${origin}/${domain}/settings/billing`,
|
||||||
cancel_url: `${origin}`,
|
cancel_url: `${origin}/${domain}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
return stripeSession.url;
|
return stripeSession.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newSubscriptionUrl = await createNewSubscription();
|
||||||
// If we don't have an existing stripe subscription
|
return newSubscriptionUrl;
|
||||||
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) {
|
export async function fetchStripeSession(sessionId: string) {
|
||||||
const stripeSession = await stripe.checkout.sessions.retrieve(sessionId);
|
const stripeSession = await stripe.checkout.sessions.retrieve(sessionId);
|
||||||
|
|
@ -521,10 +551,7 @@ export async function fetchSubscription(orgId: number) {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (subscriptions.data.length === 0) {
|
if (subscriptions.data.length === 0) {
|
||||||
return {
|
return notFound();
|
||||||
status: "no_subscription",
|
|
||||||
message: "No subscription found for this organization"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return subscriptions.data[0];
|
return subscriptions.data[0];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { getSubscriptionCheckoutRedirect} from "@/actions"
|
import { getSubscriptionCheckoutRedirect } from "@/actions"
|
||||||
import { isServiceError } from "@/lib/utils"
|
import { isServiceError } from "@/lib/utils"
|
||||||
|
|
||||||
|
|
||||||
export function CheckoutButton({ orgId }: { orgId: number }) {
|
export function CheckoutButton({ domain }: { domain: string }) {
|
||||||
const redirectToCheckout = async () => {
|
const redirectToCheckout = async () => {
|
||||||
const redirectUrl = await getSubscriptionCheckoutRedirect(orgId)
|
const redirectUrl = await getSubscriptionCheckoutRedirect(domain)
|
||||||
|
|
||||||
if(isServiceError(redirectUrl)) {
|
if (isServiceError(redirectUrl)) {
|
||||||
console.error("Failed to create checkout session")
|
console.error("Failed to create checkout session")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -18,8 +18,6 @@ export function CheckoutButton({ orgId }: { orgId: number }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Button className="w-full" onClick={redirectToCheckout}>Renew Membership</Button>
|
||||||
<Button className="w-full" onClick={redirectToCheckout}>Choose Pqweqwro</Button>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1,62 +1,84 @@
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Check } from "lucide-react"
|
import { Check } from "lucide-react"
|
||||||
import { EnterpriseContactUsButton } from "./enterpriseContactUsButton"
|
import { EnterpriseContactUsButton } from "./enterpriseContactUsButton"
|
||||||
import { CheckoutButton } from "./checkoutButton"
|
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 = [
|
const teamFeatures = [
|
||||||
"Unlimited projects",
|
"Index hundreds of repos from multiple code hosts (GitHub, GitLab, Gerrit, Gitea, etc.). Self-hosted code sources supported",
|
||||||
"Priority support",
|
"Public and private repos supported",
|
||||||
"Advanced analytics",
|
"Create sharable links to code snippets",
|
||||||
"Custom integrations",
|
"9x5 email support team@sourcebot.dev",
|
||||||
"Team collaboration tools",
|
]
|
||||||
]
|
|
||||||
|
|
||||||
const enterpriseFeatures = [
|
const enterpriseFeatures = [
|
||||||
"All Pro features",
|
"All Team features",
|
||||||
"Dedicated account manager",
|
"Dedicated Slack support channel",
|
||||||
"Custom SLA",
|
"Single tenant deployment",
|
||||||
"On-premise deployment option",
|
|
||||||
"Advanced security features",
|
"Advanced security features",
|
||||||
]
|
]
|
||||||
|
|
||||||
export function PaywallCard({ orgId }: { orgId: number }) {
|
export async function PaywallCard({ domain }: { domain: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||||
<Card>
|
<div className="max-h-44 w-auto mb-4 flex justify-center">
|
||||||
<CardHeader>
|
<Image
|
||||||
<CardTitle>Team</CardTitle>
|
src={logoDark}
|
||||||
<CardDescription>For professional developers and small teams</CardDescription>
|
className="h-18 md:h-40 w-auto hidden dark:block"
|
||||||
|
alt={"Sourcebot logo"}
|
||||||
|
priority={true}
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
src={logoLight}
|
||||||
|
className="h-18 md:h-40 w-auto block dark:hidden"
|
||||||
|
alt={"Sourcebot logo"}
|
||||||
|
priority={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-3xl font-bold text-center mb-8 text-primary">
|
||||||
|
Your subscription has expired.
|
||||||
|
</h2>
|
||||||
|
<div className="grid gap-8 md:grid-cols-2">
|
||||||
|
<Card className="border-2 border-primary/20 shadow-lg transition-all duration-300 hover:shadow-xl hover:border-primary/50 flex flex-col">
|
||||||
|
<CardHeader className="space-y-1">
|
||||||
|
<CardTitle className="text-2xl font-bold text-primary">Team</CardTitle>
|
||||||
|
<CardDescription className="text-base">For professional developers and small teams</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="flex-grow">
|
||||||
<p className="text-3xl font-bold">$10</p>
|
<div className="mb-4">
|
||||||
|
<p className="text-4xl font-bold text-primary">$10</p>
|
||||||
<p className="text-sm text-muted-foreground">per user / month</p>
|
<p className="text-sm text-muted-foreground">per user / month</p>
|
||||||
<ul className="mt-4 space-y-2">
|
</div>
|
||||||
{proFeatures.map((feature, index) => (
|
<ul className="space-y-3">
|
||||||
|
{teamFeatures.map((feature, index) => (
|
||||||
<li key={index} className="flex items-center">
|
<li key={index} className="flex items-center">
|
||||||
<Check className="mr-2 h-4 w-4 text-green-500" />
|
<Check className="mr-3 h-5 w-5 text-green-500 flex-shrink-0" />
|
||||||
{feature}
|
<span>{feature}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter>
|
<CardFooter>
|
||||||
<CheckoutButton orgId={orgId} />
|
<CheckoutButton domain={domain} />
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card className="border-2 border-primary/20 shadow-lg transition-all duration-300 hover:shadow-xl hover:border-primary/50 flex flex-col">
|
||||||
<CardHeader>
|
<CardHeader className="space-y-1">
|
||||||
<CardTitle>Enterprise</CardTitle>
|
<CardTitle className="text-2xl font-bold text-primary">Enterprise</CardTitle>
|
||||||
<CardDescription>For large organizations with custom needs</CardDescription>
|
<CardDescription className="text-base">For large organizations with custom needs</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="flex-grow">
|
||||||
<p className="text-3xl font-bold">Custom</p>
|
<div className="mb-4">
|
||||||
|
<p className="text-4xl font-bold text-primary">Custom</p>
|
||||||
<p className="text-sm text-muted-foreground">tailored to your needs</p>
|
<p className="text-sm text-muted-foreground">tailored to your needs</p>
|
||||||
<ul className="mt-4 space-y-2">
|
</div>
|
||||||
|
<ul className="space-y-3">
|
||||||
{enterpriseFeatures.map((feature, index) => (
|
{enterpriseFeatures.map((feature, index) => (
|
||||||
<li key={index} className="flex items-center">
|
<li key={index} className="flex items-center">
|
||||||
<Check className="mr-2 h-4 w-4 text-green-500" />
|
<Check className="mr-3 h-5 w-5 text-green-500 flex-shrink-0" />
|
||||||
{feature}
|
<span>{feature}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -66,6 +88,6 @@ export function PaywallCard({ orgId }: { orgId: number }) {
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@ import { prisma } from "@/prisma";
|
||||||
import { PageNotFound } from "./components/pageNotFound";
|
import { PageNotFound } from "./components/pageNotFound";
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { getOrgFromDomain } from "@/data/org";
|
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 {
|
interface LayoutProps {
|
||||||
children: React.ReactNode,
|
children: React.ReactNode,
|
||||||
|
|
@ -38,5 +43,18 @@ export default async function Layout({
|
||||||
return <PageNotFound />
|
return <PageNotFound />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const subscription = await fetchSubscription(org.id);
|
||||||
|
if (isServiceError(subscription) || (subscription.status !== "active" && subscription.status !== "trialing")) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center overflow-hidden min-h-screen">
|
||||||
|
<NavigationMenu
|
||||||
|
domain={domain}
|
||||||
|
/>
|
||||||
|
<PaywallCard domain={domain} />
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ import { UpgradeToast } from "./components/upgradeToast";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getOrgFromDomain } from "@/data/org";
|
import { getOrgFromDomain } from "@/data/org";
|
||||||
import { PageNotFound } from "./components/pageNotFound";
|
import { PageNotFound } from "./components/pageNotFound";
|
||||||
|
import { Footer } from "./components/footer";
|
||||||
|
|
||||||
|
|
||||||
export default async function Home({ params: { domain } }: { params: { domain: string } }) {
|
export default async function Home({ params: { domain } }: { params: { domain: string } }) {
|
||||||
|
|
@ -109,13 +110,7 @@ export default async function Home({ params: { domain } }: { params: { domain: s
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer className="w-full mt-auto py-4 flex flex-row justify-center items-center gap-4">
|
<Footer />
|
||||||
<Link href="https://sourcebot.dev" className="text-gray-400 text-sm hover:underline">About</Link>
|
|
||||||
<Separator orientation="vertical" className="h-4" />
|
|
||||||
<Link href="https://github.com/sourcebot-dev/sourcebot/issues/new" className="text-gray-400 text-sm hover:underline">Support</Link>
|
|
||||||
<Separator orientation="vertical" className="h-4" />
|
|
||||||
<Link href="mailto:team@sourcebot.dev" className="text-gray-400 text-sm hover:underline">Contact Us</Link>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import { AcceptInviteButton } from "./components/acceptInviteButton"
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import logoDark from "@/public/sb_logo_dark_large.png";
|
import logoDark from "@/public/sb_logo_dark_large.png";
|
||||||
import logoLight from "@/public/sb_logo_light_large.png";
|
import logoLight from "@/public/sb_logo_light_large.png";
|
||||||
|
import { fetchSubscription } from "@/actions";
|
||||||
|
import { isServiceError } from "@/lib/utils";
|
||||||
|
|
||||||
interface RedeemPageProps {
|
interface RedeemPageProps {
|
||||||
searchParams?: {
|
searchParams?: {
|
||||||
|
|
@ -80,12 +82,11 @@ export default async function RedeemPage({ searchParams }: RedeemPageProps) {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
const orgName = await prisma.org.findUnique({
|
const org = await prisma.org.findUnique({
|
||||||
where: { id: invite.orgId },
|
where: { id: invite.orgId },
|
||||||
select: { name: true },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!orgName) {
|
if (!org) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center items-center mt-8 mb-8 md:mt-18 w-full px-5">
|
<div className="flex flex-col justify-center items-center mt-8 mb-8 md:mt-18 w-full px-5">
|
||||||
<div className="max-h-44 w-auto mb-4">
|
<div className="max-h-44 w-auto mb-4">
|
||||||
|
|
@ -109,10 +110,54 @@ export default async function RedeemPage({ searchParams }: RedeemPageProps) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stripeCustomerId = org.stripeCustomerId;
|
||||||
|
if (stripeCustomerId) {
|
||||||
|
const subscription = await fetchSubscription(org.id);
|
||||||
|
console.log(org);
|
||||||
|
console.log(subscription);
|
||||||
|
if (isServiceError(subscription)) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col justify-center items-center mt-8 mb-8 md:mt-18 w-full px-5">
|
||||||
<div className="flex justify-between items-center h-screen px-6">
|
<div className="max-h-44 w-auto mb-4">
|
||||||
<h1 className="text-2xl font-bold">You have been invited to org {orgName.name}</h1>
|
<Image
|
||||||
|
src={logoDark}
|
||||||
|
className="h-18 md:h-40 w-auto hidden dark:block"
|
||||||
|
alt={"Sourcebot logo"}
|
||||||
|
priority={true}
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
src={logoLight}
|
||||||
|
className="h-18 md:h-40 w-auto block dark:hidden"
|
||||||
|
alt={"Sourcebot logo"}
|
||||||
|
priority={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-center items-center">
|
||||||
|
<h1>This organization's subscription has expired. Please renew the subscription and try again.</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col justify-center items-center mt-8 mb-8 md:mt-18 w-full px-5">
|
||||||
|
<div className="max-h-44 w-auto mb-4">
|
||||||
|
<Image
|
||||||
|
src={logoDark}
|
||||||
|
className="h-18 md:h-40 w-auto hidden dark:block"
|
||||||
|
alt={"Sourcebot logo"}
|
||||||
|
priority={true}
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
src={logoLight}
|
||||||
|
className="h-18 md:h-40 w-auto block dark:hidden"
|
||||||
|
alt={"Sourcebot logo"}
|
||||||
|
priority={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center w-full max-w-2xl">
|
||||||
|
<h1 className="text-2xl font-bold">You have been invited to org {org.name}</h1>
|
||||||
<AcceptInviteButton invite={invite} userId={user.id} />
|
<AcceptInviteButton invite={invite} userId={user.id} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,5 @@ export enum ErrorCode {
|
||||||
NOT_FOUND = 'NOT_FOUND',
|
NOT_FOUND = 'NOT_FOUND',
|
||||||
CONNECTION_SYNC_ALREADY_SCHEDULED = 'CONNECTION_SYNC_ALREADY_SCHEDULED',
|
CONNECTION_SYNC_ALREADY_SCHEDULED = 'CONNECTION_SYNC_ALREADY_SCHEDULED',
|
||||||
ORG_DOMAIN_ALREADY_EXISTS = 'ORG_DOMAIN_ALREADY_EXISTS',
|
ORG_DOMAIN_ALREADY_EXISTS = 'ORG_DOMAIN_ALREADY_EXISTS',
|
||||||
|
ORG_INVALID_SUBSCRIPTION = 'ORG_INVALID_SUBSCRIPTION',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,3 +92,11 @@ export const orgDomainExists = (): ServiceError => {
|
||||||
message: "Organization domain already exists, please try a different one.",
|
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",
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue