From f7f7be2ef0320a44f21269d5ec166f0e58f7717e Mon Sep 17 00:00:00 2001 From: javayhu Date: Mon, 7 Jul 2025 00:04:45 +0800 Subject: [PATCH] feat: add credits config in website config - Added a new credits management system with configurable credit packages in website.tsx. - Replaced hardcoded credit package definitions with a dynamic retrieval system using getCreditPackages and getCreditPackageById functions. - Updated CreditPackages and StripePaymentForm components to utilize the new credit package structure. - Removed obsolete CREDIT_PACKAGES constant from constants.ts to streamline the codebase. - Enhanced type definitions for credit packages in types.ts for better clarity and maintainability. - Updated README.md to reflect changes in credit packages configuration. --- biome.json | 2 + src/actions/credits.action.ts | 6 +- .../settings/credits/credit-packages.tsx | 57 +++++++++++++------ .../settings/credits/stripe-payment-form.tsx | 40 ++++++------- src/config/website.tsx | 33 +++++++++++ src/credits/README.md | 32 +++++++---- src/credits/index.ts | 18 ++++++ src/credits/types.ts | 22 +++++-- src/lib/constants.ts | 34 ----------- src/types/index.d.ts | 10 ++++ 10 files changed, 163 insertions(+), 91 deletions(-) create mode 100644 src/credits/index.ts diff --git a/biome.json b/biome.json index e0d7566..3a8be83 100644 --- a/biome.json +++ b/biome.json @@ -23,6 +23,7 @@ "src/components/tailark/*.tsx", "src/app/[[]locale]/preview/**", "src/payment/types.ts", + "src/credits/types.ts", "src/types/index.d.ts", "public/sw.js" ] @@ -81,6 +82,7 @@ "src/components/tailark/*.tsx", "src/app/[[]locale]/preview/**", "src/payment/types.ts", + "src/credits/types.ts", "src/types/index.d.ts", "public/sw.js" ] diff --git a/src/actions/credits.action.ts b/src/actions/credits.action.ts index 3f2e41b..76b3edc 100644 --- a/src/actions/credits.action.ts +++ b/src/actions/credits.action.ts @@ -1,6 +1,6 @@ 'use server'; -import { CREDIT_PACKAGES } from '@/lib/constants'; +import { getCreditPackageById } from '@/credits'; import { addMonthlyFreeCredits, addRegisterGiftCredits, @@ -77,7 +77,7 @@ export const createCreditPaymentIntent = actionClient const { packageId } = parsedInput; // Find the credit package - const creditPackage = CREDIT_PACKAGES.find((pkg) => pkg.id === packageId); + const creditPackage = getCreditPackageById(packageId); if (!creditPackage) { return { success: false, error: 'Invalid credit package' }; } @@ -127,7 +127,7 @@ export const confirmCreditPayment = actionClient const { packageId, paymentIntentId } = parsedInput; // Find the credit package - const creditPackage = CREDIT_PACKAGES.find((pkg) => pkg.id === packageId); + const creditPackage = getCreditPackageById(packageId); if (!creditPackage) { return { success: false, error: 'Invalid credit package' }; } diff --git a/src/components/settings/credits/credit-packages.tsx b/src/components/settings/credits/credit-packages.tsx index 55de6bf..94b4eb4 100644 --- a/src/components/settings/credits/credit-packages.tsx +++ b/src/components/settings/credits/credit-packages.tsx @@ -1,11 +1,25 @@ 'use client'; -import { createCreditPaymentIntent, getCreditsAction } from '@/actions/credits.action'; +import { + createCreditPaymentIntent, + getCreditsAction, +} from '@/actions/credits.action'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; -import { CREDIT_PACKAGES } from '@/lib/constants'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { getCreditPackageById, getCreditPackages } from '@/credits'; import { formatPrice } from '@/lib/formatter'; import { cn } from '@/lib/utils'; import { useTransactionStore } from '@/stores/transaction-store'; @@ -21,6 +35,7 @@ export function CreditPackages() { const [loadingPackage, setLoadingPackage] = useState(null); const [credits, setCredits] = useState(null); const { refreshTrigger } = useTransactionStore(); + const [paymentDialog, setPaymentDialog] = useState<{ isOpen: boolean; packageId: string | null; @@ -67,8 +82,12 @@ export function CreditPackages() { clientSecret: result.data.clientSecret, }); } else { - const errorMessage = result?.data?.error || t('failedToCreatePaymentIntent'); - console.error('CreditPackages, failed to create payment intent:', errorMessage); + const errorMessage = + result?.data?.error || t('failedToCreatePaymentIntent'); + console.error( + 'CreditPackages, failed to create payment intent:', + errorMessage + ); toast.error(errorMessage); } } catch (error) { @@ -98,15 +117,13 @@ export function CreditPackages() { }); }; - const getPackageInfo = (packageId: string) => { - return CREDIT_PACKAGES.find((pkg) => pkg.id === packageId); - }; - return (
- {t('balance')} + + {t('balance')} +
@@ -133,12 +150,20 @@ export function CreditPackages() {
- {CREDIT_PACKAGES.map((pkg) => ( - + {getCreditPackages().map((pkg) => ( + {pkg.popular && (
- + {t('popular')}
@@ -203,7 +228,7 @@ export function CreditPackages() { diff --git a/src/components/settings/credits/stripe-payment-form.tsx b/src/components/settings/credits/stripe-payment-form.tsx index 8f1f8b9..965b197 100644 --- a/src/components/settings/credits/stripe-payment-form.tsx +++ b/src/components/settings/credits/stripe-payment-form.tsx @@ -3,6 +3,7 @@ import { confirmCreditPayment } from '@/actions/credits.action'; import { Button } from '@/components/ui/button'; import { Card, CardHeader, CardTitle } from '@/components/ui/card'; +import type { CreditPackage } from '@/credits/types'; import { formatPrice } from '@/lib/formatter'; import { useTransactionStore } from '@/stores/transaction-store'; import { @@ -13,19 +14,15 @@ import { } from '@stripe/react-stripe-js'; import { loadStripe } from '@stripe/stripe-js'; import { CoinsIcon, Loader2Icon } from 'lucide-react'; -import { useTheme } from 'next-themes'; import { useTranslations } from 'next-intl'; +import { useTheme } from 'next-themes'; import { useMemo, useState } from 'react'; import { toast } from 'sonner'; interface StripePaymentFormProps { clientSecret: string; packageId: string; - packageInfo: { - credits: number; - price: number; - description: string; - }; + packageInfo: CreditPackage; onPaymentSuccess: () => void; onPaymentCancel: () => void; } @@ -47,13 +44,16 @@ export function StripePaymentForm(props: StripePaymentFormProps) { }, []); const { resolvedTheme: theme } = useTheme(); - const options = useMemo(() => ({ - clientSecret: props.clientSecret, - appearance: { - theme: (theme === "dark" ? "night" : "stripe") as "night" | "stripe", - }, - loader: 'auto' as const, - }), [props.clientSecret, theme]); + const options = useMemo( + () => ({ + clientSecret: props.clientSecret, + appearance: { + theme: (theme === 'dark' ? 'night' : 'stripe') as 'night' | 'stripe', + }, + loader: 'auto' as const, + }), + [props.clientSecret, theme] + ); return ( @@ -65,11 +65,7 @@ export function StripePaymentForm(props: StripePaymentFormProps) { interface PaymentFormProps { clientSecret: string; packageId: string; - packageInfo: { - credits: number; - price: number; - description: string; - }; + packageInfo: CreditPackage; onPaymentSuccess: () => void; onPaymentCancel: () => void; } @@ -101,12 +97,12 @@ function PaymentForm({ // Confirm the payment using PaymentElement const { error } = await stripe.confirmPayment({ elements, - redirect: "if_required", + redirect: 'if_required', }); if (error) { console.error('PaymentForm, payment error:', error); - throw new Error(error.message || "Payment failed"); + throw new Error(error.message || 'Payment failed'); } else { // The payment was successful const paymentIntent = await stripe.retrievePaymentIntent(clientSecret); @@ -126,11 +122,11 @@ function PaymentForm({ // toast.success(`${packageInfo.credits} credits have been added to your account.`); } else { console.error('PaymentForm, payment error:', result?.data?.error); - throw new Error( result?.data?.error || 'Failed to confirm payment' ); + throw new Error(result?.data?.error || 'Failed to confirm payment'); } } else { console.error('PaymentForm, no payment intent found'); - throw new Error("No payment intent found"); + throw new Error('No payment intent found'); } } } catch (error) { diff --git a/src/config/website.tsx b/src/config/website.tsx index 30f8235..28c9c22 100644 --- a/src/config/website.tsx +++ b/src/config/website.tsx @@ -129,4 +129,37 @@ export const websiteConfig: WebsiteConfig = { }, }, }, + credits: { + enableCredits: true, + packages: { + basic: { + id: 'basic', + credits: 100, + price: 990, + popular: false, + description: 'Perfect for getting started', + }, + standard: { + id: 'standard', + credits: 200, + price: 1490, + popular: true, + description: 'Most popular package', + }, + premium: { + id: 'premium', + credits: 500, + price: 3990, + popular: false, + description: 'Best value for heavy users', + }, + enterprise: { + id: 'enterprise', + credits: 1000, + price: 6990, + popular: false, + description: 'Tailored for enterprises', + }, + }, + }, }; diff --git a/src/credits/README.md b/src/credits/README.md index aa96bb5..aa9f2db 100644 --- a/src/credits/README.md +++ b/src/credits/README.md @@ -41,7 +41,7 @@ This document describes the credit management system implementation for the mksa - `src/payment/types.ts` - Payment types (updated) ### Configuration -- `src/lib/constants.ts` - Credit packages configuration +- `src/config/website.tsx` - Credit packages configuration - `env.example` - Environment variables template ### Pages @@ -98,23 +98,33 @@ await addCredits({ type: 'PURCHASE', description: 'Credit purchase' }); + +// Access credit packages from config +import { websiteConfig } from '@/config/website'; +const creditPackages = Object.values(websiteConfig.credits.packages); ``` ## Credit Packages Configuration -Edit `src/lib/constants.ts` to modify available credit packages: +Edit `src/config/website.tsx` to modify available credit packages: ```typescript -export const CREDIT_PACKAGES = [ - { - id: 'basic', - credits: 100, - price: 9.99, - popular: false, - description: 'Perfect for getting started', +export const websiteConfig: WebsiteConfig = { + // ... other config + credits: { + enableCredits: true, + packages: { + basic: { + id: 'basic', + credits: 100, + price: 990, // Price in cents + popular: false, + description: 'Perfect for getting started', + }, + // ... more packages + }, }, - // ... more packages -]; +}; ``` ## Webhook Events diff --git a/src/credits/index.ts b/src/credits/index.ts new file mode 100644 index 0000000..541a9a5 --- /dev/null +++ b/src/credits/index.ts @@ -0,0 +1,18 @@ +import { websiteConfig } from '@/config/website'; + +/** + * Get credit packages + * @returns Credit packages + */ +export function getCreditPackages() { + return Object.values(websiteConfig.credits.packages); +} + +/** + * Get credit package by id + * @param id - Credit package id + * @returns Credit package + */ +export function getCreditPackageById(id: string) { + return getCreditPackages().find((pkg) => pkg.id === id); +} diff --git a/src/credits/types.ts b/src/credits/types.ts index ce266b0..083326a 100644 --- a/src/credits/types.ts +++ b/src/credits/types.ts @@ -2,9 +2,21 @@ * Credit transaction type enum */ export enum CREDIT_TRANSACTION_TYPE { - MONTHLY_REFRESH = 'MONTHLY_REFRESH', // credits earned by monthly refresh - REGISTER_GIFT = 'REGISTER_GIFT', // credits earned by register gift - PURCHASE = 'PURCHASE', // credits earned by purchase - USAGE = 'USAGE', // credits spent by usage - EXPIRE = 'EXPIRE', // credits expired + MONTHLY_REFRESH = 'MONTHLY_REFRESH', // Credits earned by monthly refresh + REGISTER_GIFT = 'REGISTER_GIFT', // Credits earned by register gift + PURCHASE = 'PURCHASE', // Credits earned by purchase + USAGE = 'USAGE', // Credits spent by usage + EXPIRE = 'EXPIRE', // Credits expired +} + +/** + * Credit package + */ +export interface CreditPackage { + id: string; // Unique identifier for the package + credits: number; // Number of credits in the package + price: number; // Price of the package in cents + popular: boolean; // Whether the package is popular + name?: string; // Display name of the package + description?: string; // Description of the package } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 39fd5d1..8acb5ca 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,38 +1,6 @@ export const PLACEHOLDER_IMAGE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAoJJREFUWEfFl4lu4zAMRO3cx/9/au6reMaOdkxTTl0grQFCRoqaT+SQotq2bV9N8rRt28xms87m83l553eZ/9vr9Wpkz+ezkT0ej+6dv1X81AFw7M4FBACPVn2c1Z3zLgDeJwHgeLFYdAARYioAEAKJEG2WAjl3gCwNYymQQ9b7/V4spmIAwO6Wy2VnAMikBWlDURBELf8CuN1uHQSrPwMAHK5WqwFELQ01AIXdAa7XawfAb3p6AOwK5+v1ugAoEq4FRSFLgavfQ49jAGQpAE5wjgGCeRrGdBArwHOPcwFcLpcGU1X0IsBuN5tNgYhaiFFwHTiAwq8I+O5xfj6fOz38K+X/fYAdb7fbAgFAjIJ6Aav3AYlQ6nfnDoDz0+lUxNiLALvf7XaDNGQ6GANQBKR85V27B4D3QQRw7hGIYlQKWGM79hSweyCUe1blXhEAogfABwHAXAcqSYkxCtHLUK3XBajSc4Dj8dilAeiSAgD2+30BAEKV4GKcAuDqB4TdYwBgPQByCgApUBoE4EJUGvxUjF3Q69/zLw3g/HA45ABKgdIQu+JPIyDnisCfAxAFNFM0EFNQ64gfS0EUoQP8ighrZSjn3oziZEQpauyKbfjbZchHUL/3AS/Dd30gAkxuRACgfO+EWQW8qwI1o+wseNuKcQiESjALvwNoMI0TcRzD4lFcPYwIM+JTF5x6HOs8yI7jeB5oKhpMRFH9UwaSCDB2Jmg4rc6E2TT0biIaG0rQhNqyhpHBcayTTSXH6vcDL7/sdqRK8LkwTsU499E8vRcAojHcZ4AxABdilgrp4lsXk8oVqgwh7+6H3phqd8J0Kk4vbx/+sZqCD/vNLya/5dT9fAH8g1WdNGgwbQAAAABJRU5ErkJggg=='; -// credit package definition (price in cents) -export const CREDIT_PACKAGES = [ - { - id: 'basic', - credits: 100, - price: 990, // 9.90 USD in cents - popular: false, - description: 'Perfect for getting started', - }, - { - id: 'standard', - credits: 200, - price: 1490, // 14.90 USD in cents - popular: true, - description: 'Most popular package', - }, - { - id: 'premium', - credits: 500, - price: 3990, // 39.90 USD in cents - popular: false, - description: 'Best value for heavy users', - }, - { - id: 'enterprise', - credits: 1000, - price: 6990, // 69.90 USD in cents - popular: false, - description: 'Tailored for enterprises', - }, -] as const; - // free monthly credits (10% of the smallest package) export const FREE_MONTHLY_CREDITS = 50; @@ -41,5 +9,3 @@ export const REGISTER_GIFT_CREDITS = 100; // default credit expiration days export const CREDIT_EXPIRE_DAYS = 30; - - diff --git a/src/types/index.d.ts b/src/types/index.d.ts index f1dd73a..16706c3 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,5 +1,6 @@ import type { ReactNode } from 'react'; import type { PricePlan } from '@/payment/types'; +import type { CreditPackage } from '@/credits/types'; /** * website config, without translations @@ -17,6 +18,7 @@ export type WebsiteConfig = { storage: StorageConfig; payment: PaymentConfig; price: PriceConfig; + credits: CreditsConfig; }; /** @@ -148,6 +150,14 @@ export interface PriceConfig { plans: Record; // Plans indexed by ID } +/** + * Credits configuration + */ +export interface CreditsConfig { + enableCredits: boolean; // Whether to enable credits + packages: Record; // Packages indexed by ID +} + /** * menu item, used for navbar links, sidebar links, footer links */