add proper stripe checkout button for user

This commit is contained in:
pablodanswer
2024-08-29 11:49:10 -07:00
parent c8f7e6185f
commit 90da2166c2
17 changed files with 790 additions and 2 deletions

View File

@@ -0,0 +1,317 @@
"""testing
Revision ID: 5be3aa848ce8
Revises: bceb1e139447
Create Date: 2024-08-28 17:15:06.247199
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = "5be3aa848ce8"
down_revision = "bceb1e139447"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"chat_message__standard_answer",
sa.Column("chat_message_id", sa.Integer(), nullable=False),
sa.Column("standard_answer_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["chat_message_id"],
["chat_message.id"],
),
sa.ForeignKeyConstraint(
["standard_answer_id"],
["standard_answer.id"],
),
sa.PrimaryKeyConstraint("chat_message_id", "standard_answer_id"),
)
op.drop_table("kombu_queue")
op.drop_index("ix_kombu_message_timestamp", table_name="kombu_message")
op.drop_index("ix_kombu_message_timestamp_id", table_name="kombu_message")
op.drop_index("ix_kombu_message_visible", table_name="kombu_message")
op.drop_table("kombu_message")
op.create_foreign_key(None, "api_key", "user", ["user_id"], ["id"])
op.create_foreign_key(None, "api_key", "user", ["owner_id"], ["id"])
op.alter_column(
"chat_folder",
"display_priority",
existing_type=sa.INTEGER(),
nullable=True,
)
op.drop_constraint("chat_message_id_key", "chat_message", type_="unique")
op.alter_column(
"credential",
"source",
existing_type=sa.VARCHAR(length=100),
nullable=False,
)
op.alter_column(
"credential",
"credential_json",
existing_type=postgresql.BYTEA(),
nullable=False,
)
op.drop_index(
"ix_document_by_connector_credential_pair_pkey__connecto_27dc",
table_name="document_by_connector_credential_pair",
)
op.alter_column(
"document_set__user", "user_id", existing_type=sa.UUID(), nullable=True
)
op.add_column(
"email_to_external_user_cache",
sa.Column(
"source_type",
sa.Enum(
"INGESTION_API",
"SLACK",
"WEB",
"GOOGLE_DRIVE",
"GMAIL",
"REQUESTTRACKER",
"GITHUB",
"GITLAB",
"GURU",
"BOOKSTACK",
"CONFLUENCE",
"SLAB",
"JIRA",
"PRODUCTBOARD",
"FILE",
"NOTION",
"ZULIP",
"LINEAR",
"HUBSPOT",
"DOCUMENT360",
"GONG",
"GOOGLE_SITES",
"ZENDESK",
"LOOPIO",
"DROPBOX",
"SHAREPOINT",
"TEAMS",
"SALESFORCE",
"DISCOURSE",
"AXERO",
"CLICKUP",
"MEDIAWIKI",
"WIKIPEDIA",
"S3",
"R2",
"GOOGLE_CLOUD_STORAGE",
"OCI_STORAGE",
"NOT_APPLICABLE",
name="documentsource",
native_enum=False,
),
nullable=False,
),
)
op.alter_column(
"inputprompt__user",
"user_id",
existing_type=sa.INTEGER(),
nullable=True,
)
op.alter_column(
"llm_provider", "provider", existing_type=sa.VARCHAR(), nullable=False
)
op.alter_column("persona__user", "user_id", existing_type=sa.UUID(), nullable=True)
op.alter_column(
"saml",
"expires_at",
existing_type=postgresql.TIMESTAMP(timezone=True),
nullable=False,
)
op.alter_column(
"search_settings",
"query_prefix",
existing_type=sa.VARCHAR(),
nullable=True,
)
op.alter_column(
"search_settings",
"passage_prefix",
existing_type=sa.VARCHAR(),
nullable=True,
)
op.alter_column(
"search_settings", "status", existing_type=sa.VARCHAR(), nullable=False
)
op.create_index(
"ix_embedding_model_future_unique",
"search_settings",
["status"],
unique=True,
postgresql_where=sa.text("status = 'FUTURE'"),
)
op.create_index(
"ix_embedding_model_present_unique",
"search_settings",
["status"],
unique=True,
postgresql_where=sa.text("status = 'PRESENT'"),
)
op.drop_constraint("standard_answer_keyword_key", "standard_answer", type_="unique")
op.create_index(
"unique_keyword_active",
"standard_answer",
["keyword", "active"],
unique=True,
postgresql_where=sa.text("active = true"),
)
op.alter_column(
"tool_call",
"tool_result",
existing_type=postgresql.JSONB(astext_type=sa.Text()),
nullable=True,
)
op.alter_column(
"user__user_group", "user_id", existing_type=sa.UUID(), nullable=True
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"user__user_group", "user_id", existing_type=sa.UUID(), nullable=False
)
op.alter_column(
"tool_call",
"tool_result",
existing_type=postgresql.JSONB(astext_type=sa.Text()),
nullable=False,
)
op.drop_index(
"unique_keyword_active",
table_name="standard_answer",
postgresql_where=sa.text("active = true"),
)
op.create_unique_constraint(
"standard_answer_keyword_key", "standard_answer", ["keyword"]
)
op.drop_index(
"ix_embedding_model_present_unique",
table_name="search_settings",
postgresql_where=sa.text("status = 'PRESENT'"),
)
op.drop_index(
"ix_embedding_model_future_unique",
table_name="search_settings",
postgresql_where=sa.text("status = 'FUTURE'"),
)
op.alter_column(
"search_settings", "status", existing_type=sa.VARCHAR(), nullable=True
)
op.alter_column(
"search_settings",
"passage_prefix",
existing_type=sa.VARCHAR(),
nullable=False,
)
op.alter_column(
"search_settings",
"query_prefix",
existing_type=sa.VARCHAR(),
nullable=False,
)
op.alter_column(
"saml",
"expires_at",
existing_type=postgresql.TIMESTAMP(timezone=True),
nullable=True,
)
op.alter_column("persona__user", "user_id", existing_type=sa.UUID(), nullable=False)
op.alter_column(
"llm_provider", "provider", existing_type=sa.VARCHAR(), nullable=True
)
op.alter_column(
"inputprompt__user",
"user_id",
existing_type=sa.INTEGER(),
nullable=False,
)
op.drop_column("email_to_external_user_cache", "source_type")
op.alter_column(
"document_set__user",
"user_id",
existing_type=sa.UUID(),
nullable=False,
)
op.create_index(
"ix_document_by_connector_credential_pair_pkey__connecto_27dc",
"document_by_connector_credential_pair",
["connector_id", "credential_id"],
unique=False,
)
op.alter_column(
"credential",
"credential_json",
existing_type=postgresql.BYTEA(),
nullable=True,
)
op.alter_column(
"credential",
"source",
existing_type=sa.VARCHAR(length=100),
nullable=True,
)
op.create_unique_constraint("chat_message_id_key", "chat_message", ["id"])
op.alter_column(
"chat_folder",
"display_priority",
existing_type=sa.INTEGER(),
nullable=False,
)
op.drop_constraint(None, "api_key", type_="foreignkey")
op.drop_constraint(None, "api_key", type_="foreignkey")
op.create_table(
"kombu_message",
sa.Column("id", sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column("visible", sa.BOOLEAN(), autoincrement=False, nullable=True),
sa.Column(
"timestamp",
postgresql.TIMESTAMP(),
autoincrement=False,
nullable=True,
),
sa.Column("payload", sa.TEXT(), autoincrement=False, nullable=False),
sa.Column("version", sa.SMALLINT(), autoincrement=False, nullable=False),
sa.Column("queue_id", sa.INTEGER(), autoincrement=False, nullable=True),
sa.ForeignKeyConstraint(
["queue_id"], ["kombu_queue.id"], name="FK_kombu_message_queue"
),
sa.PrimaryKeyConstraint("id", name="kombu_message_pkey"),
)
op.create_index(
"ix_kombu_message_visible", "kombu_message", ["visible"], unique=False
)
op.create_index(
"ix_kombu_message_timestamp_id",
"kombu_message",
["timestamp", "id"],
unique=False,
)
op.create_index(
"ix_kombu_message_timestamp",
"kombu_message",
["timestamp"],
unique=False,
)
op.create_table(
"kombu_queue",
sa.Column("id", sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column("name", sa.VARCHAR(length=200), autoincrement=False, nullable=True),
sa.PrimaryKeyConstraint("id", name="kombu_queue_pkey"),
sa.UniqueConstraint("name", name="kombu_queue_name_key"),
)
op.drop_table("chat_message__standard_answer")
# ### end Alembic commands ###

View File

@@ -366,3 +366,13 @@ CUSTOM_ANSWER_VALIDITY_CONDITIONS = json.loads(
ENTERPRISE_EDITION_ENABLED = (
os.environ.get("ENABLE_PAID_ENTERPRISE_EDITION_FEATURES", "").lower() == "true"
)
###
# CLOUD CONFIGS
###
STRIPE_PRICE = os.environ.get("STRIPE_PRICE", "price_1PsYoPHlhTYqRZib2t5ydpq5")
# STRIPE_PRICE="price_1PsYoPHlhTYqRZib2t5ydpq5"
# STRIPE_WEBHOOK_SECRET="whsec_1cd766cd6bd08590aa8c46ab5c21ac32cad77c29de2e09a152a01971d6f405d3"
# STRIPE_SECRET_KEY="sk_test_51NwZq2HlhTYqRZibT2cssHV8E5QcLAUmaRLQPMjGb5aOxOWomVxOmzRgxf82ziDBuGdPP2GIDod8xe6DyqeGgUDi00KbsHPoT4"

View File

@@ -1,13 +1,18 @@
import os
import re
from datetime import datetime
from datetime import timezone
from enum import Enum
import stripe
from email_validator import validate_email
from fastapi import APIRouter
from fastapi import Body
from fastapi import Depends
from fastapi import HTTPException
from fastapi import Request
from fastapi import status
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from sqlalchemy import Column
from sqlalchemy import desc
@@ -27,6 +32,7 @@ from danswer.auth.users import current_user
from danswer.auth.users import optional_user
from danswer.configs.app_configs import AUTH_TYPE
from danswer.configs.app_configs import SESSION_EXPIRE_TIME_SECONDS
from danswer.configs.app_configs import STRIPE_PRICE
from danswer.configs.app_configs import VALID_EMAIL_DOMAINS
from danswer.configs.constants import AuthType
from danswer.db.engine import get_session
@@ -47,6 +53,13 @@ from danswer.utils.logger import setup_logger
from ee.danswer.db.api_key import is_api_key_email_address
from ee.danswer.db.user_group import remove_curator_status__no_commit
STRIPE_SECRET_KEY = os.getenv("STRIPE_SECRET_KEY")
STRIPE_WEBHOOK_SECRET = os.getenv("STRIPE_WEBHOOK_SECRET")
# STRIPE_PRICE = os.getenv("STRIPE_PRICE")
stripe.api_key = "sk_test_51NwZq2HlhTYqRZibT2cssHV8E5QcLAUmaRLQPMjGb5aOxOWomVxOmzRgxf82ziDBuGdPP2GIDod8xe6DyqeGgUDi00KbsHPoT4"
logger = setup_logger()
router = APIRouter()
@@ -319,6 +332,66 @@ def verify_user_logged_in(
return user_info
class BillingPlanType(str, Enum):
FREE = "free"
PREMIUM = "premium"
ENTERPRISE = "enterprise"
class CheckoutSessionUpdateBillingStatus(BaseModel):
quantity: int
plan: BillingPlanType
@router.post("/create-checkout-session")
async def create_checkout_session(
request: Request,
checkout_billing: CheckoutSessionUpdateBillingStatus,
user: User = Depends(current_user),
db_session: Session = Depends(get_session),
):
quantity = checkout_billing.quantity
plan = checkout_billing.plan
logger.info(f"Creating checkout session for plan: {plan} with quantity: {quantity}")
user_email = "pablosfsanchez@gmail.com"
success_url = "http://localhost:3000/billing/success"
cancel_url = "http://localhost:3000/billing/cancel"
logger.info(f"Stripe price being used: {STRIPE_PRICE}")
logger.info(
f"Creating checkout session with success_url: {success_url} and cancel_url: {cancel_url}"
)
try:
checkout_session = stripe.checkout.Session.create(
customer_email=user_email,
line_items=[
{
"price": STRIPE_PRICE,
"quantity": quantity,
},
],
mode="subscription",
success_url=success_url,
cancel_url=cancel_url,
metadata={"tenant_id": str("random tenant")},
)
logger.info(
f"Checkout session created successfully with id: {checkout_session.id}"
)
except stripe.error.InvalidRequestError as e:
logger.error(f"Stripe error: {str(e)}")
raise HTTPException(status_code=400, detail=f"Stripe error: {str(e)}")
except Exception as e:
logger.error(f"Unexpected error: {str(e)}")
raise HTTPException(status_code=500, detail="An unexpected error occurred")
return JSONResponse({"sessionId": checkout_session.id})
"""APIs to adjust user preferences"""

View File

@@ -53,7 +53,7 @@ LOG_FILE_NAME = os.environ.get("LOG_FILE_NAME") or "danswer"
# Enable generating persistent log files for local dev environments
DEV_LOGGING_ENABLED = os.environ.get("DEV_LOGGING_ENABLED", "").lower() == "true"
# notset, debug, info, notice, warning, error, or critical
LOG_LEVEL = os.environ.get("LOG_LEVEL", "notice")
LOG_LEVEL = os.environ.get("LOG_LEVEL", "debug")
# Fields which should only be set on new search setting

10
web/package-lock.json generated
View File

@@ -15,6 +15,7 @@
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-tooltip": "^1.0.7",
"@stripe/stripe-js": "^4.4.0",
"@tremor/react": "^3.9.2",
"@types/js-cookie": "^3.0.3",
"@types/lodash": "^4.17.0",
@@ -1670,6 +1671,15 @@
"integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==",
"dev": true
},
"node_modules/@stripe/stripe-js": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-4.4.0.tgz",
"integrity": "sha512-p1WeTOwnAyXQ9I5/YC3+JXoUB6NKMR4qGjBobie2+rgYa3ftUTRS2L5qRluw/tGACty5SxqnfORCdsaymD1XjQ==",
"license": "MIT",
"engines": {
"node": ">=12.16"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",

View File

@@ -16,6 +16,7 @@
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-tooltip": "^1.0.7",
"@stripe/stripe-js": "^4.4.0",
"@tremor/react": "^3.9.2",
"@types/js-cookie": "^3.0.3",
"@types/lodash": "^4.17.0",

View File

@@ -27,9 +27,21 @@ export interface EnterpriseSettings {
custom_popup_content: string | null;
}
export enum BillingPlanType {
FREE = "free",
PREMIUM = "premium",
ENTERPRISE = "enterprise",
}
export interface CloudSettings {
numberOfSeats: number;
planType: BillingPlanType;
}
export interface CombinedSettings {
settings: Settings;
enterpriseSettings: EnterpriseSettings | null;
cloudSettings: CloudSettings | null;
customAnalyticsScript: string | null;
isMobile?: boolean;
webVersion: string | null;

View File

@@ -0,0 +1,186 @@
"use client";
import { useRouter } from "next/navigation";
import {
BillingPlanType,
EnterpriseSettings,
} from "@/app/admin/settings/interfaces";
import { useContext, useState } from "react";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { Form, Formik } from "formik";
import * as Yup from "yup";
import {
Label,
SubLabel,
TextFormField,
} from "@/components/admin/connectors/Field";
import { Button, Divider, Text, Card } from "@tremor/react";
import Link from "next/link";
import { StripeCheckoutButton } from "./StripeCheckoutButton";
import { CheckmarkIcon, XIcon } from "@/components/icons/icons";
// import { StripeCheckoutButton } from "@/components/StripeButton";
export function BillingSettings() {
const router = useRouter();
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
const settings = useContext(SettingsContext);
if (!settings) {
return null;
}
const cloudSettings = settings.cloudSettings;
// Use actual data from cloudSettings
const currentPlan = cloudSettings?.planType;
const seats = cloudSettings?.numberOfSeats!;
const features = [
{ name: "All Connector Access", included: true },
{ name: "Basic Support", included: true },
{ name: "Custom Branding", included: currentPlan !== BillingPlanType.FREE },
{
name: "Analytics Dashboard",
included: currentPlan !== BillingPlanType.FREE,
},
{ name: "Query History", included: currentPlan !== BillingPlanType.FREE },
{
name: "Priority Support",
included: currentPlan !== BillingPlanType.FREE,
},
{
name: "Service Level Agreements (SLAs)",
included: currentPlan === BillingPlanType.ENTERPRISE,
},
{
name: "Advanced Support",
included: currentPlan === BillingPlanType.ENTERPRISE,
},
{
name: "Custom Integrations",
included: currentPlan === BillingPlanType.ENTERPRISE,
},
{
name: "Dedicated Account Manager",
included: currentPlan === BillingPlanType.ENTERPRISE,
},
];
const [newSeats, setNewSeats] = useState(seats);
const [newPlan, setNewPlan] = useState(currentPlan);
return (
<div className="max-w-4xl mr-auto space-y-8 p-6">
<Card className="bg-white shadow-lg rounded-lg overflow-hidden">
<div className="px-8 py-6">
<h2 className="text-3xl font-bold text-gray-800 mb-6">
Current Plan
</h2>
<div className="space-y-6">
<div className="flex justify-between items-center">
<p className="text-lg text-gray-600">Current Plan:</p>
<span className="text-xl font-semibold text-blue-600 capitalize">
{currentPlan}
</span>
</div>
<div className="flex justify-between items-center">
<p className="text-lg text-gray-600">Current Seats:</p>
<span className="text-xl font-semibold text-blue-600">
{seats}
</span>
</div>
<Divider />
<div className="mt-6">
<label className="block text-lg font-medium text-gray-700 mb-2">
New Plan:
</label>
<select
value={newPlan}
onChange={(e) => setNewPlan(e.target.value as BillingPlanType)}
className="w-full px-4 py-2 text-lg border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent transition duration-150 ease-in-out"
>
{Object.values(BillingPlanType).map((plan) => (
<option key={plan} value={plan} className="capitalize">
{plan}
</option>
))}
</select>
</div>
<div className="mt-6">
<label className="block text-lg font-medium text-gray-700 mb-2">
New Number of Seats:
</label>
<input
type="number"
value={newSeats}
onChange={(e) => setNewSeats(Number(e.target.value))}
min="1"
className="w-full px-4 py-2 text-lg border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent transition duration-150 ease-in-out"
/>
</div>
<div className="mt-8 flex justify-center">
<StripeCheckoutButton quantity={newSeats} plan={newPlan} />
</div>
</div>
</div>
</Card>
<Card className="bg-white shadow-lg rounded-lg overflow-hidden">
<div className="px-6 py-4">
<h2 className="text-3xl font-bold text-gray-800 mb-4">Features</h2>
<ul className="space-y-3">
{features.map((feature, index) => (
<li key={index} className="flex items-center text-lg">
<span className="mr-3">
{feature.included ? (
<CheckmarkIcon className="text-success" />
) : (
<XIcon className="text-error" />
)}
</span>
<span
className={
feature.included ? "text-gray-800" : "text-gray-500"
}
>
{feature.name}
</span>
</li>
))}
</ul>
</div>
</Card>
{currentPlan !== BillingPlanType.FREE && (
<Card className="bg-white shadow-lg rounded-lg overflow-hidden">
<div className="px-8 py-6">
<h2 className="text-3xl font-bold text-gray-800 mb-4">
Tenant Merging
</h2>
<p className="text-gray-600 mb-6">
Merge your tenant with another to consolidate resources and users.
</p>
<div
className="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4 mb-6"
role="alert"
>
<p className="font-bold">Warning:</p>
<p>Merging tenants will result in the following:</p>
<ul className="list-disc list-inside mt-2">
<li>Data from the other tenant will be abandoned</li>
<li>
Billing for the merged tenant will transfer to this account
</li>
</ul>
</div>
<Button
onClick={() => {
alert("not implemented");
}}
className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-3 px-6 rounded-lg transition duration-300 shadow-md hover:shadow-lg"
>
Start Tenant Merge Process
</Button>
</div>
</Card>
)}
</div>
);
}

View File

@@ -0,0 +1,72 @@
import { SubLabel } from "@/components/admin/connectors/Field";
import { usePopup } from "@/components/admin/connectors/Popup";
import { useEffect, useState } from "react";
import Dropzone from "react-dropzone";
export function ImageUpload({
selectedFile,
setSelectedFile,
}: {
selectedFile: File | null;
setSelectedFile: (file: File) => void;
}) {
const [tmpImageUrl, setTmpImageUrl] = useState<string>("");
useEffect(() => {
if (selectedFile) {
setTmpImageUrl(URL.createObjectURL(selectedFile));
} else {
setTmpImageUrl("");
}
}, [selectedFile]);
const [dragActive, setDragActive] = useState(false);
const { popup, setPopup } = usePopup();
return (
<>
{popup}
<Dropzone
onDrop={(acceptedFiles) => {
if (acceptedFiles.length !== 1) {
setPopup({
type: "error",
message: "Only one file can be uploaded at a time",
});
}
setTmpImageUrl(URL.createObjectURL(acceptedFiles[0]));
setSelectedFile(acceptedFiles[0]);
setDragActive(false);
}}
onDragLeave={() => setDragActive(false)}
onDragEnter={() => setDragActive(true)}
>
{({ getRootProps, getInputProps }) => (
<section>
<div
{...getRootProps()}
className={
"flex flex-col items-center w-full px-4 py-12 rounded " +
"shadow-lg tracking-wide border border-border cursor-pointer" +
(dragActive ? " border-accent" : "")
}
>
<input {...getInputProps()} />
<b className="text-emphasis">
Drag and drop a .png or .jpg file, or click to select a file!
</b>
</div>
{tmpImageUrl && (
<div className="mt-4 mb-8">
<SubLabel>Uploaded Image:</SubLabel>
<img src={tmpImageUrl} className="mt-4 max-w-xs max-h-64" />
</div>
)}
</section>
)}
</Dropzone>
</>
);
}

View File

@@ -0,0 +1,59 @@
"use client";
import { useState } from "react";
import { loadStripe } from "@stripe/stripe-js";
import { buildClientUrl } from "@/lib/utilsSS";
import { BillingPlanType } from "@/app/admin/settings/interfaces";
// import { buildUrl } from '@/lib/utilsSS';
export function StripeCheckoutButton({
quantity,
plan,
}: {
quantity: number;
plan: BillingPlanType;
}) {
const [isLoading, setIsLoading] = useState(false);
const handleClick = async () => {
setIsLoading(true);
console.log(quantity);
try {
const response = await fetch("/api/create-checkout-session", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ quantity, plan }),
});
if (!response.ok) {
throw new Error("Failed to create checkout session");
}
const { sessionId } = await response.json();
const stripe = await loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);
if (stripe) {
await stripe.redirectToCheckout({ sessionId });
} else {
throw new Error("Stripe failed to load");
}
} catch (error) {
console.error("Error:", error);
} finally {
setIsLoading(false);
}
};
return (
<button
onClick={handleClick}
disabled={isLoading}
className="py-2 px-4 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-blue-300"
>
{isLoading ? "Loading..." : "Proceed to Checkout"}
</button>
);
}

View File

@@ -0,0 +1,16 @@
import { BillingSettings } from "./BillingSettings";
import { AdminPageTitle } from "@/components/admin/Title";
import { PackageIcon } from "@/components/icons/icons";
export default async function Whitelabeling() {
return (
<div className="mx-auto container">
<AdminPageTitle
title="Billing"
icon={<PackageIcon size={32} className="my-auto" />}
/>
<BillingSettings />
</div>
);
}

View File

@@ -30,15 +30,19 @@ import { usePathname } from "next/navigation";
import { SettingsContext } from "../settings/SettingsProvider";
import { useContext } from "react";
import { CustomTooltip } from "../tooltip/CustomTooltip";
import { CLOUD_ENABLED } from "@/lib/constants";
import { BellSimpleRinging } from "@phosphor-icons/react";
export function ClientLayout({
user,
children,
enableEnterprise,
cloudEnabled,
}: {
user: User | null;
children: React.ReactNode;
enableEnterprise: boolean;
cloudEnabled: boolean;
}) {
const isCurator =
user?.role === UserRole.CURATOR || user?.role === UserRole.GLOBAL_CURATOR;
@@ -317,6 +321,20 @@ export function ClientLayout({
},
]
: []),
...(cloudEnabled
? [
{
name: (
<div className="flex">
<BellSimpleRinging size={18} />
<div className="ml-1">Billing</div>
</div>
),
link: "/admin/plan",
},
]
: []),
],
},
]

View File

@@ -6,7 +6,10 @@ import {
} from "@/lib/userSS";
import { redirect } from "next/navigation";
import { ClientLayout } from "./ClientLayout";
import { SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED } from "@/lib/constants";
import {
CLOUD_ENABLED,
SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED,
} from "@/lib/constants";
import { AnnouncementBanner } from "../header/AnnouncementBanner";
export async function Layout({ children }: { children: React.ReactNode }) {
@@ -43,6 +46,7 @@ export async function Layout({ children }: { children: React.ReactNode }) {
return (
<ClientLayout
enableEnterprise={SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED}
cloudEnabled={CLOUD_ENABLED}
user={user}
>
<AnnouncementBanner />

View File

@@ -1,4 +1,6 @@
import {
BillingPlanType,
CloudSettings,
CombinedSettings,
EnterpriseSettings,
Settings,
@@ -85,10 +87,15 @@ export async function fetchSettingsSS(): Promise<CombinedSettings | null> {
}
const webVersion = getWebVersion();
const cloudSettings: CloudSettings = {
numberOfSeats: 10,
planType: BillingPlanType.PREMIUM,
};
const combinedSettings: CombinedSettings = {
settings,
enterpriseSettings,
cloudSettings,
customAnalyticsScript,
webVersion,
};

View File

@@ -54,3 +54,5 @@ export const CUSTOM_ANALYTICS_ENABLED = process.env.CUSTOM_ANALYTICS_SECRET_KEY
export const DISABLE_LLM_DOC_RELEVANCE =
process.env.DISABLE_LLM_DOC_RELEVANCE?.toLowerCase() === "true";
export const CLOUD_ENABLED = process.env.CLOUD?.toLowerCase() === "true";

View File

@@ -9,6 +9,7 @@ const eePaths = [
"/admin/performance/query-history",
"/admin/whitelabeling",
"/admin/performance/custom-analytics",
"/admin/plan",
];
const eePathsForMatcher = eePaths.map((path) => `${path}/:path*`);