Skip to content

Commit 4d2e6ad

Browse files
feat: Add the ability to disable the credit billing system
1 parent 1d94d73 commit 4d2e6ad

File tree

9 files changed

+146
-53
lines changed

9 files changed

+146
-53
lines changed

src/actions/credits.action.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from "@/utils/credits";
1010
import { CREDIT_TRANSACTION_TYPE } from "@/db/schema";
1111
import { getStripe } from "@/lib/stripe";
12-
import { MAX_TRANSACTIONS_PER_PAGE, CREDITS_EXPIRATION_YEARS } from "@/constants";
12+
import { MAX_TRANSACTIONS_PER_PAGE, CREDITS_EXPIRATION_YEARS, DISABLE_CREDIT_BILLING_SYSTEM } from "@/constants";
1313
import ms from "ms";
1414
import { withRateLimit, RATE_LIMITS } from "@/utils/with-rate-limit";
1515
import { updateAllSessionsOfUser } from "@/utils/kv-session";
@@ -68,6 +68,10 @@ export async function getTransactions({ page, limit = MAX_TRANSACTIONS_PER_PAGE
6868

6969
export async function createPaymentIntent({ packageId }: CreatePaymentIntentInput) {
7070
return withRateLimit(async () => {
71+
if (DISABLE_CREDIT_BILLING_SYSTEM) {
72+
throw new Error("Credit billing system is disabled");
73+
}
74+
7175
const session = await requireVerifiedEmail();
7276
if (!session) {
7377
throw new Error("Unauthorized");
@@ -103,6 +107,10 @@ export async function createPaymentIntent({ packageId }: CreatePaymentIntentInpu
103107

104108
export async function confirmPayment({ packageId, paymentIntentId }: PurchaseCreditsInput) {
105109
return withRateLimit(async () => {
110+
if (DISABLE_CREDIT_BILLING_SYSTEM) {
111+
throw new Error("Credit billing system is disabled");
112+
}
113+
106114
const session = await requireVerifiedEmail();
107115
if (!session) {
108116
throw new Error("Unauthorized");

src/app/(dashboard)/dashboard/billing/_components/transaction-history.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { format, isPast } from "date-fns";
1818
import { Badge } from "@/components/ui/badge";
1919
import { useTransactionStore } from "@/state/transaction";
2020
import { useQueryState } from "nuqs";
21+
import { DISABLE_CREDIT_BILLING_SYSTEM } from "@/constants";
2122

2223
type TransactionData = Awaited<ReturnType<typeof getTransactions>>
2324

@@ -51,6 +52,10 @@ export function TransactionHistory() {
5152
setPage(newPage.toString());
5253
};
5354

55+
if (DISABLE_CREDIT_BILLING_SYSTEM) {
56+
return null;
57+
}
58+
5459
if (isLoading) {
5560
return (
5661
<Card>

src/app/(dashboard)/dashboard/billing/page.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { PageHeader } from "@/components/page-header";
44
import { TransactionHistory } from "./_components/transaction-history";
55
import { CreditPackages } from "./_components/credit-packages";
66
import { NuqsAdapter } from "nuqs/adapters/next/app";
7+
import { DISABLE_CREDIT_BILLING_SYSTEM } from "@/constants";
8+
import { CreditSystemDisabled } from "@/components/credit-system-disabled";
79

810
export default async function BillingPage() {
911
const session = await getSessionFromCookie();
@@ -27,12 +29,18 @@ export default async function BillingPage() {
2729
]}
2830
/>
2931
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
30-
<CreditPackages />
31-
<div className="mt-4">
32-
<NuqsAdapter>
33-
<TransactionHistory />
34-
</NuqsAdapter>
35-
</div>
32+
{DISABLE_CREDIT_BILLING_SYSTEM ? (
33+
<CreditSystemDisabled />
34+
) : (
35+
<>
36+
<CreditPackages />
37+
<div className="mt-4">
38+
<NuqsAdapter>
39+
<TransactionHistory />
40+
</NuqsAdapter>
41+
</div>
42+
</>
43+
)}
3644
</div>
3745
</>
3846
);

src/app/(dashboard)/dashboard/marketplace/page.tsx

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { COMPONENTS } from "./components-catalog"
44
import { MarketplaceCard } from "@/components/marketplace-card"
55
import { getSessionFromCookie } from "@/utils/auth"
66
import { getUserPurchasedItems } from "@/utils/credits"
7+
import { DISABLE_CREDIT_BILLING_SYSTEM } from "@/constants"
8+
import { CreditSystemDisabled } from "@/components/credit-system-disabled"
79

810
export default async function MarketplacePage() {
911
const session = await getSessionFromCookie();
@@ -20,33 +22,39 @@ export default async function MarketplacePage() {
2022
]}
2123
/>
2224
<div className="container mx-auto px-5 pb-12">
23-
<div className="mb-8">
24-
<h1 className="text-4xl font-bold mt-4">Component Marketplace</h1>
25-
<p className="text-muted-foreground mt-2">
26-
Purchase and use our premium components using your credits
27-
</p>
28-
</div>
25+
{DISABLE_CREDIT_BILLING_SYSTEM ? (
26+
<CreditSystemDisabled />
27+
) : (
28+
<>
29+
<div className="mb-8">
30+
<h1 className="text-4xl font-bold mt-4">Component Marketplace</h1>
31+
<p className="text-muted-foreground mt-2">
32+
Purchase and use our premium components using your credits
33+
</p>
34+
</div>
2935

30-
<Alert
31-
color="warning"
32-
title="Demo Template Feature"
33-
description="This marketplace page demonstrates how to implement a credit-based billing system in your SaaS application. Feel free to use this as a starting point and customize it for your specific needs."
34-
className="mb-6"
35-
/>
36-
37-
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
38-
{COMPONENTS.map((component) => (
39-
<MarketplaceCard
40-
key={component.id}
41-
id={component.id}
42-
name={component.name}
43-
description={component.description}
44-
credits={component.credits}
45-
containerClass={component.containerClass}
46-
isPurchased={purchasedItems.has(`COMPONENT:${component.id}`)}
36+
<Alert
37+
color="warning"
38+
title="Demo Template Feature"
39+
description="This marketplace page demonstrates how to implement a credit-based billing system in your SaaS application. Feel free to use this as a starting point and customize it for your specific needs."
40+
className="mb-6"
4741
/>
48-
))}
49-
</div>
42+
43+
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
44+
{COMPONENTS.map((component) => (
45+
<MarketplaceCard
46+
key={component.id}
47+
id={component.id}
48+
name={component.name}
49+
description={component.description}
50+
credits={component.credits}
51+
containerClass={component.containerClass}
52+
isPurchased={purchasedItems.has(`COMPONENT:${component.id}`)}
53+
/>
54+
))}
55+
</div>
56+
</>
57+
)}
5058
</div>
5159
</>
5260
)

src/app/(dashboard)/dashboard/marketplace/purchase.action.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { getDB } from "@/db";
99
import { purchasedItemsTable, PURCHASABLE_ITEM_TYPE } from "@/db/schema";
1010
import { and, eq } from "drizzle-orm";
1111
import { COMPONENTS } from "@/app/(dashboard)/dashboard/marketplace/components-catalog";
12+
import { DISABLE_CREDIT_BILLING_SYSTEM } from "@/constants";
1213

1314
const purchaseSchema = z.object({
1415
itemId: z.string(),
@@ -20,6 +21,13 @@ export const purchaseAction = createServerAction()
2021
.handler(async ({ input }) => {
2122
return withRateLimit(
2223
async () => {
24+
if (DISABLE_CREDIT_BILLING_SYSTEM) {
25+
throw new ZSAError(
26+
"INSUFFICIENT_CREDITS",
27+
"Marketplace is not available when credit billing is disabled"
28+
);
29+
}
30+
2331
const session = await getSessionFromCookie();
2432

2533
if (!session) {
@@ -45,19 +53,6 @@ export const purchaseAction = createServerAction()
4553
);
4654
}
4755

48-
// Check if user has enough credits
49-
const hasCredits = await hasEnoughCredits({
50-
userId: session.userId,
51-
requiredCredits: itemDetails.credits,
52-
});
53-
54-
if (!hasCredits) {
55-
throw new ZSAError(
56-
"INSUFFICIENT_CREDITS",
57-
"You don't have enough credits to purchase this item"
58-
);
59-
}
60-
6156
const db = getDB();
6257

6358
// Check if user already owns the item
@@ -76,14 +71,27 @@ export const purchaseAction = createServerAction()
7671
);
7772
}
7873

79-
// Use credits first
74+
// Check if user has enough credits
75+
const hasCredits = await hasEnoughCredits({
76+
userId: session.userId,
77+
requiredCredits: itemDetails.credits,
78+
});
79+
80+
if (!hasCredits) {
81+
throw new ZSAError(
82+
"INSUFFICIENT_CREDITS",
83+
"You don't have enough credits to purchase this item"
84+
);
85+
}
86+
87+
// Use credits
8088
await consumeCredits({
8189
userId: session.userId,
8290
amount: itemDetails.credits,
8391
description: `Purchased ${input.itemType.toLowerCase()}: ${itemDetails.name}`,
8492
});
8593

86-
// Then add item to user's purchased items
94+
// Add item to user's purchased items
8795
await db.insert(purchasedItemsTable).values({
8896
userId: session.userId,
8997
itemType: input.itemType,

src/components/app-sidebar.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
SidebarRail,
2828
} from "@/components/ui/sidebar"
2929
import { useSessionStore } from "@/state/session"
30+
import { DISABLE_CREDIT_BILLING_SYSTEM } from "@/constants"
3031

3132
export type NavItem = {
3233
title: string
@@ -94,11 +95,11 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
9495
url: "/dashboard/teams" as Route,
9596
icon: Users,
9697
},
97-
{
98+
...(!DISABLE_CREDIT_BILLING_SYSTEM ? [{
9899
title: "Marketplace",
99-
url: "/dashboard/marketplace",
100+
url: "/dashboard/marketplace" as Route,
100101
icon: ShoppingCart,
101-
},
102+
}] : []),
102103
{
103104
title: "Billing",
104105
url: "/dashboard/billing",
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Card, CardContent } from "@/components/ui/card";
2+
import { Coins } from "lucide-react";
3+
4+
export function CreditSystemDisabled() {
5+
return (
6+
<Card>
7+
<CardContent className="flex flex-col items-center justify-center py-16 px-4">
8+
<div className="text-center space-y-4 max-w-md">
9+
<div className="flex justify-center">
10+
<Coins className="h-16 w-16 text-muted-foreground/50" />
11+
</div>
12+
<h2 className="text-2xl font-semibold">Credit System Disabled</h2>
13+
<p className="text-muted-foreground">
14+
The credit billing system is currently disabled. All features are available without credit restrictions.
15+
</p>
16+
</div>
17+
</CardContent>
18+
</Card>
19+
);
20+
}

src/constants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ export const SESSION_COOKIE_NAME = "session";
1515
export const GOOGLE_OAUTH_STATE_COOKIE_NAME = "google-oauth-state";
1616
export const GOOGLE_OAUTH_CODE_VERIFIER_COOKIE_NAME = "google-oauth-code-verifier";
1717

18+
export const DISABLE_CREDIT_BILLING_SYSTEM = false;
1819
export const CREDIT_PACKAGES = [
1920
{ id: "package-1", credits: 500, price: 5 },
2021
{ id: "package-2", credits: 1200, price: 10 },
2122
{ id: "package-3", credits: 3000, price: 20 },
2223
] as const;
23-
2424
export const CREDITS_EXPIRATION_YEARS = 2;
25-
2625
export const FREE_MONTHLY_CREDITS = CREDIT_PACKAGES[0].credits * 0.1;
26+
2727
export const MAX_TRANSACTIONS_PER_PAGE = 10;
2828
export const REDIRECT_AFTER_SIGN_IN = "/dashboard" as Route;

src/utils/credits.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { eq, sql, desc, and, lt, isNull, gt, or, asc } from "drizzle-orm";
33
import { getDB } from "@/db";
44
import { userTable, creditTransactionTable, CREDIT_TRANSACTION_TYPE, purchasedItemsTable } from "@/db/schema";
55
import { updateAllSessionsOfUser, KVSession } from "./kv-session";
6-
import { CREDIT_PACKAGES, FREE_MONTHLY_CREDITS } from "@/constants";
6+
import { CREDIT_PACKAGES, FREE_MONTHLY_CREDITS, DISABLE_CREDIT_BILLING_SYSTEM } from "@/constants";
77

88
export type CreditPackage = typeof CREDIT_PACKAGES[number];
99

@@ -26,6 +26,10 @@ function shouldRefreshCredits(session: KVSession, currentTime: Date): boolean {
2626
}
2727

2828
async function processExpiredCredits(userId: string, currentTime: Date) {
29+
if (DISABLE_CREDIT_BILLING_SYSTEM) {
30+
return;
31+
}
32+
2933
const db = getDB();
3034
// Find all expired transactions that haven't been processed and have remaining credits
3135
// Order by type to process MONTHLY_REFRESH first, then by creation date
@@ -82,6 +86,10 @@ async function processExpiredCredits(userId: string, currentTime: Date) {
8286
}
8387

8488
export async function addUserCredits(userId: string, creditsToAdd: number) {
89+
if (DISABLE_CREDIT_BILLING_SYSTEM) {
90+
return;
91+
}
92+
8593
const db = getDB();
8694
await db
8795
.update(userTable)
@@ -106,6 +114,10 @@ export async function logTransaction({
106114
expirationDate?: Date;
107115
paymentIntentId?: string;
108116
}) {
117+
if (DISABLE_CREDIT_BILLING_SYSTEM) {
118+
return;
119+
}
120+
109121
const db = getDB();
110122
await db.insert(creditTransactionTable).values({
111123
userId,
@@ -119,6 +131,10 @@ export async function logTransaction({
119131
}
120132

121133
export async function addFreeMonthlyCreditsIfNeeded(session: KVSession): Promise<number> {
134+
if (DISABLE_CREDIT_BILLING_SYSTEM) {
135+
return 0;
136+
}
137+
122138
const currentTime = new Date();
123139

124140
// Check if it's been at least a month since last refresh
@@ -203,6 +219,10 @@ export async function addFreeMonthlyCreditsIfNeeded(session: KVSession): Promise
203219
}
204220

205221
export async function hasEnoughCredits({ userId, requiredCredits }: { userId: string; requiredCredits: number }) {
222+
if (DISABLE_CREDIT_BILLING_SYSTEM) {
223+
return true;
224+
}
225+
206226
const user = await getDB().query.userTable.findFirst({
207227
where: eq(userTable.id, userId),
208228
columns: {
@@ -215,6 +235,10 @@ export async function hasEnoughCredits({ userId, requiredCredits }: { userId: st
215235
}
216236

217237
export async function consumeCredits({ userId, amount, description }: { userId: string; amount: number; description: string }) {
238+
if (DISABLE_CREDIT_BILLING_SYSTEM) {
239+
return 0;
240+
}
241+
218242
const db = getDB();
219243

220244
// First check if user has enough credits
@@ -322,6 +346,17 @@ export async function getCreditTransactions({
322346
page?: number;
323347
limit?: number;
324348
}) {
349+
if (DISABLE_CREDIT_BILLING_SYSTEM) {
350+
return {
351+
transactions: [],
352+
pagination: {
353+
total: 0,
354+
pages: 0,
355+
current: page,
356+
},
357+
};
358+
}
359+
325360
const db = getDB();
326361
const transactions = await db.query.creditTransactionTable.findMany({
327362
where: eq(creditTransactionTable.userId, userId),

0 commit comments

Comments
 (0)