✨ feat(subscription): Refactor subscription handling and update imports for better organization
This commit is contained in:
parent
3222016799
commit
2215c7f2b9
28
CHANGELOG.md
28
CHANGELOG.md
@ -1,49 +1,43 @@
|
||||
<a name="readme-top"></a>
|
||||
|
||||
# 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))
|
||||
|
||||
<a name="readme-top"></a>
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
42
apps/admin/services/admin/typings.d.ts
vendored
42
apps/admin/services/admin/typings.d.ts
vendored
@ -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;
|
||||
|
||||
@ -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<API.Response & { data?: API.GetSubscriptionResponse }>(
|
||||
'/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<API.Response & { data?: API.GetTosResponse }>('/v1/common/site/tos', {
|
||||
|
||||
56
apps/admin/services/common/typings.d.ts
vendored
56
apps/admin/services/common/typings.d.ts
vendored
@ -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;
|
||||
|
||||
@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
@ -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() {
|
||||
|
||||
221
apps/user/app/(main)/purchasing/content.tsx
Normal file
221
apps/user/app/(main)/purchasing/content.tsx
Normal file
@ -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<API.PortalPurchaseRequest>({
|
||||
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 <div className='p-6 text-center'>{t('subscriptionNotFound')}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mx-auto mt-8 flex max-w-4xl flex-col gap-8 md:grid md:grid-cols-2 md:flex-row'>
|
||||
<div className='flex flex-col gap-6'>
|
||||
<Card>
|
||||
<CardHeader>输入要用于 {common.site.site_name} 账户的电子邮件地址</CardHeader>
|
||||
<CardContent className='flex flex-col gap-2'>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<EnhancedInput
|
||||
className={cn({
|
||||
'border-destructive': !isEmailValid.valid && params.identifier !== '',
|
||||
})}
|
||||
placeholder='Email'
|
||||
type='email'
|
||||
value={params.identifier || ''}
|
||||
onValueChange={(value) => {
|
||||
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
|
||||
/>
|
||||
<p
|
||||
className={cn('text-muted-foreground text-xs', {
|
||||
'text-destructive': !isEmailValid.valid && params.identifier !== '',
|
||||
})}
|
||||
>
|
||||
{isEmailValid.message || '请填写您的电子邮件地址。'}
|
||||
</p>
|
||||
</div>
|
||||
{params.identifier && isEmailValid.valid && (
|
||||
<div className='flex flex-col gap-2'>
|
||||
<EnhancedInput
|
||||
placeholder='Password'
|
||||
type='password'
|
||||
value={params.password || ''}
|
||||
onValueChange={(value) => handleChange('password', value)}
|
||||
/>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
如果您不填写密码,我们将会自动生成密码并发送到您的邮箱。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{/* <div>
|
||||
<OAuthMethods />
|
||||
</div> */}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className='grid gap-3 p-6 text-sm'>
|
||||
<h2 className='text-xl font-semibold'>{subscription.name}</h2>
|
||||
<p className='text-muted-foreground'>{subscription.description}</p>
|
||||
<SubscribeDetail
|
||||
subscribe={{
|
||||
...subscription,
|
||||
quantity: params.quantity,
|
||||
}}
|
||||
/>
|
||||
<Separator />
|
||||
<SubscribeBilling
|
||||
order={{
|
||||
...order,
|
||||
quantity: params.quantity,
|
||||
unit_price: subscription?.unit_price,
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-6'>
|
||||
<Card>
|
||||
<CardContent className='p-6'>
|
||||
<div className='grid gap-6'>
|
||||
<DurationSelector
|
||||
quantity={params.quantity!}
|
||||
unitTime={subscription?.unit_time}
|
||||
discounts={subscription?.discount}
|
||||
onChange={(value) => handleChange('quantity', value)}
|
||||
/>
|
||||
<CouponInput
|
||||
coupon={params.coupon}
|
||||
onChange={(value) => handleChange('coupon', value)}
|
||||
/>
|
||||
<PaymentMethods
|
||||
balance={false}
|
||||
value={params.payment!}
|
||||
onChange={(value) => handleChange('payment', value)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Button
|
||||
className='w-full'
|
||||
size='lg'
|
||||
disabled={!isEmailValid.valid || loading}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{loading && <LoaderCircle className='mr-2 animate-spin' />}
|
||||
{t('buyNow')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
266
apps/user/app/(main)/purchasing/order/page.tsx
Normal file
266
apps/user/app/(main)/purchasing/order/page.tsx
Normal file
@ -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<string>();
|
||||
const [enabled, setEnabled] = useState<boolean>(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 (
|
||||
<main className='container lg:mt-16'>
|
||||
<div className='grid gap-4 xl:grid-cols-2'>
|
||||
<Card className='order-2 xl:order-1'>
|
||||
<CardHeader className='bg-muted/50 flex flex-row items-start'>
|
||||
<div className='grid gap-0.5'>
|
||||
<CardTitle className='flex flex-col text-lg'>
|
||||
{t('orderNumber')}
|
||||
<span>{data?.order_no}</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t('createdAt')}: {formatDate(data?.created_at)}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className='grid gap-3 p-6 text-sm'>
|
||||
<div className='font-semibold'>{t('paymentMethod')}</div>
|
||||
<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>}
|
||||
</dt>
|
||||
</div>
|
||||
</dl>
|
||||
<Separator />
|
||||
|
||||
{data?.status && [1, 2].includes(data.status) && (
|
||||
<SubscribeDetail
|
||||
subscribe={{
|
||||
...data?.subscribe,
|
||||
quantity: data?.quantity,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{data?.status === 3 && (
|
||||
<>
|
||||
<div className='font-semibold'>{t('resetTraffic')}</div>
|
||||
<ul className='grid grid-cols-2 gap-3 *:flex *:items-center *:justify-between lg:grid-cols-1'>
|
||||
<li className='flex items-center justify-between'>
|
||||
<span className='text-muted-foreground line-clamp-2 flex-1'>
|
||||
{t('resetPrice')}
|
||||
</span>
|
||||
<span>
|
||||
<Display type='currency' value={data.amount} />
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
|
||||
{data?.status === 4 && (
|
||||
<>
|
||||
<div className='font-semibold'>{t('balanceRecharge')}</div>
|
||||
<ul className='grid grid-cols-2 gap-3 *:flex *:items-center *:justify-between lg:grid-cols-1'>
|
||||
<li className='flex items-center justify-between'>
|
||||
<span className='text-muted-foreground line-clamp-2 flex-1'>
|
||||
{t('rechargeAmount')}
|
||||
</span>
|
||||
<span>
|
||||
<Display type='currency' value={data.amount} />
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
<Separator />
|
||||
<SubscribeBilling
|
||||
order={{
|
||||
...data,
|
||||
unit_price: data?.subscribe?.unit_price,
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className='order-1 flex flex-auto items-center justify-center xl:order-2'>
|
||||
<CardContent className='py-16'>
|
||||
{data?.status && [2, 5].includes(data?.status) && (
|
||||
<div className='flex flex-col items-center gap-8 text-center'>
|
||||
<h3 className='text-2xl font-bold tracking-tight'>{t('paymentSuccess')}</h3>
|
||||
<Icon icon='mdi:success-circle-outline' className='text-7xl text-green-500' />
|
||||
<div className='flex gap-4'>
|
||||
<Button asChild>
|
||||
<Link href='/dashboard'>{t('subscribeNow')}</Link>
|
||||
</Button>
|
||||
<Button variant='outline'>
|
||||
<Link href='/document'>{t('viewDocument')}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{data?.status === 1 && payment?.type === 'url' && (
|
||||
<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>
|
||||
<Icon icon='mdi:access-time' className='text-muted-foreground text-7xl' />
|
||||
<div className='flex gap-4'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (payment?.checkout_url) {
|
||||
window.location.href = payment?.checkout_url;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('goToPayment')}
|
||||
</Button>
|
||||
<Button variant='outline'>
|
||||
<Link href='/'>{t('productList')}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data?.status === 1 && payment?.type === 'qr' && (
|
||||
<div className='flex flex-col items-center gap-8 text-center'>
|
||||
<h3 className='text-2xl font-bold tracking-tight'>{t('scanToPay')}</h3>
|
||||
<p className='flex items-center text-3xl font-bold'>{countdownDisplay}</p>
|
||||
<QRCodeCanvas
|
||||
value={payment?.checkout_url || ''}
|
||||
size={208}
|
||||
imageSettings={{
|
||||
src: `/payment/alipay_f2f.svg`,
|
||||
width: 24,
|
||||
height: 24,
|
||||
excavate: true,
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
|
||||
{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>
|
||||
<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>
|
||||
)}
|
||||
|
||||
{data?.status && [3, 4].includes(data?.status) && (
|
||||
<div className='flex flex-col items-center gap-8 text-center'>
|
||||
<h3 className='text-2xl font-bold tracking-tight'>{t('orderClosed')}</h3>
|
||||
<Icon icon='mdi:cancel' className='text-7xl text-red-500' />
|
||||
<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>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
109
apps/user/app/(main)/purchasing/order/stripe.tsx
Normal file
109
apps/user/app/(main)/purchasing/order/stripe.tsx
Normal file
@ -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<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;
|
||||
23
apps/user/app/(main)/purchasing/page.tsx
Normal file
23
apps/user/app/(main)/purchasing/page.tsx
Normal file
@ -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 (
|
||||
<main className='container space-y-16'>
|
||||
<Content subscription={subscription} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<main className='bg-muted/50 flex h-full min-h-screen items-center'>
|
||||
<div className='flex size-full flex-auto flex-col lg:flex-row'>
|
||||
@ -95,38 +81,7 @@ export default function Page() {
|
||||
)}
|
||||
</div>
|
||||
<div className='py-8'>
|
||||
{OAUTH_METHODS?.length > 0 && (
|
||||
<>
|
||||
<div className='after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t'>
|
||||
<span className='bg-background text-muted-foreground relative z-10 px-2'>
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
<div className='mt-6 flex justify-center gap-4 *:size-12 *:p-2'>
|
||||
{OAUTH_METHODS?.map((method: any) => {
|
||||
return (
|
||||
<Button
|
||||
key={method}
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
asChild
|
||||
onClick={async () => {
|
||||
const { data } = await oAuthLogin({
|
||||
method,
|
||||
redirect: `${window.location.origin}/oauth/${method}`,
|
||||
});
|
||||
if (data.data?.redirect) {
|
||||
window.location.href = data.data?.redirect;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon={icons[method as keyof typeof icons]} />
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<OAuthMethods />
|
||||
</div>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-5'>
|
||||
|
||||
56
apps/user/components/auth/oauth-methods.tsx
Normal file
56
apps/user/components/auth/oauth-methods.tsx
Normal file
@ -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 && (
|
||||
<>
|
||||
<div className='after:border-border relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t'>
|
||||
<span className='bg-background text-muted-foreground relative z-10 px-2'>
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
<div className='mt-6 flex justify-center gap-4 *:size-12 *:p-2'>
|
||||
{OAUTH_METHODS?.map((method: any) => {
|
||||
return (
|
||||
<Button
|
||||
key={method}
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
asChild
|
||||
onClick={async () => {
|
||||
const { data } = await oAuthLogin({
|
||||
method,
|
||||
redirect: `${window.location.origin}/oauth/${method}`,
|
||||
});
|
||||
if (data.data?.redirect) {
|
||||
window.location.href = data.data?.redirect;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon={icons[method as keyof typeof icons]} />
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -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 (
|
||||
<motion.section
|
||||
initial={{ opacity: 0 }}
|
||||
@ -51,7 +46,7 @@ export function ProductShowcase() {
|
||||
{t('product_showcase_description')}
|
||||
</motion.p>
|
||||
<div className='mx-auto flex flex-wrap justify-center gap-8 overflow-x-auto overflow-y-hidden *:max-w-80 *:flex-auto'>
|
||||
{data?.map((item, index) => (
|
||||
{subscriptionData?.map((item, index) => (
|
||||
<motion.div
|
||||
key={item.id}
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
@ -132,7 +127,9 @@ export function ProductShowcase() {
|
||||
className='absolute bottom-0 left-0 w-full rounded-b-xl rounded-t-none'
|
||||
asChild
|
||||
>
|
||||
<Link href='/subscribe'>{t('subscribe')}</Link>
|
||||
<Link href={user ? '/subscribe' : `/purchasing?id=${item.id}`}>
|
||||
{t('subscribe')}
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</CardFooter>
|
||||
17
apps/user/components/main/product-showcase/index.tsx
Normal file
17
apps/user/components/main/product-showcase/index.tsx
Normal file
@ -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 <Content subscriptionData={subscriptionList} />;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -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<PaymentMethodsProps> = ({ value, onChange }) => {
|
||||
const PaymentMethods: React.FC<PaymentMethodsProps> = ({ 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 (
|
||||
<>
|
||||
<div className='font-semibold'>{t('paymentMethod')}</div>
|
||||
<RadioGroup className='grid grid-cols-5 gap-2' value={value} onValueChange={onChange}>
|
||||
<RadioGroup
|
||||
className='grid grid-cols-2 gap-2 md:grid-cols-5'
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
>
|
||||
{data?.map((item) => (
|
||||
<div key={item.mark}>
|
||||
<RadioGroupItem value={item.mark} id={item.mark} className='peer sr-only' />
|
||||
|
||||
@ -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<PurchaseP
|
||||
const response = await purchase(params as API.PurchaseOrderRequest);
|
||||
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}`,
|
||||
});
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { checkoutOrder, recharge } from '@/services/user/order';
|
||||
import { recharge } from '@/services/user/order';
|
||||
import { getAvailablePaymentMethods } from '@/services/user/payment';
|
||||
import { purchaseCheckout } from '@/services/user/portal';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button, ButtonProps } from '@workspace/ui/components/button';
|
||||
import {
|
||||
@ -133,7 +134,7 @@ export default function Recharge(props: Readonly<ButtonProps>) {
|
||||
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}`,
|
||||
});
|
||||
|
||||
@ -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<RenewalProps>) {
|
||||
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}`,
|
||||
});
|
||||
|
||||
@ -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<ResetTrafficP
|
||||
const response = await resetTraffic(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}`,
|
||||
});
|
||||
|
||||
@ -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<API.Response & { data?: API.GetSubscriptionResponse }>(
|
||||
'/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<API.Response & { data?: API.GetTosResponse }>('/v1/common/site/tos', {
|
||||
|
||||
56
apps/user/services/common/typings.d.ts
vendored
56
apps/user/services/common/typings.d.ts
vendored
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<API.Response & { data?: API.CheckoutOrderResponse }>('/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<API.Response & { data?: any }>('/v1/public/order/close', {
|
||||
|
||||
91
apps/user/services/user/portal.ts
Normal file
91
apps/user/services/user/portal.ts
Normal file
@ -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<API.Response & { data?: API.CheckoutOrderResponse }>(
|
||||
'/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<API.Response & { data?: API.QueryPurchaseOrderResponse }>(
|
||||
'/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<API.Response & { data?: API.GetAvailablePaymentMethodsResponse }>(
|
||||
'/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<API.Response & { data?: API.PrePurchaseOrderResponse }>('/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<API.Response & { data?: API.PortalPurchaseResponse }>(
|
||||
'/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<API.Response & { data?: API.GetSubscriptionResponse }>(
|
||||
'/v1/public/portal/subscribe',
|
||||
{
|
||||
method: 'GET',
|
||||
...(options || {}),
|
||||
},
|
||||
);
|
||||
}
|
||||
85
apps/user/services/user/typings.d.ts
vendored
85
apps/user/services/user/typings.d.ts
vendored
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user