feat: add credits section to avatar configuration and update translations
- Added "Credits" entry to the avatar configuration for navigation. - Updated English and Chinese translation files to include "Credits" label. - Refactored error messages in credit payment actions for clarity. - Enhanced loading state management in CreditPackages component. - Replaced icons in CreditPackages component for improved UI consistency.
This commit is contained in:
parent
fe2b1bbe39
commit
13bee49f90
@ -432,6 +432,7 @@
|
||||
"avatar": {
|
||||
"dashboard": "Dashboard",
|
||||
"billing": "Billing",
|
||||
"credits": "Credits",
|
||||
"settings": "Settings"
|
||||
}
|
||||
},
|
||||
|
@ -433,6 +433,7 @@
|
||||
"avatar": {
|
||||
"dashboard": "工作台",
|
||||
"billing": "账单",
|
||||
"credits": "积分",
|
||||
"settings": "设置"
|
||||
}
|
||||
},
|
||||
|
@ -1,18 +1,17 @@
|
||||
'use server';
|
||||
|
||||
import { CREDIT_PACKAGES } from '@/lib/constants';
|
||||
import {
|
||||
addMonthlyFreeCredits,
|
||||
addRegisterGiftCredits,
|
||||
consumeCredits,
|
||||
getUserCredits,
|
||||
addCredits,
|
||||
} from '@/lib/credits';
|
||||
import { getSession } from '@/lib/server';
|
||||
import { confirmPaymentIntent, createPaymentIntent } from '@/payment';
|
||||
import { createSafeActionClient } from 'next-safe-action';
|
||||
import { z } from 'zod';
|
||||
import { createPaymentIntent, confirmPaymentIntent } from '@/payment';
|
||||
import { CREDIT_PACKAGES } from '@/lib/constants';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { z } from 'zod';
|
||||
|
||||
const actionClient = createSafeActionClient();
|
||||
|
||||
@ -73,7 +72,7 @@ export const createCreditPaymentIntent = actionClient
|
||||
.schema(createPaymentIntentSchema)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const session = await getSession();
|
||||
if (!session) return { success: false, error: 'User not authenticated' };
|
||||
if (!session) return { success: false, error: 'Unauthorized' };
|
||||
|
||||
const { packageId } = parsedInput;
|
||||
|
||||
@ -115,7 +114,7 @@ export const confirmCreditPayment = actionClient
|
||||
.schema(confirmPaymentSchema)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const session = await getSession();
|
||||
if (!session) return { success: false, error: 'User not authenticated' };
|
||||
if (!session) return { success: false, error: 'Unauthorized' };
|
||||
|
||||
const { packageId, paymentIntentId } = parsedInput;
|
||||
|
||||
|
@ -8,29 +8,29 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u
|
||||
import { CREDIT_PACKAGES } from '@/lib/constants';
|
||||
import { formatPrice } from '@/lib/formatter';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CheckIcon, CoinsIcon, Loader2Icon } from 'lucide-react';
|
||||
import { CircleCheckBigIcon, CoinsIcon, Loader2Icon } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Separator } from '../../ui/separator';
|
||||
import { StripePaymentForm } from './stripe-payment-form';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function CreditPackages() {
|
||||
const [loadingCredits, setLoadingCredits] = useState(true);
|
||||
const [loadingPackage, setLoadingPackage] = useState<string | null>(null);
|
||||
const [credits, setCredits] = useState<number | null>(null);
|
||||
const [paymentDialog, setPaymentDialog] = useState<{
|
||||
isOpen: boolean;
|
||||
clientSecret: string | null;
|
||||
packageId: string | null;
|
||||
clientSecret: string | null;
|
||||
}>({
|
||||
isOpen: false,
|
||||
clientSecret: null,
|
||||
packageId: null,
|
||||
clientSecret: null,
|
||||
});
|
||||
const [credits, setCredits] = useState<number | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetchCredits = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setLoadingCredits(true);
|
||||
const result = await getCreditsAction();
|
||||
if (result?.data?.success) {
|
||||
console.log('CreditPackages, fetched credits:', result.data.credits);
|
||||
@ -44,7 +44,7 @@ export function CreditPackages() {
|
||||
console.error('CreditPackages, failed to fetch credits:', error);
|
||||
toast.error('Failed to fetch credits');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoadingCredits(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -55,13 +55,12 @@ export function CreditPackages() {
|
||||
const handlePurchase = async (packageId: string) => {
|
||||
try {
|
||||
setLoadingPackage(packageId);
|
||||
|
||||
const result = await createCreditPaymentIntent({ packageId });
|
||||
if (result?.data?.success && result?.data?.clientSecret) {
|
||||
setPaymentDialog({
|
||||
isOpen: true,
|
||||
clientSecret: result.data.clientSecret,
|
||||
packageId,
|
||||
clientSecret: result.data.clientSecret,
|
||||
});
|
||||
} else {
|
||||
const errorMessage = result?.data?.error || 'Failed to create payment intent';
|
||||
@ -77,11 +76,11 @@ export function CreditPackages() {
|
||||
};
|
||||
|
||||
const handlePaymentSuccess = () => {
|
||||
console.log('CreditPackages, payment successful');
|
||||
console.log('CreditPackages, payment success');
|
||||
setPaymentDialog({
|
||||
isOpen: false,
|
||||
clientSecret: null,
|
||||
packageId: null,
|
||||
clientSecret: null,
|
||||
});
|
||||
|
||||
// Refresh credit balance without page reload
|
||||
@ -95,8 +94,8 @@ export function CreditPackages() {
|
||||
console.log('CreditPackages, payment cancelled');
|
||||
setPaymentDialog({
|
||||
isOpen: false,
|
||||
clientSecret: null,
|
||||
packageId: null,
|
||||
clientSecret: null,
|
||||
});
|
||||
};
|
||||
|
||||
@ -108,14 +107,14 @@ export function CreditPackages() {
|
||||
<div className="space-y-6">
|
||||
<Card className="w-full">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-lg font-medium">Credit Balance</CardTitle>
|
||||
<CardTitle className="text-lg font-semibold">Credit Balance</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<CoinsIcon className="h-4 w-4 text-muted-foreground" />
|
||||
<div className="text-2xl font-bold">
|
||||
{loading ? (
|
||||
{loadingCredits ? (
|
||||
<span className="animate-pulse">...</span>
|
||||
) : (
|
||||
credits?.toLocaleString() || 0
|
||||
@ -127,7 +126,7 @@ export function CreditPackages() {
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="text-lg font-medium">Credit Packages</h2>
|
||||
<h2 className="text-lg font-semibold">Credit Packages</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Purchase additional credits to use our services
|
||||
</p>
|
||||
@ -153,7 +152,8 @@ export function CreditPackages() {
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<div className="text-left">
|
||||
<div className="text-2xl font-semibold flex items-center gap-2">
|
||||
<CoinsIcon className="h-4 w-4 text-muted-foreground" /> {pkg.credits.toLocaleString()}
|
||||
<CoinsIcon className="h-4 w-4 text-muted-foreground" />
|
||||
{pkg.credits.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
@ -164,7 +164,7 @@ export function CreditPackages() {
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground text-left py-2 flex items-center gap-2">
|
||||
<CheckIcon className="h-4 w-4 text-green-500" />
|
||||
<CircleCheckBigIcon className="h-4 w-4 text-green-500" />
|
||||
{pkg.description}
|
||||
</div>
|
||||
|
||||
|
@ -28,15 +28,23 @@ interface StripePaymentFormProps {
|
||||
onPaymentCancel: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* StripePaymentForm is a component that displays a payment form for a credit package.
|
||||
* It uses the Stripe Elements API to display a payment form.
|
||||
*
|
||||
* @param props - The props for the StripePaymentForm component.
|
||||
* @returns The StripePaymentForm component.
|
||||
*/
|
||||
export function StripePaymentForm(props: StripePaymentFormProps) {
|
||||
const { resolvedTheme: theme } = useTheme();
|
||||
if (!process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) {
|
||||
throw new Error('NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is not set');
|
||||
}
|
||||
|
||||
const stripePromise = useMemo(() => {
|
||||
if (!process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) {
|
||||
throw new Error('NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is not set');
|
||||
}
|
||||
return loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
|
||||
return loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
|
||||
}, []);
|
||||
|
||||
const { resolvedTheme: theme } = useTheme();
|
||||
const options = useMemo(() => ({
|
||||
clientSecret: props.clientSecret,
|
||||
appearance: {
|
||||
@ -110,11 +118,7 @@ function PaymentForm({
|
||||
onPaymentSuccess();
|
||||
} else {
|
||||
console.error('PaymentForm, payment error:', result?.data?.error);
|
||||
throw new Error(
|
||||
result?.data?.error ||
|
||||
result?.serverError ||
|
||||
'Failed to confirm payment'
|
||||
);
|
||||
throw new Error( result?.data?.error || 'Failed to confirm payment' );
|
||||
}
|
||||
} else {
|
||||
console.error('PaymentForm, no payment intent found');
|
||||
@ -180,7 +184,7 @@ function PaymentForm({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Pay {formatPrice(packageInfo.price, 'USD')}
|
||||
Pay {/* {formatPrice(packageInfo.price, 'USD')} */}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
@ -3,6 +3,7 @@
|
||||
import { Routes } from '@/routes';
|
||||
import type { MenuItem } from '@/types';
|
||||
import {
|
||||
CoinsIcon,
|
||||
CreditCardIcon,
|
||||
LayoutDashboardIcon,
|
||||
Settings2Icon,
|
||||
@ -33,6 +34,11 @@ export function getAvatarLinks(): MenuItem[] {
|
||||
href: Routes.SettingsBilling,
|
||||
icon: <CreditCardIcon className="size-4 shrink-0" />,
|
||||
},
|
||||
{
|
||||
title: t('credits'),
|
||||
href: Routes.SettingsCredits,
|
||||
icon: <CoinsIcon className="size-4 shrink-0" />,
|
||||
},
|
||||
{
|
||||
title: t('settings'),
|
||||
href: Routes.SettingsProfile,
|
||||
|
Loading…
Reference in New Issue
Block a user