feat: implement credit packages management with translations and server integration

- Added new credit packages structure in English and Chinese JSON files for better localization.
- Introduced server-side functions to retrieve credit packages and package details.
- Updated client-side components to utilize new credit package retrieval methods.
- Refactored existing code to enhance modularity and maintainability by separating client and server logic.
- Removed obsolete credit package retrieval functions to streamline the codebase.
This commit is contained in:
javayhu 2025-07-07 00:47:43 +08:00
parent f7f7be2ef0
commit e430a0c319
9 changed files with 186 additions and 56 deletions

View File

@ -111,6 +111,24 @@
}
}
},
"CreditPackages": {
"basic": {
"name": "Basic",
"description": "Basic features for personal use"
},
"standard": {
"name": "Standard",
"description": "Standard features for personal use"
},
"premium": {
"name": "Premium",
"description": "Premium features for personal use"
},
"enterprise": {
"name": "Enterprise",
"description": "Enterprise features for personal use"
}
},
"NotFoundPage": {
"title": "404",
"message": "Sorry, the page you are looking for does not exist.",
@ -498,8 +516,8 @@
"error": "Failed to unban user"
},
"close": "Close"
}
},
}
},
"settings": {
"title": "Settings",
"profile": {

View File

@ -112,6 +112,24 @@
}
}
},
"CreditPackages": {
"basic": {
"name": "基础版",
"description": "基础版功能介绍放这里"
},
"standard": {
"name": "标准版",
"description": "标准版功能介绍放这里"
},
"premium": {
"name": "高级版",
"description": "高级版功能介绍放这里"
},
"enterprise": {
"name": "企业版",
"description": "企业版功能介绍放这里"
}
},
"NotFoundPage": {
"title": "404",
"message": "抱歉,您正在寻找的页面不存在",

View File

@ -1,12 +1,12 @@
'use server';
import { getCreditPackageById } from '@/credits';
import {
addMonthlyFreeCredits,
addRegisterGiftCredits,
consumeCredits,
getUserCredits,
} from '@/credits/credits';
import { getCreditPackageByIdInServer } from '@/credits/server';
import { getSession } from '@/lib/server';
import { confirmPaymentIntent, createPaymentIntent } from '@/payment';
import { createSafeActionClient } from 'next-safe-action';
@ -77,7 +77,7 @@ export const createCreditPaymentIntent = actionClient
const { packageId } = parsedInput;
// Find the credit package
const creditPackage = getCreditPackageById(packageId);
const creditPackage = getCreditPackageByIdInServer(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 = getCreditPackageById(packageId);
const creditPackage = getCreditPackageByIdInServer(packageId);
if (!creditPackage) {
return { success: false, error: 'Invalid credit package' };
}

View File

@ -19,7 +19,11 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { getCreditPackageById, getCreditPackages } from '@/credits';
import {
getCreditPackageByIdInClient,
getCreditPackagesInClient,
} from '@/credits/client';
import type { CreditPackage } from '@/credits/types';
import { formatPrice } from '@/lib/formatter';
import { cn } from '@/lib/utils';
import { useTransactionStore } from '@/stores/transaction-store';
@ -117,6 +121,10 @@ export function CreditPackages() {
});
};
const getPackageInfo = (packageId: string): CreditPackage | undefined => {
return getCreditPackageByIdInClient(packageId);
};
return (
<div className="space-y-6">
<Card className="w-full">
@ -150,7 +158,7 @@ export function CreditPackages() {
</CardHeader>
<CardContent>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
{getCreditPackages().map((pkg) => (
{getCreditPackagesInClient().map((pkg) => (
<Card
key={pkg.id}
className={cn(
@ -224,15 +232,17 @@ export function CreditPackages() {
<DialogTitle>{t('completePurchase')}</DialogTitle>
</DialogHeader>
{paymentDialog.clientSecret && paymentDialog.packageId && (
<StripePaymentForm
clientSecret={paymentDialog.clientSecret}
packageId={paymentDialog.packageId}
packageInfo={getCreditPackageById(paymentDialog.packageId)!}
onPaymentSuccess={handlePaymentSuccess}
onPaymentCancel={handlePaymentCancel}
/>
)}
{paymentDialog.clientSecret &&
paymentDialog.packageId &&
getPackageInfo(paymentDialog.packageId) && (
<StripePaymentForm
clientSecret={paymentDialog.clientSecret}
packageId={paymentDialog.packageId}
packageInfo={getPackageInfo(paymentDialog.packageId)!}
onPaymentSuccess={handlePaymentSuccess}
onPaymentCancel={handlePaymentCancel}
/>
)}
</DialogContent>
</Dialog>
</div>

View File

@ -77,9 +77,9 @@ function PaymentForm({
onPaymentSuccess,
onPaymentCancel,
}: PaymentFormProps) {
const t = useTranslations('Dashboard.settings.credits.packages');
const stripe = useStripe();
const elements = useElements();
const t = useTranslations('Dashboard.settings.credits.packages');
const [processing, setProcessing] = useState(false);
const { triggerRefresh } = useTransactionStore();
@ -103,31 +103,31 @@ function PaymentForm({
if (error) {
console.error('PaymentForm, payment error:', error);
throw new Error(error.message || 'Payment failed');
} else {
// The payment was successful
const paymentIntent = await stripe.retrievePaymentIntent(clientSecret);
if (paymentIntent.paymentIntent) {
const result = await confirmCreditPayment({
packageId,
paymentIntentId: paymentIntent.paymentIntent.id,
});
}
if (result?.data?.success) {
console.log('PaymentForm, payment success');
// Trigger refresh for transaction-dependent UI components
triggerRefresh();
// The payment was successful
const paymentIntent = await stripe.retrievePaymentIntent(clientSecret);
if (paymentIntent.paymentIntent) {
const result = await confirmCreditPayment({
packageId,
paymentIntentId: paymentIntent.paymentIntent.id,
});
// Show success toast
onPaymentSuccess();
// 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');
}
if (result?.data?.success) {
console.log('PaymentForm, payment success');
// Trigger refresh for transaction-dependent UI components
triggerRefresh();
// Show success toast
onPaymentSuccess();
// toast.success(`${packageInfo.credits} credits have been added to your account.`);
} else {
console.error('PaymentForm, no payment intent found');
throw new Error('No payment intent found');
console.error('PaymentForm, payment error:', result?.data?.error);
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');
}
} catch (error) {
console.error('PaymentForm, payment error:', error);

View File

@ -0,0 +1,58 @@
'use client';
import type { CreditPackage } from '@/credits/types';
import { useTranslations } from 'next-intl';
import { websiteConfig } from './website';
/**
* Get credit packages with translations for client components
*
* NOTICE: This function should only be used in client components.
* If you need to get the credit packages in server components, use getAllCreditPackages instead.
* Use this function when showing the credit packages to the user.
*
* docs:
* https://mksaas.com/docs/config/credits
*
* @returns The credit packages with translated content
*/
export function getCreditPackages(): Record<string, CreditPackage> {
const t = useTranslations('CreditPackages');
const creditConfig = websiteConfig.credits;
const packages: Record<string, CreditPackage> = {};
// Add translated content to each plan
if (creditConfig.packages.basic) {
packages.basic = {
...creditConfig.packages.basic,
name: t('basic.name'),
description: t('basic.description'),
};
}
if (creditConfig.packages.standard) {
packages.standard = {
...creditConfig.packages.standard,
name: t('standard.name'),
description: t('standard.description'),
};
}
if (creditConfig.packages.premium) {
packages.premium = {
...creditConfig.packages.premium,
name: t('premium.name'),
description: t('premium.description'),
};
}
if (creditConfig.packages.enterprise) {
packages.enterprise = {
...creditConfig.packages.enterprise,
name: t('enterprise.name'),
description: t('enterprise.description'),
};
}
return packages;
}

21
src/credits/client.ts Normal file
View File

@ -0,0 +1,21 @@
import { getCreditPackages } from '@/config/credits-config';
import type { CreditPackage } from './types';
/**
* Get credit packages, used in client components
* @returns Credit packages
*/
export function getCreditPackagesInClient(): CreditPackage[] {
return Object.values(getCreditPackages());
}
/**
* Get credit package by id, used in client components
* @param id - Credit package id
* @returns Credit package
*/
export function getCreditPackageByIdInClient(
id: string
): CreditPackage | undefined {
return getCreditPackagesInClient().find((pkg) => pkg.id === id);
}

View File

@ -1,18 +0,0 @@
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);
}

23
src/credits/server.ts Normal file
View File

@ -0,0 +1,23 @@
import { websiteConfig } from '@/config/website';
import type { CreditPackage } from './types';
/**
* Get all credit packages, used in server components
* @returns Credit packages
*/
export function getAllCreditPackagesInServer(): CreditPackage[] {
return Object.values(websiteConfig.credits.packages);
}
/**
* Get credit package by id, used in server components
* @param id - Credit package id
* @returns Credit package
*/
export function getCreditPackageByIdInServer(
id: string
): CreditPackage | undefined {
return websiteConfig.credits.packages[
id as keyof typeof websiteConfig.credits.packages
];
}