🐛 fix(changelog): Update change log style
This commit is contained in:
@@ -4,6 +4,7 @@ NEXT_PUBLIC_DEFAULT_LANGUAGE=en-US
|
||||
# Site URL and API URL
|
||||
NEXT_PUBLIC_SITE_URL=https://user.ppanel.dev
|
||||
NEXT_PUBLIC_API_URL=https://api.ppanel.dev
|
||||
NEXT_PUBLIC_CDN_URL=https://cdn.jsdelivr.net
|
||||
|
||||
# Home Page Settings
|
||||
NEXT_PUBLIC_HOME_USER_COUNT=999
|
||||
|
||||
@@ -32,8 +32,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@workspace/ui/componen
|
||||
import { Separator } from '@workspace/ui/components/separator';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { formatDate, isBrowser } from '@workspace/ui/utils';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
import { differenceInDays, formatDate, isBrowser } from '@workspace/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
@@ -58,7 +57,11 @@ export default function Content() {
|
||||
|
||||
const [protocol, setProtocol] = useState('');
|
||||
|
||||
const { data: userSubscribe = [], refetch } = useQuery({
|
||||
const {
|
||||
data: userSubscribe = [],
|
||||
refetch,
|
||||
isLoading,
|
||||
} = useQuery({
|
||||
queryKey: ['queryUserSubscribe'],
|
||||
queryFn: async () => {
|
||||
const { data } = await queryUserSubscribe();
|
||||
@@ -89,10 +92,27 @@ export default function Content() {
|
||||
<>
|
||||
{userSubscribe.length ? (
|
||||
<>
|
||||
<h2 className='flex items-center gap-1.5 font-semibold'>
|
||||
<Icon icon='uil:servers' className='size-5' />
|
||||
{t('mySubscriptions')}
|
||||
</h2>
|
||||
<div className='flex items-center justify-between'>
|
||||
<h2 className='flex items-center gap-1.5 font-semibold'>
|
||||
<Icon icon='uil:servers' className='size-5' />
|
||||
{t('mySubscriptions')}
|
||||
</h2>
|
||||
<div className='flex gap-2'>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='outline'
|
||||
onClick={() => {
|
||||
refetch();
|
||||
}}
|
||||
className={isLoading ? 'animate-pulse' : ''}
|
||||
>
|
||||
<Icon icon='uil:sync' />
|
||||
</Button>
|
||||
<Button size='sm' asChild>
|
||||
<Link href='/subscribe'>{t('purchaseSubscription')}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-wrap justify-between gap-4'>
|
||||
<Tabs
|
||||
value={platform}
|
||||
@@ -206,12 +226,13 @@ export default function Content() {
|
||||
<span className='text-2xl font-semibold'>
|
||||
{item.reset_time
|
||||
? differenceInDays(new Date(item.reset_time), new Date())
|
||||
: t('unknown')}
|
||||
: t('noReset')}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className='text-muted-foreground'>{t('expirationDays')}</span>
|
||||
<span className='text-2xl font-semibold'>
|
||||
{}
|
||||
{item.expire_time
|
||||
? differenceInDays(new Date(item.expire_time), new Date()) || t('unknown')
|
||||
: t('noLimit')}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { Display } from '@/components/display';
|
||||
import StripePayment from '@/components/payment/stripe';
|
||||
import { SubscribeBilling } from '@/components/subscribe/billing';
|
||||
import { SubscribeDetail } from '@/components/subscribe/detail';
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
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';
|
||||
@@ -24,7 +26,6 @@ import { useTranslations } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
import { QRCodeCanvas } from 'qrcode.react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import StripePayment from './stripe';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('order');
|
||||
@@ -48,9 +49,15 @@ export default function Page() {
|
||||
|
||||
const { data: payment } = useQuery({
|
||||
enabled: !!orderNo && data?.status === 1,
|
||||
queryKey: ['checkoutOrder', orderNo],
|
||||
queryKey: ['purchaseCheckout', orderNo],
|
||||
queryFn: async () => {
|
||||
const { data } = await checkoutOrder({ orderNo: orderNo!, returnUrl: window.location.href });
|
||||
const { data } = await purchaseCheckout({
|
||||
orderNo: orderNo!,
|
||||
returnUrl: window.location.href,
|
||||
});
|
||||
if (data.data?.type === 'url' && data.data.checkout_url) {
|
||||
window.open(data.data.checkout_url, '_blank');
|
||||
}
|
||||
return data?.data;
|
||||
},
|
||||
});
|
||||
@@ -99,7 +106,7 @@ export default function Page() {
|
||||
<dl className='grid gap-3'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<dt className='text-muted-foreground'>
|
||||
{data?.method && <Badge>{t(`methods.${data?.method}`)}</Badge>}
|
||||
<Badge>{data?.payment.name || data?.payment.platform}</Badge>
|
||||
</dt>
|
||||
</div>
|
||||
</dl>
|
||||
@@ -169,7 +176,7 @@ export default function Page() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{data?.status === 1 && payment?.type === 'link' && (
|
||||
{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>
|
||||
@@ -218,17 +225,17 @@ export default function Page() {
|
||||
|
||||
{data?.status === 1 && payment?.type === 'stripe' && (
|
||||
<div className='flex flex-col items-center gap-8 text-center'>
|
||||
<h3 className='text-2xl font-bold tracking-tight'>{t('scanToPay')}</h3>
|
||||
<h3 className='text-2xl font-bold tracking-tight'>{t('waitingForPayment')}</h3>
|
||||
<p className='flex items-center text-3xl font-bold'>{countdownDisplay}</p>
|
||||
{payment.stripe && <StripePayment {...payment.stripe} />}
|
||||
<div className='flex gap-4'>
|
||||
{/* <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> */}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import { Elements, useStripe } from '@stripe/react-stripe-js';
|
||||
import { loadStripe, PaymentIntentResult } from '@stripe/stripe-js';
|
||||
import { QRCodeCanvas } from 'qrcode.react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
interface StripePaymentProps {
|
||||
method: string;
|
||||
client_secret: string;
|
||||
publishable_key: string;
|
||||
}
|
||||
|
||||
const StripePayment: React.FC<StripePaymentProps> = ({
|
||||
method,
|
||||
client_secret,
|
||||
publishable_key,
|
||||
}) => {
|
||||
const stripePromise = useMemo(() => loadStripe(publishable_key), [publishable_key]);
|
||||
|
||||
return (
|
||||
<Elements stripe={stripePromise}>
|
||||
<CheckoutForm method={method} client_secret={client_secret} />
|
||||
</Elements>
|
||||
);
|
||||
};
|
||||
|
||||
const CheckoutForm: React.FC<Omit<StripePaymentProps, 'publishable_key'>> = ({
|
||||
client_secret,
|
||||
method,
|
||||
}) => {
|
||||
const stripe = useStripe();
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [qrCodeUrl, setQrCodeUrl] = useState<string | null>(null);
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
const handleError = useCallback((message: string) => {
|
||||
setErrorMessage(message);
|
||||
setIsSubmitted(false);
|
||||
}, []);
|
||||
|
||||
const confirmPayment = useCallback(async (): Promise<PaymentIntentResult | null> => {
|
||||
if (!stripe) {
|
||||
handleError('Stripe.js is not loaded.');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (method === 'alipay') {
|
||||
return await stripe.confirmAlipayPayment(
|
||||
client_secret,
|
||||
{ return_url: window.location.href },
|
||||
{ handleActions: false },
|
||||
);
|
||||
}
|
||||
|
||||
return await stripe.confirmWechatPayPayment(
|
||||
client_secret,
|
||||
{
|
||||
payment_method_options: { wechat_pay: { client: 'web' } },
|
||||
},
|
||||
{ handleActions: false },
|
||||
);
|
||||
}, [client_secret, method, stripe, handleError]);
|
||||
|
||||
const autoSubmit = useCallback(async () => {
|
||||
if (isSubmitted) return;
|
||||
|
||||
setIsSubmitted(true);
|
||||
|
||||
try {
|
||||
const result = await confirmPayment();
|
||||
if (!result) return;
|
||||
|
||||
const { error, paymentIntent } = result;
|
||||
if (error) return handleError(error.message!);
|
||||
|
||||
if (paymentIntent?.status === 'requires_action') {
|
||||
const nextAction = paymentIntent.next_action as any;
|
||||
const qrUrl =
|
||||
method === 'alipay'
|
||||
? nextAction?.alipay_handle_redirect?.url
|
||||
: nextAction?.wechat_pay_display_qr_code?.image_url_svg;
|
||||
|
||||
setQrCodeUrl(qrUrl || null);
|
||||
}
|
||||
} catch (error) {
|
||||
handleError('An unexpected error occurred');
|
||||
}
|
||||
}, [confirmPayment, isSubmitted, handleError, method]);
|
||||
|
||||
useEffect(() => {
|
||||
autoSubmit();
|
||||
}, [autoSubmit]);
|
||||
|
||||
return qrCodeUrl ? (
|
||||
<QRCodeCanvas
|
||||
value={qrCodeUrl}
|
||||
size={208}
|
||||
imageSettings={{
|
||||
src: `/payment/${method}.svg`,
|
||||
width: 24,
|
||||
height: 24,
|
||||
excavate: true,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
errorMessage
|
||||
);
|
||||
};
|
||||
|
||||
export default StripePayment;
|
||||
@@ -21,11 +21,11 @@ export default function Page() {
|
||||
<>
|
||||
<Card className='mb-4'>
|
||||
<CardContent className='p-6'>
|
||||
<h2 className='text-foreground mb-4 text-2xl font-bold'>{t('totalAssets')}</h2>
|
||||
<h2 className='text-foreground mb-4 text-2xl font-bold'>{t('assetOverview')}</h2>
|
||||
<div className='mb-4'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div>
|
||||
<p className='text-sm font-medium'>总资产</p>
|
||||
<p className='text-sm font-medium'>{t('totalAssets')}</p>
|
||||
<p className='text-3xl font-bold'>
|
||||
<Display type='currency' value={totalAssets} />
|
||||
</p>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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: -1,
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
'use client';
|
||||
|
||||
import { Display } from '@/components/display';
|
||||
import StripePayment from '@/components/payment/stripe';
|
||||
import { SubscribeBilling } from '@/components/subscribe/billing';
|
||||
import { SubscribeDetail } from '@/components/subscribe/detail';
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
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';
|
||||
|
||||
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.open(data.data.checkout_url, '_blank');
|
||||
}
|
||||
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'>
|
||||
<Badge>{data?.payment.name || data?.payment.platform}</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('waitingForPayment')}</h3>
|
||||
<p className='flex items-center text-3xl font-bold'>{countdownDisplay}</p>
|
||||
{payment.stripe && <StripePayment {...payment.stripe} />}
|
||||
{/* <div className='flex gap-4'>
|
||||
<Button asChild>
|
||||
<Link href='/subscribe'>{t('productList')}</Link>
|
||||
</Button>
|
||||
<Button asChild variant='outline'>
|
||||
<Link href='/order'>{t('orderList')}</Link>
|
||||
</Button>
|
||||
</div> */}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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>
|
||||
);
|
||||
}
|
||||
@@ -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'>
|
||||
|
||||
@@ -96,8 +96,8 @@ export default function Affiliate() {
|
||||
<CardContent className='p-3 text-sm'>
|
||||
<ul className='grid grid-cols-2 gap-3 *:flex *:flex-col'>
|
||||
<li className='font-semibold'>
|
||||
<span className='text-muted-foreground'>{t('userEmail')}</span>
|
||||
<span>{item.email}</span>
|
||||
<span className='text-muted-foreground'>{t('userIdentifier')}</span>
|
||||
<span>{item.identifier}</span>
|
||||
</li>
|
||||
<li className='font-semibold'>
|
||||
<span className='text-muted-foreground'>{t('registrationTime')}</span>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -67,12 +67,14 @@ export default function Footer() {
|
||||
</nav>
|
||||
<div>
|
||||
<strong className='text-foreground'>{site.site_name}</strong> © All rights reserved.
|
||||
<Link href='/tos' className='ml-2 underline'>
|
||||
{t('tos')}
|
||||
</Link>
|
||||
<Link href='/privacy-policy' className='ml-2 underline'>
|
||||
{t('privacyPolicy')}
|
||||
</Link>
|
||||
<div>
|
||||
<Link href='/tos' className='underline'>
|
||||
{t('tos')}
|
||||
</Link>
|
||||
<Link href='/privacy-policy' className='ml-2 underline'>
|
||||
{t('privacyPolicy')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function Header() {
|
||||
{site.site_logo && (
|
||||
<Image src={site.site_logo} width={48} height={48} alt='logo' unoptimized />
|
||||
)}
|
||||
<span>{site.site_name}</span>
|
||||
<span className=''>{site.site_name}</span>
|
||||
</Link>
|
||||
);
|
||||
return (
|
||||
|
||||
+12
-15
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
import {
|
||||
CardCvcElement,
|
||||
CardExpiryElement,
|
||||
CardNumberElement,
|
||||
Elements,
|
||||
useElements,
|
||||
useStripe,
|
||||
} from '@stripe/react-stripe-js';
|
||||
import {
|
||||
loadStripe,
|
||||
PaymentIntentResult,
|
||||
StripeCardNumberElementOptions,
|
||||
StripeElementStyle,
|
||||
} from '@stripe/stripe-js';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Input } from '@workspace/ui/components/input';
|
||||
import { Label } from '@workspace/ui/components/label';
|
||||
import { CheckCircle } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { QRCodeCanvas } from 'qrcode.react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
interface StripePaymentProps {
|
||||
method: string;
|
||||
client_secret: string;
|
||||
publishable_key: string;
|
||||
}
|
||||
|
||||
interface CardPaymentFormProps {
|
||||
clientSecret: string;
|
||||
onError: (message: string) => void;
|
||||
}
|
||||
|
||||
const CardPaymentForm: React.FC<CardPaymentFormProps> = ({ clientSecret, onError }) => {
|
||||
const stripe = useStripe();
|
||||
const { theme, systemTheme } = useTheme();
|
||||
const elements = useElements();
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [succeeded, setSucceeded] = useState(false);
|
||||
const [errors, setErrors] = useState<{
|
||||
cardNumber?: string;
|
||||
cardExpiry?: string;
|
||||
cardCvc?: string;
|
||||
name?: string;
|
||||
}>({});
|
||||
const [cardholderName, setCardholderName] = useState('');
|
||||
const t = useTranslations('payment.stripe.card');
|
||||
|
||||
const currentTheme = theme === 'system' ? systemTheme : theme;
|
||||
const elementStyle: StripeElementStyle = {
|
||||
base: {
|
||||
'fontSize': '16px',
|
||||
'color': currentTheme === 'dark' ? '#fff' : '#000',
|
||||
'::placeholder': {
|
||||
color: '#aab7c4',
|
||||
},
|
||||
},
|
||||
invalid: {
|
||||
color: '#EF4444',
|
||||
iconColor: '#EF4444',
|
||||
},
|
||||
};
|
||||
|
||||
const elementOptions: StripeCardNumberElementOptions = {
|
||||
style: elementStyle,
|
||||
showIcon: true,
|
||||
};
|
||||
|
||||
const handleChange = (event: any, field: keyof typeof errors) => {
|
||||
if (event.error) {
|
||||
setErrors((prev) => ({ ...prev, [field]: event.error.message }));
|
||||
} else {
|
||||
setErrors((prev) => ({ ...prev, [field]: undefined }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!stripe || !elements) {
|
||||
onError(t('loading'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cardholderName.trim()) {
|
||||
setErrors((prev) => ({ ...prev, name: t('name_required') }));
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
|
||||
const cardNumber = elements.getElement(CardNumberElement);
|
||||
const cardExpiry = elements.getElement(CardExpiryElement);
|
||||
const cardCvc = elements.getElement(CardCvcElement);
|
||||
|
||||
if (!cardNumber || !cardExpiry || !cardCvc) {
|
||||
onError(t('element_error'));
|
||||
setProcessing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, {
|
||||
payment_method: {
|
||||
card: cardNumber,
|
||||
billing_details: {
|
||||
name: cardholderName,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
onError(error.message || t('payment_failed'));
|
||||
setProcessing(false);
|
||||
} else if (paymentIntent && paymentIntent.status === 'succeeded') {
|
||||
setSucceeded(true);
|
||||
setProcessing(false);
|
||||
} else {
|
||||
onError(t('processing'));
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
{succeeded ? (
|
||||
<div className='py-6 text-center'>
|
||||
<div className='mb-4 flex justify-center'>
|
||||
<CheckCircle className='h-12 w-12 text-green-500' />
|
||||
</div>
|
||||
<p className='text-xl font-medium'>{t('success_title')}</p>
|
||||
<p className='text-muted-foreground mt-2'>{t('success_message')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className='space-y-4'>
|
||||
{/* Cardholder Name */}
|
||||
<div className='space-y-1'>
|
||||
<Label htmlFor='cardholderName' className='text-sm font-medium'>
|
||||
{t('card_name')}
|
||||
</Label>
|
||||
<Input
|
||||
id='cardholderName'
|
||||
type='text'
|
||||
value={cardholderName}
|
||||
onChange={(e) => setCardholderName(e.target.value)}
|
||||
placeholder={t('name_placeholder')}
|
||||
className={errors.name ? 'border-destructive' : ''}
|
||||
/>
|
||||
{errors.name && <p className='text-destructive text-xs'>{errors.name}</p>}
|
||||
</div>
|
||||
|
||||
{/* Card Number */}
|
||||
<div className='space-y-1'>
|
||||
<Label htmlFor='cardNumber' className='text-sm font-medium'>
|
||||
{t('card_number')}
|
||||
</Label>
|
||||
<div className='relative'>
|
||||
<div
|
||||
className={`focus-within:border-primary focus-within:ring-primary rounded-md border p-3 focus-within:ring-1 ${errors.cardNumber ? 'border-red-500' : ''}`}
|
||||
>
|
||||
<CardNumberElement
|
||||
id='cardNumber'
|
||||
options={elementOptions}
|
||||
onChange={(e) => handleChange(e, 'cardNumber')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{errors.cardNumber && <p className='text-destructive text-xs'>{errors.cardNumber}</p>}
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-2 gap-4'>
|
||||
{/* Expiry Date */}
|
||||
<div className='space-y-1'>
|
||||
<Label htmlFor='cardExpiry' className='text-sm font-medium'>
|
||||
{t('expiry_date')}
|
||||
</Label>
|
||||
<div
|
||||
className={`focus-within:border-primary focus-within:ring-primary rounded-md border p-3 focus-within:ring-1 ${errors.cardExpiry ? 'border-red-500' : ''}`}
|
||||
>
|
||||
<CardExpiryElement
|
||||
id='cardExpiry'
|
||||
options={{ style: elementStyle }}
|
||||
onChange={(e) => handleChange(e, 'cardExpiry')}
|
||||
/>
|
||||
</div>
|
||||
{errors.cardExpiry && (
|
||||
<p className='text-destructive text-xs'>{errors.cardExpiry}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Security Code */}
|
||||
<div className='space-y-1'>
|
||||
<Label htmlFor='cardCvc' className='text-sm font-medium'>
|
||||
{t('security_code')}
|
||||
</Label>
|
||||
<div
|
||||
className={`focus-within:border-primary focus-within:ring-primary rounded-md border p-3 focus-within:ring-1 ${errors.cardCvc ? 'border-red-500' : ''}`}
|
||||
>
|
||||
<CardCvcElement
|
||||
id='cardCvc'
|
||||
options={{ style: elementStyle }}
|
||||
onChange={(e) => handleChange(e, 'cardCvc')}
|
||||
/>
|
||||
</div>
|
||||
{errors.cardCvc && <p className='text-destructive text-xs'>{errors.cardCvc}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-6 flex flex-col space-y-4'>
|
||||
<Button type='submit' disabled={processing || !stripe || !elements} className='w-full'>
|
||||
{processing ? t('processing_button') : t('pay_button')}
|
||||
</Button>
|
||||
<p className='text-muted-foreground text-center text-xs'>{t('secure_notice')}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const StripePayment: React.FC<StripePaymentProps> = ({
|
||||
method,
|
||||
client_secret,
|
||||
publishable_key,
|
||||
}) => {
|
||||
const stripePromise = useMemo(() => loadStripe(publishable_key), [publishable_key]);
|
||||
|
||||
return (
|
||||
<Elements stripe={stripePromise}>
|
||||
<CheckoutForm method={method} client_secret={client_secret} />
|
||||
</Elements>
|
||||
);
|
||||
};
|
||||
|
||||
const CheckoutForm: React.FC<Omit<StripePaymentProps, 'publishable_key'>> = ({
|
||||
client_secret,
|
||||
method,
|
||||
}) => {
|
||||
const stripe = useStripe();
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [qrCodeUrl, setQrCodeUrl] = useState<string | null>(null);
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
const t = useTranslations('payment.stripe');
|
||||
|
||||
const handleError = useCallback((message: string) => {
|
||||
setErrorMessage(message);
|
||||
setIsSubmitted(false);
|
||||
}, []);
|
||||
|
||||
const confirmPayment = useCallback(async (): Promise<PaymentIntentResult | null> => {
|
||||
if (!stripe) {
|
||||
handleError(t('card.loading'));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (method === 'alipay') {
|
||||
return await stripe.confirmAlipayPayment(
|
||||
client_secret,
|
||||
{ return_url: window.location.href },
|
||||
{ handleActions: false },
|
||||
);
|
||||
}
|
||||
if (method === 'wechat_pay') {
|
||||
return await stripe.confirmWechatPayPayment(
|
||||
client_secret,
|
||||
{
|
||||
payment_method_options: { wechat_pay: { client: 'web' } },
|
||||
},
|
||||
{ handleActions: false },
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, [client_secret, method, stripe, handleError, t]);
|
||||
|
||||
const autoSubmit = useCallback(async () => {
|
||||
if (isSubmitted || method === 'card') return;
|
||||
|
||||
setIsSubmitted(true);
|
||||
|
||||
try {
|
||||
const result = await confirmPayment();
|
||||
if (!result) return;
|
||||
|
||||
const { error, paymentIntent } = result;
|
||||
if (error) return handleError(error.message!);
|
||||
|
||||
if (paymentIntent?.status === 'requires_action') {
|
||||
const nextAction = paymentIntent.next_action as any;
|
||||
const qrUrl =
|
||||
method === 'alipay'
|
||||
? nextAction?.alipay_handle_redirect?.url
|
||||
: nextAction?.wechat_pay_display_qr_code?.image_url_svg;
|
||||
|
||||
setQrCodeUrl(qrUrl || null);
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(t('error'));
|
||||
}
|
||||
}, [confirmPayment, isSubmitted, handleError, method, t]);
|
||||
|
||||
useEffect(() => {
|
||||
autoSubmit();
|
||||
}, [autoSubmit]);
|
||||
|
||||
return method === 'card' ? (
|
||||
<div className='min-w-80 text-left'>
|
||||
<CardPaymentForm clientSecret={client_secret} onError={handleError} />
|
||||
</div>
|
||||
) : qrCodeUrl ? (
|
||||
<>
|
||||
<QRCodeCanvas
|
||||
value={qrCodeUrl}
|
||||
size={208}
|
||||
imageSettings={{
|
||||
src: `/payment/${method}.svg`,
|
||||
width: 24,
|
||||
height: 24,
|
||||
excavate: true,
|
||||
}}
|
||||
/>
|
||||
<p className='text-muted-foreground mt-4 text-center'>{t(`qrcode.${method}`)}</p>
|
||||
</>
|
||||
) : (
|
||||
errorMessage
|
||||
);
|
||||
};
|
||||
|
||||
export default StripePayment;
|
||||
@@ -27,11 +27,7 @@ const DurationSelector: React.FC<DurationSelectorProps> = ({
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const DurationOption: React.FC<{ value: string; label: string; discount?: number }> = ({
|
||||
value,
|
||||
label,
|
||||
discount,
|
||||
}) => (
|
||||
const DurationOption: React.FC<{ value: string; label: string }> = ({ value, label }) => (
|
||||
<div className='relative'>
|
||||
<RadioGroupItem value={value} id={value} className='peer sr-only' />
|
||||
<Label
|
||||
@@ -39,11 +35,14 @@ const DurationSelector: React.FC<DurationSelectorProps> = ({
|
||||
className='border-muted bg-popover hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary relative flex h-full flex-col items-center justify-center gap-2 rounded-md border-2 p-2'
|
||||
>
|
||||
{label}
|
||||
{discount && <Badge variant='destructive'>-{discount}%</Badge>}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 查找当前选中项的折扣信息
|
||||
const currentDiscount = discounts?.find((item) => item.quantity === quantity)?.discount;
|
||||
const discountPercentage = currentDiscount ? 100 - currentDiscount : 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='font-semibold'>{t('purchaseDuration')}</div>
|
||||
@@ -58,10 +57,19 @@ const DurationSelector: React.FC<DurationSelectorProps> = ({
|
||||
key={item.quantity}
|
||||
value={String(item.quantity)}
|
||||
label={`${item.quantity} / ${t(unitTime)}`}
|
||||
discount={100 - item.discount}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
<div className='flex items-center justify-between'>
|
||||
<span className='text-muted-foreground text-sm'>{t('discountInfo')}:</span>
|
||||
{discountPercentage > 0 ? (
|
||||
<Badge variant='destructive' className='h-6 text-sm'>
|
||||
-{discountPercentage}% {t('discount')}
|
||||
</Badge>
|
||||
) : (
|
||||
<span className='text-muted-foreground h-6 text-sm'>--</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,49 +1,68 @@
|
||||
'use client';
|
||||
|
||||
import { getAvailablePaymentMethods } from '@/services/user/payment';
|
||||
import { getAvailablePaymentMethods } from '@/services/user/portal';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Label } from '@workspace/ui/components/label';
|
||||
import { RadioGroup, RadioGroupItem } from '@workspace/ui/components/radio-group';
|
||||
import { cn } from '@workspace/ui/lib/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
import React, { memo } from 'react';
|
||||
|
||||
interface PaymentMethodsProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
balance?: boolean;
|
||||
}
|
||||
|
||||
const PaymentMethods: React.FC<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]?.id) onChange(methods[0]?.id);
|
||||
if (balance) return methods;
|
||||
return methods.filter((item) => item.id !== -1);
|
||||
},
|
||||
});
|
||||
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={String(value)}
|
||||
onValueChange={(val) => {
|
||||
console.log(val);
|
||||
onChange(Number(val));
|
||||
}}
|
||||
>
|
||||
{data?.map((item) => (
|
||||
<div key={item.mark}>
|
||||
<RadioGroupItem value={item.mark} id={item.mark} className='peer sr-only' />
|
||||
<div key={item.id} className='relative'>
|
||||
<RadioGroupItem
|
||||
value={String(item.id)}
|
||||
id={String(item.id)}
|
||||
className='absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0'
|
||||
/>
|
||||
<Label
|
||||
htmlFor={item.mark}
|
||||
className='border-muted bg-popover hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary flex flex-col items-center justify-between rounded-md border-2 py-2'
|
||||
htmlFor={String(item.id)}
|
||||
className={cn(
|
||||
'border-muted bg-popover hover:bg-accent hover:text-accent-foreground flex flex-col items-center justify-between rounded-md border-2 py-2',
|
||||
String(value) === String(item.id) ? 'border-primary' : '',
|
||||
)}
|
||||
>
|
||||
<div className='mb-3 size-12'>
|
||||
<Image
|
||||
src={item.icon || `/payment/${item.mark}.svg`}
|
||||
src={item.icon || `/payment/balance.svg`}
|
||||
width={48}
|
||||
height={48}
|
||||
alt={item.name || t(`methods.${item.mark}`)}
|
||||
alt={item.name}
|
||||
/>
|
||||
</div>
|
||||
<span className='w-full overflow-hidden text-ellipsis whitespace-nowrap text-center'>
|
||||
{item.name || t(`methods.${item.mark}`)}
|
||||
{item.name}
|
||||
</span>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import CouponInput from '@/components/subscribe/coupon-input';
|
||||
import DurationSelector from '@/components/subscribe/duration-selector';
|
||||
import PaymentMethods from '@/components/subscribe/payment-methods';
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { checkoutOrder, preCreateOrder, purchase } from '@/services/user/order';
|
||||
import { preCreateOrder, purchase } from '@/services/user/order';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Card, CardContent } from '@workspace/ui/components/card';
|
||||
@@ -29,7 +29,7 @@ export default function Purchase({ subscribe, setSubscribe }: Readonly<PurchaseP
|
||||
const [params, setParams] = useState<Partial<API.PurchaseOrderRequest>>({
|
||||
quantity: 1,
|
||||
subscribe_id: 0,
|
||||
payment: 'balance',
|
||||
payment: -1,
|
||||
coupon: '',
|
||||
});
|
||||
const [loading, startTransition] = useTransition();
|
||||
@@ -69,20 +69,11 @@ export default function Purchase({ subscribe, setSubscribe }: Readonly<PurchaseP
|
||||
const response = await purchase(params as API.PurchaseOrderRequest);
|
||||
const orderNo = response.data.data?.order_no;
|
||||
if (orderNo) {
|
||||
const { data } = await checkoutOrder({
|
||||
orderNo,
|
||||
returnUrl: `${window.location.origin}/payment?order_no=${orderNo}`,
|
||||
});
|
||||
const type = data.data?.type;
|
||||
const checkout_url = data.data?.checkout_url;
|
||||
if (type === 'link') {
|
||||
window.location.href = checkout_url!;
|
||||
}
|
||||
getUserInfo();
|
||||
router.push(`/payment?order_no=${orderNo}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
/* empty */
|
||||
}
|
||||
});
|
||||
}, [params, router, getUserInfo]);
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { checkoutOrder, recharge } from '@/services/user/order';
|
||||
import { getAvailablePaymentMethods } from '@/services/user/payment';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { recharge } from '@/services/user/order';
|
||||
import { Button, ButtonProps } from '@workspace/ui/components/button';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -13,15 +11,13 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@workspace/ui/components/dialog';
|
||||
import { Label } from '@workspace/ui/components/label';
|
||||
import { RadioGroup, RadioGroupItem } from '@workspace/ui/components/radio-group';
|
||||
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
|
||||
import { unitConversion } from '@workspace/ui/utils';
|
||||
import { LoaderCircle } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState, useTransition } from 'react';
|
||||
import { useState, useTransition } from 'react';
|
||||
import PaymentMethods from './payment-methods';
|
||||
|
||||
export default function Recharge(props: Readonly<ButtonProps>) {
|
||||
const t = useTranslations('subscribe');
|
||||
@@ -34,27 +30,9 @@ export default function Recharge(props: Readonly<ButtonProps>) {
|
||||
|
||||
const [params, setParams] = useState<API.RechargeOrderRequest>({
|
||||
amount: 0,
|
||||
payment: '',
|
||||
payment: 1,
|
||||
});
|
||||
|
||||
const { data: paymentMethods } = useQuery({
|
||||
enabled: open,
|
||||
queryKey: ['getAvailablePaymentMethods'],
|
||||
queryFn: async () => {
|
||||
const { data } = await getAvailablePaymentMethods();
|
||||
return data.data?.list || [];
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (paymentMethods?.length) {
|
||||
setParams((prev) => ({
|
||||
...prev,
|
||||
payment: paymentMethods.find((item) => item.mark !== 'balance')?.mark as string,
|
||||
}));
|
||||
}
|
||||
}, [paymentMethods]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
@@ -86,43 +64,10 @@ export default function Recharge(props: Readonly<ButtonProps>) {
|
||||
suffix={currency.currency_unit}
|
||||
/>
|
||||
</div>
|
||||
<div className='font-semibold'>{t('paymentMethod')}</div>
|
||||
<RadioGroup
|
||||
className='grid grid-cols-5 gap-2'
|
||||
<PaymentMethods
|
||||
value={params.payment}
|
||||
onValueChange={(value) => {
|
||||
setParams({
|
||||
...params,
|
||||
payment: value,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{paymentMethods
|
||||
?.filter((item) => item.mark !== 'balance')
|
||||
?.map((item) => {
|
||||
return (
|
||||
<div key={item.mark}>
|
||||
<RadioGroupItem value={item.mark} id={item.mark} className='peer sr-only' />
|
||||
<Label
|
||||
htmlFor={item.mark}
|
||||
className='border-muted bg-popover hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary flex flex-col items-center justify-between rounded-md border-2 py-2'
|
||||
>
|
||||
<div className='mb-3 size-12'>
|
||||
<Image
|
||||
src={item.icon || `/payment/${item.mark}.svg`}
|
||||
width={48}
|
||||
height={48}
|
||||
alt={item.name!}
|
||||
/>
|
||||
</div>
|
||||
<span className='w-full overflow-hidden text-ellipsis whitespace-nowrap text-center'>
|
||||
{item.name || t(`methods.${item.mark}`)}
|
||||
</span>
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</RadioGroup>
|
||||
onChange={(value) => setParams({ ...params, payment: value })}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className='fixed bottom-0 left-0 w-full rounded-none md:relative md:mt-6'
|
||||
@@ -133,15 +78,6 @@ 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({
|
||||
orderNo,
|
||||
returnUrl: `${window.location.origin}/payment?order_no=${orderNo}`,
|
||||
});
|
||||
const type = data.data?.type;
|
||||
const checkout_url = data.data?.checkout_url;
|
||||
if (type === 'link') {
|
||||
window.location.href = checkout_url!;
|
||||
}
|
||||
router.push(`/payment?order_no=${orderNo}`);
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import CouponInput from '@/components/subscribe/coupon-input';
|
||||
import DurationSelector from '@/components/subscribe/duration-selector';
|
||||
import PaymentMethods from '@/components/subscribe/payment-methods';
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { checkoutOrder, preCreateOrder, renewal } from '@/services/user/order';
|
||||
import { preCreateOrder, renewal } from '@/services/user/order';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import { Card, CardContent } from '@workspace/ui/components/card';
|
||||
@@ -35,7 +35,7 @@ export default function Renewal({ id, subscribe }: Readonly<RenewalProps>) {
|
||||
const router = useRouter();
|
||||
const [params, setParams] = useState<Partial<API.RenewalOrderRequest>>({
|
||||
quantity: 1,
|
||||
payment: 'balance',
|
||||
payment: -1,
|
||||
coupon: '',
|
||||
user_subscribe_id: id,
|
||||
});
|
||||
@@ -77,20 +77,11 @@ 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({
|
||||
orderNo,
|
||||
returnUrl: `${window.location.origin}/payment?order_no=${orderNo}`,
|
||||
});
|
||||
const type = data.data?.type;
|
||||
const checkout_url = data.data?.checkout_url;
|
||||
if (type === 'link') {
|
||||
window.location.href = checkout_url!;
|
||||
}
|
||||
getUserInfo();
|
||||
router.push(`/payment?order_no=${orderNo}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
/* empty */
|
||||
}
|
||||
});
|
||||
}, [params, router, getUserInfo]);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
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 { Button } from '@workspace/ui/components/button';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -28,7 +28,7 @@ export default function ResetTraffic({ id, replacement }: Readonly<ResetTrafficP
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const router = useRouter();
|
||||
const [params, setParams] = useState<API.ResetTrafficOrderRequest>({
|
||||
payment: 'balance',
|
||||
payment: -1,
|
||||
user_subscribe_id: id,
|
||||
});
|
||||
const [loading, startTransition] = useTransition();
|
||||
@@ -84,15 +84,6 @@ 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({
|
||||
orderNo,
|
||||
returnUrl: `${window.location.origin}/payment?order_no=${orderNo}`,
|
||||
});
|
||||
const type = data.data?.type;
|
||||
const checkout_url = data.data?.checkout_url;
|
||||
if (type === 'link') {
|
||||
window.location.href = checkout_url!;
|
||||
}
|
||||
getUserInfo();
|
||||
router.push(`/payment?order_no=${orderNo}`);
|
||||
}
|
||||
|
||||
@@ -4,13 +4,11 @@ import { navs } from '@/config/navs';
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { Logout } from '@/utils/common';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@workspace/ui/components/avatar';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@workspace/ui/components/dropdown-menu';
|
||||
@@ -27,37 +25,61 @@ export function UserNav() {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size='icon' variant='default'>
|
||||
<Avatar className='size-8'>
|
||||
<AvatarImage alt={user?.avatar ?? ''} src={user?.avatar ?? ''} />
|
||||
<AvatarFallback className='rounded-none bg-transparent'>
|
||||
<div className='bg-background hover:bg-accent flex cursor-pointer items-center gap-2 rounded-full border px-2 py-1.5 transition-colors duration-200'>
|
||||
<Avatar className='h-6 w-6'>
|
||||
<AvatarImage
|
||||
alt={user?.avatar ?? ''}
|
||||
src={user?.auth_methods?.[0]?.auth_identifier ?? ''}
|
||||
className='object-cover'
|
||||
/>
|
||||
<AvatarFallback className='from-primary/90 to-primary text-background bg-gradient-to-br font-medium'>
|
||||
{user?.auth_methods?.[0]?.auth_identifier.toUpperCase().charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
<span className='max-w-[40px] truncate text-sm sm:max-w-[100px]'>
|
||||
{user?.auth_methods?.[0]?.auth_identifier.split('@')[0]}
|
||||
</span>
|
||||
<Icon icon='lucide:chevron-down' className='text-muted-foreground size-4' />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent forceMount align='end' className='w-56'>
|
||||
<DropdownMenuLabel className='font-normal'>
|
||||
<div className='flex flex-col space-y-1'>
|
||||
<p className='text-muted-foreground text-xs leading-none'>ID: {user?.id}</p>
|
||||
<DropdownMenuContent forceMount align='end' className='w-64'>
|
||||
<div className='flex items-center justify-start gap-2 p-2'>
|
||||
<Avatar className='h-10 w-10'>
|
||||
<AvatarImage
|
||||
alt={user?.avatar ?? ''}
|
||||
src={user?.avatar ?? ''}
|
||||
className='object-cover'
|
||||
/>
|
||||
<AvatarFallback className='from-primary/90 to-primary text-background bg-gradient-to-br'>
|
||||
{user?.auth_methods?.[0]?.auth_identifier.toUpperCase().charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className='flex flex-col space-y-0.5'>
|
||||
<p className='text-sm font-medium leading-none'>
|
||||
{user?.auth_methods?.[0]?.auth_identifier.split('@')[0]}
|
||||
</p>
|
||||
<p className='text-muted-foreground text-xs'>
|
||||
{user?.auth_methods?.[0]?.auth_identifier}
|
||||
</p>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
{navs.map((nav) => (
|
||||
<DropdownMenuGroup key={nav.title}>
|
||||
{/* {nav.items && <DropdownMenuLabel>{t(nav.title)}</DropdownMenuLabel>} */}
|
||||
{(nav.items || [nav]).map((item) => (
|
||||
<DropdownMenuItem
|
||||
key={item.title}
|
||||
onClick={() => {
|
||||
router.push(`${item.url}`);
|
||||
}}
|
||||
className='flex cursor-pointer items-center gap-2 py-2'
|
||||
>
|
||||
<Icon className='mr-2 size-4 flex-none' icon={item.icon!} />
|
||||
<span className='truncate'>{t(item.title)}</span>
|
||||
<Icon className='text-muted-foreground size-4 flex-none' icon={item.icon!} />
|
||||
<span className='flex-grow truncate'>{t(item.title)}</span>
|
||||
<Icon
|
||||
icon='lucide:chevron-right'
|
||||
className='text-muted-foreground size-4 opacity-50'
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
@@ -68,10 +90,10 @@ export function UserNav() {
|
||||
Logout();
|
||||
setUser();
|
||||
}}
|
||||
className='text-destructive focus:text-destructive flex cursor-pointer items-center gap-2 py-2'
|
||||
>
|
||||
<Icon className='mr-2 size-4 flex-none' icon='uil:exit' />
|
||||
|
||||
{t('logout')}
|
||||
<Icon className='size-4 flex-none' icon='uil:exit' />
|
||||
<span className='flex-grow'>{t('logout')}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -9,6 +9,8 @@ export const NEXT_PUBLIC_DEFAULT_LANGUAGE =
|
||||
|
||||
export const NEXT_PUBLIC_SITE_URL = env('NEXT_PUBLIC_SITE_URL') ?? process.env.NEXT_PUBLIC_SITE_URL;
|
||||
export const NEXT_PUBLIC_API_URL = env('NEXT_PUBLIC_API_URL') ?? process.env.NEXT_PUBLIC_API_URL;
|
||||
export const NEXT_PUBLIC_CDN_URL =
|
||||
env('NEXT_PUBLIC_CDN_URL') || process.env.NEXT_PUBLIC_CDN_URL || 'https://fastly.jsdelivr.net';
|
||||
|
||||
export const NEXT_PUBLIC_DEFAULT_USER_EMAIL =
|
||||
env('NEXT_PUBLIC_DEFAULT_USER_EMAIL') ?? process.env.NEXT_PUBLIC_DEFAULT_USER_EMAIL;
|
||||
|
||||
@@ -69,6 +69,7 @@ export const useGlobalStore = create<GlobalStore>((set, get) => ({
|
||||
verify_code_interval: 60,
|
||||
},
|
||||
oauth_methods: [],
|
||||
web_ad: false,
|
||||
},
|
||||
user: undefined,
|
||||
setCommon: (common) =>
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"inviteRecords": "Záznamy o pozvání",
|
||||
"registrationTime": "Čas registrace",
|
||||
"totalCommission": "Celková provize",
|
||||
"userEmail": "Uživatelský e-mail"
|
||||
"userIdentifier": "Identifikátor uživatele"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"mySubscriptions": "Moje předplatné",
|
||||
"nextResetDays": "Příští reset/den",
|
||||
"noLimit": "Bez omezení",
|
||||
"noReset": "Žádné obnovení",
|
||||
"prompt": "Výzva",
|
||||
"purchaseSubscription": "Zakoupit předplatné",
|
||||
"qrCode": "QR kód",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"stripe": {
|
||||
"card": {
|
||||
"card_name": "Jméno držitele karty",
|
||||
"card_number": "Číslo karty",
|
||||
"element_error": "Nelze získat prvek karty.",
|
||||
"expiry_date": "Datum expirace",
|
||||
"loading": "Stripe.js není načten. Zkuste to prosím znovu později.",
|
||||
"name_placeholder": "Zadejte jméno držitele karty",
|
||||
"name_required": "Jméno držitele karty je povinné",
|
||||
"pay_button": "Zaplatit nyní",
|
||||
"payment_failed": "Platba se nezdařila, zkuste to prosím znovu.",
|
||||
"processing": "Platba se zpracovává, prosím zkontrolujte výsledek později.",
|
||||
"processing_button": "Zpracovává se...",
|
||||
"secure_notice": "Vaše platební informace jsou bezpečně šifrovány",
|
||||
"security_code": "Bezpečnostní kód",
|
||||
"success_message": "Děkujeme za vaši platbu!",
|
||||
"success_title": "Platba byla úspěšná"
|
||||
},
|
||||
"error": "Došlo k neočekávané chybě",
|
||||
"qrcode": {
|
||||
"alipay": "Naskenujte pomocí Alipay pro platbu",
|
||||
"wechat_pay": "Naskenujte pomocí WeChat pro platbu"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@
|
||||
"connectionSpeed": "Rychlost připojení",
|
||||
"productDetail": "Detaily produktu"
|
||||
},
|
||||
"discount": "Sleva",
|
||||
"discountInfo": "Informace o slevě",
|
||||
"enterAmount": "Zadejte částku dobití",
|
||||
"enterCoupon": "Zadejte kód kupónu",
|
||||
"methods": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"amount": "Částka",
|
||||
"assetOverview": "Přehled aktiv",
|
||||
"balance": "zůstatek",
|
||||
"commission": "Provize",
|
||||
"createdAt": "čas",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"inviteRecords": "Einladungsprotokolle",
|
||||
"registrationTime": "Registrierungszeit",
|
||||
"totalCommission": "Gesamtprovision",
|
||||
"userEmail": "Benutzer-E-Mail"
|
||||
"userIdentifier": "Benutzerkennung"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"mySubscriptions": "Meine Abonnements",
|
||||
"nextResetDays": "Nächster Reset/Tage",
|
||||
"noLimit": "Kein Limit",
|
||||
"noReset": "Kein Zurücksetzen",
|
||||
"prompt": "Aufforderung",
|
||||
"purchaseSubscription": "Abonnement kaufen",
|
||||
"qrCode": "QR-Code",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"stripe": {
|
||||
"card": {
|
||||
"card_name": "Karteninhabername",
|
||||
"card_number": "Kartennummer",
|
||||
"element_error": "Konnte das Kartenelement nicht abrufen.",
|
||||
"expiry_date": "Ablaufdatum",
|
||||
"loading": "Stripe.js ist nicht geladen. Bitte versuchen Sie es später erneut.",
|
||||
"name_placeholder": "Geben Sie den Namen des Karteninhabers ein",
|
||||
"name_required": "Der Name des Karteninhabers ist erforderlich",
|
||||
"pay_button": "Jetzt bezahlen",
|
||||
"payment_failed": "Zahlung fehlgeschlagen, bitte versuchen Sie es erneut.",
|
||||
"processing": "Zahlung wird verarbeitet, bitte überprüfen Sie das Ergebnis später.",
|
||||
"processing_button": "Wird verarbeitet...",
|
||||
"secure_notice": "Ihre Zahlungsinformationen sind sicher verschlüsselt",
|
||||
"security_code": "Sicherheitscode",
|
||||
"success_message": "Vielen Dank für Ihre Zahlung!",
|
||||
"success_title": "Zahlung Erfolgreich"
|
||||
},
|
||||
"error": "Ein unerwarteter Fehler ist aufgetreten",
|
||||
"qrcode": {
|
||||
"alipay": "Mit Alipay scannen, um zu bezahlen",
|
||||
"wechat_pay": "Mit WeChat scannen, um zu bezahlen"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@
|
||||
"connectionSpeed": "Verbindungsgeschwindigkeit",
|
||||
"productDetail": "Produktdetails"
|
||||
},
|
||||
"discount": "Rabatt",
|
||||
"discountInfo": "Rabattinformationen",
|
||||
"enterAmount": "Geben Sie den Aufladebetrag ein",
|
||||
"enterCoupon": "Gutscheincode eingeben",
|
||||
"methods": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"amount": "Betrag",
|
||||
"assetOverview": "Vermögensübersicht",
|
||||
"balance": "Kontostand",
|
||||
"commission": "Provision",
|
||||
"createdAt": "Zeit",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"inviteRecords": "Invite Records",
|
||||
"registrationTime": "Registration Time",
|
||||
"totalCommission": "Total Commission",
|
||||
"userEmail": "User Email"
|
||||
"userIdentifier": "User Identifier"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"mySubscriptions": "My Subscriptions",
|
||||
"nextResetDays": "Next Reset in Days",
|
||||
"noLimit": "No Limit",
|
||||
"noReset": "No Reset",
|
||||
"prompt": "Prompt",
|
||||
"purchaseSubscription": "Purchase Subscription",
|
||||
"qrCode": "QR Code",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"stripe": {
|
||||
"card": {
|
||||
"card_name": "Cardholder Name",
|
||||
"card_number": "Card Number",
|
||||
"element_error": "Unable to get card element.",
|
||||
"expiry_date": "Expiry Date",
|
||||
"loading": "Stripe.js is not loaded. Please try again later.",
|
||||
"name_placeholder": "Enter cardholder name",
|
||||
"name_required": "Cardholder name is required",
|
||||
"pay_button": "Pay Now",
|
||||
"payment_failed": "Payment failed, please try again.",
|
||||
"processing": "Payment processing, please check the result later.",
|
||||
"processing_button": "Processing...",
|
||||
"secure_notice": "Your payment information is securely encrypted",
|
||||
"security_code": "Security Code",
|
||||
"success_message": "Thank you for your payment!",
|
||||
"success_title": "Payment Successful"
|
||||
},
|
||||
"error": "An unexpected error occurred",
|
||||
"qrcode": {
|
||||
"alipay": "Scan with Alipay to pay",
|
||||
"wechat_pay": "Scan with WeChat to pay"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@
|
||||
"connectionSpeed": "Connection Speed",
|
||||
"productDetail": "Product Details"
|
||||
},
|
||||
"discount": "Discount",
|
||||
"discountInfo": "Discount Info",
|
||||
"enterAmount": "Enter recharge amount",
|
||||
"enterCoupon": "Enter Coupon Code",
|
||||
"methods": {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"amount": "Amount",
|
||||
"assetOverview": "Asset Overview",
|
||||
"balance": "Balance",
|
||||
"commission": "Commission",
|
||||
"createdAt": "Time",
|
||||
"giftAmount": "Girt Amount",
|
||||
"totalAssets": "Asset overview",
|
||||
"totalAssets": "Total Assets",
|
||||
"type": {
|
||||
"0": "Type",
|
||||
"1": "Recharge",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"inviteRecords": "Registros de invitación",
|
||||
"registrationTime": "Hora de registro",
|
||||
"totalCommission": "Comisión total",
|
||||
"userEmail": "Correo electrónico del usuario"
|
||||
"userIdentifier": "Identificador de usuario"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"mySubscriptions": "Mis suscripciones",
|
||||
"nextResetDays": "Próximo reinicio/días",
|
||||
"noLimit": "Sin límite",
|
||||
"noReset": "Sin Reinicio",
|
||||
"prompt": "sugerencia",
|
||||
"purchaseSubscription": "Comprar suscripción",
|
||||
"qrCode": "Código QR",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"stripe": {
|
||||
"card": {
|
||||
"card_name": "Nombre del titular de la tarjeta",
|
||||
"card_number": "Número de tarjeta",
|
||||
"element_error": "No se pudo obtener el elemento de la tarjeta.",
|
||||
"expiry_date": "Fecha de caducidad",
|
||||
"loading": "Stripe.js no está cargado. Por favor, inténtalo de nuevo más tarde.",
|
||||
"name_placeholder": "Introduce el nombre del titular de la tarjeta",
|
||||
"name_required": "Se requiere el nombre del titular de la tarjeta",
|
||||
"pay_button": "Pagar Ahora",
|
||||
"payment_failed": "El pago ha fallado, por favor inténtalo de nuevo.",
|
||||
"processing": "Procesando el pago, por favor verifica el resultado más tarde.",
|
||||
"processing_button": "Procesando...",
|
||||
"secure_notice": "Tu información de pago está encriptada de forma segura",
|
||||
"security_code": "Código de seguridad",
|
||||
"success_message": "¡Gracias por tu pago!",
|
||||
"success_title": "Pago Exitoso"
|
||||
},
|
||||
"error": "Ocurrió un error inesperado",
|
||||
"qrcode": {
|
||||
"alipay": "Escanea con Alipay para pagar",
|
||||
"wechat_pay": "Escanea con WeChat para pagar"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@
|
||||
"connectionSpeed": "Velocidad de conexión",
|
||||
"productDetail": "Detalles del producto"
|
||||
},
|
||||
"discount": "Descuento",
|
||||
"discountInfo": "Información del Descuento",
|
||||
"enterAmount": "Ingrese el monto de recarga",
|
||||
"enterCoupon": "Introduce el código de cupón",
|
||||
"methods": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"amount": "Cantidad",
|
||||
"assetOverview": "Descripción del activo",
|
||||
"balance": "saldo",
|
||||
"commission": "Comisión",
|
||||
"createdAt": "hora",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"inviteRecords": "Registros de invitación",
|
||||
"registrationTime": "Hora de registro",
|
||||
"totalCommission": "Comisión total",
|
||||
"userEmail": "Correo electrónico del usuario"
|
||||
"userIdentifier": "Identificador de Usuario"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"mySubscriptions": "Mis suscripciones",
|
||||
"nextResetDays": "Próximo reinicio/días",
|
||||
"noLimit": "Sin Límite",
|
||||
"noReset": "Sin Reinicio",
|
||||
"prompt": "Sugerencia",
|
||||
"purchaseSubscription": "Comprar suscripción",
|
||||
"qrCode": "Código QR",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"stripe": {
|
||||
"card": {
|
||||
"card_name": "Nombre del titular de la tarjeta",
|
||||
"card_number": "Número de tarjeta",
|
||||
"element_error": "No se pudo obtener el elemento de la tarjeta.",
|
||||
"expiry_date": "Fecha de expiración",
|
||||
"loading": "Stripe.js no se ha cargado. Por favor, inténtalo de nuevo más tarde.",
|
||||
"name_placeholder": "Ingresa el nombre del titular de la tarjeta",
|
||||
"name_required": "Se requiere el nombre del titular de la tarjeta",
|
||||
"pay_button": "Pagar Ahora",
|
||||
"payment_failed": "El pago falló, por favor inténtalo de nuevo.",
|
||||
"processing": "Procesando el pago, por favor verifica el resultado más tarde.",
|
||||
"processing_button": "Procesando...",
|
||||
"secure_notice": "Tu información de pago está encriptada de forma segura",
|
||||
"security_code": "Código de seguridad",
|
||||
"success_message": "¡Gracias por tu pago!",
|
||||
"success_title": "Pago Exitoso"
|
||||
},
|
||||
"error": "Ocurrió un error inesperado",
|
||||
"qrcode": {
|
||||
"alipay": "Escanea con Alipay para pagar",
|
||||
"wechat_pay": "Escanea con WeChat para pagar"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@
|
||||
"connectionSpeed": "Velocidad de conexión",
|
||||
"productDetail": "Detalles del producto"
|
||||
},
|
||||
"discount": "Descuento",
|
||||
"discountInfo": "Información del Descuento",
|
||||
"enterAmount": "Ingresa el monto de recarga",
|
||||
"enterCoupon": "Ingresa el código de cupón",
|
||||
"methods": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"amount": "Monto",
|
||||
"assetOverview": "Descripción del Activo",
|
||||
"balance": "saldo",
|
||||
"commission": "Comisión",
|
||||
"createdAt": "Hora",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"inviteRecords": "سوابق دعوت",
|
||||
"registrationTime": "زمان ثبتنام",
|
||||
"totalCommission": "کمیسیون کل",
|
||||
"userEmail": "ایمیل کاربر"
|
||||
"userIdentifier": "شناسه کاربر"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"mySubscriptions": "اشتراکهای من",
|
||||
"nextResetDays": "روزهای باقیمانده تا بازنشانی بعدی",
|
||||
"noLimit": "بدون محدودیت",
|
||||
"noReset": "عدم بازنشانی",
|
||||
"prompt": "پیشنهاد",
|
||||
"purchaseSubscription": "خرید اشتراک",
|
||||
"qrCode": "کد QR",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"stripe": {
|
||||
"card": {
|
||||
"card_name": "نام دارنده کارت",
|
||||
"card_number": "شماره کارت",
|
||||
"element_error": "عدم توانایی در دریافت عنصر کارت.",
|
||||
"expiry_date": "تاریخ انقضا",
|
||||
"loading": "Stripe.js بارگذاری نشده است. لطفاً بعداً دوباره تلاش کنید.",
|
||||
"name_placeholder": "نام دارنده کارت را وارد کنید",
|
||||
"name_required": "نام دارنده کارت الزامی است",
|
||||
"pay_button": "همین حالا پرداخت کنید",
|
||||
"payment_failed": "پرداخت ناموفق بود، لطفاً دوباره تلاش کنید.",
|
||||
"processing": "پرداخت در حال پردازش است، لطفاً نتیجه را بعداً بررسی کنید.",
|
||||
"processing_button": "در حال پردازش...",
|
||||
"secure_notice": "اطلاعات پرداخت شما به صورت ایمن رمزگذاری شده است",
|
||||
"security_code": "کد امنیتی",
|
||||
"success_message": "از پرداخت شما متشکریم!",
|
||||
"success_title": "پرداخت موفق"
|
||||
},
|
||||
"error": "یک خطای غیرمنتظره رخ داده است",
|
||||
"qrcode": {
|
||||
"alipay": "برای پرداخت با Alipay اسکن کنید",
|
||||
"wechat_pay": "برای پرداخت با WeChat اسکن کنید"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@
|
||||
"connectionSpeed": "سرعت اتصال",
|
||||
"productDetail": "جزئیات محصول"
|
||||
},
|
||||
"discount": "تخفیف",
|
||||
"discountInfo": "اطلاعات تخفیف",
|
||||
"enterAmount": "مبلغ شارژ را وارد کنید",
|
||||
"enterCoupon": "کد تخفیف را وارد کنید",
|
||||
"methods": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"amount": "مقدار",
|
||||
"assetOverview": "بررسی دارایی",
|
||||
"balance": "تعادل",
|
||||
"commission": "کمیسیون",
|
||||
"createdAt": "زمان",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"inviteRecords": "Kutsuhistoria",
|
||||
"registrationTime": "Rekisteröintiaika",
|
||||
"totalCommission": "Kokonaisprovisio",
|
||||
"userEmail": "Käyttäjän sähköposti"
|
||||
"userIdentifier": "Käyttäjän tunnus"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"mySubscriptions": "Omat tilaukset",
|
||||
"nextResetDays": "Seuraava nollaus/päivää",
|
||||
"noLimit": "Ei rajoitusta",
|
||||
"noReset": "Ei nollata",
|
||||
"prompt": "kehotus",
|
||||
"purchaseSubscription": "Osta tilaus",
|
||||
"qrCode": "QR-koodi",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"stripe": {
|
||||
"card": {
|
||||
"card_name": "Kortinhaltijan nimi",
|
||||
"card_number": "Korttinumero",
|
||||
"element_error": "Korttielementtiä ei voitu saada.",
|
||||
"expiry_date": "Voimassaoloaika",
|
||||
"loading": "Stripe.js ei ole ladattu. Yritä myöhemmin uudelleen.",
|
||||
"name_placeholder": "Syötä kortinhaltijan nimi",
|
||||
"name_required": "Kortinhaltijan nimi on pakollinen",
|
||||
"pay_button": "Maksa nyt",
|
||||
"payment_failed": "Maksu epäonnistui, yritä uudelleen.",
|
||||
"processing": "Maksua käsitellään, tarkista tulos myöhemmin.",
|
||||
"processing_button": "Käsitellään...",
|
||||
"secure_notice": "Maksutietosi on salattu turvallisesti",
|
||||
"security_code": "Turvakoodi",
|
||||
"success_message": "Kiitos maksustasi!",
|
||||
"success_title": "Maksu onnistui"
|
||||
},
|
||||
"error": "Odottamaton virhe tapahtui",
|
||||
"qrcode": {
|
||||
"alipay": "Skannaa Alipaylla maksamista varten",
|
||||
"wechat_pay": "Skannaa WeChatilla maksamista varten"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@
|
||||
"connectionSpeed": "Yhteysnopeus",
|
||||
"productDetail": "Tuotteen tiedot"
|
||||
},
|
||||
"discount": "Alennus",
|
||||
"discountInfo": "Alennustiedot",
|
||||
"enterAmount": "Syötä latausmäärä",
|
||||
"enterCoupon": "Syötä alennuskoodi",
|
||||
"methods": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"amount": "Määrä",
|
||||
"assetOverview": "Omaisuuden yleiskatsaus",
|
||||
"balance": "Saldo",
|
||||
"commission": "Komissio",
|
||||
"createdAt": "Aika",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"inviteRecords": "Historique des invitations",
|
||||
"registrationTime": "Heure d'inscription",
|
||||
"totalCommission": "Commission totale",
|
||||
"userEmail": "Adresse e-mail de l'utilisateur"
|
||||
"userIdentifier": "Identifiant de l'utilisateur"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"mySubscriptions": "Mes abonnements",
|
||||
"nextResetDays": "Prochain réinitialisation/jours",
|
||||
"noLimit": "Pas de limite",
|
||||
"noReset": "Pas de réinitialisation",
|
||||
"prompt": "invite",
|
||||
"purchaseSubscription": "Acheter un abonnement",
|
||||
"qrCode": "Code QR",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"stripe": {
|
||||
"card": {
|
||||
"card_name": "Nom du titulaire de la carte",
|
||||
"card_number": "Numéro de la carte",
|
||||
"element_error": "Impossible d'obtenir l'élément de carte.",
|
||||
"expiry_date": "Date d'expiration",
|
||||
"loading": "Stripe.js n'est pas chargé. Veuillez réessayer plus tard.",
|
||||
"name_placeholder": "Entrez le nom du titulaire de la carte",
|
||||
"name_required": "Le nom du titulaire de la carte est requis",
|
||||
"pay_button": "Payer maintenant",
|
||||
"payment_failed": "Le paiement a échoué, veuillez réessayer.",
|
||||
"processing": "Traitement du paiement, veuillez vérifier le résultat plus tard.",
|
||||
"processing_button": "Traitement...",
|
||||
"secure_notice": "Vos informations de paiement sont cryptées de manière sécurisée",
|
||||
"security_code": "Code de sécurité",
|
||||
"success_message": "Merci pour votre paiement !",
|
||||
"success_title": "Paiement réussi"
|
||||
},
|
||||
"error": "Une erreur inattendue est survenue",
|
||||
"qrcode": {
|
||||
"alipay": "Scannez avec Alipay pour payer",
|
||||
"wechat_pay": "Scannez avec WeChat pour payer"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@
|
||||
"connectionSpeed": "Vitesse de connexion",
|
||||
"productDetail": "Détails du produit"
|
||||
},
|
||||
"discount": "Remise",
|
||||
"discountInfo": "Informations sur la remise",
|
||||
"enterAmount": "Entrez le montant de la recharge",
|
||||
"enterCoupon": "Entrez le code promo",
|
||||
"methods": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"amount": "Montant",
|
||||
"assetOverview": "Aperçu des actifs",
|
||||
"balance": "Solde",
|
||||
"commission": "Commission",
|
||||
"createdAt": "temps",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"inviteRecords": "आमंत्रण रिकॉर्ड",
|
||||
"registrationTime": "पंजीकरण समय",
|
||||
"totalCommission": "कुल कमीशन",
|
||||
"userEmail": "उपयोगकर्ता ईमेल"
|
||||
"userIdentifier": "उपयोगकर्ता पहचान"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"mySubscriptions": "मेरी सदस्यताएँ",
|
||||
"nextResetDays": "अगली रीसेट/दिन",
|
||||
"noLimit": "कोई सीमा नहीं",
|
||||
"noReset": "कोई रीसेट नहीं",
|
||||
"prompt": "प्रॉम्प्ट",
|
||||
"purchaseSubscription": "सदस्यता खरीदें",
|
||||
"qrCode": "क्यूआर कोड",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"stripe": {
|
||||
"card": {
|
||||
"card_name": "कार्डधारक का नाम",
|
||||
"card_number": "कार्ड नंबर",
|
||||
"element_error": "कार्ड तत्व प्राप्त करने में असमर्थ।",
|
||||
"expiry_date": "समाप्ति तिथि",
|
||||
"loading": "Stripe.js लोड नहीं हुआ है। कृपया बाद में पुनः प्रयास करें।",
|
||||
"name_placeholder": "कार्डधारक का नाम दर्ज करें",
|
||||
"name_required": "कार्डधारक का नाम आवश्यक है",
|
||||
"pay_button": "अभी भुगतान करें",
|
||||
"payment_failed": "भुगतान विफल, कृपया पुनः प्रयास करें।",
|
||||
"processing": "भुगतान प्रक्रिया में है, कृपया बाद में परिणाम जांचें।",
|
||||
"processing_button": "प्रसंस्करण हो रहा है...",
|
||||
"secure_notice": "आपकी भुगतान जानकारी सुरक्षित रूप से एन्क्रिप्ट की गई है",
|
||||
"security_code": "सुरक्षा कोड",
|
||||
"success_message": "आपके भुगतान के लिए धन्यवाद!",
|
||||
"success_title": "भुगतान सफल"
|
||||
},
|
||||
"error": "एक अप्रत्याशित त्रुटि हुई",
|
||||
"qrcode": {
|
||||
"alipay": "भुगतान करने के लिए Alipay के साथ स्कैन करें",
|
||||
"wechat_pay": "भुगतान करने के लिए WeChat के साथ स्कैन करें"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@
|
||||
"connectionSpeed": "कनेक्शन गति",
|
||||
"productDetail": "उत्पाद विवरण"
|
||||
},
|
||||
"discount": "छूट",
|
||||
"discountInfo": "छूट जानकारी",
|
||||
"enterAmount": "रिचार्ज राशि दर्ज करें",
|
||||
"enterCoupon": "कूपन कोड दर्ज करें",
|
||||
"methods": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"amount": "राशि",
|
||||
"assetOverview": "संपत्ति अवलोकन",
|
||||
"balance": "शेष",
|
||||
"commission": "आयोग",
|
||||
"createdAt": "समय",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"inviteRecords": "Meghívási rekordok",
|
||||
"registrationTime": "Regisztráció ideje",
|
||||
"totalCommission": "Teljes jutalék",
|
||||
"userEmail": "Felhasználó e-mail"
|
||||
"userIdentifier": "Felhasználói azonosító"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"mySubscriptions": "Előfizetéseim",
|
||||
"nextResetDays": "Következő visszaállítás/nap",
|
||||
"noLimit": "Nincs korlát",
|
||||
"noReset": "Nincs visszaállítás",
|
||||
"prompt": "figyelmeztetés",
|
||||
"purchaseSubscription": "Előfizetés vásárlása",
|
||||
"qrCode": "QR-kód",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"stripe": {
|
||||
"card": {
|
||||
"card_name": "Kártyabirtokos neve",
|
||||
"card_number": "Kártyaszám",
|
||||
"element_error": "Nem sikerült megszerezni a kártya elemet.",
|
||||
"expiry_date": "Lejárati dátum",
|
||||
"loading": "A Stripe.js nem töltődött be. Kérjük, próbálja újra később.",
|
||||
"name_placeholder": "Írd be a kártyabirtokos nevét",
|
||||
"name_required": "A kártyabirtokos neve kötelező",
|
||||
"pay_button": "Fizetés most",
|
||||
"payment_failed": "A fizetés meghiúsult, kérjük, próbálja újra.",
|
||||
"processing": "Fizetés feldolgozása, kérjük, ellenőrizze az eredményt később.",
|
||||
"processing_button": "Feldolgozás...",
|
||||
"secure_notice": "A fizetési információi biztonságosan titkosítva vannak",
|
||||
"security_code": "Biztonsági kód",
|
||||
"success_message": "Köszönjük a fizetését!",
|
||||
"success_title": "Fizetés Sikeres"
|
||||
},
|
||||
"error": "Váratlan hiba történt",
|
||||
"qrcode": {
|
||||
"alipay": "Fizesd a Alipay segítségével",
|
||||
"wechat_pay": "Fizesd a WeChat segítségével"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@
|
||||
"connectionSpeed": "Kapcsolati sebesség",
|
||||
"productDetail": "Termék részletei"
|
||||
},
|
||||
"discount": "Kedvezmény",
|
||||
"discountInfo": "Kedvezmény Információ",
|
||||
"enterAmount": "Adja meg a feltöltési összeget",
|
||||
"enterCoupon": "Adja meg a kuponkódot",
|
||||
"methods": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"amount": "Összeg",
|
||||
"assetOverview": "Eszköz áttekintés",
|
||||
"balance": "Egyenleg",
|
||||
"commission": "Jutalék",
|
||||
"createdAt": "idő",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"inviteRecords": "招待記録",
|
||||
"registrationTime": "登録時間",
|
||||
"totalCommission": "総手数料",
|
||||
"userEmail": "ユーザーのメールアドレス"
|
||||
"userIdentifier": "ユーザー識別子"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"mySubscriptions": "私の購読",
|
||||
"nextResetDays": "次のリセット/日",
|
||||
"noLimit": "無制限",
|
||||
"noReset": "リセットしない",
|
||||
"prompt": "プロンプト",
|
||||
"purchaseSubscription": "サブスクリプションを購入",
|
||||
"qrCode": "QRコード",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"stripe": {
|
||||
"card": {
|
||||
"card_name": "カード名義人",
|
||||
"card_number": "カード番号",
|
||||
"element_error": "カード要素を取得できませんでした。",
|
||||
"expiry_date": "有効期限",
|
||||
"loading": "Stripe.jsが読み込まれていません。後でもう一度お試しください。",
|
||||
"name_placeholder": "カード名義人を入力してください",
|
||||
"name_required": "カード名義人は必須です",
|
||||
"pay_button": "今すぐ支払う",
|
||||
"payment_failed": "支払いに失敗しました。もう一度お試しください。",
|
||||
"processing": "支払い処理中です。結果を後で確認してください。",
|
||||
"processing_button": "処理中...",
|
||||
"secure_notice": "お支払い情報は安全に暗号化されています",
|
||||
"security_code": "セキュリティコード",
|
||||
"success_message": "お支払いありがとうございます!",
|
||||
"success_title": "支払い成功"
|
||||
},
|
||||
"error": "予期しないエラーが発生しました",
|
||||
"qrcode": {
|
||||
"alipay": "Alipayで支払うにはスキャンしてください",
|
||||
"wechat_pay": "WeChatで支払うにはスキャンしてください"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@
|
||||
"connectionSpeed": "接続速度",
|
||||
"productDetail": "商品詳細"
|
||||
},
|
||||
"discount": "割引",
|
||||
"discountInfo": "割引情報",
|
||||
"enterAmount": "リチャージ金額を入力してください",
|
||||
"enterCoupon": "クーポンコードを入力",
|
||||
"methods": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"amount": "金額",
|
||||
"assetOverview": "資産概要",
|
||||
"balance": "残高",
|
||||
"commission": "手数料",
|
||||
"createdAt": "時間",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"inviteRecords": "초대 기록",
|
||||
"registrationTime": "등록 시간",
|
||||
"totalCommission": "총 수수료",
|
||||
"userEmail": "사용자 이메일"
|
||||
"userIdentifier": "사용자 식별자"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"mySubscriptions": "내 구독",
|
||||
"nextResetDays": "다음 초기화/일",
|
||||
"noLimit": "제한 없음",
|
||||
"noReset": "리셋 없음",
|
||||
"prompt": "프롬프트",
|
||||
"purchaseSubscription": "구독 구매",
|
||||
"qrCode": "QR코드",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"stripe": {
|
||||
"card": {
|
||||
"card_name": "카드 소지자 이름",
|
||||
"card_number": "카드 번호",
|
||||
"element_error": "카드 요소를 가져올 수 없습니다.",
|
||||
"expiry_date": "유효 기간",
|
||||
"loading": "Stripe.js가 로드되지 않았습니다. 나중에 다시 시도해 주세요.",
|
||||
"name_placeholder": "카드 소지자 이름을 입력하세요",
|
||||
"name_required": "카드 소지자 이름은 필수입니다",
|
||||
"pay_button": "지금 결제하기",
|
||||
"payment_failed": "결제에 실패했습니다. 다시 시도해 주세요.",
|
||||
"processing": "결제 처리 중입니다. 결과를 나중에 확인해 주세요.",
|
||||
"processing_button": "처리 중...",
|
||||
"secure_notice": "귀하의 결제 정보는 안전하게 암호화됩니다.",
|
||||
"security_code": "보안 코드",
|
||||
"success_message": "결제해 주셔서 감사합니다!",
|
||||
"success_title": "결제 성공"
|
||||
},
|
||||
"error": "예기치 않은 오류가 발생했습니다.",
|
||||
"qrcode": {
|
||||
"alipay": "Alipay로 스캔하여 결제하세요",
|
||||
"wechat_pay": "WeChat으로 스캔하여 결제하세요"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@
|
||||
"connectionSpeed": "연결 속도",
|
||||
"productDetail": "제품 세부 정보"
|
||||
},
|
||||
"discount": "할인",
|
||||
"discountInfo": "할인 정보",
|
||||
"enterAmount": "충전 금액을 입력하세요",
|
||||
"enterCoupon": "쿠폰 코드 입력",
|
||||
"methods": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"amount": "금액",
|
||||
"assetOverview": "자산 개요",
|
||||
"balance": "잔액",
|
||||
"commission": "위원회",
|
||||
"createdAt": "시간",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"inviteRecords": "Invitasjonsoppføringer",
|
||||
"registrationTime": "Registreringstid",
|
||||
"totalCommission": "Totalprovisjon",
|
||||
"userEmail": "Brukerens e-post"
|
||||
"userIdentifier": "Brukeridentifikator"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"mySubscriptions": "Mine abonnementer",
|
||||
"nextResetDays": "Neste tilbakestilling/dager",
|
||||
"noLimit": "Ingen grense",
|
||||
"noReset": "Ingen tilbakestilling",
|
||||
"prompt": "Hint",
|
||||
"purchaseSubscription": "Kjøp abonnement",
|
||||
"qrCode": "QR-kode",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"stripe": {
|
||||
"card": {
|
||||
"card_name": "Kortholderens navn",
|
||||
"card_number": "Kortnummer",
|
||||
"element_error": "Kunne ikke hente kortelement.",
|
||||
"expiry_date": "Utløpsdato",
|
||||
"loading": "Stripe.js er ikke lastet inn. Vennligst prøv igjen senere.",
|
||||
"name_placeholder": "Skriv inn kortholderens navn",
|
||||
"name_required": "Kortholderens navn er påkrevd",
|
||||
"pay_button": "Betal Nå",
|
||||
"payment_failed": "Betaling mislyktes, vennligst prøv igjen.",
|
||||
"processing": "Betaling behandles, vennligst sjekk resultatet senere.",
|
||||
"processing_button": "Behandler...",
|
||||
"secure_notice": "Din betalingsinformasjon er sikkert kryptert",
|
||||
"security_code": "Sikkerhetskode",
|
||||
"success_message": "Takk for betalingen!",
|
||||
"success_title": "Betaling Vel Lykket"
|
||||
},
|
||||
"error": "En uventet feil oppstod",
|
||||
"qrcode": {
|
||||
"alipay": "Skann med Alipay for å betale",
|
||||
"wechat_pay": "Skann med WeChat for å betale"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@
|
||||
"connectionSpeed": "Tilkoblingshastighet",
|
||||
"productDetail": "Produktdetaljer"
|
||||
},
|
||||
"discount": "Rabatt",
|
||||
"discountInfo": "Rabattinformasjon",
|
||||
"enterAmount": "Skriv inn påfyllingsbeløp",
|
||||
"enterCoupon": "Skriv inn kupongkode",
|
||||
"methods": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"amount": "Beløp",
|
||||
"assetOverview": "Eiendomsoversikt",
|
||||
"balance": "Balanse",
|
||||
"commission": "Kommisjon",
|
||||
"createdAt": "Tid",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"inviteRecords": "Rekordy zaproszeń",
|
||||
"registrationTime": "Czas rejestracji",
|
||||
"totalCommission": "Łączna prowizja",
|
||||
"userEmail": "Adres e-mail użytkownika"
|
||||
"userIdentifier": "Identyfikator Użytkownika"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"mySubscriptions": "Moje subskrypcje",
|
||||
"nextResetDays": "Następny reset/dni",
|
||||
"noLimit": "Bez limitu",
|
||||
"noReset": "Brak resetu",
|
||||
"prompt": "Podpowiedź",
|
||||
"purchaseSubscription": "Zakup subskrypcji",
|
||||
"qrCode": "Kod QR",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"stripe": {
|
||||
"card": {
|
||||
"card_name": "Imię i nazwisko posiadacza karty",
|
||||
"card_number": "Numer karty",
|
||||
"element_error": "Nie można uzyskać elementu karty.",
|
||||
"expiry_date": "Data ważności",
|
||||
"loading": "Stripe.js nie został załadowany. Proszę spróbować ponownie później.",
|
||||
"name_placeholder": "Wprowadź imię i nazwisko posiadacza karty",
|
||||
"name_required": "Imię i nazwisko posiadacza karty jest wymagane",
|
||||
"pay_button": "Zapłać teraz",
|
||||
"payment_failed": "Płatność nie powiodła się, proszę spróbować ponownie.",
|
||||
"processing": "Przetwarzanie płatności, proszę sprawdzić wynik później.",
|
||||
"processing_button": "Przetwarzanie...",
|
||||
"secure_notice": "Twoje dane płatnicze są bezpiecznie szyfrowane",
|
||||
"security_code": "Kod zabezpieczający",
|
||||
"success_message": "Dziękujemy za dokonanie płatności!",
|
||||
"success_title": "Płatność zakończona sukcesem"
|
||||
},
|
||||
"error": "Wystąpił nieoczekiwany błąd",
|
||||
"qrcode": {
|
||||
"alipay": "Skanuj z Alipay, aby zapłacić",
|
||||
"wechat_pay": "Skanuj z WeChat, aby zapłacić"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@
|
||||
"connectionSpeed": "Prędkość połączenia",
|
||||
"productDetail": "Szczegóły produktu"
|
||||
},
|
||||
"discount": "Zniżka",
|
||||
"discountInfo": "Informacje o zniżce",
|
||||
"enterAmount": "Wprowadź kwotę doładowania",
|
||||
"enterCoupon": "Wprowadź kod kuponu",
|
||||
"methods": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"amount": "kwota",
|
||||
"assetOverview": "Przegląd aktywów",
|
||||
"balance": "saldo",
|
||||
"commission": "Prowizja",
|
||||
"createdAt": "czas",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"inviteRecords": "Registros de Convite",
|
||||
"registrationTime": "Hora de Registro",
|
||||
"totalCommission": "Comissão Total",
|
||||
"userEmail": "Email do usuário"
|
||||
"userIdentifier": "Identificador do Usuário"
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"mySubscriptions": "Minhas assinaturas",
|
||||
"nextResetDays": "Próxima redefinição/dias",
|
||||
"noLimit": "Sem Limite",
|
||||
"noReset": "Sem Redefinição",
|
||||
"prompt": "Sugestão",
|
||||
"purchaseSubscription": "Comprar Assinatura",
|
||||
"qrCode": "Código QR",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"stripe": {
|
||||
"card": {
|
||||
"card_name": "Nome do Titular",
|
||||
"card_number": "Número do Cartão",
|
||||
"element_error": "Não foi possível obter o elemento do cartão.",
|
||||
"expiry_date": "Data de Validade",
|
||||
"loading": "Stripe.js não está carregado. Por favor, tente novamente mais tarde.",
|
||||
"name_placeholder": "Digite o nome do titular",
|
||||
"name_required": "O nome do titular é obrigatório",
|
||||
"pay_button": "Pagar Agora",
|
||||
"payment_failed": "Pagamento falhou, por favor tente novamente.",
|
||||
"processing": "Processando pagamento, por favor verifique o resultado mais tarde.",
|
||||
"processing_button": "Processando...",
|
||||
"secure_notice": "Suas informações de pagamento estão criptografadas com segurança",
|
||||
"security_code": "Código de Segurança",
|
||||
"success_message": "Obrigado pelo seu pagamento!",
|
||||
"success_title": "Pagamento Bem-Sucedido"
|
||||
},
|
||||
"error": "Ocorreu um erro inesperado",
|
||||
"qrcode": {
|
||||
"alipay": "Escaneie com Alipay para pagar",
|
||||
"wechat_pay": "Escaneie com WeChat para pagar"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@
|
||||
"connectionSpeed": "Velocidade de conexão",
|
||||
"productDetail": "Detalhes do produto"
|
||||
},
|
||||
"discount": "Desconto",
|
||||
"discountInfo": "Informações sobre o Desconto",
|
||||
"enterAmount": "Digite o valor da recarga",
|
||||
"enterCoupon": "Insira o Código do Cupom",
|
||||
"methods": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user