🐛 fix(payment): Fix payment related type definitions and update payment method references

This commit is contained in:
web@ppanel
2025-03-12 21:17:04 +07:00
parent 7fa3a57df4
commit c3138a863d
15 changed files with 88 additions and 191 deletions
+3 -3
View File
@@ -1,6 +1,7 @@
'use client';
import { Display } from '@/components/display';
import StripePayment from '@/components/payment/stripe';
import { SubscribeBilling } from '@/components/subscribe/billing';
import { SubscribeDetail } from '@/components/subscribe/detail';
import useGlobalStore from '@/config/use-global';
@@ -25,7 +26,6 @@ import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { QRCodeCanvas } from 'qrcode.react';
import { useEffect, useState } from 'react';
import StripePayment from './stripe';
export default function Page() {
const t = useTranslations('order');
@@ -106,7 +106,7 @@ export default function Page() {
<dl className='grid gap-3'>
<div className='flex items-center justify-between'>
<dt className='text-muted-foreground'>
{data?.method && <Badge>{data?.method}</Badge>}
<Badge>{data?.payment.name || data?.payment.platform}</Badge>
</dt>
</div>
</dl>
@@ -223,7 +223,7 @@ export default function Page() {
</div>
)}
{data?.status === 1 && payment?.type === 'Stripe' && (
{data?.status === 1 && payment?.type === 'stripe' && (
<div className='flex flex-col items-center gap-8 text-center'>
<h3 className='text-2xl font-bold tracking-tight'>{t('waitingForPayment')}</h3>
<p className='flex items-center text-3xl font-bold'>{countdownDisplay}</p>
@@ -1,327 +0,0 @@
import {
CardCvcElement,
CardExpiryElement,
CardNumberElement,
Elements,
useElements,
useStripe,
} from '@stripe/react-stripe-js';
import {
loadStripe,
PaymentIntentResult,
StripeCardNumberElementOptions,
StripeElementStyle,
} from '@stripe/stripe-js';
import { Button } from '@workspace/ui/components/button';
import { Input } from '@workspace/ui/components/input';
import { Label } from '@workspace/ui/components/label';
import { CheckCircle } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useTheme } from 'next-themes';
import { QRCodeCanvas } from 'qrcode.react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
interface StripePaymentProps {
method: string;
client_secret: string;
publishable_key: string;
}
interface CardPaymentFormProps {
clientSecret: string;
onError: (message: string) => void;
}
const CardPaymentForm: React.FC<CardPaymentFormProps> = ({ clientSecret, onError }) => {
const stripe = useStripe();
const { theme, systemTheme } = useTheme();
const elements = useElements();
const [processing, setProcessing] = useState(false);
const [succeeded, setSucceeded] = useState(false);
const [errors, setErrors] = useState<{
cardNumber?: string;
cardExpiry?: string;
cardCvc?: string;
name?: string;
}>({});
const [cardholderName, setCardholderName] = useState('');
const t = useTranslations('payment.stripe.card');
const currentTheme = theme === 'system' ? systemTheme : theme;
const elementStyle: StripeElementStyle = {
base: {
'fontSize': '16px',
'color': currentTheme === 'dark' ? '#fff' : '#000',
'::placeholder': {
color: '#aab7c4',
},
},
invalid: {
color: '#EF4444',
iconColor: '#EF4444',
},
};
const elementOptions: StripeCardNumberElementOptions = {
style: elementStyle,
showIcon: true,
};
const handleChange = (event: any, field: keyof typeof errors) => {
if (event.error) {
setErrors((prev) => ({ ...prev, [field]: event.error.message }));
} else {
setErrors((prev) => ({ ...prev, [field]: undefined }));
}
};
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
if (!stripe || !elements) {
onError(t('loading'));
return;
}
if (!cardholderName.trim()) {
setErrors((prev) => ({ ...prev, name: t('name_required') }));
return;
}
setProcessing(true);
const cardNumber = elements.getElement(CardNumberElement);
const cardExpiry = elements.getElement(CardExpiryElement);
const cardCvc = elements.getElement(CardCvcElement);
if (!cardNumber || !cardExpiry || !cardCvc) {
onError(t('element_error'));
setProcessing(false);
return;
}
const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: cardNumber,
billing_details: {
name: cardholderName,
},
},
});
if (error) {
onError(error.message || t('payment_failed'));
setProcessing(false);
} else if (paymentIntent && paymentIntent.status === 'succeeded') {
setSucceeded(true);
setProcessing(false);
} else {
onError(t('processing'));
setProcessing(false);
}
};
return (
<form onSubmit={handleSubmit}>
{succeeded ? (
<div className='py-6 text-center'>
<div className='mb-4 flex justify-center'>
<CheckCircle className='h-12 w-12 text-green-500' />
</div>
<p className='text-xl font-medium'>{t('success_title')}</p>
<p className='text-muted-foreground mt-2'>{t('success_message')}</p>
</div>
) : (
<>
<div className='space-y-4'>
{/* Cardholder Name */}
<div className='space-y-1'>
<Label htmlFor='cardholderName' className='text-sm font-medium'>
{t('card_name')}
</Label>
<Input
id='cardholderName'
type='text'
value={cardholderName}
onChange={(e) => setCardholderName(e.target.value)}
placeholder={t('name_placeholder')}
className={errors.name ? 'border-red-500' : ''}
/>
{errors.name && <p className='text-xs text-red-500'>{errors.name}</p>}
</div>
{/* Card Number */}
<div className='space-y-1'>
<Label htmlFor='cardNumber' className='text-sm font-medium'>
{t('card_number')}
</Label>
<div className='relative'>
<div
className={`focus-within:border-primary focus-within:ring-primary rounded-md border p-3 focus-within:ring-1 ${errors.cardNumber ? 'border-red-500' : ''}`}
>
<CardNumberElement
id='cardNumber'
options={elementOptions}
onChange={(e) => handleChange(e, 'cardNumber')}
/>
</div>
</div>
{errors.cardNumber && <p className='text-xs text-red-500'>{errors.cardNumber}</p>}
</div>
<div className='grid grid-cols-2 gap-4'>
{/* Expiry Date */}
<div className='space-y-1'>
<Label htmlFor='cardExpiry' className='text-sm font-medium'>
{t('expiry_date')}
</Label>
<div
className={`focus-within:border-primary focus-within:ring-primary rounded-md border p-3 focus-within:ring-1 ${errors.cardExpiry ? 'border-red-500' : ''}`}
>
<CardExpiryElement
id='cardExpiry'
options={{ style: elementStyle }}
onChange={(e) => handleChange(e, 'cardExpiry')}
/>
</div>
{errors.cardExpiry && <p className='text-xs text-red-500'>{errors.cardExpiry}</p>}
</div>
{/* Security Code */}
<div className='space-y-1'>
<Label htmlFor='cardCvc' className='text-sm font-medium'>
{t('security_code')}
</Label>
<div
className={`focus-within:border-primary focus-within:ring-primary rounded-md border p-3 focus-within:ring-1 ${errors.cardCvc ? 'border-red-500' : ''}`}
>
<CardCvcElement
id='cardCvc'
options={{ style: elementStyle }}
onChange={(e) => handleChange(e, 'cardCvc')}
/>
</div>
{errors.cardCvc && <p className='text-xs text-red-500'>{errors.cardCvc}</p>}
</div>
</div>
</div>
<div className='mt-6 flex flex-col space-y-4'>
<Button type='submit' disabled={processing || !stripe || !elements} className='w-full'>
{processing ? t('processing_button') : t('pay_button')}
</Button>
<p className='text-muted-foreground text-center text-xs'>{t('secure_notice')}</p>
</div>
</>
)}
</form>
);
};
const StripePayment: React.FC<StripePaymentProps> = ({
method,
client_secret,
publishable_key,
}) => {
const stripePromise = useMemo(() => loadStripe(publishable_key), [publishable_key]);
return (
<Elements stripe={stripePromise}>
<CheckoutForm method={method} client_secret={client_secret} />
</Elements>
);
};
const CheckoutForm: React.FC<Omit<StripePaymentProps, 'publishable_key'>> = ({
client_secret,
method,
}) => {
const stripe = useStripe();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [qrCodeUrl, setQrCodeUrl] = useState<string | null>(null);
const [isSubmitted, setIsSubmitted] = useState(false);
const t = useTranslations('payment.stripe');
const handleError = useCallback((message: string) => {
setErrorMessage(message);
setIsSubmitted(false);
}, []);
const confirmPayment = useCallback(async (): Promise<PaymentIntentResult | null> => {
if (!stripe) {
handleError(t('card.loading'));
return null;
}
if (method === 'alipay') {
return await stripe.confirmAlipayPayment(
client_secret,
{ return_url: window.location.href },
{ handleActions: false },
);
}
if (method === 'wechat_pay') {
return await stripe.confirmWechatPayPayment(
client_secret,
{
payment_method_options: { wechat_pay: { client: 'web' } },
},
{ handleActions: false },
);
}
return null;
}, [client_secret, method, stripe, handleError, t]);
const autoSubmit = useCallback(async () => {
if (isSubmitted || method === 'card') return;
setIsSubmitted(true);
try {
const result = await confirmPayment();
if (!result) return;
const { error, paymentIntent } = result;
if (error) return handleError(error.message!);
if (paymentIntent?.status === 'requires_action') {
const nextAction = paymentIntent.next_action as any;
const qrUrl =
method === 'alipay'
? nextAction?.alipay_handle_redirect?.url
: nextAction?.wechat_pay_display_qr_code?.image_url_svg;
setQrCodeUrl(qrUrl || null);
}
} catch (error) {
handleError(t('error'));
}
}, [confirmPayment, isSubmitted, handleError, method, t]);
useEffect(() => {
autoSubmit();
}, [autoSubmit]);
return method === 'card' ? (
<div className='min-w-80 text-left'>
<CardPaymentForm clientSecret={client_secret} onError={handleError} />
</div>
) : qrCodeUrl ? (
<>
<QRCodeCanvas
value={qrCodeUrl}
size={208}
imageSettings={{
src: `/payment/${method}.svg`,
width: 24,
height: 24,
excavate: true,
}}
/>
<p className='text-muted-foreground mt-4 text-center'>{t(`qrcode.${method}`)}</p>
</>
) : (
errorMessage
);
};
export default StripePayment;
+12 -12
View File
@@ -1,6 +1,7 @@
'use client';
import { Display } from '@/components/display';
import StripePayment from '@/components/payment/stripe';
import { SubscribeBilling } from '@/components/subscribe/billing';
import { SubscribeDetail } from '@/components/subscribe/detail';
import useGlobalStore from '@/config/use-global';
@@ -25,7 +26,6 @@ import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { QRCodeCanvas } from 'qrcode.react';
import { useEffect, useState } from 'react';
import StripePayment from './stripe';
export default function Page() {
const t = useTranslations('order');
@@ -111,7 +111,7 @@ export default function Page() {
<dl className='grid gap-3'>
<div className='flex items-center justify-between'>
<dt className='text-muted-foreground'>
{data?.payment && <Badge>{t(`methods.${data?.payment}`)}</Badge>}
<Badge>{data?.payment.name || data?.payment.platform}</Badge>
</dt>
</div>
</dl>
@@ -228,19 +228,19 @@ export default function Page() {
</div>
)}
{data?.status === 1 && payment?.type === 'stripe' && (
{data?.status === 1 && payment?.type === 'Stripe' && (
<div className='flex flex-col items-center gap-8 text-center'>
<h3 className='text-2xl font-bold tracking-tight'>{t('scanToPay')}</h3>
<h3 className='text-2xl font-bold tracking-tight'>{t('waitingForPayment')}</h3>
<p className='flex items-center text-3xl font-bold'>{countdownDisplay}</p>
{payment.stripe && <StripePayment {...payment.stripe} />}
<div className='flex gap-4'>
<Button asChild>
<Link href='/subscribe'>{t('productList')}</Link>
</Button>
<Button asChild variant='outline'>
<Link href='/order'>{t('orderList')}</Link>
</Button>
</div>
{/* <div className='flex gap-4'>
<Button asChild>
<Link href='/subscribe'>{t('productList')}</Link>
</Button>
<Button asChild variant='outline'>
<Link href='/order'>{t('orderList')}</Link>
</Button>
</div> */}
</div>
)}
@@ -1,109 +0,0 @@
import { Elements, useStripe } from '@stripe/react-stripe-js';
import { loadStripe, PaymentIntentResult } from '@stripe/stripe-js';
import { QRCodeCanvas } from 'qrcode.react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
interface StripePaymentProps {
method: string;
client_secret: string;
publishable_key: string;
}
const StripePayment: React.FC<StripePaymentProps> = ({
method,
client_secret,
publishable_key,
}) => {
const stripePromise = useMemo(() => loadStripe(publishable_key), [publishable_key]);
return (
<Elements stripe={stripePromise}>
<CheckoutForm method={method} client_secret={client_secret} />
</Elements>
);
};
const CheckoutForm: React.FC<Omit<StripePaymentProps, 'publishable_key'>> = ({
client_secret,
method,
}) => {
const stripe = useStripe();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [qrCodeUrl, setQrCodeUrl] = useState<string | null>(null);
const [isSubmitted, setIsSubmitted] = useState(false);
const handleError = useCallback((message: string) => {
setErrorMessage(message);
setIsSubmitted(false);
}, []);
const confirmPayment = useCallback(async (): Promise<PaymentIntentResult | null> => {
if (!stripe) {
handleError('Stripe.js is not loaded.');
return null;
}
if (method === 'alipay') {
return await stripe.confirmAlipayPayment(
client_secret,
{ return_url: window.location.href },
{ handleActions: false },
);
}
return await stripe.confirmWechatPayPayment(
client_secret,
{
payment_method_options: { wechat_pay: { client: 'web' } },
},
{ handleActions: false },
);
}, [client_secret, method, stripe, handleError]);
const autoSubmit = useCallback(async () => {
if (isSubmitted) return;
setIsSubmitted(true);
try {
const result = await confirmPayment();
if (!result) return;
const { error, paymentIntent } = result;
if (error) return handleError(error.message!);
if (paymentIntent?.status === 'requires_action') {
const nextAction = paymentIntent.next_action as any;
const qrUrl =
method === 'alipay'
? nextAction?.alipay_handle_redirect?.url
: nextAction?.wechat_pay_display_qr_code?.image_url_svg;
setQrCodeUrl(qrUrl || null);
}
} catch (error) {
handleError('An unexpected error occurred');
}
}, [confirmPayment, isSubmitted, handleError, method]);
useEffect(() => {
autoSubmit();
}, [autoSubmit]);
return qrCodeUrl ? (
<QRCodeCanvas
value={qrCodeUrl}
size={208}
imageSettings={{
src: `/payment/${method}.svg`,
width: 24,
height: 24,
excavate: true,
}}
/>
) : (
errorMessage
);
};
export default StripePayment;