From 265519a03d191b82ba77f8e27bc58421508ca009 Mon Sep 17 00:00:00 2001 From: speakeloudest Date: Tue, 12 Aug 2025 02:58:18 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/SubscribeCard/index.tsx | 248 ++++++++++ .../(content)/(user)/dashboard/content.tsx | 460 +----------------- .../(user)/document/tutorial-button.tsx | 6 +- .../(main)/(content)/(user)/ticket/page.tsx | 2 +- apps/user/components/Modal.tsx | 63 +++ apps/user/components/affiliate/index.tsx | 13 +- .../components/main/OfferDialog/index.tsx | 74 +-- apps/user/components/subscribe/purchase.tsx | 2 +- apps/user/components/subscribe/recharge.tsx | 51 +- apps/user/config/use-global.tsx | 261 +++++----- apps/user/services/common/typings.d.ts | 3 + .../airo-ui/src/components/AiroButton.tsx | 4 +- .../src/custom-components/enhanced-input.tsx | 28 +- 13 files changed, 567 insertions(+), 648 deletions(-) create mode 100644 apps/user/app/(main)/(content)/(user)/dashboard/components/SubscribeCard/index.tsx create mode 100644 apps/user/components/Modal.tsx diff --git a/apps/user/app/(main)/(content)/(user)/dashboard/components/SubscribeCard/index.tsx b/apps/user/app/(main)/(content)/(user)/dashboard/components/SubscribeCard/index.tsx new file mode 100644 index 0000000..c14ba7a --- /dev/null +++ b/apps/user/app/(main)/(content)/(user)/dashboard/components/SubscribeCard/index.tsx @@ -0,0 +1,248 @@ +import { Display } from '@/components/display'; +import Renewal from '@/components/subscribe/renewal'; +import { resetUserSubscribeToken } from '@/services/user/user'; +import { Button } from '@workspace/airo-ui/components/button'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@workspace/airo-ui/components/select'; +import { useTranslations } from 'next-intl'; +import { toast } from 'sonner'; + +import SvgIcon from '@/components/SvgIcon'; +import useGlobalStore from '@/config/use-global'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@workspace/airo-ui/components/alert-dialog'; +import { Popover, PopoverContent, PopoverTrigger } from '@workspace/airo-ui/components/popover'; +import { Tabs, TabsList, TabsTrigger } from '@workspace/airo-ui/components/tabs'; +import { differenceInDays } from '@workspace/airo-ui/utils'; +import { QRCodeCanvas } from 'qrcode.react'; +import { useEffect, useState } from 'react'; +import { CopyToClipboard } from 'react-copy-to-clipboard'; +interface SubscribeCardProps { + userSubscribeData: API.UserSubscribe; + protocol: string[]; + refetch: () => Promise; +} + +const SubscribeCard = (props: SubscribeCardProps) => { + const t = useTranslations('dashboard'); + const { userSubscribeData } = props; + + const { getUserSubscribe } = useGlobalStore(); + + const [protocol, setProtocol] = useState(''); + const [userSubscribeProtocol, setUserSubscribeProtocol] = useState([]); + const [userSubscribeProtocolCurrent, setUserSubscribeProtocolCurrent] = useState(0); + + useEffect(() => { + const list = getUserSubscribe(userSubscribeData.token, protocol); + setUserSubscribeProtocol(list); + if (list.length > 0) { + setUserSubscribeProtocolCurrent(0); + } + }, [props.userSubscribeData.token, protocol]); + + return ( +
+

+ {t('copySubscriptionLinkOrScanQrCode')} +

+ + {/* 统计信息 */} +
+
+
+

+ {t('totalTraffic')} +

+

+ +

+
+
+

+ {t('nextResetDays')} +

+

+ {userSubscribeData.reset_time + ? differenceInDays(new Date(userSubscribeData.reset_time), new Date()) + : t('noReset')} +

+
+
+

+ {t('expirationDays')} +

+

+ {userSubscribeData.expire_time + ? differenceInDays(new Date(userSubscribeData.expire_time), new Date()) || + t('unknown') + : t('noLimit')} +

+
+
+
+ + {/* 订阅链接 */} +
+
+ {props.protocol.length > 1 && ( + + + {['all', ...(props.protocol || [])].map((item) => ( + + {item} + + ))} + + + )} +
+
+
+ + +
+
+
+ {userSubscribeProtocol[userSubscribeProtocolCurrent]} +
+ { + if (result) { + toast.success(t('copySuccess')); + } + }} + > + + + + +
+
+
+ + + +
+ +
+
+ +
+

+ {t('scanCodeToSubscribe')} +

+ +
+
+
+
+
+ + + + + + + {t('prompt')} + {t('confirmResetSubscription')} + + + {t('cancel')} + { + await resetUserSubscribeToken({ + user_subscribe_id: userSubscribeData.id || 0, + }); + await props.refetch(); + toast.success(t('resetSuccess')); + }} + > + {t('confirm')} + + + + + +
+
+
+ ); +}; + +export default SubscribeCard; diff --git a/apps/user/app/(main)/(content)/(user)/dashboard/content.tsx b/apps/user/app/(main)/(content)/(user)/dashboard/content.tsx index 3025572..58cc942 100644 --- a/apps/user/app/(main)/(content)/(user)/dashboard/content.tsx +++ b/apps/user/app/(main)/(content)/(user)/dashboard/content.tsx @@ -2,25 +2,17 @@ import { Display } from '@/components/display'; import Recharge from '@/components/subscribe/recharge'; -import Renewal from '@/components/subscribe/renewal'; import ResetTraffic from '@/components/subscribe/reset-traffic'; import useGlobalStore from '@/config/use-global'; import { getStat } from '@/services/common/common'; -import { queryUserSubscribe, resetUserSubscribeToken } from '@/services/user/user'; +import { queryUserSubscribe } from '@/services/user/user'; import { useQuery } from '@tanstack/react-query'; import { Button } from '@workspace/airo-ui/components/button'; import { Card } from '@workspace/airo-ui/components/card'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@workspace/airo-ui/components/select'; import { useTranslations } from 'next-intl'; import Link from 'next/link'; -import { useEffect, useRef, useState } from 'react'; -import { toast } from 'sonner'; +import { useRef } from 'react'; +import SubScribeCard from './components/SubscribeCard/index'; import { AnnouncementDialog, @@ -31,25 +23,10 @@ import { PopupRef, } from '@/app/(main)/(content)/(user)/dashboard/components/Announcement/Popup'; import { Empty } from '@/components/empty'; -import SvgIcon from '@/components/SvgIcon'; import { queryAnnouncement } from '@/services/user/announcement'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from '@workspace/airo-ui/components/alert-dialog'; -import { Popover, PopoverContent, PopoverTrigger } from '@workspace/airo-ui/components/popover'; -import { Tabs, TabsList, TabsTrigger } from '@workspace/airo-ui/components/tabs'; +import { queryOrderList } from '@/services/user/order'; import { default as Airo_Empty } from '@workspace/airo-ui/custom-components/empty'; -import { differenceInDays, formatDate } from '@workspace/airo-ui/utils'; -import { QRCodeCanvas } from 'qrcode.react'; -import { CopyToClipboard } from 'react-copy-to-clipboard'; +import { formatDate } from '@workspace/airo-ui/utils'; const platforms: (keyof API.ApplicationPlatform)[] = [ 'windows', @@ -62,16 +39,8 @@ const platforms: (keyof API.ApplicationPlatform)[] = [ export default function Content() { const t = useTranslations('dashboard'); - const { getUserSubscribe, getAppSubLink } = useGlobalStore(); - const [protocol, setProtocol] = useState(''); - const [userSubscribeProtocol, setUserSubscribeProtocol] = useState([]); - const [userSubscribeProtocolCurrent, setUserSubscribeProtocolCurrent] = useState(''); - const { - data: userSubscribe = [], - refetch, - isLoading, - } = useQuery({ + const { data: userSubscribe = [], refetch } = useQuery({ queryKey: ['queryUserSubscribe'], queryFn: async () => { const { data } = await queryUserSubscribe(); @@ -103,15 +72,13 @@ export default function Content() { }, }); - useEffect(() => { - if (data && userSubscribe?.length > 0 && !userSubscribeProtocol.length) { - const list = getUserSubscribe(userSubscribe[0]?.token, data.protocol); - setUserSubscribeProtocol(list); - if (list.length > 0) { - setUserSubscribeProtocolCurrent(list[0]); - } - } - }, [data, userSubscribe, userSubscribeProtocol.length]); + const { data: orderData } = useQuery({ + queryKey: ['orderData'], + queryFn: async () => { + const { data } = await queryOrderList({ status: 5 }); + return data?.[0] ?? {}; + }, + }); const statusWatermarks = { 2: t('finished'), @@ -122,14 +89,6 @@ export default function Content() { const { user } = useGlobalStore(); const totalAssets = (user?.balance || 0) + (user?.commission || 0) + (user?.gift_amount || 0); - // 获取当前选中项的显示标签 - const getCurrentLabel = () => { - const currentIndex = userSubscribeProtocol.findIndex( - (url) => url === userSubscribeProtocolCurrent, - ); - return currentIndex !== -1 ? `${t('subscriptionUrl')}${currentIndex + 1}` : t('address1'); - }; - const popupRef = useRef(null); const dialogRef = useRef(null); return ( @@ -148,7 +107,11 @@ export default function Content() {
- {userSubscribe?.length > 0 ?
1
: t('noYPlan')} + {userSubscribe?.length > 0 && userSubscribe[0]?.status === 1 && orderData + ? orderData?.quantity === 1 + ? t('annualMonthPlanUser') + : t('annualYearPlanUser') + : t('noYPlan')}
@@ -157,7 +120,7 @@ export default function Content() { {t('accountBalance')} @@ -330,391 +293,16 @@ export default function Content() { {userSubscribe?.[0] && data?.protocol ? ( -
-

- {t('copySubscriptionLinkOrScanQrCode')} -

- - {/* 统计信息 */} -
-
-
-

- {t('totalTraffic')} -

-

- -

-
-
-

- {t('nextResetDays')} -

-

- {userSubscribe?.[0].reset_time - ? differenceInDays(new Date(userSubscribe?.[0].reset_time), new Date()) - : t('noReset')} -

-
-
-

- {t('expirationDays')} -

-

- {userSubscribe?.[0]?.expire_time - ? differenceInDays(new Date(userSubscribe?.[0].expire_time), new Date()) || - t('unknown') - : t('noLimit')} -

-
-
-
- - {/* 订阅链接 */} -
-
- {data?.protocol && data?.protocol.length > 1 && ( - - - {['all', ...(data?.protocol || [])].map((item) => ( - - {item} - - ))} - - - )} -
-
-
- - -
-
-
- {userSubscribeProtocolCurrent} -
- { - if (result) { - toast.success(t('copySuccess')); - } - }} - > - - - - -
-
-
- - - -
- -
-
- -
-

- {t('scanCodeToSubscribe')} -

- -
-
-
-
-
- - - - - - - {t('prompt')} - - {t('confirmResetSubscription')} - - - - {t('cancel')} - { - await resetUserSubscribeToken({ - user_subscribe_id: userSubscribe?.[0]?.id || 0, - }); - await refetch(); - toast.success(t('resetSuccess')); - }} - > - {t('confirm')} - - - - - -
-
-
+ ) : ( )} - {/*{userSubscribe.length ? ( - <> -
-

- - {t('mySubscriptions')} -

-
- - -
-
- - {userSubscribe.map((item) => { - return ( - - ); - })} - - ) : ( - <> -

- - {t('purchaseSubscription')} -

- - - )}*/} ); } diff --git a/apps/user/app/(main)/(content)/(user)/document/tutorial-button.tsx b/apps/user/app/(main)/(content)/(user)/document/tutorial-button.tsx index d4a8a15..6e6ceb5 100644 --- a/apps/user/app/(main)/(content)/(user)/document/tutorial-button.tsx +++ b/apps/user/app/(main)/(content)/(user)/document/tutorial-button.tsx @@ -2,8 +2,8 @@ import { getTutorial } from '@/utils/tutorial'; import { useQuery } from '@tanstack/react-query'; +import { buttonVariants } from '@workspace/airo-ui/components/AiroButton'; import { Avatar, AvatarFallback, AvatarImage } from '@workspace/airo-ui/components/avatar'; -import { buttonVariants } from '@workspace/airo-ui/components/button'; import { Markdown } from '@workspace/airo-ui/custom-components/markdown'; import { useOutsideClick } from '@workspace/airo-ui/hooks/use-outside-click'; import { cn } from '@workspace/airo-ui/lib/utils'; @@ -153,9 +153,9 @@ export function TutorialButton({ items }: { items: Item[] }) { layoutId={`button-${item.title}-${id}`} className={cn( buttonVariants({ - variant: 'secondary', + variant: 'primary', }), - 'rounded-full border-[#A8D4ED] bg-[#A8D4ED] px-[35px] py-[9px] text-center text-xs font-bold text-white hover:bg-[#225BA9] hover:text-white sm:min-w-[100px]', + 'sm:min-w-[100px]', )} > {t('read')} diff --git a/apps/user/app/(main)/(content)/(user)/ticket/page.tsx b/apps/user/app/(main)/(content)/(user)/ticket/page.tsx index 0a04a96..6a0eb90 100644 --- a/apps/user/app/(main)/(content)/(user)/ticket/page.tsx +++ b/apps/user/app/(main)/(content)/(user)/ticket/page.tsx @@ -191,7 +191,7 @@ export default function Page() { - ); -}; - // 星级评分组件 const StarRating = ({ rating, maxRating = 5 }: { rating: number; maxRating?: number }) => (
@@ -186,9 +162,26 @@ const PlanCard = forwardRef< isFirstCard?: boolean; } >(({ plan, onSubscribe, isFirstCard = false }, ref) => { - const handleSubscribe = () => { + const { user } = useGlobalStore(); + const { openLoginDialog } = useLoginDialog(); + const t = useTranslations('components.offerDialog'); + const ModalRef = useRef(null); + async function handleSubscribe() { + if (!user) { + // 强制登陆 + openLoginDialog(false); + return; + } + + // 有生效套餐进行弹窗提示 + const { data } = await queryUserSubscribe(); + if (data?.data?.list?.[0].status === 1) { + ModalRef.current.show(); + return; + } + onSubscribe?.(plan); - }; + } return (
{/* 订阅按钮 */} - + {/* 功能列表 */} + + onSubscribe?.(plan)} + />
); }); @@ -350,7 +359,6 @@ const OfferDialog = forwardRef((props, ref) => { // 处理订阅点击 const handleSubscribe = (plan: ProcessedPlanData) => { setSelectedPlan(plan); - console.log('用户选择了套餐:', plan); // 这里可以添加订阅逻辑,比如跳转到支付页面或显示确认对话框 PurchaseRef.current.show(plan, tabValue); }; @@ -412,12 +420,6 @@ const OfferDialog = forwardRef((props, ref) => { -20% {/* 小三角箭头 */} - {/* */} ((props, ref) => { const response = await purchase(params as API.PurchaseOrderRequest); const orderNo = response.data.data?.order_no; if (orderNo) { - await getUserInfo(); const data = await purchaseCheckout({ orderNo: orderNo, returnUrl: window.location.href, }); + await getUserInfo(); if (data.data?.type === 'url' && data.data.checkout_url) { window.open(data.data.checkout_url, '_blank'); } else { diff --git a/apps/user/components/subscribe/recharge.tsx b/apps/user/components/subscribe/recharge.tsx index 2a361ce..7507b50 100644 --- a/apps/user/components/subscribe/recharge.tsx +++ b/apps/user/components/subscribe/recharge.tsx @@ -3,7 +3,7 @@ import useGlobalStore from '@/config/use-global'; import { recharge } from '@/services/user/order'; import { AiroButton } from '@workspace/airo-ui/components/AiroButton'; -import { Button, ButtonProps } from '@workspace/airo-ui/components/button'; +import { ButtonProps } from '@workspace/airo-ui/components/button'; import { Dialog, DialogContent, @@ -43,13 +43,13 @@ export default function Recharge(props: Readonly) { - {t('balanceRecharge')} + {t('balanceRecharge')} {t('rechargeDescription')} -
+
{t('rechargeAmount')}
-
+
) { onChange={(value) => setParams({ ...params, payment: value })} />
- + }); + }} + > + {loading && } + {t('rechargeNow')} + +
diff --git a/apps/user/config/use-global.tsx b/apps/user/config/use-global.tsx index 8f425db..11a7c49 100644 --- a/apps/user/config/use-global.tsx +++ b/apps/user/config/use-global.tsx @@ -2,6 +2,7 @@ import { NEXT_PUBLIC_API_URL, NEXT_PUBLIC_SITE_URL } from '@/config/constants'; import { queryUserInfo } from '@/services/user/user'; import { extractDomain } from '@workspace/airo-ui/utils'; import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; export interface GlobalStore { common: API.GetGlobalConfigResponse; @@ -13,139 +14,141 @@ export interface GlobalStore { getAppSubLink: (type: string, url: string) => string; } -export const useGlobalStore = create((set, get) => ({ - common: { - site: { - host: '', - site_name: '', - site_desc: '', - site_logo: '', - keywords: '', - custom_html: '', - custom_data: '', - }, - verify: { - turnstile_site_key: '', - enable_login_verify: false, - enable_register_verify: false, - enable_reset_password_verify: false, - }, - auth: { - mobile: { - enable: false, - enable_whitelist: false, - whitelist: [], +export const useGlobalStore = create()( + devtools((set, get) => ({ + common: { + site: { + host: '', + site_name: '', + site_desc: '', + site_logo: '', + keywords: '', + custom_html: '', + custom_data: '', }, - email: { - enable: false, - enable_verify: false, - enable_domain_suffix: false, - domain_suffix_list: '', + verify: { + turnstile_site_key: '', + enable_login_verify: false, + enable_register_verify: false, + enable_reset_password_verify: false, }, - register: { - stop_register: false, - enable_ip_register_limit: false, - ip_register_limit: 0, - ip_register_limit_duration: 0, + auth: { + mobile: { + enable: false, + enable_whitelist: false, + whitelist: [], + }, + email: { + enable: false, + enable_verify: false, + enable_domain_suffix: false, + domain_suffix_list: '', + }, + register: { + stop_register: false, + enable_ip_register_limit: false, + ip_register_limit: 0, + ip_register_limit_duration: 0, + }, }, - }, - invite: { - forced_invite: false, - referral_percentage: 0, - only_first_purchase: false, - }, - currency: { - currency_unit: 'USD', - currency_symbol: '$', - }, - subscribe: { - single_model: false, - subscribe_path: '', - subscribe_domain: '', - pan_domain: false, - }, - verify_code: { - verify_code_expire_time: 5, - verify_code_limit: 15, - verify_code_interval: 60, - }, - oauth_methods: [], - web_ad: false, - }, - user: undefined, - setCommon: (common) => - set((state) => ({ - common: { - ...state.common, - ...common, + invite: { + forced_invite: false, + referral_percentage: 0, + only_first_purchase: false, }, - })), - setUser: (user) => set({ user }), - getUserInfo: async () => { - try { - const { data } = await queryUserInfo(); - set({ user: data.data }); - } catch (error) { - console.error('Failed to refresh user:', error); - } - }, - getUserSubscribe: (uuid: string, type?: string) => { - const { pan_domain, subscribe_domain, subscribe_path } = get().common.subscribe || {}; - const domains = subscribe_domain - ? subscribe_domain.split('\n') - : [extractDomain(NEXT_PUBLIC_API_URL || NEXT_PUBLIC_SITE_URL || '', pan_domain)]; - - return domains.map((domain) => { - if (pan_domain) { - if (type) return `https://${uuid}.${type}.${domain}`; - return `https://${uuid}.${domain}`; - } else { - if (type) return `https://${domain}${subscribe_path}?token=${uuid}&type=${type}`; - return `https://${domain}${subscribe_path}?token=${uuid}`; + currency: { + currency_unit: 'USD', + currency_symbol: '$', + }, + subscribe: { + single_model: false, + subscribe_path: '', + subscribe_domain: '', + pan_domain: false, + }, + verify_code: { + verify_code_expire_time: 5, + verify_code_limit: 15, + verify_code_interval: 60, + }, + oauth_methods: [], + web_ad: false, + }, + user: undefined, + setCommon: (common) => + set((state) => ({ + common: { + ...state.common, + ...common, + }, + })), + setUser: (user) => set({ user }), + getUserInfo: async () => { + try { + const { data } = await queryUserInfo(); + set({ user: data.data }); + } catch (error) { + console.error('Failed to refresh user:', error); } - }); - }, - getAppSubLink: (type: string, url: string) => { - const name = get().common?.site?.site_name || ''; - switch (type) { - case 'Clash': - return `clash://install-config?url=${url}&name=${name}`; - case 'Hiddify': - return `hiddify://import/${url}#${name}`; - case 'Loon': - return `loon://import?sub=${encodeURIComponent(url)}`; - case 'NekoBox': - return `sn://subscription?url=${url}&name=${name}`; - case 'NekoRay': - return `sn://subscription?url=${url}&name=${name}`; - // case 'Netch': - // return ``; - case 'Quantumult X': - return `quantumult-x://add-resource?remote-resource=${encodeURIComponent( - JSON.stringify({ - server_remote: [`${url}, tag=${name}`], - }), - )}`; - case 'Shadowrocket': - return `shadowrocket://add/sub://${window.btoa(url)}?remark=${encodeURIComponent(name)}`; - case 'Singbox': - return `sing-box://import-remote-profile?url=${encodeURIComponent(url)}#${name}`; - case 'Surfboard': - return `surfboard:///install-config?url=${encodeURIComponent(url)}`; - case 'Surge': - return `surge:///install-config?url=${encodeURIComponent(url)}`; - case 'V2box': - return `v2box://install-sub?url=${encodeURIComponent(url)}&name=${name}`; - // case 'V2rayN': - // return `v2rayn://install-sub?url=${encodeURIComponent(url)}&name=${name}`; - case 'V2rayNg': - return `v2rayng://install-sub?url=${encodeURIComponent(url)}#${name}`; - case 'Stash': - return `stash://install-config?url=${encodeURIComponent(url)}&name=${name}`; - default: - return ''; - } - }, -})); + }, + getUserSubscribe: (uuid: string, type?: string) => { + const { pan_domain, subscribe_domain, subscribe_path } = get().common.subscribe || {}; + const domains = subscribe_domain + ? subscribe_domain.split('\n') + : [extractDomain(NEXT_PUBLIC_API_URL || NEXT_PUBLIC_SITE_URL || '', pan_domain)]; + + return domains.map((domain) => { + if (pan_domain) { + if (type) return `https://${uuid}.${type}.${domain}`; + return `https://${uuid}.${domain}`; + } else { + if (type) return `https://${domain}${subscribe_path}?token=${uuid}&type=${type}`; + return `https://${domain}${subscribe_path}?token=${uuid}`; + } + }); + }, + getAppSubLink: (type: string, url: string) => { + const name = get().common?.site?.site_name || ''; + switch (type) { + case 'Clash': + return `clash://install-config?url=${url}&name=${name}`; + case 'Hiddify': + return `hiddify://import/${url}#${name}`; + case 'Loon': + return `loon://import?sub=${encodeURIComponent(url)}`; + case 'NekoBox': + return `sn://subscription?url=${url}&name=${name}`; + case 'NekoRay': + return `sn://subscription?url=${url}&name=${name}`; + // case 'Netch': + // return ``; + case 'Quantumult X': + return `quantumult-x://add-resource?remote-resource=${encodeURIComponent( + JSON.stringify({ + server_remote: [`${url}, tag=${name}`], + }), + )}`; + case 'Shadowrocket': + return `shadowrocket://add/sub://${window.btoa(url)}?remark=${encodeURIComponent(name)}`; + case 'Singbox': + return `sing-box://import-remote-profile?url=${encodeURIComponent(url)}#${name}`; + case 'Surfboard': + return `surfboard:///install-config?url=${encodeURIComponent(url)}`; + case 'Surge': + return `surge:///install-config?url=${encodeURIComponent(url)}`; + case 'V2box': + return `v2box://install-sub?url=${encodeURIComponent(url)}&name=${name}`; + // case 'V2rayN': + // return `v2rayn://install-sub?url=${encodeURIComponent(url)}&name=${name}`; + case 'V2rayNg': + return `v2rayng://install-sub?url=${encodeURIComponent(url)}#${name}`; + case 'Stash': + return `stash://install-config?url=${encodeURIComponent(url)}&name=${name}`; + default: + return ''; + } + }, + })), +); export default useGlobalStore; diff --git a/apps/user/services/common/typings.d.ts b/apps/user/services/common/typings.d.ts index afaa74d..963ab1e 100644 --- a/apps/user/services/common/typings.d.ts +++ b/apps/user/services/common/typings.d.ts @@ -319,6 +319,9 @@ declare namespace API { forced_invite: boolean; referral_percentage: number; only_first_purchase: boolean; + first_purchase_percentage: number; + first_yearly_purchase_percentage: number; + non_first_purchase_percentage: number; }; type LoginResponse = { diff --git a/packages/airo-ui/src/components/AiroButton.tsx b/packages/airo-ui/src/components/AiroButton.tsx index 44ffa47..19c80bb 100644 --- a/packages/airo-ui/src/components/AiroButton.tsx +++ b/packages/airo-ui/src/components/AiroButton.tsx @@ -10,8 +10,8 @@ const buttonVariants = cva( variants: { variant: { default: 'bg-[#0F2C53] text-primary-foreground shadow hover:bg-[#225BA9]', - primary: 'bg-[#A8D4ED] text-primary-foreground shadow hover:bg-[#225BA9] ', - danger: 'bg-[#F8BFD2] text-primary-foreground shadow hover:bg-[#FF4248]', + primary: 'bg-[#225BA9] text-primary-foreground shadow hover:bg-[#0F2C53] ', + danger: 'bg-[#FF4248] text-primary-foreground shadow hover:bg-[#E22C2E]', destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90', outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground', diff --git a/packages/airo-ui/src/custom-components/enhanced-input.tsx b/packages/airo-ui/src/custom-components/enhanced-input.tsx index eb4bd60..28693c4 100644 --- a/packages/airo-ui/src/custom-components/enhanced-input.tsx +++ b/packages/airo-ui/src/custom-components/enhanced-input.tsx @@ -116,7 +116,9 @@ export function EnhancedInput({ const renderPrefix = () => { return typeof prefix === 'string' ? ( -
{prefix}
+
+ {prefix} +
) : ( prefix ); @@ -124,7 +126,9 @@ export function EnhancedInput({ const renderSuffix = () => { return typeof suffix === 'string' ? ( -
{suffix}
+
+ {suffix} +
) : ( suffix ); @@ -133,20 +137,22 @@ export function EnhancedInput({ return (
{renderPrefix()} - +
+ +
{renderSuffix()}
);