diff --git a/CHANGELOG.md b/CHANGELOG.md index 188dd07..c4ec8da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,49 +1,43 @@ + # Changelog # [1.0.0-beta.26](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.25...v1.0.0-beta.26) (2025-03-02) - ### 🐛 Bug Fixes -* **icon**: Comment out unused icon collection imports ([f17bf8d](https://github.com/perfect-panel/ppanel-web/commit/f17bf8d)) +- **icon**: Comment out unused icon collection imports ([f17bf8d](https://github.com/perfect-panel/ppanel-web/commit/f17bf8d)) # [1.0.0-beta.25](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.24...v1.0.0-beta.25) (2025-03-01) - ### ✨ Features -* **auth**: Add privacy policy link to the footer ([8e16ef1](https://github.com/perfect-panel/ppanel-web/commit/8e16ef1)) - +- **auth**: Add privacy policy link to the footer ([8e16ef1](https://github.com/perfect-panel/ppanel-web/commit/8e16ef1)) ### 🐛 Bug Fixes -* **dashboard**: Display subscription creation date in user dashboard ([d0e6df0](https://github.com/perfect-panel/ppanel-web/commit/d0e6df0)) -* **request**: Add error code 40005 to trigger logout ([71bf002](https://github.com/perfect-panel/ppanel-web/commit/71bf002)) -* **subscribe**: Update payment return URL ([2b80496](https://github.com/perfect-panel/ppanel-web/commit/2b80496)) +- **dashboard**: Display subscription creation date in user dashboard ([d0e6df0](https://github.com/perfect-panel/ppanel-web/commit/d0e6df0)) +- **request**: Add error code 40005 to trigger logout ([71bf002](https://github.com/perfect-panel/ppanel-web/commit/71bf002)) +- **subscribe**: Update payment return URL ([2b80496](https://github.com/perfect-panel/ppanel-web/commit/2b80496)) # [1.0.0-beta.24](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.23...v1.0.0-beta.24) (2025-02-27) - ### ♻ Code Refactoring -* **ui**: Optimize document display ([2ca2992](https://github.com/perfect-panel/ppanel-web/commit/2ca2992)) -* Reduce code complexity and improve readability ([e11f18c](https://github.com/perfect-panel/ppanel-web/commit/e11f18c)) - +- **ui**: Optimize document display ([2ca2992](https://github.com/perfect-panel/ppanel-web/commit/2ca2992)) +- Reduce code complexity and improve readability ([e11f18c](https://github.com/perfect-panel/ppanel-web/commit/e11f18c)) ### ✨ Features -* **loading**: Add loading components and integrate them in Providers ([d5847fa](https://github.com/perfect-panel/ppanel-web/commit/d5847fa)) - +- **loading**: Add loading components and integrate them in Providers ([d5847fa](https://github.com/perfect-panel/ppanel-web/commit/d5847fa)) ### 🎫 Chores -* **merge**: Add advertising module and device settings ([0130e02](https://github.com/perfect-panel/ppanel-web/commit/0130e02)) - +- **merge**: Add advertising module and device settings ([0130e02](https://github.com/perfect-panel/ppanel-web/commit/0130e02)) ### 🐛 Bug Fixes -* **locales**: Order recharge related fields ([35210fe](https://github.com/perfect-panel/ppanel-web/commit/35210fe)) +- **locales**: Order recharge related fields ([35210fe](https://github.com/perfect-panel/ppanel-web/commit/35210fe)) diff --git a/apps/admin/app/dashboard/user/[id]/user-subscription/subscription-detail.tsx b/apps/admin/app/dashboard/user/[id]/user-subscription/subscription-detail.tsx index b893d21..012c057 100644 --- a/apps/admin/app/dashboard/user/[id]/user-subscription/subscription-detail.tsx +++ b/apps/admin/app/dashboard/user/[id]/user-subscription/subscription-detail.tsx @@ -115,7 +115,7 @@ export function SubscriptionDetail({ user_id: userId, subscribe_id: subscriptionId, ...pagination, - }); + } as API.GetUserSubscribeTrafficLogsParams); return { list: data.data?.list || [], total: data.data?.total || 0, diff --git a/apps/admin/services/admin/typings.d.ts b/apps/admin/services/admin/typings.d.ts index 565e79d..da8b2f7 100644 --- a/apps/admin/services/admin/typings.d.ts +++ b/apps/admin/services/admin/typings.d.ts @@ -120,17 +120,6 @@ declare namespace API { ids: number[]; }; - type CheckoutOrderRequest = { - orderNo: string; - returnUrl?: string; - }; - - type CheckoutOrderResponse = { - type: string; - checkout_url?: string; - stripe?: StripePayment; - }; - type CloseOrderRequest = { orderNo: string; }; @@ -488,7 +477,7 @@ declare namespace API { }; type GetAvailablePaymentMethodsResponse = { - list: PaymentConfig[]; + list: PaymenMethod[]; }; type GetCouponListParams = { @@ -797,6 +786,8 @@ declare namespace API { size: number; user_id: number; subscribe_id: number; + start_time: number; + end_time: number; }; type GetUserSubscribeTrafficLogsRequest = { @@ -804,6 +795,8 @@ declare namespace API { size: number; user_id: number; subscribe_id: number; + start_time: number; + end_time: number; }; type GetUserSubscribeTrafficLogsResponse = { @@ -931,6 +924,16 @@ declare namespace API { list?: OrdersStatistics[]; }; + type PaymenMethod = { + id: number; + name: string; + mark: string; + icon: string; + fee_mode: number; + fee_percent: number; + fee_amount: number; + }; + type PaymentConfig = { id: number; name: string; @@ -1029,6 +1032,21 @@ declare namespace API { list: OrderDetail[]; }; + type QuerySubscribeGroupListResponse = { + list: SubscribeGroup[]; + total: number; + }; + + type QuerySubscribeListResponse = { + list: Subscribe[]; + total: number; + }; + + type QueryUserAffiliateListResponse = { + list: UserAffiliate[]; + total: number; + }; + type RechargeOrderRequest = { amount: number; payment: string; diff --git a/apps/admin/services/common/common.ts b/apps/admin/services/common/common.ts index 685ef9c..11fa9a5 100644 --- a/apps/admin/services/common/common.ts +++ b/apps/admin/services/common/common.ts @@ -58,17 +58,6 @@ export async function getStat(options?: { [key: string]: any }) { }); } -/** Get Subscription GET /v1/common/site/subscribe */ -export async function getSubscription(options?: { [key: string]: any }) { - return request( - '/v1/common/site/subscribe', - { - method: 'GET', - ...(options || {}), - }, - ); -} - /** Get Tos Content GET /v1/common/site/tos */ export async function getTos(options?: { [key: string]: any }) { return request('/v1/common/site/tos', { diff --git a/apps/admin/services/common/typings.d.ts b/apps/admin/services/common/typings.d.ts index 566eeb9..878dfce 100644 --- a/apps/admin/services/common/typings.d.ts +++ b/apps/admin/services/common/typings.d.ts @@ -90,17 +90,6 @@ declare namespace API { enabled: boolean; }; - type CheckoutOrderRequest = { - orderNo: string; - returnUrl?: string; - }; - - type CheckoutOrderResponse = { - type: string; - checkout_url?: string; - stripe?: StripePayment; - }; - type CheckUserParams = { email: string; }; @@ -175,7 +164,7 @@ declare namespace API { }; type GetAvailablePaymentMethodsResponse = { - list: PaymentConfig[]; + list: PaymenMethod[]; }; type GetGlobalConfigResponse = { @@ -196,14 +185,24 @@ declare namespace API { protocol: string[]; }; - type GetSubscriptionResponse = { - list: Subscribe[]; - }; - type GetTosResponse = { tos_content: string; }; + type GetUserSubscribeTrafficLogsRequest = { + page: number; + size: number; + user_id: number; + subscribe_id: number; + start_time: number; + end_time: number; + }; + + type GetUserSubscribeTrafficLogsResponse = { + list: TrafficLog[]; + total: number; + }; + type GoogleLoginCallbackRequest = { code: string; state: string; @@ -329,6 +328,16 @@ declare namespace API { updated_at: number; }; + type PaymenMethod = { + id: number; + name: string; + mark: string; + icon: string; + fee_mode: number; + fee_percent: number; + fee_amount: number; + }; + type PaymentConfig = { id: number; name: string; @@ -417,6 +426,21 @@ declare namespace API { list: OrderDetail[]; }; + type QuerySubscribeGroupListResponse = { + list: SubscribeGroup[]; + total: number; + }; + + type QuerySubscribeListResponse = { + list: Subscribe[]; + total: number; + }; + + type QueryUserAffiliateListResponse = { + list: UserAffiliate[]; + total: number; + }; + type RechargeOrderRequest = { amount: number; payment: string; diff --git a/apps/user/app/(main)/(user)/payment/page.tsx b/apps/user/app/(main)/(user)/payment/page.tsx index 8449812..4226f1d 100644 --- a/apps/user/app/(main)/(user)/payment/page.tsx +++ b/apps/user/app/(main)/(user)/payment/page.tsx @@ -4,7 +4,8 @@ import { Display } from '@/components/display'; import { SubscribeBilling } from '@/components/subscribe/billing'; import { SubscribeDetail } from '@/components/subscribe/detail'; import useGlobalStore from '@/config/use-global'; -import { checkoutOrder, queryOrderDetail } from '@/services/user/order'; +import { queryOrderDetail } from '@/services/user/order'; +import { purchaseCheckout } from '@/services/user/portal'; import { useQuery } from '@tanstack/react-query'; import { Badge } from '@workspace/ui/components/badge'; import { Button } from '@workspace/ui/components/button'; @@ -50,7 +51,10 @@ export default function Page() { enabled: !!orderNo && data?.status === 1, queryKey: ['checkoutOrder', orderNo], queryFn: async () => { - const { data } = await checkoutOrder({ orderNo: orderNo!, returnUrl: window.location.href }); + const { data } = await purchaseCheckout({ + orderNo: orderNo!, + returnUrl: window.location.href, + }); return data?.data; }, }); diff --git a/apps/user/app/(main)/page.tsx b/apps/user/app/(main)/page.tsx index a62603b..3d540bb 100644 --- a/apps/user/app/(main)/page.tsx +++ b/apps/user/app/(main)/page.tsx @@ -1,6 +1,6 @@ import { GlobalMap } from '@/components/main/global-map'; import { Hero } from '@/components/main/hero'; -import { ProductShowcase } from '@/components/main/product-showcase'; +import { ProductShowcase } from '@/components/main/product-showcase/index'; import { Stats } from '@/components/main/stats'; export default function Home() { diff --git a/apps/user/app/(main)/purchasing/content.tsx b/apps/user/app/(main)/purchasing/content.tsx new file mode 100644 index 0000000..39a44a5 --- /dev/null +++ b/apps/user/app/(main)/purchasing/content.tsx @@ -0,0 +1,221 @@ +'use client'; + +import { SubscribeBilling } from '@/components/subscribe/billing'; +import CouponInput from '@/components/subscribe/coupon-input'; +import { SubscribeDetail } from '@/components/subscribe/detail'; +import DurationSelector from '@/components/subscribe/duration-selector'; +import PaymentMethods from '@/components/subscribe/payment-methods'; +import useGlobalStore from '@/config/use-global'; +import { prePurchaseOrder, purchase } from '@/services/user/portal'; +import { useQuery } from '@tanstack/react-query'; +import { Button } from '@workspace/ui/components/button'; +import { Card, CardContent, CardHeader } from '@workspace/ui/components/card'; +import { Separator } from '@workspace/ui/components/separator'; +import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input'; +import { cn } from '@workspace/ui/lib/utils'; +import { LoaderCircle } from 'lucide-react'; +import { useTranslations } from 'next-intl'; +import { useRouter } from 'next/navigation'; +import { useCallback, useEffect, useState, useTransition } from 'react'; + +export default function Content({ subscription }: { subscription?: API.Subscribe }) { + const t = useTranslations('subscribe'); + const { common } = useGlobalStore(); + const router = useRouter(); + const [params, setParams] = useState({ + quantity: 1, + subscribe_id: 0, + payment: '', + coupon: '', + platform: 'email', + identifier: '', + password: '', + }); + const [loading, startTransition] = useTransition(); + const [isEmailValid, setIsEmailValid] = useState({ + valid: false, + message: '', + }); + + const { data: order } = useQuery({ + enabled: !!subscription?.id && !!params.payment, + queryKey: ['preCreateOrder', params.coupon, params.quantity, params.payment], + queryFn: async () => { + const { data } = await prePurchaseOrder({ + ...params, + subscribe_id: subscription?.id as number, + } as API.PrePurchaseOrderRequest); + return data.data; + }, + }); + + useEffect(() => { + if (subscription) { + setParams((prev) => ({ + ...prev, + quantity: 1, + subscribe_id: subscription?.id, + })); + } + }, [subscription]); + + const handleChange = useCallback((field: keyof typeof params, value: string | number) => { + setParams((prev) => ({ + ...prev, + [field]: value, + })); + }, []); + + const handleSubmit = useCallback(async () => { + startTransition(async () => { + try { + const { data } = await purchase(params); + console.log(data); + const { order_no, check_url, type } = data.data!; + if (order_no) { + if (type === 'link') { + window.location.href = check_url!; + } + router.push(`/purchasing/order?order_no=${order_no}`); + } + } catch (error) { + console.log(error); + } + }); + }, [params, router, subscription?.id]); + + if (!subscription) { + return
{t('subscriptionNotFound')}
; + } + + return ( +
+
+ + 输入要用于 {common.site.site_name} 账户的电子邮件地址 + +
+ { + const email = value as string; + setParams((prev) => ({ + ...prev, + identifier: email, + })); + const reg = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!reg.test(email)) { + setIsEmailValid({ + valid: false, + message: '请输入有效的邮箱地址', + }); + } else if (common.auth.email.enable_domain_suffix) { + const domain = email.split('@')[1]; + const isValid = common.auth.email?.domain_suffix_list + .split('\n') + .includes(domain || ''); + if (!isValid) { + setIsEmailValid({ + valid: false, + message: '邮箱域名不在白名单中', + }); + return; + } + } else { + setIsEmailValid({ + valid: true, + message: '', + }); + } + }} + required + /> +

