- {data?.map((item, index) => (
+ {subscriptionData?.map((item, index) => (
- {t('subscribe')}
+
+ {t('subscribe')}
+
diff --git a/apps/user/components/main/product-showcase/index.tsx b/apps/user/components/main/product-showcase/index.tsx
new file mode 100644
index 0000000..27046de
--- /dev/null
+++ b/apps/user/components/main/product-showcase/index.tsx
@@ -0,0 +1,17 @@
+import { getSubscription } from '@/services/user/portal';
+import { Content } from './content';
+
+export async function ProductShowcase() {
+ try {
+ const { data } = await getSubscription({
+ skipErrorHandler: true,
+ });
+ const subscriptionList = data.data?.list || [];
+
+ if (subscriptionList.length === 0) return null;
+
+ return
;
+ } catch (error) {
+ return null;
+ }
+}
diff --git a/apps/user/components/payment/stripe.tsx b/apps/user/components/payment/stripe.tsx
new file mode 100644
index 0000000..99028be
--- /dev/null
+++ b/apps/user/components/payment/stripe.tsx
@@ -0,0 +1,329 @@
+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
= ({ 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 (
+
+ );
+};
+
+const StripePayment: React.FC = ({
+ method,
+ client_secret,
+ publishable_key,
+}) => {
+ const stripePromise = useMemo(() => loadStripe(publishable_key), [publishable_key]);
+
+ return (
+
+
+
+ );
+};
+
+const CheckoutForm: React.FC> = ({
+ client_secret,
+ method,
+}) => {
+ const stripe = useStripe();
+ const [errorMessage, setErrorMessage] = useState(null);
+ const [qrCodeUrl, setQrCodeUrl] = useState(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 => {
+ 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' ? (
+
+
+
+ ) : qrCodeUrl ? (
+ <>
+
+ {t(`qrcode.${method}`)}
+ >
+ ) : (
+ errorMessage
+ );
+};
+
+export default StripePayment;
diff --git a/apps/user/components/subscribe/duration-selector.tsx b/apps/user/components/subscribe/duration-selector.tsx
index 4b2f146..666d9c6 100644
--- a/apps/user/components/subscribe/duration-selector.tsx
+++ b/apps/user/components/subscribe/duration-selector.tsx
@@ -27,11 +27,7 @@ const DurationSelector: React.FC = ({
[onChange],
);
- const DurationOption: React.FC<{ value: string; label: string; discount?: number }> = ({
- value,
- label,
- discount,
- }) => (
+ const DurationOption: React.FC<{ value: string; label: string }> = ({ value, label }) => (
);
+ // 查找当前选中项的折扣信息
+ const currentDiscount = discounts?.find((item) => item.quantity === quantity)?.discount;
+ const discountPercentage = currentDiscount ? 100 - currentDiscount : 0;
+
return (
<>
{t('purchaseDuration')}
@@ -58,10 +57,19 @@ const DurationSelector: React.FC = ({
key={item.quantity}
value={String(item.quantity)}
label={`${item.quantity} / ${t(unitTime)}`}
- discount={100 - item.discount}
/>
))}
+
+ {t('discountInfo')}:
+ {discountPercentage > 0 ? (
+
+ -{discountPercentage}% {t('discount')}
+
+ ) : (
+ --
+ )}
+
>
);
};
diff --git a/apps/user/components/subscribe/payment-methods.tsx b/apps/user/components/subscribe/payment-methods.tsx
index d440fe9..b2a5c25 100644
--- a/apps/user/components/subscribe/payment-methods.tsx
+++ b/apps/user/components/subscribe/payment-methods.tsx
@@ -1,49 +1,68 @@
'use client';
-import { getAvailablePaymentMethods } from '@/services/user/payment';
+import { getAvailablePaymentMethods } from '@/services/user/portal';
import { useQuery } from '@tanstack/react-query';
import { Label } from '@workspace/ui/components/label';
import { RadioGroup, RadioGroupItem } from '@workspace/ui/components/radio-group';
+import { cn } from '@workspace/ui/lib/utils';
import { useTranslations } from 'next-intl';
import Image from 'next/image';
import React, { memo } from 'react';
interface PaymentMethodsProps {
- value: string;
- onChange: (value: string) => void;
+ value: number;
+ onChange: (value: number) => void;
+ balance?: boolean;
}
-const PaymentMethods: React.FC = ({ value, onChange }) => {
+const PaymentMethods: React.FC = ({ value, onChange, balance = true }) => {
const t = useTranslations('subscribe');
const { data } = useQuery({
- queryKey: ['getAvailablePaymentMethods'],
+ queryKey: ['getAvailablePaymentMethods', { balance }],
queryFn: async () => {
const { data } = await getAvailablePaymentMethods();
- return data.data?.list || [];
+ const methods = data.data?.list || [];
+ if (!value && methods[0]?.id) onChange(methods[0]?.id);
+ if (balance) return methods;
+ return methods.filter((item) => item.id !== -1);
},
});
return (
<>
{t('paymentMethod')}
-
+ {
+ console.log(val);
+ onChange(Number(val));
+ }}
+ >
{data?.map((item) => (
-
-
+
+
diff --git a/apps/user/components/subscribe/purchase.tsx b/apps/user/components/subscribe/purchase.tsx
index 88e1ce7..89fad07 100644
--- a/apps/user/components/subscribe/purchase.tsx
+++ b/apps/user/components/subscribe/purchase.tsx
@@ -4,7 +4,7 @@ import CouponInput from '@/components/subscribe/coupon-input';
import DurationSelector from '@/components/subscribe/duration-selector';
import PaymentMethods from '@/components/subscribe/payment-methods';
import useGlobalStore from '@/config/use-global';
-import { checkoutOrder, preCreateOrder, purchase } from '@/services/user/order';
+import { preCreateOrder, purchase } from '@/services/user/order';
import { useQuery } from '@tanstack/react-query';
import { Button } from '@workspace/ui/components/button';
import { Card, CardContent } from '@workspace/ui/components/card';
@@ -29,7 +29,7 @@ export default function Purchase({ subscribe, setSubscribe }: Readonly
>({
quantity: 1,
subscribe_id: 0,
- payment: 'balance',
+ payment: -1,
coupon: '',
});
const [loading, startTransition] = useTransition();
@@ -69,20 +69,11 @@ export default function Purchase({ subscribe, setSubscribe }: Readonly) {
const t = useTranslations('subscribe');
@@ -34,27 +30,9 @@ export default function Recharge(props: Readonly) {
const [params, setParams] = useState({
amount: 0,
- payment: '',
+ payment: 1,
});
- const { data: paymentMethods } = useQuery({
- enabled: open,
- queryKey: ['getAvailablePaymentMethods'],
- queryFn: async () => {
- const { data } = await getAvailablePaymentMethods();
- return data.data?.list || [];
- },
- });
-
- useEffect(() => {
- if (paymentMethods?.length) {
- setParams((prev) => ({
- ...prev,
- payment: paymentMethods.find((item) => item.mark !== 'balance')?.mark as string,
- }));
- }
- }, [paymentMethods]);
-
return (
- {t('paymentMethod')}
- {
- setParams({
- ...params,
- payment: value,
- });
- }}
- >
- {paymentMethods
- ?.filter((item) => item.mark !== 'balance')
- ?.map((item) => {
- return (
-
-
-
-
- );
- })}
-
+ onChange={(value) => setParams({ ...params, payment: value })}
+ />