mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-08-03 13:43:18 +02:00
add proper stripe checkout button for user
This commit is contained in:
317
backend/alembic/versions/5be3aa848ce8_testing.py
Normal file
317
backend/alembic/versions/5be3aa848ce8_testing.py
Normal 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 ###
|
@@ -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"
|
||||
|
@@ -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"""
|
||||
|
||||
|
||||
|
@@ -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
10
web/package-lock.json
generated
@@ -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",
|
||||
|
@@ -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",
|
||||
|
@@ -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;
|
||||
|
186
web/src/app/ee/admin/plan/BillingSettings.tsx
Normal file
186
web/src/app/ee/admin/plan/BillingSettings.tsx
Normal 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>
|
||||
);
|
||||
}
|
72
web/src/app/ee/admin/plan/ImageUpload.tsx
Normal file
72
web/src/app/ee/admin/plan/ImageUpload.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
59
web/src/app/ee/admin/plan/StripeCheckoutButton.tsx
Normal file
59
web/src/app/ee/admin/plan/StripeCheckoutButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
16
web/src/app/ee/admin/plan/page.tsx
Normal file
16
web/src/app/ee/admin/plan/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
]
|
||||
|
@@ -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 />
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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";
|
||||
|
@@ -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*`);
|
||||
|
||||
|
Reference in New Issue
Block a user