+ {isEmailValid.message || '请填写您的电子邮件地址。'} +

+
+ {params.identifier && isEmailValid.valid && ( +
+ handleChange('password', value)} + /> +

+ 如果您不填写密码,我们将会自动生成密码并发送到您的邮箱。 +

+
+ )} + {/*
+ +
*/} +
+
+ + +

{subscription.name}

+

{subscription.description}

+ + + +
+
+
+ +
+ + +
+ handleChange('quantity', value)} + /> + handleChange('coupon', value)} + /> + handleChange('payment', value)} + /> +
+
+
+ + +
+
+ ); +} diff --git a/apps/user/app/(main)/purchasing/order/page.tsx b/apps/user/app/(main)/purchasing/order/page.tsx new file mode 100644 index 0000000..39c2d7c --- /dev/null +++ b/apps/user/app/(main)/purchasing/order/page.tsx @@ -0,0 +1,266 @@ +'use client'; + +import { Display } from '@/components/display'; +import { SubscribeBilling } from '@/components/subscribe/billing'; +import { SubscribeDetail } from '@/components/subscribe/detail'; +import useGlobalStore from '@/config/use-global'; +import { purchaseCheckout, queryPurchaseOrder } from '@/services/user/portal'; +import { setAuthorization } from '@/utils/common'; +import { useQuery } from '@tanstack/react-query'; +import { Badge } from '@workspace/ui/components/badge'; +import { Button } from '@workspace/ui/components/button'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@workspace/ui/components/card'; +import { Separator } from '@workspace/ui/components/separator'; +import { Icon } from '@workspace/ui/custom-components/icon'; +import { formatDate } from '@workspace/ui/utils'; +import { useCountDown } from 'ahooks'; +import { addMinutes, format } from 'date-fns'; +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'); + const { getUserInfo } = useGlobalStore(); + const [orderNo, setOrderNo] = useState(); + const [enabled, setEnabled] = useState(false); + + const { data } = useQuery({ + enabled: enabled, + queryKey: ['queryPurchaseOrder', orderNo], + queryFn: async () => { + const { data } = await queryPurchaseOrder({ order_no: orderNo! }); + if (data?.data?.status !== 1) { + setEnabled(false); + if (data?.data?.token) { + setAuthorization(data?.data?.token); + await new Promise((resolve) => setTimeout(resolve, 100)); + await getUserInfo(); + } + } + return data?.data; + }, + refetchInterval: 3000, + }); + + const { data: payment } = useQuery({ + enabled: !!orderNo && data?.status === 1, + queryKey: ['purchaseCheckout', orderNo], + queryFn: async () => { + const { data } = await purchaseCheckout({ + orderNo: orderNo!, + returnUrl: window.location.href, + }); + if (data.data?.type === 'url' && data.data?.checkout_url) { + window.location.href = data.data?.checkout_url; + } + return data?.data; + }, + }); + + useEffect(() => { + const searchParams = new URLSearchParams(location.search); + if (searchParams.get('order_no')) { + setOrderNo(searchParams.get('order_no')!); + setEnabled(true); + } + }, []); + + const [countDown, formattedRes] = useCountDown({ + targetDate: data && format(addMinutes(data?.created_at, 15), "yyyy-MM-dd'T'HH:mm:ss.SSSxxx"), + }); + + const { hours, minutes, seconds } = formattedRes; + + const countdownDisplay = + countDown > 0 ? ( + <> + {hours.toString().length === 1 ? `0${hours}` : hours} :{' '} + {minutes.toString().length === 1 ? `0${minutes}` : minutes} :{' '} + {seconds.toString().length === 1 ? `0${seconds}` : seconds} + + ) : ( + <>{t('timeExpired')} + ); + + return ( +
+
+ + +
+ + {t('orderNumber')} + {data?.order_no} + + + {t('createdAt')}: {formatDate(data?.created_at)} + +
+
+ +
{t('paymentMethod')}
+
+
+
+ {data?.payment && {t(`methods.${data?.payment}`)}} +
+
+
+ + + {data?.status && [1, 2].includes(data.status) && ( + + )} + {data?.status === 3 && ( + <> +
{t('resetTraffic')}
+
    +
  • + + {t('resetPrice')} + + + + +
  • +
+ + )} + + {data?.status === 4 && ( + <> +
{t('balanceRecharge')}
+
    +
  • + + {t('rechargeAmount')} + + + + +
  • +
+ + )} + + +
+
+ + + {data?.status && [2, 5].includes(data?.status) && ( +
+

{t('paymentSuccess')}

+ +
+ + +
+
+ )} + {data?.status === 1 && payment?.type === 'url' && ( +
+

{t('waitingForPayment')}

+

{countdownDisplay}

+ +
+ + +
+
+ )} + + {data?.status === 1 && payment?.type === 'qr' && ( +
+

{t('scanToPay')}

+

{countdownDisplay}

+ +
+ + +
+
+ )} + + {data?.status === 1 && payment?.type === 'stripe' && ( +
+

{t('scanToPay')}

+

{countdownDisplay}

+ {payment.stripe && } +
+ + +
+
+ )} + + {data?.status && [3, 4].includes(data?.status) && ( +
+

{t('orderClosed')}

+ +
+ + +
+
+ )} +
+
+
+
+ ); +} diff --git a/apps/user/app/(main)/purchasing/order/stripe.tsx b/apps/user/app/(main)/purchasing/order/stripe.tsx new file mode 100644 index 0000000..62f5741 --- /dev/null +++ b/apps/user/app/(main)/purchasing/order/stripe.tsx @@ -0,0 +1,109 @@ +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 = ({ + 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 handleError = useCallback((message: string) => { + setErrorMessage(message); + setIsSubmitted(false); + }, []); + + const confirmPayment = useCallback(async (): Promise => { + 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 ? ( + + ) : ( + errorMessage + ); +}; + +export default StripePayment; diff --git a/apps/user/app/(main)/purchasing/page.tsx b/apps/user/app/(main)/purchasing/page.tsx new file mode 100644 index 0000000..dd0e9be --- /dev/null +++ b/apps/user/app/(main)/purchasing/page.tsx @@ -0,0 +1,23 @@ +import { getSubscription } from '@/services/user/portal'; +import Content from './content'; + +export default async function Page({ + searchParams, +}: { + searchParams: Promise<{ + id: string; + }>; +}) { + const { id } = await searchParams; + const { data } = await getSubscription({ + skipErrorHandler: true, + }); + const subscriptionList = data.data?.list || []; + const subscription = subscriptionList.find((item) => item.id === Number(id)); + + return ( +
+ +
+ ); +} diff --git a/apps/user/app/auth/page.tsx b/apps/user/app/auth/page.tsx index 6fa4e43..5864082 100644 --- a/apps/user/app/auth/page.tsx +++ b/apps/user/app/auth/page.tsx @@ -1,13 +1,11 @@ 'use client'; +import { OAuthMethods } from '@/components/auth/oauth-methods'; import LanguageSwitch from '@/components/language-switch'; import ThemeSwitch from '@/components/theme-switch'; import useGlobalStore from '@/config/use-global'; -import { oAuthLogin } from '@/services/common/oauth'; import { DotLottieReact } from '@lottiefiles/dotlottie-react'; -import { Button } from '@workspace/ui/components/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs'; -import { Icon } from '@workspace/ui/custom-components/icon'; import LoginLottie from '@workspace/ui/lotties/login.json'; import { useTranslations } from 'next-intl'; import Image from 'next/legacy/image'; @@ -15,18 +13,10 @@ import Link from 'next/link'; import EmailAuthForm from './email/auth-form'; import PhoneAuthForm from './phone/auth-form'; -const icons = { - apple: 'uil:apple', - google: 'logos:google-icon', - facebook: 'logos:facebook', - github: 'uil:github', - telegram: 'logos:telegram', -}; - export default function Page() { const t = useTranslations('auth'); const { common } = useGlobalStore(); - const { site, auth, oauth_methods } = common; + const { site, auth } = common; const AUTH_METHODS = [ { @@ -41,10 +31,6 @@ export default function Page() { }, ].filter((method) => method.enabled); - const OAUTH_METHODS = oauth_methods?.filter( - (method) => !['mobile', 'email', 'device'].includes(method), - ); - return (
@@ -95,38 +81,7 @@ export default function Page() { )}
- {OAUTH_METHODS?.length > 0 && ( - <> -
- - Or continue with - -
-
- {OAUTH_METHODS?.map((method: any) => { - return ( - - ); - })} -
- - )} +
diff --git a/apps/user/components/auth/oauth-methods.tsx b/apps/user/components/auth/oauth-methods.tsx new file mode 100644 index 0000000..c805e65 --- /dev/null +++ b/apps/user/components/auth/oauth-methods.tsx @@ -0,0 +1,56 @@ +'use client'; + +import useGlobalStore from '@/config/use-global'; +import { oAuthLogin } from '@/services/common/oauth'; +import { Button } from '@workspace/ui/components/button'; +import { Icon } from '@workspace/ui/custom-components/icon'; + +const icons = { + apple: 'uil:apple', + google: 'logos:google-icon', + facebook: 'logos:facebook', + github: 'uil:github', + telegram: 'logos:telegram', +}; + +export function OAuthMethods() { + const { common } = useGlobalStore(); + const { oauth_methods } = common; + const OAUTH_METHODS = oauth_methods?.filter( + (method) => !['mobile', 'email', 'device'].includes(method), + ); + return ( + OAUTH_METHODS?.length > 0 && ( + <> +
+ + Or continue with + +
+
+ {OAUTH_METHODS?.map((method: any) => { + return ( + + ); + })} +
+ + ) + ); +} diff --git a/apps/user/components/main/product-showcase.tsx b/apps/user/components/main/product-showcase/content.tsx similarity index 90% rename from apps/user/components/main/product-showcase.tsx rename to apps/user/components/main/product-showcase/content.tsx index 4083e63..8f859a7 100644 --- a/apps/user/components/main/product-showcase.tsx +++ b/apps/user/components/main/product-showcase/content.tsx @@ -2,8 +2,7 @@ import { Display } from '@/components/display'; import { SubscribeDetail } from '@/components/subscribe/detail'; -import { getSubscription } from '@/services/common/common'; -import { useQuery } from '@tanstack/react-query'; +import useGlobalStore from '@/config/use-global'; import { Button } from '@workspace/ui/components/button'; import { Card, CardContent, CardFooter, CardHeader } from '@workspace/ui/components/card'; import { Separator } from '@workspace/ui/components/separator'; @@ -14,19 +13,15 @@ import { useTranslations } from 'next-intl'; import Link from 'next/link'; import { Key, ReactNode } from 'react'; -export function ProductShowcase() { +interface ProductShowcaseProps { + subscriptionData: API.Subscribe[]; +} + +export function Content({ subscriptionData }: ProductShowcaseProps) { const t = useTranslations('index'); - const { data } = useQuery({ - queryKey: ['getSubscription'], - queryFn: async () => { - const { data } = await getSubscription({ - skipErrorHandler: true, - }); - return data.data?.list || []; - }, - }); - if (data?.length === 0) return null; + const { user } = useGlobalStore(); + return (
- {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/subscribe/payment-methods.tsx b/apps/user/components/subscribe/payment-methods.tsx index d440fe9..c768ce1 100644 --- a/apps/user/components/subscribe/payment-methods.tsx +++ b/apps/user/components/subscribe/payment-methods.tsx @@ -1,6 +1,8 @@ 'use client'; -import { getAvailablePaymentMethods } from '@/services/user/payment'; +import { getAvailablePaymentMethods } from '@/services/user/portal'; +// import { getAvailablePaymentMethods } from '@/services/user/payment'; +// import { getAvailablePaymentMethods } from '@/services/user/payment'; import { useQuery } from '@tanstack/react-query'; import { Label } from '@workspace/ui/components/label'; import { RadioGroup, RadioGroupItem } from '@workspace/ui/components/radio-group'; @@ -11,22 +13,30 @@ import React, { memo } from 'react'; interface PaymentMethodsProps { value: string; onChange: (value: string) => 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]?.mark) onChange(methods[0]?.mark); + if (balance) return methods; + return methods.filter((item) => item.mark !== 'balance'); }, }); return ( <>
{t('paymentMethod')}
- + {data?.map((item) => (
diff --git a/apps/user/components/subscribe/purchase.tsx b/apps/user/components/subscribe/purchase.tsx index 88e1ce7..4ec165f 100644 --- a/apps/user/components/subscribe/purchase.tsx +++ b/apps/user/components/subscribe/purchase.tsx @@ -4,7 +4,8 @@ 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 { purchaseCheckout } from '@/services/user/portal'; import { useQuery } from '@tanstack/react-query'; import { Button } from '@workspace/ui/components/button'; import { Card, CardContent } from '@workspace/ui/components/card'; @@ -69,7 +70,7 @@ export default function Purchase({ subscribe, setSubscribe }: Readonly) { const response = await recharge(params); const orderNo = response.data.data?.order_no; if (orderNo) { - const { data } = await checkoutOrder({ + const { data } = await purchaseCheckout({ orderNo, returnUrl: `${window.location.origin}/payment?order_no=${orderNo}`, }); diff --git a/apps/user/components/subscribe/renewal.tsx b/apps/user/components/subscribe/renewal.tsx index f5ebcf8..c8faf58 100644 --- a/apps/user/components/subscribe/renewal.tsx +++ b/apps/user/components/subscribe/renewal.tsx @@ -4,7 +4,8 @@ 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, renewal } from '@/services/user/order'; +import { preCreateOrder, renewal } from '@/services/user/order'; +import { purchaseCheckout } from '@/services/user/portal'; import { useQuery } from '@tanstack/react-query'; import { Button } from '@workspace/ui/components/button'; import { Card, CardContent } from '@workspace/ui/components/card'; @@ -77,7 +78,7 @@ export default function Renewal({ id, subscribe }: Readonly) { const response = await renewal(params as API.RenewalOrderRequest); const orderNo = response.data.data?.order_no; if (orderNo) { - const { data } = await checkoutOrder({ + const { data } = await purchaseCheckout({ orderNo, returnUrl: `${window.location.origin}/payment?order_no=${orderNo}`, }); diff --git a/apps/user/components/subscribe/reset-traffic.tsx b/apps/user/components/subscribe/reset-traffic.tsx index 42d8413..33640a8 100644 --- a/apps/user/components/subscribe/reset-traffic.tsx +++ b/apps/user/components/subscribe/reset-traffic.tsx @@ -2,7 +2,8 @@ import { Display } from '@/components/display'; import useGlobalStore from '@/config/use-global'; -import { checkoutOrder, resetTraffic } from '@/services/user/order'; +import { resetTraffic } from '@/services/user/order'; +import { purchaseCheckout } from '@/services/user/portal'; import { Button } from '@workspace/ui/components/button'; import { Dialog, @@ -84,7 +85,7 @@ export default function ResetTraffic({ id, replacement }: Readonly( - '/v1/common/site/subscribe', - { - method: 'GET', - ...(options || {}), - }, - ); -} - /** Get Tos Content GET /v1/common/site/tos */ export async function getTos(options?: { [key: string]: any }) { return request('/v1/common/site/tos', { diff --git a/apps/user/services/common/typings.d.ts b/apps/user/services/common/typings.d.ts index 566eeb9..878dfce 100644 --- a/apps/user/services/common/typings.d.ts +++ b/apps/user/services/common/typings.d.ts @@ -90,17 +90,6 @@ declare namespace API { enabled: boolean; }; - type CheckoutOrderRequest = { - orderNo: string; - returnUrl?: string; - }; - - type CheckoutOrderResponse = { - type: string; - checkout_url?: string; - stripe?: StripePayment; - }; - type CheckUserParams = { email: string; }; @@ -175,7 +164,7 @@ declare namespace API { }; type GetAvailablePaymentMethodsResponse = { - list: PaymentConfig[]; + list: PaymenMethod[]; }; type GetGlobalConfigResponse = { @@ -196,14 +185,24 @@ declare namespace API { protocol: string[]; }; - type GetSubscriptionResponse = { - list: Subscribe[]; - }; - type GetTosResponse = { tos_content: string; }; + type GetUserSubscribeTrafficLogsRequest = { + page: number; + size: number; + user_id: number; + subscribe_id: number; + start_time: number; + end_time: number; + }; + + type GetUserSubscribeTrafficLogsResponse = { + list: TrafficLog[]; + total: number; + }; + type GoogleLoginCallbackRequest = { code: string; state: string; @@ -329,6 +328,16 @@ declare namespace API { updated_at: number; }; + type PaymenMethod = { + id: number; + name: string; + mark: string; + icon: string; + fee_mode: number; + fee_percent: number; + fee_amount: number; + }; + type PaymentConfig = { id: number; name: string; @@ -417,6 +426,21 @@ declare namespace API { list: OrderDetail[]; }; + type QuerySubscribeGroupListResponse = { + list: SubscribeGroup[]; + total: number; + }; + + type QuerySubscribeListResponse = { + list: Subscribe[]; + total: number; + }; + + type QueryUserAffiliateListResponse = { + list: UserAffiliate[]; + total: number; + }; + type RechargeOrderRequest = { amount: number; payment: string; diff --git a/apps/user/services/user/index.ts b/apps/user/services/user/index.ts index a3487d7..f988131 100644 --- a/apps/user/services/user/index.ts +++ b/apps/user/services/user/index.ts @@ -1,11 +1,12 @@ // @ts-ignore -/* eslint-disable */ + // API 更新时间: // API 唯一标识: import * as announcement from './announcement'; import * as document from './document'; import * as order from './order'; import * as payment from './payment'; +import * as portal from './portal'; import * as subscribe from './subscribe'; import * as ticket from './ticket'; import * as user from './user'; @@ -14,6 +15,7 @@ export default { document, order, payment, + portal, subscribe, ticket, user, diff --git a/apps/user/services/user/order.ts b/apps/user/services/user/order.ts index 59db689..aeebc8d 100644 --- a/apps/user/services/user/order.ts +++ b/apps/user/services/user/order.ts @@ -2,21 +2,6 @@ /* eslint-disable */ import request from '@/utils/request'; -/** Checkout order POST /v1/public/order/checkout */ -export async function checkoutOrder( - body: API.CheckoutOrderRequest, - options?: { [key: string]: any }, -) { - return request('/v1/public/order/checkout', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - data: body, - ...(options || {}), - }); -} - /** Close order POST /v1/public/order/close */ export async function closeOrder(body: API.CloseOrderRequest, options?: { [key: string]: any }) { return request('/v1/public/order/close', { diff --git a/apps/user/services/user/portal.ts b/apps/user/services/user/portal.ts new file mode 100644 index 0000000..abac9f9 --- /dev/null +++ b/apps/user/services/user/portal.ts @@ -0,0 +1,91 @@ +// @ts-ignore +/* eslint-disable */ +import request from '@/utils/request'; + +/** Purchase Checkout POST /v1/public/portal/order/checkout */ +export async function purchaseCheckout( + body: API.CheckoutOrderRequest, + options?: { [key: string]: any }, +) { + return request( + '/v1/public/portal/order/checkout', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: body, + ...(options || {}), + }, + ); +} + +/** Query Purchase Order GET /v1/public/portal/order/status */ +export async function queryPurchaseOrder( + // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) + params: API.QueryPurchaseOrderParams, + options?: { [key: string]: any }, +) { + return request( + '/v1/public/portal/order/status', + { + method: 'GET', + params: { + ...params, + }, + ...(options || {}), + }, + ); +} + +/** Get available payment methods GET /v1/public/portal/payment-method */ +export async function getAvailablePaymentMethods(options?: { [key: string]: any }) { + return request( + '/v1/public/portal/payment-method', + { + method: 'GET', + ...(options || {}), + }, + ); +} + +/** Pre Purchase Order POST /v1/public/portal/pre */ +export async function prePurchaseOrder( + body: API.PrePurchaseOrderRequest, + options?: { [key: string]: any }, +) { + return request('/v1/public/portal/pre', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: body, + ...(options || {}), + }); +} + +/** Purchase subscription POST /v1/public/portal/purchase */ +export async function purchase(body: API.PortalPurchaseRequest, options?: { [key: string]: any }) { + return request( + '/v1/public/portal/purchase', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: body, + ...(options || {}), + }, + ); +} + +/** Get Subscription GET /v1/public/portal/subscribe */ +export async function getSubscription(options?: { [key: string]: any }) { + return request( + '/v1/public/portal/subscribe', + { + method: 'GET', + ...(options || {}), + }, + ); +} diff --git a/apps/user/services/user/typings.d.ts b/apps/user/services/user/typings.d.ts index 06e50d6..4e4b8c8 100644 --- a/apps/user/services/user/typings.d.ts +++ b/apps/user/services/user/typings.d.ts @@ -187,7 +187,7 @@ declare namespace API { }; type GetAvailablePaymentMethodsResponse = { - list: PaymentConfig[]; + list: PaymenMethod[]; }; type GetLoginLogParams = { @@ -224,6 +224,24 @@ declare namespace API { total: number; }; + type GetSubscriptionResponse = { + list: Subscribe[]; + }; + + type GetUserSubscribeTrafficLogsRequest = { + page: number; + size: number; + user_id: number; + subscribe_id: number; + start_time: number; + end_time: number; + }; + + type GetUserSubscribeTrafficLogsResponse = { + list: TrafficLog[]; + total: number; + }; + type GetUserTicketDetailRequest = { id: number; }; @@ -351,6 +369,16 @@ declare namespace API { updated_at: number; }; + type PaymenMethod = { + id: number; + name: string; + mark: string; + icon: string; + fee_mode: number; + fee_percent: number; + fee_amount: number; + }; + type PaymentConfig = { id: number; name: string; @@ -364,6 +392,21 @@ declare namespace API { enable: boolean; }; + type PortalPurchaseRequest = { + identifier: string; + platform: string; + password?: string; + payment: string; + subscribe_id: number; + quantity: number; + coupon?: string; + turnstile_token?: string; + }; + + type PortalPurchaseResponse = { + order_no: string; + }; + type PreOrderResponse = { price: number; amount: number; @@ -374,6 +417,22 @@ declare namespace API { fee_amount: number; }; + type PrePurchaseOrderRequest = { + payment: string; + subscribe_id: number; + quantity: number; + coupon?: string; + }; + + type PrePurchaseOrderResponse = { + price: number; + amount: number; + discount: number; + coupon: string; + coupon_discount: number; + fee_amount: number; + }; + type PreRenewalOrderResponse = { orderNo: string; }; @@ -467,6 +526,30 @@ declare namespace API { list: OrderDetail[]; }; + type QueryPurchaseOrderParams = { + order_no: string; + }; + + type QueryPurchaseOrderRequest = { + order_no: string; + }; + + type QueryPurchaseOrderResponse = { + order_no: string; + subscribe: Subscribe; + quantity: number; + price: number; + amount: number; + discount: number; + coupon: string; + coupon_discount: number; + fee_amount: number; + payment: string; + status: number; + created_at: number; + token?: string; + }; + type QuerySubscribeGroupListResponse = { list: SubscribeGroup[]; total: number; diff --git a/apps/user/utils/common.ts b/apps/user/utils/common.ts index 28ac162..932315f 100644 --- a/apps/user/utils/common.ts +++ b/apps/user/utils/common.ts @@ -52,6 +52,7 @@ export function Logout() { const pathname = location.pathname; if ( !['', '/', '/auth', '/tos', '/privacy-policy'].includes(pathname) && + !pathname.startsWith('/purchasing') && !pathname.startsWith('/oauth/') ) { setRedirectUrl(location.pathname);