✨ 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>
|
<a name="readme-top"></a>
|
||||||
|
|
||||||
# Changelog
|
# 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)
|
# [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
|
### 🐛 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)
|
# [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
|
### ✨ 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
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
* **dashboard**: Display subscription creation date in user dashboard ([d0e6df0](https://github.com/perfect-panel/ppanel-web/commit/d0e6df0))
|
- **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))
|
- **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))
|
- **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)
|
# [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
|
### ♻ Code Refactoring
|
||||||
|
|
||||||
* **ui**: Optimize document display ([2ca2992](https://github.com/perfect-panel/ppanel-web/commit/2ca2992))
|
- **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))
|
- Reduce code complexity and improve readability ([e11f18c](https://github.com/perfect-panel/ppanel-web/commit/e11f18c))
|
||||||
|
|
||||||
|
|
||||||
### ✨ Features
|
### ✨ 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
|
### 🎫 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
|
### 🐛 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>
|
<a name="readme-top"></a>
|
||||||
|
|
||||||
|
|||||||
@ -115,7 +115,7 @@ export function SubscriptionDetail({
|
|||||||
user_id: userId,
|
user_id: userId,
|
||||||
subscribe_id: subscriptionId,
|
subscribe_id: subscriptionId,
|
||||||
...pagination,
|
...pagination,
|
||||||
});
|
} as API.GetUserSubscribeTrafficLogsParams);
|
||||||
return {
|
return {
|
||||||
list: data.data?.list || [],
|
list: data.data?.list || [],
|
||||||
total: data.data?.total || 0,
|
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[];
|
ids: number[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type CheckoutOrderRequest = {
|
|
||||||
orderNo: string;
|
|
||||||
returnUrl?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CheckoutOrderResponse = {
|
|
||||||
type: string;
|
|
||||||
checkout_url?: string;
|
|
||||||
stripe?: StripePayment;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CloseOrderRequest = {
|
type CloseOrderRequest = {
|
||||||
orderNo: string;
|
orderNo: string;
|
||||||
};
|
};
|
||||||
@ -488,7 +477,7 @@ declare namespace API {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type GetAvailablePaymentMethodsResponse = {
|
type GetAvailablePaymentMethodsResponse = {
|
||||||
list: PaymentConfig[];
|
list: PaymenMethod[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type GetCouponListParams = {
|
type GetCouponListParams = {
|
||||||
@ -797,6 +786,8 @@ declare namespace API {
|
|||||||
size: number;
|
size: number;
|
||||||
user_id: number;
|
user_id: number;
|
||||||
subscribe_id: number;
|
subscribe_id: number;
|
||||||
|
start_time: number;
|
||||||
|
end_time: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type GetUserSubscribeTrafficLogsRequest = {
|
type GetUserSubscribeTrafficLogsRequest = {
|
||||||
@ -804,6 +795,8 @@ declare namespace API {
|
|||||||
size: number;
|
size: number;
|
||||||
user_id: number;
|
user_id: number;
|
||||||
subscribe_id: number;
|
subscribe_id: number;
|
||||||
|
start_time: number;
|
||||||
|
end_time: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type GetUserSubscribeTrafficLogsResponse = {
|
type GetUserSubscribeTrafficLogsResponse = {
|
||||||
@ -931,6 +924,16 @@ declare namespace API {
|
|||||||
list?: OrdersStatistics[];
|
list?: OrdersStatistics[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PaymenMethod = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
mark: string;
|
||||||
|
icon: string;
|
||||||
|
fee_mode: number;
|
||||||
|
fee_percent: number;
|
||||||
|
fee_amount: number;
|
||||||
|
};
|
||||||
|
|
||||||
type PaymentConfig = {
|
type PaymentConfig = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@ -1029,6 +1032,21 @@ declare namespace API {
|
|||||||
list: OrderDetail[];
|
list: OrderDetail[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type QuerySubscribeGroupListResponse = {
|
||||||
|
list: SubscribeGroup[];
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type QuerySubscribeListResponse = {
|
||||||
|
list: Subscribe[];
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type QueryUserAffiliateListResponse = {
|
||||||
|
list: UserAffiliate[];
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
type RechargeOrderRequest = {
|
type RechargeOrderRequest = {
|
||||||
amount: number;
|
amount: number;
|
||||||
payment: string;
|
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 */
|
/** Get Tos Content GET /v1/common/site/tos */
|
||||||
export async function getTos(options?: { [key: string]: any }) {
|
export async function getTos(options?: { [key: string]: any }) {
|
||||||
return request<API.Response & { data?: API.GetTosResponse }>('/v1/common/site/tos', {
|
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;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CheckoutOrderRequest = {
|
|
||||||
orderNo: string;
|
|
||||||
returnUrl?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CheckoutOrderResponse = {
|
|
||||||
type: string;
|
|
||||||
checkout_url?: string;
|
|
||||||
stripe?: StripePayment;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CheckUserParams = {
|
type CheckUserParams = {
|
||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
@ -175,7 +164,7 @@ declare namespace API {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type GetAvailablePaymentMethodsResponse = {
|
type GetAvailablePaymentMethodsResponse = {
|
||||||
list: PaymentConfig[];
|
list: PaymenMethod[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type GetGlobalConfigResponse = {
|
type GetGlobalConfigResponse = {
|
||||||
@ -196,14 +185,24 @@ declare namespace API {
|
|||||||
protocol: string[];
|
protocol: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type GetSubscriptionResponse = {
|
|
||||||
list: Subscribe[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type GetTosResponse = {
|
type GetTosResponse = {
|
||||||
tos_content: string;
|
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 = {
|
type GoogleLoginCallbackRequest = {
|
||||||
code: string;
|
code: string;
|
||||||
state: string;
|
state: string;
|
||||||
@ -329,6 +328,16 @@ declare namespace API {
|
|||||||
updated_at: number;
|
updated_at: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PaymenMethod = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
mark: string;
|
||||||
|
icon: string;
|
||||||
|
fee_mode: number;
|
||||||
|
fee_percent: number;
|
||||||
|
fee_amount: number;
|
||||||
|
};
|
||||||
|
|
||||||
type PaymentConfig = {
|
type PaymentConfig = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@ -417,6 +426,21 @@ declare namespace API {
|
|||||||
list: OrderDetail[];
|
list: OrderDetail[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type QuerySubscribeGroupListResponse = {
|
||||||
|
list: SubscribeGroup[];
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type QuerySubscribeListResponse = {
|
||||||
|
list: Subscribe[];
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type QueryUserAffiliateListResponse = {
|
||||||
|
list: UserAffiliate[];
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
type RechargeOrderRequest = {
|
type RechargeOrderRequest = {
|
||||||
amount: number;
|
amount: number;
|
||||||
payment: string;
|
payment: string;
|
||||||
|
|||||||
@ -4,7 +4,8 @@ import { Display } from '@/components/display';
|
|||||||
import { SubscribeBilling } from '@/components/subscribe/billing';
|
import { SubscribeBilling } from '@/components/subscribe/billing';
|
||||||
import { SubscribeDetail } from '@/components/subscribe/detail';
|
import { SubscribeDetail } from '@/components/subscribe/detail';
|
||||||
import useGlobalStore from '@/config/use-global';
|
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 { useQuery } from '@tanstack/react-query';
|
||||||
import { Badge } from '@workspace/ui/components/badge';
|
import { Badge } from '@workspace/ui/components/badge';
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
@ -50,7 +51,10 @@ export default function Page() {
|
|||||||
enabled: !!orderNo && data?.status === 1,
|
enabled: !!orderNo && data?.status === 1,
|
||||||
queryKey: ['checkoutOrder', orderNo],
|
queryKey: ['checkoutOrder', orderNo],
|
||||||
queryFn: async () => {
|
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;
|
return data?.data;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { GlobalMap } from '@/components/main/global-map';
|
import { GlobalMap } from '@/components/main/global-map';
|
||||||
import { Hero } from '@/components/main/hero';
|
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';
|
import { Stats } from '@/components/main/stats';
|
||||||
|
|
||||||
export default function Home() {
|
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';
|
'use client';
|
||||||
|
|
||||||
|
import { OAuthMethods } from '@/components/auth/oauth-methods';
|
||||||
import LanguageSwitch from '@/components/language-switch';
|
import LanguageSwitch from '@/components/language-switch';
|
||||||
import ThemeSwitch from '@/components/theme-switch';
|
import ThemeSwitch from '@/components/theme-switch';
|
||||||
import useGlobalStore from '@/config/use-global';
|
import useGlobalStore from '@/config/use-global';
|
||||||
import { oAuthLogin } from '@/services/common/oauth';
|
|
||||||
import { DotLottieReact } from '@lottiefiles/dotlottie-react';
|
import { DotLottieReact } from '@lottiefiles/dotlottie-react';
|
||||||
import { Button } from '@workspace/ui/components/button';
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
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 LoginLottie from '@workspace/ui/lotties/login.json';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import Image from 'next/legacy/image';
|
import Image from 'next/legacy/image';
|
||||||
@ -15,18 +13,10 @@ import Link from 'next/link';
|
|||||||
import EmailAuthForm from './email/auth-form';
|
import EmailAuthForm from './email/auth-form';
|
||||||
import PhoneAuthForm from './phone/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() {
|
export default function Page() {
|
||||||
const t = useTranslations('auth');
|
const t = useTranslations('auth');
|
||||||
const { common } = useGlobalStore();
|
const { common } = useGlobalStore();
|
||||||
const { site, auth, oauth_methods } = common;
|
const { site, auth } = common;
|
||||||
|
|
||||||
const AUTH_METHODS = [
|
const AUTH_METHODS = [
|
||||||
{
|
{
|
||||||
@ -41,10 +31,6 @@ export default function Page() {
|
|||||||
},
|
},
|
||||||
].filter((method) => method.enabled);
|
].filter((method) => method.enabled);
|
||||||
|
|
||||||
const OAUTH_METHODS = oauth_methods?.filter(
|
|
||||||
(method) => !['mobile', 'email', 'device'].includes(method),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className='bg-muted/50 flex h-full min-h-screen items-center'>
|
<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'>
|
<div className='flex size-full flex-auto flex-col lg:flex-row'>
|
||||||
@ -95,38 +81,7 @@ export default function Page() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='py-8'>
|
<div className='py-8'>
|
||||||
{OAUTH_METHODS?.length > 0 && (
|
<OAuthMethods />
|
||||||
<>
|
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
<div className='flex items-center gap-5'>
|
<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 { Display } from '@/components/display';
|
||||||
import { SubscribeDetail } from '@/components/subscribe/detail';
|
import { SubscribeDetail } from '@/components/subscribe/detail';
|
||||||
import { getSubscription } from '@/services/common/common';
|
import useGlobalStore from '@/config/use-global';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import { Card, CardContent, CardFooter, CardHeader } from '@workspace/ui/components/card';
|
import { Card, CardContent, CardFooter, CardHeader } from '@workspace/ui/components/card';
|
||||||
import { Separator } from '@workspace/ui/components/separator';
|
import { Separator } from '@workspace/ui/components/separator';
|
||||||
@ -14,19 +13,15 @@ import { useTranslations } from 'next-intl';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Key, ReactNode } from 'react';
|
import { Key, ReactNode } from 'react';
|
||||||
|
|
||||||
export function ProductShowcase() {
|
interface ProductShowcaseProps {
|
||||||
|
subscriptionData: API.Subscribe[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Content({ subscriptionData }: ProductShowcaseProps) {
|
||||||
const t = useTranslations('index');
|
const t = useTranslations('index');
|
||||||
|
|
||||||
const { data } = useQuery({
|
const { user } = useGlobalStore();
|
||||||
queryKey: ['getSubscription'],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await getSubscription({
|
|
||||||
skipErrorHandler: true,
|
|
||||||
});
|
|
||||||
return data.data?.list || [];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (data?.length === 0) return null;
|
|
||||||
return (
|
return (
|
||||||
<motion.section
|
<motion.section
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
@ -51,7 +46,7 @@ export function ProductShowcase() {
|
|||||||
{t('product_showcase_description')}
|
{t('product_showcase_description')}
|
||||||
</motion.p>
|
</motion.p>
|
||||||
<div className='mx-auto flex flex-wrap justify-center gap-8 overflow-x-auto overflow-y-hidden *:max-w-80 *:flex-auto'>
|
<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
|
<motion.div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
initial={{ opacity: 0, y: 50 }}
|
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'
|
className='absolute bottom-0 left-0 w-full rounded-b-xl rounded-t-none'
|
||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
<Link href='/subscribe'>{t('subscribe')}</Link>
|
<Link href={user ? '/subscribe' : `/purchasing?id=${item.id}`}>
|
||||||
|
{t('subscribe')}
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</CardFooter>
|
</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';
|
'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 { useQuery } from '@tanstack/react-query';
|
||||||
import { Label } from '@workspace/ui/components/label';
|
import { Label } from '@workspace/ui/components/label';
|
||||||
import { RadioGroup, RadioGroupItem } from '@workspace/ui/components/radio-group';
|
import { RadioGroup, RadioGroupItem } from '@workspace/ui/components/radio-group';
|
||||||
@ -11,22 +13,30 @@ import React, { memo } from 'react';
|
|||||||
interface PaymentMethodsProps {
|
interface PaymentMethodsProps {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
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 t = useTranslations('subscribe');
|
||||||
|
|
||||||
const { data } = useQuery({
|
const { data } = useQuery({
|
||||||
queryKey: ['getAvailablePaymentMethods'],
|
queryKey: ['getAvailablePaymentMethods', { balance }],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await getAvailablePaymentMethods();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='font-semibold'>{t('paymentMethod')}</div>
|
<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) => (
|
{data?.map((item) => (
|
||||||
<div key={item.mark}>
|
<div key={item.mark}>
|
||||||
<RadioGroupItem value={item.mark} id={item.mark} className='peer sr-only' />
|
<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 DurationSelector from '@/components/subscribe/duration-selector';
|
||||||
import PaymentMethods from '@/components/subscribe/payment-methods';
|
import PaymentMethods from '@/components/subscribe/payment-methods';
|
||||||
import useGlobalStore from '@/config/use-global';
|
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 { useQuery } from '@tanstack/react-query';
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import { Card, CardContent } from '@workspace/ui/components/card';
|
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 response = await purchase(params as API.PurchaseOrderRequest);
|
||||||
const orderNo = response.data.data?.order_no;
|
const orderNo = response.data.data?.order_no;
|
||||||
if (orderNo) {
|
if (orderNo) {
|
||||||
const { data } = await checkoutOrder({
|
const { data } = await purchaseCheckout({
|
||||||
orderNo,
|
orderNo,
|
||||||
returnUrl: `${window.location.origin}/payment?order_no=${orderNo}`,
|
returnUrl: `${window.location.origin}/payment?order_no=${orderNo}`,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import useGlobalStore from '@/config/use-global';
|
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 { getAvailablePaymentMethods } from '@/services/user/payment';
|
||||||
|
import { purchaseCheckout } from '@/services/user/portal';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Button, ButtonProps } from '@workspace/ui/components/button';
|
import { Button, ButtonProps } from '@workspace/ui/components/button';
|
||||||
import {
|
import {
|
||||||
@ -133,7 +134,7 @@ export default function Recharge(props: Readonly<ButtonProps>) {
|
|||||||
const response = await recharge(params);
|
const response = await recharge(params);
|
||||||
const orderNo = response.data.data?.order_no;
|
const orderNo = response.data.data?.order_no;
|
||||||
if (orderNo) {
|
if (orderNo) {
|
||||||
const { data } = await checkoutOrder({
|
const { data } = await purchaseCheckout({
|
||||||
orderNo,
|
orderNo,
|
||||||
returnUrl: `${window.location.origin}/payment?order_no=${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 DurationSelector from '@/components/subscribe/duration-selector';
|
||||||
import PaymentMethods from '@/components/subscribe/payment-methods';
|
import PaymentMethods from '@/components/subscribe/payment-methods';
|
||||||
import useGlobalStore from '@/config/use-global';
|
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 { useQuery } from '@tanstack/react-query';
|
||||||
import { Button } from '@workspace/ui/components/button';
|
import { Button } from '@workspace/ui/components/button';
|
||||||
import { Card, CardContent } from '@workspace/ui/components/card';
|
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 response = await renewal(params as API.RenewalOrderRequest);
|
||||||
const orderNo = response.data.data?.order_no;
|
const orderNo = response.data.data?.order_no;
|
||||||
if (orderNo) {
|
if (orderNo) {
|
||||||
const { data } = await checkoutOrder({
|
const { data } = await purchaseCheckout({
|
||||||
orderNo,
|
orderNo,
|
||||||
returnUrl: `${window.location.origin}/payment?order_no=${orderNo}`,
|
returnUrl: `${window.location.origin}/payment?order_no=${orderNo}`,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
import { Display } from '@/components/display';
|
import { Display } from '@/components/display';
|
||||||
import useGlobalStore from '@/config/use-global';
|
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 { Button } from '@workspace/ui/components/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -84,7 +85,7 @@ export default function ResetTraffic({ id, replacement }: Readonly<ResetTrafficP
|
|||||||
const response = await resetTraffic(params);
|
const response = await resetTraffic(params);
|
||||||
const orderNo = response.data.data?.order_no;
|
const orderNo = response.data.data?.order_no;
|
||||||
if (orderNo) {
|
if (orderNo) {
|
||||||
const { data } = await checkoutOrder({
|
const { data } = await purchaseCheckout({
|
||||||
orderNo,
|
orderNo,
|
||||||
returnUrl: `${window.location.origin}/payment?order_no=${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 */
|
/** Get Tos Content GET /v1/common/site/tos */
|
||||||
export async function getTos(options?: { [key: string]: any }) {
|
export async function getTos(options?: { [key: string]: any }) {
|
||||||
return request<API.Response & { data?: API.GetTosResponse }>('/v1/common/site/tos', {
|
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;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CheckoutOrderRequest = {
|
|
||||||
orderNo: string;
|
|
||||||
returnUrl?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CheckoutOrderResponse = {
|
|
||||||
type: string;
|
|
||||||
checkout_url?: string;
|
|
||||||
stripe?: StripePayment;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CheckUserParams = {
|
type CheckUserParams = {
|
||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
@ -175,7 +164,7 @@ declare namespace API {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type GetAvailablePaymentMethodsResponse = {
|
type GetAvailablePaymentMethodsResponse = {
|
||||||
list: PaymentConfig[];
|
list: PaymenMethod[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type GetGlobalConfigResponse = {
|
type GetGlobalConfigResponse = {
|
||||||
@ -196,14 +185,24 @@ declare namespace API {
|
|||||||
protocol: string[];
|
protocol: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type GetSubscriptionResponse = {
|
|
||||||
list: Subscribe[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type GetTosResponse = {
|
type GetTosResponse = {
|
||||||
tos_content: string;
|
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 = {
|
type GoogleLoginCallbackRequest = {
|
||||||
code: string;
|
code: string;
|
||||||
state: string;
|
state: string;
|
||||||
@ -329,6 +328,16 @@ declare namespace API {
|
|||||||
updated_at: number;
|
updated_at: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PaymenMethod = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
mark: string;
|
||||||
|
icon: string;
|
||||||
|
fee_mode: number;
|
||||||
|
fee_percent: number;
|
||||||
|
fee_amount: number;
|
||||||
|
};
|
||||||
|
|
||||||
type PaymentConfig = {
|
type PaymentConfig = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@ -417,6 +426,21 @@ declare namespace API {
|
|||||||
list: OrderDetail[];
|
list: OrderDetail[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type QuerySubscribeGroupListResponse = {
|
||||||
|
list: SubscribeGroup[];
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type QuerySubscribeListResponse = {
|
||||||
|
list: Subscribe[];
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type QueryUserAffiliateListResponse = {
|
||||||
|
list: UserAffiliate[];
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
type RechargeOrderRequest = {
|
type RechargeOrderRequest = {
|
||||||
amount: number;
|
amount: number;
|
||||||
payment: string;
|
payment: string;
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
/* eslint-disable */
|
|
||||||
// API 更新时间:
|
// API 更新时间:
|
||||||
// API 唯一标识:
|
// API 唯一标识:
|
||||||
import * as announcement from './announcement';
|
import * as announcement from './announcement';
|
||||||
import * as document from './document';
|
import * as document from './document';
|
||||||
import * as order from './order';
|
import * as order from './order';
|
||||||
import * as payment from './payment';
|
import * as payment from './payment';
|
||||||
|
import * as portal from './portal';
|
||||||
import * as subscribe from './subscribe';
|
import * as subscribe from './subscribe';
|
||||||
import * as ticket from './ticket';
|
import * as ticket from './ticket';
|
||||||
import * as user from './user';
|
import * as user from './user';
|
||||||
@ -14,6 +15,7 @@ export default {
|
|||||||
document,
|
document,
|
||||||
order,
|
order,
|
||||||
payment,
|
payment,
|
||||||
|
portal,
|
||||||
subscribe,
|
subscribe,
|
||||||
ticket,
|
ticket,
|
||||||
user,
|
user,
|
||||||
|
|||||||
@ -2,21 +2,6 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import request from '@/utils/request';
|
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 */
|
/** Close order POST /v1/public/order/close */
|
||||||
export async function closeOrder(body: API.CloseOrderRequest, options?: { [key: string]: any }) {
|
export async function closeOrder(body: API.CloseOrderRequest, options?: { [key: string]: any }) {
|
||||||
return request<API.Response & { data?: any }>('/v1/public/order/close', {
|
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 = {
|
type GetAvailablePaymentMethodsResponse = {
|
||||||
list: PaymentConfig[];
|
list: PaymenMethod[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type GetLoginLogParams = {
|
type GetLoginLogParams = {
|
||||||
@ -224,6 +224,24 @@ declare namespace API {
|
|||||||
total: number;
|
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 = {
|
type GetUserTicketDetailRequest = {
|
||||||
id: number;
|
id: number;
|
||||||
};
|
};
|
||||||
@ -351,6 +369,16 @@ declare namespace API {
|
|||||||
updated_at: number;
|
updated_at: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type PaymenMethod = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
mark: string;
|
||||||
|
icon: string;
|
||||||
|
fee_mode: number;
|
||||||
|
fee_percent: number;
|
||||||
|
fee_amount: number;
|
||||||
|
};
|
||||||
|
|
||||||
type PaymentConfig = {
|
type PaymentConfig = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@ -364,6 +392,21 @@ declare namespace API {
|
|||||||
enable: boolean;
|
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 = {
|
type PreOrderResponse = {
|
||||||
price: number;
|
price: number;
|
||||||
amount: number;
|
amount: number;
|
||||||
@ -374,6 +417,22 @@ declare namespace API {
|
|||||||
fee_amount: number;
|
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 = {
|
type PreRenewalOrderResponse = {
|
||||||
orderNo: string;
|
orderNo: string;
|
||||||
};
|
};
|
||||||
@ -467,6 +526,30 @@ declare namespace API {
|
|||||||
list: OrderDetail[];
|
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 = {
|
type QuerySubscribeGroupListResponse = {
|
||||||
list: SubscribeGroup[];
|
list: SubscribeGroup[];
|
||||||
total: number;
|
total: number;
|
||||||
|
|||||||
@ -52,6 +52,7 @@ export function Logout() {
|
|||||||
const pathname = location.pathname;
|
const pathname = location.pathname;
|
||||||
if (
|
if (
|
||||||
!['', '/', '/auth', '/tos', '/privacy-policy'].includes(pathname) &&
|
!['', '/', '/auth', '/tos', '/privacy-policy'].includes(pathname) &&
|
||||||
|
!pathname.startsWith('/purchasing') &&
|
||||||
!pathname.startsWith('/oauth/')
|
!pathname.startsWith('/oauth/')
|
||||||
) {
|
) {
|
||||||
setRedirectUrl(location.pathname);
|
setRedirectUrl(location.pathname);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user