Переглянути джерело

feat: 支付页面按钮点击反馈

BaiLuoYan 1 місяць тому
батько
коміт
b06adc66fe

+ 6 - 2
src/locales/en-US/pages.ts

@@ -21,6 +21,8 @@ export default {
         title: 'Purchase NOMO VPN Plan',
         selecPlan: 'Select a plan that suits you',
         selectPayMethod: 'Select your payment method',
+        pleaseSelectPlan: 'Please select a plan',
+        pleaseSelectPayMethod: 'Please select a payment method',
         userInfo: {
             account: 'Your Account',
             planExpireTime: 'Plan Expire Time',
@@ -43,7 +45,8 @@ export default {
         },
         payFlow: {
             loginTitle: 'Please log in',
-            loginPrompt: 'Log in to continue payment. No account? <linkText>Go to app</linkText> to register, or <downloadLink>download app</downloadLink>',
+            loginPrompt:
+                'Log in to continue payment. No account? <linkText>Go to app</linkText> to register, or <downloadLink>download app</downloadLink>',
             username: 'Username',
             password: 'Password',
             loginSubmit: 'Log in',
@@ -53,7 +56,8 @@ export default {
             goToPayDesc: 'Click the button below to go to the payment page',
             goToPayButton: 'Go to pay',
             waitingTitle: 'Waiting for payment',
-            waitingDesc: 'Please complete payment on the payment page. This dialog will close automatically when done.',
+            waitingDesc:
+                'Please complete payment on the payment page. This dialog will close automatically when done.',
             closeWaiting: 'Close',
             paySuccess: 'Payment successful',
             payFailed: 'Payment failed',

+ 6 - 2
src/locales/fa-IR/pages.ts

@@ -21,6 +21,8 @@ export default {
         title: 'خرید پلن NOMO VPN',
         selecPlan: 'یک پلن را انتخاب کنید که به شما مناسب است',
         selectPayMethod: 'روش پرداخت خود را انتخاب کنید',
+        pleaseSelectPlan: 'لطفاً یک پلن انتخاب کنید',
+        pleaseSelectPayMethod: 'لطفاً روش پرداخت را انتخاب کنید',
         userInfo: {
             account: 'حساب شما',
             planExpireTime: 'زمان انقضای پلن',
@@ -43,7 +45,8 @@ export default {
         },
         payFlow: {
             loginTitle: 'لطفاً وارد شوید',
-            loginPrompt: 'برای ادامه پرداخت وارد شوید. حساب ندارید؟ <linkText>به اپ بروید</linkText> برای ثبت‌نام، یا <downloadLink>دانلود اپ</downloadLink>',
+            loginPrompt:
+                'برای ادامه پرداخت وارد شوید. حساب ندارید؟ <linkText>به اپ بروید</linkText> برای ثبت‌نام، یا <downloadLink>دانلود اپ</downloadLink>',
             username: 'نام کاربری',
             password: 'رمز عبور',
             loginSubmit: 'ورود',
@@ -53,7 +56,8 @@ export default {
             goToPayDesc: 'برای رفتن به صفحه پرداخت دکمه زیر را بزنید',
             goToPayButton: 'پرداخت',
             waitingTitle: 'در انتظار پرداخت',
-            waitingDesc: 'لطفاً در صفحه پرداخت عملیات را تکمیل کنید. پس از اتمام این پنجره بسته می‌شود.',
+            waitingDesc:
+                'لطفاً در صفحه پرداخت عملیات را تکمیل کنید. پس از اتمام این پنجره بسته می‌شود.',
             closeWaiting: 'بستن',
             paySuccess: 'پرداخت موفق',
             payFailed: 'پرداخت ناموفق',

+ 4 - 1
src/locales/zh-CN/pages.ts

@@ -21,6 +21,8 @@ export default {
         title: '购买 NOMO VPN 套餐',
         selecPlan: '选择一个适合您的套餐',
         selectPayMethod: '选择您的支付方式',
+        pleaseSelectPlan: '请选择套餐',
+        pleaseSelectPayMethod: '请选择支付方式',
         userInfo: {
             account: '您的账户',
             planExpireTime: '套餐到期时间',
@@ -43,7 +45,8 @@ export default {
         },
         payFlow: {
             loginTitle: '请先登录',
-            loginPrompt: '登录后即可继续支付。没有账号?<linkText>前往 APP</linkText> 注册,或<downloadLink>下载 APP </downloadLink>',
+            loginPrompt:
+                '登录后即可继续支付。没有账号?<linkText>前往 APP</linkText> 注册,或<downloadLink>下载 APP </downloadLink>',
             username: '用户名',
             password: '密码',
             loginSubmit: '登录',

+ 39 - 31
src/pages/pricing/components/OrderSummary/index.tsx

@@ -7,45 +7,53 @@ import { useResponsive } from '@/hooks/useSize';
 
 import LabelValueItem from '../LabelValueItem';
 import type { Plan } from '../../useService';
-import { useAction } from './useAction';
 import { useService } from './useService';
 
+const PRICING_FORM_ID = 'pricing-form';
+
 export interface OrderSummaryProps {
+    formId?: string;
     selectedPlan: Plan | null;
     selectedPayMethod: string | null;
 }
 
-const OrderSummary = memo(({ selectedPlan, selectedPayMethod }: OrderSummaryProps) => {
-    const { t } = useTranslation();
-    const { isMobile } = useResponsive();
-    const { orderTotal } = useService({ selectedPlan });
-    const { handlePayNow } = useAction({ selectedPlan, selectedPayMethod });
-
-    return (
-        <div
-            className={`flex flex-col bg-[#1B1D22] rounded-xl shadow-[0px_4px_10px_0px_rgba(0,0,0,0.05)] ${isMobile ? 'p-[14px] gap-2' : 'p-[25px_30px_25px_25px] gap-5'}`}
-        >
-            <div className={`flex flex-col ${isMobile ? 'gap-2' : 'gap-5'}`}>
-                <LabelValueItem
-                    label={t('pages.pricing.orderSummary.orderTotal')}
-                    value={orderTotal}
-                    valueColor="text-[#0EA5E9]"
-                />
-            </div>
-            <p className="text-[#646776] text-xs font-normal leading-[1.4]">
-                {t('pages.pricing.orderSummary.terms')}
-            </p>
-            <div className={`flex gap-5 ${isMobile ? 'flex-col' : 'flex-row'}`}>
-                <Button
-                    className={`bg-[#0EA5E9] text-white text-sm font-medium leading-[1.4] uppercase rounded-[25px] px-[42px] py-[10px] h-auto border-none hover:bg-[#0EA5E9]/80 ${!isMobile ? 'w-[300px]' : ''}`}
-                    onClick={handlePayNow}
-                >
-                    {t('pages.pricing.orderSummary.goPayNow')}
-                </Button>
+const OrderSummary = memo(
+    ({
+        formId = PRICING_FORM_ID,
+        selectedPlan,
+        selectedPayMethod: _selectedPayMethod,
+    }: OrderSummaryProps) => {
+        const { t } = useTranslation();
+        const { isMobile } = useResponsive();
+        const { orderTotal } = useService({ selectedPlan });
+
+        return (
+            <div
+                className={`flex flex-col bg-[#1B1D22] rounded-xl shadow-[0px_4px_10px_0px_rgba(0,0,0,0.05)] ${isMobile ? 'p-[14px] gap-2' : 'p-[25px_30px_25px_25px] gap-5'}`}
+            >
+                <div className={`flex flex-col ${isMobile ? 'gap-2' : 'gap-5'}`}>
+                    <LabelValueItem
+                        label={t('pages.pricing.orderSummary.orderTotal')}
+                        value={orderTotal}
+                        valueColor="text-[#0EA5E9]"
+                    />
+                </div>
+                <p className="text-[#646776] text-xs font-normal leading-[1.4]">
+                    {t('pages.pricing.orderSummary.terms')}
+                </p>
+                <div className={`flex gap-5 ${isMobile ? 'flex-col' : 'flex-row'}`}>
+                    <Button
+                        form={formId}
+                        htmlType="submit"
+                        className={`bg-[#0EA5E9] text-white text-sm font-medium leading-[1.4] uppercase rounded-[25px] px-[42px] py-[10px] h-auto border-none hover:bg-[#0EA5E9]/80 ${!isMobile ? 'w-[300px]' : ''}`}
+                    >
+                        {t('pages.pricing.orderSummary.goPayNow')}
+                    </Button>
+                </div>
             </div>
-        </div>
-    );
-});
+        );
+    }
+);
 
 OrderSummary.displayName = 'OrderSummary';
 

+ 0 - 167
src/pages/pricing/components/OrderSummary/useAction.tsx

@@ -1,167 +0,0 @@
-import { useCallback } from 'react';
-
-import { Trans, useTranslation } from 'react-i18next';
-
-import LoginForm from '@/components/LoginForm';
-import { useAppUrls } from '@/hooks/useAppUrls';
-import { dialogModel } from '@/models/dialogModel';
-import { fetchPayOrderCreate } from '@/services/config';
-import { getToken } from '@/utils/authUtils';
-import { currentUnixTimestamp } from '@/utils/timeUtils';
-
-import { PayWaitingContent } from './PayWaitingContent';
-import type { Plan } from '../../useService';
-
-const ORDER_TYPE_PLAN = 1;
-
-export interface UseActionReturn {
-    handlePayNow: () => void;
-}
-
-export interface UseActionParams {
-    selectedPlan: Plan | null;
-    selectedPayMethod: string | null;
-}
-
-export function useAction({ selectedPlan, selectedPayMethod }: UseActionParams): UseActionReturn {
-    const { t } = useTranslation();
-    const { openDialog, closeDialog } = dialogModel.useModel();
-    const { deeplinkUrl, downloadUrlByPlatform } = useAppUrls();
-
-
-    const handlePayNow = useCallback(() => {
-        if (!selectedPlan || !selectedPayMethod) return;
-        console.log('🚀 ~ useAction.tsx:30 ~ useAction ~ downloadUrlByPlatform:', downloadUrlByPlatform);
-        console.log('🚀 ~ useAction.tsx:30 ~ useAction ~ deeplinkUrl:', deeplinkUrl);
-        const token = getToken();
-        const expired =
-            !token?.accessExpires || (token.accessExpires ?? 0) - currentUnixTimestamp() <= 0;
-        if (!token?.accessToken || expired) {
-            const id = openDialog({
-                title: t('pages.pricing.payFlow.loginTitle'),
-                content: (
-                    <div className="flex flex-col gap-2">
-                        <p className="text-white/80 text-sm mb-2">
-                            <Trans
-                                i18nKey="pages.pricing.payFlow.loginPrompt"
-                                components={{
-                                    linkText: (() => {
-                                        const Wrap = ({
-                                            children,
-                                        }: {
-                                            children?: React.ReactNode;
-                                        }) =>
-                                            deeplinkUrl ? (
-                                                <a
-                                                    href={deeplinkUrl}
-                                                    className="text-[#0EA5E9] hover:underline"
-                                                    target="_blank"
-                                                    rel="noopener noreferrer"
-                                                >
-                                                    {children}
-                                                </a>
-                                            ) : (
-                                                <span>{children}</span>
-                                            );
-                                        return <Wrap />;
-                                    })(),
-                                    downloadLink: (() => {
-                                        const Wrap = ({
-                                            children,
-                                        }: {
-                                            children?: React.ReactNode;
-                                        }) =>
-                                            downloadUrlByPlatform ? (
-                                                <a
-                                                    href={downloadUrlByPlatform}
-                                                    className="text-[#0EA5E9] hover:underline"
-                                                    target="_blank"
-                                                    rel="noopener noreferrer"
-                                                >
-                                                    {children}
-                                                </a>
-                                            ) : (
-                                                <span>{children}</span>
-                                            );
-                                        return <Wrap />;
-                                    })(),
-                                }}
-                            />
-                        </p>
-                        <LoginForm onSuccess={() => closeDialog(id)} />
-                    </div>
-                ),
-                maskClosable: true,
-            });
-            return;
-        }
-
-        fetchPayOrderCreate({
-            orderType: ORDER_TYPE_PLAN,
-            payType: selectedPayMethod,
-            channelItemId: selectedPlan.id,
-        })
-            .then((res) => {
-                const order = res?.data?.userPayOrder;
-                if (!order?.payUrl || !order?.orderId) return;
-
-                openDialog({
-                    title: t('pages.pricing.payFlow.goToPayTitle'),
-                    content: (
-                        <p className="text-white/90 text-sm leading-[1.43]">
-                            {t('pages.pricing.payFlow.goToPayDesc')}
-                        </p>
-                    ),
-                    buttons: [
-                        {
-                            label: t('pages.pricing.payFlow.goToPayButton'),
-                            variant: 'primary',
-                            onClick: (
-                                _e: React.MouseEvent<HTMLButtonElement>,
-                                dialogId: string
-                            ) => {
-                                window.open(order.payUrl, '_blank');
-                                closeDialog(dialogId);
-                                const waitId = openDialog({
-                                    title: t('pages.pricing.payFlow.waitingTitle'),
-                                    content: (
-                                        <PayWaitingContent
-                                            orderId={order.orderId}
-                                            onClose={() => closeDialog(waitId)}
-                                        />
-                                    ),
-                                    buttons: [
-                                        {
-                                            label: t('pages.pricing.payFlow.closeWaiting'),
-                                            variant: 'secondary',
-                                            onClick: (
-                                                _e: React.MouseEvent<HTMLButtonElement>,
-                                                dialogId: string
-                                            ) => {
-                                                closeDialog(dialogId);
-                                            },
-                                        },
-                                    ],
-                                    maskClosable: false,
-                                });
-                            },
-                        },
-                    ],
-                    maskClosable: true,
-                });
-            })
-            .catch(() => {});
-    }, [
-        selectedPlan,
-        selectedPayMethod,
-        t,
-        openDialog,
-        closeDialog,
-        deeplinkUrl,
-        downloadUrlByPlatform,
-    ]);
-
-    return {
-        handlePayNow,
-    };
-}

+ 138 - 55
src/pages/pricing/index.tsx

@@ -1,5 +1,6 @@
 import { memo, useRef } from 'react';
 
+import { Form } from 'antd';
 import { useTranslation } from 'react-i18next';
 
 import { useResponsive } from '@/hooks/useSize';
@@ -11,20 +12,101 @@ import UserInfo from './components/UserInfo';
 import { useAction } from './useAction';
 import { usePlanCardsHeightSync } from './usePlanCardsHeightSync';
 import { useService } from './useService';
+import type { Plan } from './useService';
+
+const PRICING_FORM_ID = 'pricing-form';
+
+export interface PlanSelectorProps {
+    value?: string;
+    onChange?: (planId: string) => void;
+    plans: Plan[];
+    planCardsContainerRef: React.RefObject<HTMLDivElement | null>;
+    isMobile: boolean;
+}
+
+function PlanSelector({
+    value,
+    onChange,
+    plans,
+    planCardsContainerRef,
+    isMobile,
+}: PlanSelectorProps) {
+    return (
+        <div
+            ref={planCardsContainerRef as React.RefObject<HTMLDivElement>}
+            data-count={plans.length}
+            className={
+                isMobile
+                    ? 'flex-col-c gap-5'
+                    : 'grid grid-cols-4 gap-x-8 gap-y-4 [&[data-count="1"]]:grid-cols-3 [&[data-count="2"]]:grid-cols-3 [&[data-count="3"]]:grid-cols-3'
+            }
+        >
+            {plans.map((plan) => (
+                <PlanCard
+                    key={plan.id}
+                    id={plan.id}
+                    title={plan.title}
+                    subTitle={plan.subTitle}
+                    introduce={plan.introduce}
+                    tag={plan.tag}
+                    tagType={plan.tagType}
+                    isSelected={value === plan.id}
+                    onClick={() => onChange?.(plan.id)}
+                />
+            ))}
+        </div>
+    );
+}
+
+export interface PayMethodSelectorProps {
+    value?: string;
+    onChange?: (payType: string) => void;
+    payMethods: API.PayTypeItem[];
+    isMobile: boolean;
+}
+
+function PayMethodSelector({ value, onChange, payMethods, isMobile }: PayMethodSelectorProps) {
+    return (
+        <div
+            className={
+                isMobile
+                    ? 'flex-col-c gap-4'
+                    : 'flex flex-wrap justify-start gap-x-8 gap-y-4 [&>*]:flex-[1_1_calc(50%-1rem)] [&>*]:max-w-[calc(50%-1rem)]'
+            }
+        >
+            {payMethods.map((item) => (
+                <PayMethodCard
+                    key={item.payType}
+                    item={item}
+                    isSelected={value === item.payType}
+                    onClick={() => onChange?.(item.payType)}
+                />
+            ))}
+        </div>
+    );
+}
 
 const Pricing = memo(() => {
     const { t } = useTranslation();
     const { isMobile } = useResponsive();
+    const [form] = Form.useForm<{ planId: string; payMethod: string }>();
     const planCardsContainerRef = useRef<HTMLDivElement>(null);
-    const { selectedPlanId, handlePlanClick, selectedPayMethod, handlePayMethodClick } =
-        useAction();
-
     const { plans, payMethods } = useService();
+    const { handlePayNow } = useAction();
 
-    const selectedPlan = plans.find((plan) => plan.id === selectedPlanId) || null;
+    const planId = Form.useWatch('planId', form);
+    const payMethod = Form.useWatch('payMethod', form);
+    const selectedPlan = plans.find((p) => p.id === planId) ?? null;
+    const selectedPayMethod = payMethod ?? null;
 
     usePlanCardsHeightSync(planCardsContainerRef, isMobile, plans.length);
 
+    const onFinish = (values: { planId: string; payMethod: string }) => {
+        const plan = plans.find((p) => p.id === values.planId);
+        if (!plan || !values.payMethod) return;
+        handlePayNow(plan, values.payMethod);
+    };
+
     return (
         <div className="flex items-start justify-center">
             <div className={`max-w-[1440px] w-full ${isMobile ? 'px-0' : 'px-[30px]'}`}>
@@ -37,60 +119,61 @@ const Pricing = memo(() => {
                         {t('pages.pricing.title')}
                     </span>
                     <UserInfo />
-                    <div className="flex flex-col gap-5">
-                        <span
-                            className={`text-white font-semibold leading-[1.43] ${isMobile ? 'text-[16px]' : 'text-[22px]'}`}
-                        >
-                            {t('pages.pricing.selecPlan')}
-                        </span>
-                        <div
-                            ref={planCardsContainerRef}
-                            data-count={plans.length}
-                            className={
-                                isMobile
-                                    ? 'flex-col-c gap-5'
-                                    : 'grid grid-cols-4 gap-x-8 gap-y-4 [&[data-count="1"]]:grid-cols-3 [&[data-count="2"]]:grid-cols-3 [&[data-count="3"]]:grid-cols-3'
-                            }
-                        >
-                            {plans.map((plan) => (
-                                <PlanCard
-                                    key={plan.id}
-                                    id={plan.id}
-                                    title={plan.title}
-                                    subTitle={plan.subTitle}
-                                    introduce={plan.introduce}
-                                    tag={plan.tag}
-                                    tagType={plan.tagType}
-                                    isSelected={selectedPlanId === plan.id}
-                                    onClick={() => handlePlanClick(plan.id)}
+                    <Form
+                        form={form}
+                        id={PRICING_FORM_ID}
+                        layout="vertical"
+                        onFinish={onFinish}
+                        className="flex flex-col gap-5"
+                    >
+                        <div className="flex flex-col gap-5">
+                            <span
+                                className={`text-white font-semibold leading-[1.43] ${isMobile ? 'text-[16px]' : 'text-[22px]'}`}
+                            >
+                                {t('pages.pricing.selecPlan')}
+                            </span>
+                            <Form.Item
+                                name="planId"
+                                rules={[
+                                    {
+                                        required: true,
+                                        message: t('pages.pricing.pleaseSelectPlan'),
+                                    },
+                                ]}
+                                className="mb-0 [&_.ant-form-item-explain]:mt-3"
+                            >
+                                <PlanSelector
+                                    plans={plans}
+                                    planCardsContainerRef={planCardsContainerRef}
+                                    isMobile={isMobile}
                                 />
-                            ))}
+                            </Form.Item>
                         </div>
-                    </div>
-                    <div className="flex flex-col gap-4 mt-1">
-                        <span
-                            className={`text-white font-semibold leading-[1.43] ${isMobile ? 'text-[16px]' : 'text-[22px]'}`}
-                        >
-                            {t('pages.pricing.selectPayMethod')}
-                        </span>
-                        <div
-                            className={
-                                isMobile
-                                    ? 'flex-col-c gap-4'
-                                    : 'flex flex-wrap justify-start gap-x-8 gap-y-4 [&>*]:flex-[1_1_calc(50%-1rem)] [&>*]:max-w-[calc(50%-1rem)]'
-                            }
-                        >
-                            {payMethods.map((item) => (
-                                <PayMethodCard
-                                    key={item.payType}
-                                    item={item}
-                                    isSelected={selectedPayMethod === item.payType}
-                                    onClick={() => handlePayMethodClick(item.payType)}
-                                />
-                            ))}
+                        <div className="flex flex-col gap-4 mt-1">
+                            <span
+                                className={`text-white font-semibold leading-[1.43] ${isMobile ? 'text-[16px]' : 'text-[22px]'}`}
+                            >
+                                {t('pages.pricing.selectPayMethod')}
+                            </span>
+                            <Form.Item
+                                name="payMethod"
+                                rules={[
+                                    {
+                                        required: true,
+                                        message: t('pages.pricing.pleaseSelectPayMethod'),
+                                    },
+                                ]}
+                                className="mb-0 [&_.ant-form-item-explain]:mt-3"
+                            >
+                                <PayMethodSelector payMethods={payMethods} isMobile={isMobile} />
+                            </Form.Item>
                         </div>
-                    </div>
-                    <OrderSummary selectedPlan={selectedPlan} selectedPayMethod={selectedPayMethod}/>
+                        <OrderSummary
+                            formId={PRICING_FORM_ID}
+                            selectedPlan={selectedPlan}
+                            selectedPayMethod={selectedPayMethod}
+                        />
+                    </Form>
                 </div>
             </div>
         </div>

+ 0 - 28
src/pages/pricing/useAction.ts

@@ -1,28 +0,0 @@
-import { useState, useCallback } from 'react';
-
-export interface UseActionReturn {
-    selectedPlanId: string | null;
-    handlePlanClick: (planId: string) => void;
-    selectedPayMethod: string | null;
-    handlePayMethodClick: (payType: string) => void;
-}
-
-export function useAction(): UseActionReturn {
-    const [selectedPlanId, setSelectedPlanId] = useState<string | null>(null);
-    const [selectedPayMethod, setSelectedPayMethod] = useState<string | null>(null);
-
-    const handlePlanClick = useCallback((planId: string) => {
-        setSelectedPlanId(planId);
-    }, []);
-
-    const handlePayMethodClick = useCallback((payType: string) => {
-        setSelectedPayMethod(payType);
-    }, []);
-
-    return {
-        selectedPlanId,
-        handlePlanClick,
-        selectedPayMethod,
-        handlePayMethodClick,
-    };
-}

+ 153 - 0
src/pages/pricing/useAction.tsx

@@ -0,0 +1,153 @@
+import { useCallback } from 'react';
+
+import { Trans, useTranslation } from 'react-i18next';
+
+import LoginForm from '@/components/LoginForm';
+import { useAppUrls } from '@/hooks/useAppUrls';
+import { dialogModel } from '@/models/dialogModel';
+import { fetchPayOrderCreate } from '@/services/config';
+import { getToken } from '@/utils/authUtils';
+import { currentUnixTimestamp } from '@/utils/timeUtils';
+
+import { PayWaitingContent } from './components/OrderSummary/PayWaitingContent';
+import type { Plan } from './useService';
+
+const ORDER_TYPE_PLAN = 1;
+
+export interface UseActionReturn {
+    handlePayNow: (plan: Plan, payMethod: string) => void;
+}
+
+export function useAction(): UseActionReturn {
+    const { t } = useTranslation();
+    const { openDialog, closeDialog } = dialogModel.useModel();
+    const { deeplinkUrl, downloadUrlByPlatform } = useAppUrls();
+
+    const handlePayNow = useCallback(
+        (selectedPlan: Plan, selectedPayMethod: string) => {
+            const token = getToken();
+            const expired =
+                !token?.accessExpires || (token.accessExpires ?? 0) - currentUnixTimestamp() <= 0;
+            if (!token?.accessToken || expired) {
+                const id = openDialog({
+                    title: t('pages.pricing.payFlow.loginTitle'),
+                    content: (
+                        <div className="flex flex-col gap-2">
+                            <p className="text-white/80 text-sm mb-2">
+                                <Trans
+                                    i18nKey="pages.pricing.payFlow.loginPrompt"
+                                    components={{
+                                        linkText: (() => {
+                                            const Wrap = ({
+                                                children,
+                                            }: {
+                                                children?: React.ReactNode;
+                                            }) =>
+                                                deeplinkUrl ? (
+                                                    <a
+                                                        href={deeplinkUrl}
+                                                        className="text-[#0EA5E9] hover:underline"
+                                                        target="_blank"
+                                                        rel="noopener noreferrer"
+                                                    >
+                                                        {children}
+                                                    </a>
+                                                ) : (
+                                                    <span>{children}</span>
+                                                );
+                                            return <Wrap />;
+                                        })(),
+                                        downloadLink: (() => {
+                                            const Wrap = ({
+                                                children,
+                                            }: {
+                                                children?: React.ReactNode;
+                                            }) =>
+                                                downloadUrlByPlatform ? (
+                                                    <a
+                                                        href={downloadUrlByPlatform}
+                                                        className="text-[#0EA5E9] hover:underline"
+                                                        target="_blank"
+                                                        rel="noopener noreferrer"
+                                                    >
+                                                        {children}
+                                                    </a>
+                                                ) : (
+                                                    <span>{children}</span>
+                                                );
+                                            return <Wrap />;
+                                        })(),
+                                    }}
+                                />
+                            </p>
+                            <LoginForm onSuccess={() => closeDialog(id)} />
+                        </div>
+                    ),
+                    maskClosable: true,
+                });
+                return;
+            }
+
+            fetchPayOrderCreate({
+                orderType: ORDER_TYPE_PLAN,
+                payType: selectedPayMethod,
+                channelItemId: selectedPlan.id,
+            })
+                .then((res) => {
+                    const order = res?.data?.userPayOrder;
+                    if (!order?.payUrl || !order?.orderId) return;
+
+                    openDialog({
+                        title: t('pages.pricing.payFlow.goToPayTitle'),
+                        content: (
+                            <p className="text-white/90 text-sm leading-[1.43]">
+                                {t('pages.pricing.payFlow.goToPayDesc')}
+                            </p>
+                        ),
+                        buttons: [
+                            {
+                                label: t('pages.pricing.payFlow.goToPayButton'),
+                                variant: 'primary',
+                                onClick: (
+                                    _e: React.MouseEvent<HTMLButtonElement>,
+                                    dialogId: string
+                                ) => {
+                                    window.open(order.payUrl, '_blank');
+                                    closeDialog(dialogId);
+                                    const waitId = openDialog({
+                                        title: t('pages.pricing.payFlow.waitingTitle'),
+                                        content: (
+                                            <PayWaitingContent
+                                                orderId={order.orderId}
+                                                onClose={() => closeDialog(waitId)}
+                                            />
+                                        ),
+                                        buttons: [
+                                            {
+                                                label: t('pages.pricing.payFlow.closeWaiting'),
+                                                variant: 'secondary',
+                                                onClick: (
+                                                    _e: React.MouseEvent<HTMLButtonElement>,
+                                                    dialogId: string
+                                                ) => {
+                                                    closeDialog(dialogId);
+                                                },
+                                            },
+                                        ],
+                                        maskClosable: false,
+                                    });
+                                },
+                            },
+                        ],
+                        maskClosable: true,
+                    });
+                })
+                .catch(() => {});
+        },
+        [t, openDialog, closeDialog, deeplinkUrl, downloadUrlByPlatform]
+    );
+
+    return {
+        handlePayNow,
+    };
+}