mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-21 14:12:42 +02:00
Billing fixes (#3976)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
export enum GatingType {
|
||||
FULL = "full",
|
||||
PARTIAL = "partial",
|
||||
NONE = "none",
|
||||
export enum ApplicationStatus {
|
||||
PAYMENT_REMINDER = "payment_reminder",
|
||||
GATED_ACCESS = "gated_access",
|
||||
ACTIVE = "active",
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
@@ -11,7 +11,7 @@ export interface Settings {
|
||||
needs_reindexing: boolean;
|
||||
gpu_enabled: boolean;
|
||||
pro_search_disabled: boolean | null;
|
||||
product_gating: GatingType;
|
||||
application_status: ApplicationStatus;
|
||||
auto_scroll: boolean;
|
||||
}
|
||||
|
||||
|
@@ -2291,8 +2291,6 @@ export function ChatPage({
|
||||
bg-opacity-80
|
||||
duration-300
|
||||
ease-in-out
|
||||
|
||||
|
||||
${
|
||||
!untoggled && (showHistorySidebar || sidebarVisible)
|
||||
? "opacity-100 w-[250px] translate-x-0"
|
||||
|
73
web/src/app/ee/admin/billing/BillingAlerts.tsx
Normal file
73
web/src/app/ee/admin/billing/BillingAlerts.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React from "react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { CircleAlert, Info } from "lucide-react";
|
||||
import { BillingInformation, BillingStatus } from "./interfaces";
|
||||
|
||||
export function BillingAlerts({
|
||||
billingInformation,
|
||||
}: {
|
||||
billingInformation: BillingInformation;
|
||||
}) {
|
||||
const isTrialing = billingInformation.status === BillingStatus.TRIALING;
|
||||
const isCancelled = billingInformation.cancel_at_period_end;
|
||||
const isExpired =
|
||||
new Date(billingInformation.current_period_end) < new Date();
|
||||
const noPaymentMethod = !billingInformation.payment_method_enabled;
|
||||
|
||||
const messages: string[] = [];
|
||||
|
||||
if (isExpired) {
|
||||
messages.push(
|
||||
"Your subscription has expired. Please resubscribe to continue using the service."
|
||||
);
|
||||
}
|
||||
if (isCancelled && !isExpired) {
|
||||
messages.push(
|
||||
`Your subscription will cancel on ${new Date(
|
||||
billingInformation.current_period_end
|
||||
).toLocaleDateString()}. You can resubscribe before this date to remain uninterrupted.`
|
||||
);
|
||||
}
|
||||
if (isTrialing) {
|
||||
messages.push(
|
||||
`You're currently on a trial. Your trial ends on ${
|
||||
billingInformation.trial_end
|
||||
? new Date(billingInformation.trial_end).toLocaleDateString()
|
||||
: "N/A"
|
||||
}.`
|
||||
);
|
||||
}
|
||||
if (noPaymentMethod) {
|
||||
messages.push(
|
||||
"You currently have no payment method on file. Please add one to avoid service interruption."
|
||||
);
|
||||
}
|
||||
|
||||
const variant = isExpired || noPaymentMethod ? "destructive" : "default";
|
||||
|
||||
if (messages.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Alert variant={variant}>
|
||||
<AlertTitle className="flex items-center space-x-2">
|
||||
{variant === "destructive" ? (
|
||||
<CircleAlert className="h-4 w-4" />
|
||||
) : (
|
||||
<Info className="h-4 w-4" />
|
||||
)}
|
||||
<span>
|
||||
{variant === "destructive"
|
||||
? "Important Subscription Notice"
|
||||
: "Subscription Notice"}
|
||||
</span>
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<ul className="list-disc list-inside space-y-1 mt-2">
|
||||
{messages.map((msg, idx) => (
|
||||
<li key={idx}>{msg}</li>
|
||||
))}
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
@@ -1,18 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { CreditCard, ArrowFatUp } from "@phosphor-icons/react";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { loadStripe } from "@stripe/stripe-js";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { SettingsIcon } from "@/components/icons/icons";
|
||||
import {
|
||||
updateSubscriptionQuantity,
|
||||
fetchCustomerPortal,
|
||||
statusToDisplay,
|
||||
useBillingInformation,
|
||||
} from "./utils";
|
||||
import { useEffect } from "react";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { fetchCustomerPortal, useBillingInformation } from "./utils";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CreditCard, ArrowFatUp } from "@phosphor-icons/react";
|
||||
import { SubscriptionSummary } from "./SubscriptionSummary";
|
||||
import { BillingAlerts } from "./BillingAlerts";
|
||||
|
||||
export default function BillingInformationPage() {
|
||||
const router = useRouter();
|
||||
@@ -24,9 +27,6 @@ export default function BillingInformationPage() {
|
||||
isLoading,
|
||||
} = useBillingInformation();
|
||||
|
||||
if (error) {
|
||||
console.error("Failed to fetch billing information:", error);
|
||||
}
|
||||
useEffect(() => {
|
||||
const url = new URL(window.location.href);
|
||||
if (url.searchParams.has("session_id")) {
|
||||
@@ -35,22 +35,33 @@ export default function BillingInformationPage() {
|
||||
"Congratulations! Your subscription has been updated successfully.",
|
||||
type: "success",
|
||||
});
|
||||
// Remove the session_id from the URL
|
||||
url.searchParams.delete("session_id");
|
||||
window.history.replaceState({}, "", url.toString());
|
||||
// You might want to refresh the billing information here
|
||||
// by calling an API endpoint to get the latest data
|
||||
}
|
||||
}, [setPopup]);
|
||||
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
return <div className="text-center py-8">Loading...</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.error("Failed to fetch billing information:", error);
|
||||
return (
|
||||
<div className="text-center py-8 text-red-500">
|
||||
Error loading billing information. Please try again later.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!billingInformation) {
|
||||
return (
|
||||
<div className="text-center py-8">No billing information available.</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleManageSubscription = async () => {
|
||||
try {
|
||||
const response = await fetchCustomerPortal();
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(
|
||||
@@ -61,11 +72,9 @@ export default function BillingInformationPage() {
|
||||
}
|
||||
|
||||
const { url } = await response.json();
|
||||
|
||||
if (!url) {
|
||||
throw new Error("No portal URL returned from the server");
|
||||
}
|
||||
|
||||
router.push(url);
|
||||
} catch (error) {
|
||||
console.error("Error creating customer portal session:", error);
|
||||
@@ -75,138 +84,39 @@ export default function BillingInformationPage() {
|
||||
});
|
||||
}
|
||||
};
|
||||
if (!billingInformation) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="bg-background-50 rounded-lg p-8 border border-background-200">
|
||||
{popup}
|
||||
{popup}
|
||||
<Card className="shadow-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl font-bold flex items-center">
|
||||
<CreditCard className="mr-4 text-muted-foreground" size={24} />
|
||||
Subscription Details
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<SubscriptionSummary billingInformation={billingInformation} />
|
||||
<BillingAlerts billingInformation={billingInformation} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<h2 className="text-2xl font-bold mb-6 text-text-800 flex items-center">
|
||||
{/* <CreditCard className="mr-4 text-text-600" size={24} /> */}
|
||||
Subscription Details
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-white p-5 rounded-lg shadow-sm transition-all duration-300 hover:shadow-md">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<p className="text-lg font-medium text-text-700">Seats</p>
|
||||
<p className="text-sm text-text-500">
|
||||
Number of licensed users
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xl font-semibold text-text-900">
|
||||
{billingInformation.seats}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-5 rounded-lg shadow-sm transition-all duration-300 hover:shadow-md">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<p className="text-lg font-medium text-text-700">
|
||||
Subscription Status
|
||||
</p>
|
||||
<p className="text-sm text-text-500">
|
||||
Current state of your subscription
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xl font-semibold text-text-900">
|
||||
{statusToDisplay(billingInformation.subscription_status)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-5 rounded-lg shadow-sm transition-all duration-300 hover:shadow-md">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<p className="text-lg font-medium text-text-700">
|
||||
Billing Start
|
||||
</p>
|
||||
<p className="text-sm text-text-500">
|
||||
Start date of current billing cycle
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xl font-semibold text-text-900">
|
||||
{new Date(
|
||||
billingInformation.billing_start
|
||||
).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-5 rounded-lg shadow-sm transition-all duration-300 hover:shadow-md">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<p className="text-lg font-medium text-text-700">Billing End</p>
|
||||
<p className="text-sm text-text-500">
|
||||
End date of current billing cycle
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xl font-semibold text-text-900">
|
||||
{new Date(billingInformation.billing_end).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!billingInformation.payment_method_enabled && (
|
||||
<div className="mt-4 p-4 bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700">
|
||||
<p className="font-bold">Notice:</p>
|
||||
<p>
|
||||
You'll need to add a payment method before your trial ends to
|
||||
continue using the service.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{billingInformation.subscription_status === "trialing" ? (
|
||||
<div className="bg-white p-5 rounded-lg shadow-sm transition-all duration-300 hover:shadow-md mt-8">
|
||||
<p className="text-lg font-medium text-text-700">
|
||||
No cap on users during trial
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-4 mt-8">
|
||||
<div className="flex items-center space-x-4">
|
||||
<p className="text-lg font-medium text-text-700">
|
||||
Current Seats:
|
||||
</p>
|
||||
<p className="text-xl font-semibold text-text-900">
|
||||
{billingInformation.seats}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-text-500">
|
||||
Seats automatically update based on adding, removing, or inviting
|
||||
users.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-5 rounded-lg shadow-sm transition-all duration-300 hover:shadow-md">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<p className="text-lg font-medium text-text-700">
|
||||
Manage Subscription
|
||||
</p>
|
||||
<p className="text-sm text-text-500">
|
||||
View your plan, update payment, or change subscription
|
||||
</p>
|
||||
</div>
|
||||
<SettingsIcon className="text-text-600" size={20} />
|
||||
</div>
|
||||
<button
|
||||
onClick={handleManageSubscription}
|
||||
className="bg-background-600 text-white px-4 py-2 rounded-md hover:bg-background-700 transition duration-300 ease-in-out focus:outline-none focus:ring-2 focus:ring-text-500 focus:ring-opacity-50 font-medium shadow-sm text-sm flex items-center justify-center"
|
||||
>
|
||||
<ArrowFatUp className="mr-2" size={16} />
|
||||
Manage Subscription
|
||||
</button>
|
||||
</div>
|
||||
<Card className="shadow-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl font-semibold">
|
||||
Manage Subscription
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
View your plan, update payment, or change subscription
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button onClick={handleManageSubscription} className="w-full">
|
||||
<ArrowFatUp className="mr-2" size={16} />
|
||||
Manage Subscription
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
17
web/src/app/ee/admin/billing/InfoItem.tsx
Normal file
17
web/src/app/ee/admin/billing/InfoItem.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
|
||||
interface InfoItemProps {
|
||||
title: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function InfoItem({ title, value }: InfoItemProps) {
|
||||
return (
|
||||
<div className="bg-muted p-4 rounded-lg">
|
||||
<p className="text-sm font-medium text-muted-foreground mb-1">{title}</p>
|
||||
<p className="text-lg font-semibold text-foreground dark:text-white">
|
||||
{value}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
33
web/src/app/ee/admin/billing/SubscriptionSummary.tsx
Normal file
33
web/src/app/ee/admin/billing/SubscriptionSummary.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
import { InfoItem } from "./InfoItem";
|
||||
import { statusToDisplay } from "./utils";
|
||||
|
||||
interface SubscriptionSummaryProps {
|
||||
billingInformation: any;
|
||||
}
|
||||
|
||||
export function SubscriptionSummary({
|
||||
billingInformation,
|
||||
}: SubscriptionSummaryProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<InfoItem
|
||||
title="Subscription Status"
|
||||
value={statusToDisplay(billingInformation.status)}
|
||||
/>
|
||||
<InfoItem title="Seats" value={billingInformation.seats.toString()} />
|
||||
<InfoItem
|
||||
title="Billing Start"
|
||||
value={new Date(
|
||||
billingInformation.current_period_start
|
||||
).toLocaleDateString()}
|
||||
/>
|
||||
<InfoItem
|
||||
title="Billing End"
|
||||
value={new Date(
|
||||
billingInformation.current_period_end
|
||||
).toLocaleDateString()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
19
web/src/app/ee/admin/billing/interfaces.ts
Normal file
19
web/src/app/ee/admin/billing/interfaces.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface BillingInformation {
|
||||
status: string;
|
||||
trial_end: Date | null;
|
||||
current_period_end: Date;
|
||||
payment_method_enabled: boolean;
|
||||
cancel_at_period_end: boolean;
|
||||
current_period_start: Date;
|
||||
number_of_seats: number;
|
||||
canceled_at: Date | null;
|
||||
trial_start: Date | null;
|
||||
seats: number;
|
||||
}
|
||||
|
||||
export enum BillingStatus {
|
||||
TRIALING = "trialing",
|
||||
ACTIVE = "active",
|
||||
CANCELLED = "cancelled",
|
||||
EXPIRED = "expired",
|
||||
}
|
@@ -3,10 +3,16 @@ import BillingInformationPage from "./BillingInformationPage";
|
||||
import { MdOutlineCreditCard } from "react-icons/md";
|
||||
|
||||
export interface BillingInformation {
|
||||
stripe_subscription_id: string;
|
||||
status: string;
|
||||
current_period_start: Date;
|
||||
current_period_end: Date;
|
||||
number_of_seats: number;
|
||||
cancel_at_period_end: boolean;
|
||||
canceled_at: Date | null;
|
||||
trial_start: Date | null;
|
||||
trial_end: Date | null;
|
||||
seats: number;
|
||||
subscription_status: string;
|
||||
billing_start: Date;
|
||||
billing_end: Date;
|
||||
payment_method_enabled: boolean;
|
||||
}
|
||||
|
||||
|
@@ -35,9 +35,16 @@ export const statusToDisplay = (status: string) => {
|
||||
|
||||
export const useBillingInformation = () => {
|
||||
const url = "/api/tenants/billing-information";
|
||||
const swrResponse = useSWR<BillingInformation>(url, (url: string) =>
|
||||
fetch(url).then((res) => res.json())
|
||||
);
|
||||
const swrResponse = useSWR<BillingInformation>(url, async (url: string) => {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json();
|
||||
throw new Error(
|
||||
errorData.message || "Failed to fetch billing information"
|
||||
);
|
||||
}
|
||||
return res.json();
|
||||
});
|
||||
|
||||
return {
|
||||
...swrResponse,
|
||||
|
@@ -13,7 +13,10 @@ import {
|
||||
import { Metadata } from "next";
|
||||
import { buildClientUrl } from "@/lib/utilsSS";
|
||||
import { Inter } from "next/font/google";
|
||||
import { EnterpriseSettings, GatingType } from "./admin/settings/interfaces";
|
||||
import {
|
||||
EnterpriseSettings,
|
||||
ApplicationStatus,
|
||||
} from "./admin/settings/interfaces";
|
||||
import { fetchAssistantData } from "@/lib/chat/fetchAssistantdata";
|
||||
import { AppProvider } from "@/components/context/AppProvider";
|
||||
import { PHProvider } from "./providers";
|
||||
@@ -28,6 +31,7 @@ import { WebVitals } from "./web-vitals";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import CloudError from "@/components/errorPages/CloudErrorPage";
|
||||
import Error from "@/components/errorPages/ErrorPage";
|
||||
import AccessRestrictedPage from "@/components/errorPages/AccessRestrictedPage";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
@@ -75,7 +79,7 @@ export default async function RootLayout({
|
||||
]);
|
||||
|
||||
const productGating =
|
||||
combinedSettings?.settings.product_gating ?? GatingType.NONE;
|
||||
combinedSettings?.settings.application_status ?? ApplicationStatus.ACTIVE;
|
||||
|
||||
const getPageContent = async (content: React.ReactNode) => (
|
||||
<html
|
||||
@@ -130,40 +134,16 @@ export default async function RootLayout({
|
||||
</html>
|
||||
);
|
||||
|
||||
if (productGating === ApplicationStatus.GATED_ACCESS) {
|
||||
return getPageContent(<AccessRestrictedPage />);
|
||||
}
|
||||
|
||||
if (!combinedSettings) {
|
||||
return getPageContent(
|
||||
NEXT_PUBLIC_CLOUD_ENABLED ? <CloudError /> : <Error />
|
||||
);
|
||||
}
|
||||
|
||||
if (productGating === GatingType.FULL) {
|
||||
return getPageContent(
|
||||
<div className="flex flex-col items-center justify-center min-h-screen">
|
||||
<div className="mb-2 flex items-center max-w-[175px]">
|
||||
<LogoType />
|
||||
</div>
|
||||
<CardSection className="w-full max-w-md">
|
||||
<h1 className="text-2xl font-bold mb-4 text-error">
|
||||
Access Restricted
|
||||
</h1>
|
||||
<p className="text-text-500 mb-4">
|
||||
We regret to inform you that your access to Onyx has been
|
||||
temporarily suspended due to a lapse in your subscription.
|
||||
</p>
|
||||
<p className="text-text-500 mb-4">
|
||||
To reinstate your access and continue benefiting from Onyx's
|
||||
powerful features, please update your payment information.
|
||||
</p>
|
||||
<p className="text-text-500">
|
||||
If you're an admin, you can resolve this by visiting the
|
||||
billing section. For other users, please reach out to your
|
||||
administrator to address this matter.
|
||||
</p>
|
||||
</CardSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { assistants, hasAnyConnectors, hasImageCompatibleModel } =
|
||||
assistantsData;
|
||||
|
||||
|
@@ -33,6 +33,9 @@ import { MdOutlineCreditCard } from "react-icons/md";
|
||||
import { UserSettingsModal } from "@/app/chat/modal/UserSettingsModal";
|
||||
import { usePopup } from "./connectors/Popup";
|
||||
import { useChatContext } from "../context/ChatContext";
|
||||
import { ApplicationStatus } from "@/app/admin/settings/interfaces";
|
||||
import Link from "next/link";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
export function ClientLayout({
|
||||
user,
|
||||
@@ -74,6 +77,23 @@ export function ClientLayout({
|
||||
defaultModel={user?.preferences?.default_model!}
|
||||
/>
|
||||
)}
|
||||
{settings?.settings.application_status ===
|
||||
ApplicationStatus.PAYMENT_REMINDER && (
|
||||
<div className="fixed top-2 left-1/2 transform -translate-x-1/2 bg-amber-400 dark:bg-amber-500 text-gray-900 dark:text-gray-100 p-4 rounded-lg shadow-lg z-50 max-w-md text-center">
|
||||
<strong className="font-bold">Warning:</strong> Your trial ends in
|
||||
less than 2 days and no payment method has been added.
|
||||
<div className="mt-2">
|
||||
<Link href="/admin/billing">
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-amber-600 hover:bg-amber-700 text-white"
|
||||
>
|
||||
Update Billing Information
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="default-scrollbar flex-none text-text-settings-sidebar bg-background-sidebar dark:bg-[#000] w-[250px] overflow-x-hidden z-20 pt-2 pb-8 h-full border-r border-border dark:border-none miniscroll overflow-auto">
|
||||
<AdminSidebar
|
||||
|
148
web/src/components/errorPages/AccessRestrictedPage.tsx
Normal file
148
web/src/components/errorPages/AccessRestrictedPage.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
"use client";
|
||||
import { FiLock } from "react-icons/fi";
|
||||
import ErrorPageLayout from "./ErrorPageLayout";
|
||||
import { fetchCustomerPortal } from "@/app/ee/admin/billing/utils";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { logout } from "@/lib/user";
|
||||
import { loadStripe } from "@stripe/stripe-js";
|
||||
import { NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY } from "@/lib/constants";
|
||||
|
||||
const fetchResubscriptionSession = async () => {
|
||||
const response = await fetch("/api/tenants/create-subscription-session", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to create resubscription session");
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export default function AccessRestricted() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
const handleManageSubscription = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetchCustomerPortal();
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(
|
||||
`Failed to create customer portal session: ${
|
||||
errorData.message || response.statusText
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
const { url } = await response.json();
|
||||
|
||||
if (!url) {
|
||||
throw new Error("No portal URL returned from the server");
|
||||
}
|
||||
|
||||
router.push(url);
|
||||
} catch (error) {
|
||||
console.error("Error creating customer portal session:", error);
|
||||
setError("Error opening customer portal. Please try again later.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResubscribe = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
if (!NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) {
|
||||
setError("Stripe public key not found");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { sessionId } = await fetchResubscriptionSession();
|
||||
const stripe = await loadStripe(NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
|
||||
|
||||
if (stripe) {
|
||||
await stripe.redirectToCheckout({ sessionId });
|
||||
} else {
|
||||
throw new Error("Stripe failed to load");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating resubscription session:", error);
|
||||
setError("Error opening resubscription page. Please try again later.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ErrorPageLayout>
|
||||
<h1 className="text-2xl font-semibold flex items-center gap-2 mb-4 text-gray-800 dark:text-gray-200">
|
||||
<p>Access Restricted</p>
|
||||
<FiLock className="text-error inline-block" />
|
||||
</h1>
|
||||
<div className="space-y-4 text-gray-600 dark:text-gray-300">
|
||||
<p>
|
||||
We regret to inform you that your access to Onyx has been temporarily
|
||||
suspended due to a lapse in your subscription.
|
||||
</p>
|
||||
<p>
|
||||
To reinstate your access and continue benefiting from Onyx's
|
||||
powerful features, please update your payment information.
|
||||
</p>
|
||||
<p>
|
||||
If you're an admin, you can manage your subscription by clicking
|
||||
the button below. For other users, please reach out to your
|
||||
administrator to address this matter.
|
||||
</p>
|
||||
<div className="flex flex-col space-y-4 sm:flex-row sm:space-y-0 sm:space-x-4">
|
||||
<Button
|
||||
onClick={handleResubscribe}
|
||||
disabled={isLoading}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{isLoading ? "Loading..." : "Resubscribe"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleManageSubscription}
|
||||
disabled={isLoading}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Manage Existing Subscription
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
await logout();
|
||||
window.location.reload();
|
||||
}}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Log out
|
||||
</Button>
|
||||
</div>
|
||||
{error && <p className="text-error">{error}</p>}
|
||||
<p>
|
||||
Need help? Join our{" "}
|
||||
<a
|
||||
className="text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
href="https://join.slack.com/t/danswer/shared_invite/zt-1w76msxmd-HJHLe3KNFIAIzk_0dSOKaQ"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Slack community
|
||||
</a>{" "}
|
||||
for support.
|
||||
</p>
|
||||
</div>
|
||||
</ErrorPageLayout>
|
||||
);
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
CombinedSettings,
|
||||
EnterpriseSettings,
|
||||
GatingType,
|
||||
ApplicationStatus,
|
||||
Settings,
|
||||
} from "@/app/admin/settings/interfaces";
|
||||
import {
|
||||
@@ -45,7 +45,7 @@ export async function fetchSettingsSS(): Promise<CombinedSettings | null> {
|
||||
if (results[0].status === 403 || results[0].status === 401) {
|
||||
settings = {
|
||||
auto_scroll: true,
|
||||
product_gating: GatingType.NONE,
|
||||
application_status: ApplicationStatus.ACTIVE,
|
||||
gpu_enabled: false,
|
||||
maximum_chat_retention_days: null,
|
||||
notifications: [],
|
||||
|
@@ -91,3 +91,6 @@ export const NEXT_PUBLIC_ENABLE_CHROME_EXTENSION =
|
||||
export const NEXT_PUBLIC_INCLUDE_ERROR_POPUP_SUPPORT_LINK =
|
||||
process.env.NEXT_PUBLIC_INCLUDE_ERROR_POPUP_SUPPORT_LINK?.toLowerCase() ===
|
||||
"true";
|
||||
|
||||
export const NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY =
|
||||
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;
|
||||
|
Reference in New Issue
Block a user