From 3ae63c40ce0d68f56f8fcb6913ad3c16e9d344eb Mon Sep 17 00:00:00 2001 From: cyrbuzz Date: Fri, 31 Oct 2025 17:45:51 +0800 Subject: [PATCH 01/11] feat: subscribe --- src/components/CreateFlexPlan/index.tsx | 413 +++++------------- .../DeploymentInfo/DeploymentInfo.tsx | 31 +- src/components/GetEndpoint/index.tsx | 92 ++-- src/hooks/useConsumerHostServices.tsx | 105 +++++ .../MyFlexPlans/MyHostedPlan/MyHostedPlan.tsx | 277 ++++++++---- 5 files changed, 468 insertions(+), 450 deletions(-) diff --git a/src/components/CreateFlexPlan/index.tsx b/src/components/CreateFlexPlan/index.tsx index 96c465a2..c1497f32 100644 --- a/src/components/CreateFlexPlan/index.tsx +++ b/src/components/CreateFlexPlan/index.tsx @@ -2,33 +2,27 @@ // SPDX-License-Identifier: Apache-2.0 import React, { FC, useEffect, useMemo, useRef, useState } from 'react'; -import { AiOutlineInfoCircle } from 'react-icons/ai'; import { specialApiKeyName } from '@components/GetEndpoint'; -import { PriceQueriesChart } from '@components/IndexerDetails/PriceQueries'; import { ApproveContract } from '@components/ModalApproveToken'; import TokenTooltip from '@components/TokenTooltip/TokenTooltip'; import { useSQToken } from '@containers'; -import { SQT_TOKEN_ADDRESS, useAccount } from '@containers/Web3'; +import { useAccount } from '@containers/Web3'; import { useAddAllowance } from '@hooks/useAddAllowance'; import { GetUserApiKeys, - IGetHostingPlans, - IPostHostingPlansParams, + IGetUserSubscription, isConsumerHostError, useConsumerHostServices, } from '@hooks/useConsumerHostServices'; import { ProjectDetailsQuery } from '@hooks/useProjectFromQuery'; import { useSqtPrice } from '@hooks/useSqtPrice'; import { Steps, Typography } from '@subql/components'; -import { ProjectType as contractProjectType } from '@subql/contract-sdk'; -import { ProjectType } from '@subql/network-query'; import { formatSQT, useAsyncMemo, useGetDeploymentBoosterTotalAmountByDeploymentIdQuery } from '@subql/react-hooks'; import { parseError, TOKEN, tokenDecimals } from '@utils'; -import { Button, Checkbox, Divider, Form, InputNumber, Popover, Tooltip } from 'antd'; +import { Button, Checkbox, Divider, Form, InputNumber, Tooltip } from 'antd'; import BigNumberJs from 'bignumber.js'; import clsx from 'clsx'; -import { BigNumber } from 'ethers'; -import { formatUnits, parseEther } from 'ethers/lib/utils'; +import { parseEther } from 'ethers/lib/utils'; import { useWeb3Store } from 'src/stores'; @@ -38,49 +32,36 @@ interface IProps { project: Pick; deploymentId: string; prevApiKey?: GetUserApiKeys; - prevHostingPlan?: IGetHostingPlans; + prevSubscription?: IGetUserSubscription; onSuccess?: () => void; onBack?: () => void; } -const converFlexPlanPrice = (price: string) => { - return BigNumberJs(formatUnits(price, tokenDecimals[SQT_TOKEN_ADDRESS])).multipliedBy(1000); -}; - -const subqlProjectTypeToContractType = { - [ProjectType.LLM]: 0, // no for now - [ProjectType.RPC]: contractProjectType.RPC, - [ProjectType.SUBQUERY]: contractProjectType.SUBQUERY, - [ProjectType.SQ_DICT]: contractProjectType.SQ_DICT, - [ProjectType.SUBGRAPH]: contractProjectType.SUBGRAPH, -}; - // TODO: split the component to smaller components -const CreateFlexPlan: FC = ({ deploymentId, project, prevHostingPlan, prevApiKey, onSuccess, onBack }) => { +const CreateFlexPlan: FC = ({ deploymentId, project, prevSubscription, prevApiKey, onSuccess, onBack }) => { const { address: account } = useAccount(); const { contracts } = useWeb3Store(); - const [form] = Form.useForm(); const [depositForm] = Form.useForm<{ amount: string }>(); const depositAmount = Form.useWatch('amount', depositForm); - const priceValue = Form.useWatch('price', form); - const maximumValue = Form.useWatch('maximum', form); const { consumerHostAllowance, consumerHostBalance, balance } = useSQToken(); const { addAllowance } = useAddAllowance(); const sqtPrice = useSqtPrice(); const mounted = useRef(false); const [currentStep, setCurrentStep] = React.useState(0); - const [selectedPlan, setSelectedPlan] = useState<'economy' | 'performance' | 'custom'>('custom'); const [nextBtnLoading, setNextBtnLoading] = useState(false); - const [displayTransactions, setDisplayTransactions] = useState<('allowance' | 'deposit' | 'createApiKey')[]>([]); + const [displayTransactions, setDisplayTransactions] = useState< + ('allowance' | 'deposit' | 'createApiKey' | 'subscribe')[] + >([]); const [transacitonNumbers, setTransactionNumbers] = useState<{ [key in string]: number }>({ allowance: 1, deposit: 2, createApiKey: 3, + subscribe: 4, }); - const [transactionStep, setTransactionStep] = useState<'allowance' | 'deposit' | 'createApiKey' | undefined>( - 'allowance', - ); + const [transactionStep, setTransactionStep] = useState< + 'allowance' | 'deposit' | 'createApiKey' | 'subscribe' | undefined + >('allowance'); const deploymentBooster = useGetDeploymentBoosterTotalAmountByDeploymentIdQuery({ variables: { @@ -92,34 +73,11 @@ const CreateFlexPlan: FC = ({ deploymentId, project, prevHostingPlan, pr const [depositBalance] = useMemo(() => consumerHostBalance.result.data ?? [], [consumerHostBalance.result.data]); - const { - getProjects, - createNewApiKey, - createHostingPlanApi, - updateHostingPlanApi, - getUserApiKeysApi, - refreshUserInfo, - getChannelLimit, - getDominantPrice, - } = useConsumerHostServices({ - alert: true, - autoLogin: false, - }); - - const flexPlans = useAsyncMemo(async () => { - try { - const res = await getProjects({ - projectId: BigNumber.from(project.id).toString(), - deployment: deploymentId, - }); - - if (res.data?.indexers?.length) { - return res.data.indexers; - } - } catch (e) { - return []; - } - }, [project.id, deploymentId]); + const { createNewApiKey, createSubscription, getUserApiKeysApi, refreshUserInfo, getChannelLimit } = + useConsumerHostServices({ + alert: true, + autoLogin: false, + }); const estimatedChannelLimit = useAsyncMemo(async () => { try { @@ -153,70 +111,9 @@ const CreateFlexPlan: FC = ({ deploymentId, project, prevHostingPlan, pr }, [estimatedChannelLimit]); const minDeposit = useMemo(() => { - // const sortedMinial = BigNumberJs(depositRequireFromConsumerHost).minus( - // formatSQT(depositBalance?.toString() || '0'), - // ); - // return sortedMinial.lte(0) ? 0 : sortedMinial.toNumber(); return 10000; }, [depositRequireFromConsumerHost, depositBalance]); - const estimatedPriceInfo = useMemo(() => { - if (!flexPlans.data || flexPlans.data.length === 0) { - return { - economy: BigNumberJs(0), - performance: BigNumberJs(0), - }; - } - - // ASC - const sortedFlexPlans = flexPlans.data.map((i) => converFlexPlanPrice(i.price)).sort((a, b) => (a.lt(b) ? -1 : 1)); - const maxPrice = sortedFlexPlans.at(-1); - - // if less than 3, both economy and performance should be the highest price - if (flexPlans.data?.length <= 3) { - return { - economy: maxPrice, - performance: maxPrice, - }; - } - - if (flexPlans.data?.length <= 5) { - return { - economy: sortedFlexPlans[2], - performance: maxPrice, - }; - } - - const economyIndex = Math.ceil(flexPlans.data.length * 0.4) < 2 ? 2 : Math.ceil(flexPlans.data.length * 0.4); - const performanceIndex = Math.ceil(flexPlans.data.length * 0.8) < 4 ? 4 : Math.ceil(flexPlans.data.length * 0.8); - - return { - economy: sortedFlexPlans[economyIndex], - performance: sortedFlexPlans[performanceIndex], - }; - }, [flexPlans]); - - const matchedCount = React.useMemo(() => { - if (!priceValue || !flexPlans.data?.length) return `Matched Node Operators: 0`; - const count = flexPlans.data.filter((i) => { - const prices1000 = converFlexPlanPrice(i.price); - return prices1000.lte(priceValue); - }).length; - return `Matched Node Operators: ${count}`; - }, [priceValue, flexPlans]); - - const enoughReq = useMemo(() => { - const priceVal = priceValue || (form.getFieldsValue(true)['price'] as string); - if (!priceVal || depositBalance?.eq(0) || !depositBalance) return 0; - - return BigNumberJs(formatSQT(depositBalance.toString())) - .div(BigNumberJs(priceVal.toString())) - .multipliedBy(1000) - .decimalPlaces(0) - ?.toNumber() - ?.toLocaleString(); - }, [depositBalance, priceValue, form, currentStep]); - const needAddAllowance = useMemo(() => { const allowance = consumerHostAllowance.result.data; if (allowance?.eq(0) && depositAmount && depositAmount !== 0) return true; @@ -230,35 +127,30 @@ const CreateFlexPlan: FC = ({ deploymentId, project, prevHostingPlan, pr const needCreateApiKey = useMemo(() => !prevApiKey, [prevApiKey]); + const needSubscribe = useMemo(() => !prevSubscription, [prevSubscription]); + const nextBtnText = useMemo(() => { if (currentStep === 0) return 'Next'; - if (currentStep === 1) return 'Deposit SQT'; - - if (currentStep === 2) { - if (!displayTransactions.length) return 'Create Flex Plan'; + if (currentStep === 1) { + if (!displayTransactions.length) return 'Subscribe to Project'; if (transactionStep) { const currentStepNumber = transacitonNumbers[transactionStep]; return `Approve Transaction ${currentStepNumber}${ - currentStepNumber === displayTransactions.length ? ' and Create Flex Plan' : '' + currentStepNumber === displayTransactions.length ? ' and Subscribe' : '' }`; } - return 'Create Flex Plan'; + return 'Subscribe to Project'; } return 'Next'; }, [currentStep, displayTransactions, transacitonNumbers, transactionStep]); const suggestDeposit = useMemo(() => { - const inputEstimated = BigNumberJs(priceValue || '0') - .multipliedBy(20) - .multipliedBy(maximumValue || 2); - - if (inputEstimated.lt(depositRequireFromConsumerHost)) return depositRequireFromConsumerHost.toLocaleString(); - return inputEstimated.toNumber().toLocaleString(); - }, [depositRequireFromConsumerHost, priceValue, maximumValue]); + return depositRequireFromConsumerHost.toLocaleString(); + }, [depositRequireFromConsumerHost]); const renderTransactionDisplay = useMemo(() => { const allowanceDom = (index: number) => { @@ -279,7 +171,7 @@ const CreateFlexPlan: FC = ({ deploymentId, project, prevHostingPlan, pr This grants permission for SubQuery to manage your Billing Account automatically to pay node operators for - charges incurred in this new Flex Plan + charges incurred in this Flex Plan @@ -329,7 +221,31 @@ const CreateFlexPlan: FC = ({ deploymentId, project, prevHostingPlan, pr This is a transaction to open a state channel and generate a personal API key for your account to secure - your new Flex Plan endpoint + your Flex Plan endpoint + + + + ); + }; + + const subscribeDom = (index: number) => { + return ( +
+
+ + {!needSubscribe && } + {index}. Subscribe to Project + + + Subscribe to this project to get access to the Flex Plan endpoint
@@ -340,40 +256,24 @@ const CreateFlexPlan: FC = ({ deploymentId, project, prevHostingPlan, pr allowance: allowanceDom, deposit: depositDom, createApiKey: createApiKeysDom, + subscribe: subscribeDom, }; return displayTransactions.map((i, index) => { return dicts[i](index + 1); }); - }, [displayTransactions, transactionStep, needCreateApiKey, needAddAllowance, needDepositMore, depositForm]); - - const suggestHostingPlanPrice = useAsyncMemo(async () => { - try { - const price = await getDominantPrice({ - ptype: subqlProjectTypeToContractType[project.type], - }); - - return formatSQT(BigNumberJs(price.data.avg_price).multipliedBy(1000).toString()); - } catch { - return ''; - } - }, [project.type]); + }, [ + displayTransactions, + transactionStep, + needCreateApiKey, + needAddAllowance, + needDepositMore, + needSubscribe, + depositForm, + ]); const handleNextStep = async (options?: { skipDeposit?: boolean }) => { if (currentStep === 0) { - if (!selectedPlan) return; - if (selectedPlan !== 'custom') { - form.setFieldValue('price', estimatedPriceInfo[selectedPlan]?.toString()); - form.setFieldValue('maximum', selectedPlan === 'economy' ? 8 : 15); - } else { - await form.validateFields(); - form.setFieldValue('maximum', form.getFieldValue('maximum') || 2); - } - - setCurrentStep(1); - } - - if (currentStep === 1) { const { skipDeposit = false } = options || {}; if (!skipDeposit) { await depositForm.validateFields(); @@ -392,15 +292,19 @@ const CreateFlexPlan: FC = ({ deploymentId, project, prevHostingPlan, pr if (needCreateApiKey) { newDisplayTransactions.push('createApiKey'); } + if (needSubscribe) { + newDisplayTransactions.push('subscribe'); + } if (newDisplayTransactions.includes('allowance')) { setTransactionStep('allowance'); } else if (newDisplayTransactions.includes('deposit')) { setTransactionStep('deposit'); - } else { + } else if (newDisplayTransactions.includes('createApiKey')) { setTransactionStep('createApiKey'); + } else { + setTransactionStep('subscribe'); } - // TODO: make a enum // @ts-ignore setDisplayTransactions(newDisplayTransactions); @@ -413,14 +317,15 @@ const CreateFlexPlan: FC = ({ deploymentId, project, prevHostingPlan, pr {} as { [key in string]: number }, ), ); - setCurrentStep(2); + setCurrentStep(1); } - if (currentStep === 2) { + if (currentStep === 1) { setNextBtnLoading(true); - const getNextStepAndSet = (transactionName: 'allowance' | 'deposit' | 'createApiKeys') => { + const getNextStepAndSet = (transactionName: 'allowance' | 'deposit' | 'createApiKey' | 'subscribe') => { const index = displayTransactions.findIndex((i) => i === transactionName) + 1; if (index < displayTransactions.length) { + // @ts-ignore setTransactionStep(displayTransactions[index]); } else { setTransactionStep(undefined); @@ -471,7 +376,28 @@ const CreateFlexPlan: FC = ({ deploymentId, project, prevHostingPlan, pr } } - getNextStepAndSet('createApiKeys'); + getNextStepAndSet('createApiKey'); + + const currentStepNumber = transacitonNumbers['createApiKey']; + + if (currentStepNumber !== displayTransactions.length) { + return; + } + } + + if (needSubscribe) { + setTransactionStep('subscribe'); + + const projectIdNumber = parseInt(project.id.replace('0x', ''), 16); + const subscriptionRes = await createSubscription({ + project_id: projectIdNumber, + }); + + if (isConsumerHostError(subscriptionRes.data)) { + throw new Error(subscriptionRes.data.error); + } + + getNextStepAndSet('subscribe'); } try { @@ -480,29 +406,6 @@ const CreateFlexPlan: FC = ({ deploymentId, project, prevHostingPlan, pr // don't care of this } - const price = form.getFieldsValue(true)['price']; - const maximum = form.getFieldsValue(true)['maximum']; - - const createOrUpdate = prevHostingPlan ? updateHostingPlanApi : createHostingPlanApi; - // if already created the plan, just update it. - const minExpiration = estimatedChannelLimit?.data?.channelMinExpiration || 3600 * 24 * 14; - const expiration = flexPlans?.data?.sort((a, b) => b.max_time - a.max_time)[0].max_time || 0; - const maximumValue = - (estimatedChannelLimit.data?.channelMaxNum || 0) < Math.ceil(maximum) - ? estimatedChannelLimit.data?.channelMaxNum - : Math.ceil(maximum); - const res = await createOrUpdate({ - deploymentId: deploymentId, - price: parseEther(`${price}`).div(1000).toString(), - maximum: maximumValue || 2, - expiration: expiration < minExpiration ? minExpiration : expiration, - id: prevHostingPlan?.id || '0', - }); - - if (isConsumerHostError(res.data)) { - throw new Error(res.data.error); - } - await onSuccess?.(); } catch (e) { parseError(e, { @@ -536,9 +439,6 @@ const CreateFlexPlan: FC = ({ deploymentId, project, prevHostingPlan, pr = ({ deploymentId, project, prevHostingPlan, pr ]} > - {currentStep === 0 && ( - <> - - SubQuery will automatically allocate qualified Node Operators to your endpoint based on price and - performance. Please select the type of plan you would like (you can change this later). - - -
{ - setSelectedPlan('custom'); - if (selectedPlan !== 'custom') { - form.resetFields(); - } - }} - > - Enter a custom price - {selectedPlan === 'custom' && ( - <> - - Please enter a custom price, and an optional limit - - -
- - Maximum Price - - - - - - - The average price of per 1000 requests is {suggestHostingPlanPrice.data} {TOKEN}. - { - form.setFieldsValue({ - price: suggestHostingPlanPrice.data, - }); - }} - > - Set it - - -
- ) : ( - '' - ) - } - placement="topLeft" - > - - - - - - {matchedCount} - - - - - - )} - - - )} - - {/* need the Form element render, so can trace the state */} -
+
Every wallet has a Billing Account where you must deposit SQT that you authorise SubQuery to deduct for Flex Plan payments. If this Billing Account runs out of SQT, your Flex plan will automatically be cancelled and @@ -640,36 +458,17 @@ const CreateFlexPlan: FC = ({ deploymentId, project, prevHostingPlan, pr You can easily withdraw unused SQT from this Billing Account at any time without any unlocking period. - We recommend ensuring that there is sufficient SQT in your billing account so that you don’t run out + We recommend ensuring that there is sufficient SQT in your billing account so that you don't run out unexpectedly. -
-
- Your selected plan: - - - {selectedPlan} - -
-
- - {form.getFieldValue('price')} {TOKEN} - - Per 1000 reqs - {sqtPrice !== '0' && (~US${estimatedUs(form.getFieldValue('price'))})} -
-
{depositBalance?.eq(0) || !depositBalance ? ( <> You must deposit SQT to open this billing account - You must deposit SQT to create this flex plan, we suggest {suggestDeposit} {TOKEN} + You must deposit SQT to subscribe to this project, we suggest {suggestDeposit} {TOKEN} ) : ( @@ -682,7 +481,7 @@ const CreateFlexPlan: FC = ({ deploymentId, project, prevHostingPlan, pr {TOKEN} - This is enough to pay for {enoughReq} requests, we suggest {suggestDeposit} {TOKEN} + We suggest depositing at least {suggestDeposit} {TOKEN} )} @@ -742,13 +541,13 @@ const CreateFlexPlan: FC = ({ deploymentId, project, prevHostingPlan, pr
- {currentStep === 2 && ( + {currentStep === 1 && ( <> {displayTransactions.length ? ( You must now approve {displayTransactions.length > 1 ? 'a few transactions' : 'a transaction'} using your - connected wallet to initiate this Flex Plan. You must approve all transactions if in order to create a - Flex Plan + connected wallet to subscribe to this project. You must approve all transactions in order to complete the + subscription. ) : ( '' @@ -777,7 +576,7 @@ const CreateFlexPlan: FC = ({ deploymentId, project, prevHostingPlan, pr Back - {currentStep === 1 && + {currentStep === 0 && !BigNumberJs( deploymentBooster.data?.deploymentBoosterSummariesByConsumer?.aggregates?.sum?.totalAmount.toString() || '0', ).isZero() ? ( @@ -796,13 +595,7 @@ const CreateFlexPlan: FC = ({ deploymentId, project, prevHostingPlan, pr ) : ( '' )} -
diff --git a/src/components/DeploymentInfo/DeploymentInfo.tsx b/src/components/DeploymentInfo/DeploymentInfo.tsx index fcb0f3f3..74faf462 100644 --- a/src/components/DeploymentInfo/DeploymentInfo.tsx +++ b/src/components/DeploymentInfo/DeploymentInfo.tsx @@ -66,6 +66,11 @@ export const DeploymentInfo: React.FC = ({ project, deploymentId, type, m onClick={() => { onClick && onClick(); }} + style={{ + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + }} >
{project?.name && ( @@ -88,19 +93,21 @@ export const DeploymentInfo: React.FC = ({ project, deploymentId, type, m
)} -
- - {versionHeader} - - - - - {deploymentId - ? `${deploymentId.slice(0, 5)}...${deploymentId.slice(deploymentId.length - 5, deploymentId.length)}` - : '-'} + {deploymentId && ( +
+ + {versionHeader} - -
+ + + + {deploymentId + ? `${deploymentId.slice(0, 5)}...${deploymentId.slice(deploymentId.length - 5, deploymentId.length)}` + : '-'} + + +
+ )} diff --git a/src/components/GetEndpoint/index.tsx b/src/components/GetEndpoint/index.tsx index c4ee1c13..17b0b629 100644 --- a/src/components/GetEndpoint/index.tsx +++ b/src/components/GetEndpoint/index.tsx @@ -10,7 +10,9 @@ import { useAccount } from '@containers/Web3'; import { GetUserApiKeys, IGetHostingPlans, + IGetUserSubscription, isConsumerHostError, + isNotSubscribed, useConsumerHostServices, } from '@hooks/useConsumerHostServices'; import { ProjectDetailsQuery } from '@hooks/useProjectFromQuery'; @@ -84,17 +86,18 @@ const GetEndpoint: FC = ({ deploymentId, project, actionBtn, initialOpen const [freeOrFlexPlan, setFreeOrFlexPlan] = React.useState<'free' | 'flexPlan'>('flexPlan'); const [nextBtnLoading, setNextBtnLoading] = useState(false); - const [userHostingPlan, setUserHostingPlan] = useState([]); + const [currentSubscription, setCurrentSubscription] = useState(null); const [userApiKeys, setUserApiKeys] = useState([]); - const { getHostingPlanApi, checkIfHasLogin, getUserApiKeysApi, createNewApiKey } = useConsumerHostServices({ - alert: false, - autoLogin: false, - }); + const { getUserSubscriptionByProject, createSubscription, checkIfHasLogin, getUserApiKeysApi, createNewApiKey } = + useConsumerHostServices({ + alert: false, + autoLogin: false, + }); - const createdHostingPlan = useMemo(() => { - return userHostingPlan.find((plan) => plan.deployment.deployment === deploymentId && plan.is_actived); - }, [userHostingPlan]); + const hasActiveSubscription = useMemo(() => { + return currentSubscription?.is_active || false; + }, [currentSubscription]); const createdApiKey = useMemo(() => { return userApiKeys.find((key) => key.name === specialApiKeyName); @@ -104,16 +107,16 @@ const GetEndpoint: FC = ({ deploymentId, project, actionBtn, initialOpen if (currentStep === 'select') { if (freeOrFlexPlan === 'free') return 'View Free Public Endpoint'; if (freeOrFlexPlan === 'flexPlan') { - if (createdHostingPlan) { + if (hasActiveSubscription) { return 'View Flex Plan Endpoint'; } - return 'Create Flex Plan'; + return 'Subscribe to Project'; } } if (currentStep === 'checkFree' || currentStep === 'checkEndpointWithApiKey') return 'Copy endpoint and Close'; - return 'Create Flex Plan'; - }, [freeOrFlexPlan, currentStep, createdHostingPlan]); + return 'Subscribe to Project'; + }, [freeOrFlexPlan, currentStep, hasActiveSubscription]); const httpEndpointWithApiKey = useMemo(() => { return getHttpEndpointWithApiKey(deploymentId, createdApiKey?.value || ''); @@ -263,13 +266,12 @@ const GetEndpoint: FC = ({ deploymentId, project, actionBtn, initialOpen checkFree: makeEndpointResult(sponsoredProjects[project.id], true), createFlexPlan: ( { await checkIfHasLogin(); - await fetchHostingPlanAndApiKeys(); + await fetchSubscriptionAndApiKeys(); setCurrentStep('checkEndpointWithApiKey'); }} onBack={() => { @@ -285,37 +287,46 @@ const GetEndpoint: FC = ({ deploymentId, project, actionBtn, initialOpen }[currentStep]; }, [freeOrFlexPlan, project, currentStep, deploymentId, account, httpEndpointWithApiKey, wsEndpointWithApiKey]); - const fetchHostingPlan = async () => { + const fetchSubscription = async () => { try { setNextBtnLoading(true); - const hostingPlan = await getHostingPlanApi({ - account, - }); + const projectIdNumber = parseInt(project.id.replace('0x', ''), 16); + const subscriptionRes = await getUserSubscriptionByProject(projectIdNumber); - if (!isConsumerHostError(hostingPlan.data)) { - setUserHostingPlan(hostingPlan.data); + if (!isConsumerHostError(subscriptionRes.data)) { + if (isNotSubscribed(subscriptionRes.data)) { + // 没有订阅,创建新订阅 + const newSubscription = await createSubscription({ project_id: projectIdNumber }); - // no hosting plan then skip fetch api key, - if (!hostingPlan.data.find((i) => i.deployment.deployment === deploymentId && i.is_actived)) - return { - data: [], - }; + if (!isConsumerHostError(newSubscription.data)) { + setCurrentSubscription(newSubscription.data); + return { data: newSubscription.data }; + } else { + setCurrentSubscription(null); + return { data: null }; + } + } else { + // 已有订阅 + setCurrentSubscription(subscriptionRes.data); + return { data: subscriptionRes.data }; + } } else { - return { - data: [], - }; + setCurrentSubscription(null); + return { data: null }; } - - return hostingPlan; + } catch (e) { + parseError(e, { alert: true }); + setCurrentSubscription(null); + return { data: null }; } finally { setNextBtnLoading(false); } }; - const fetchHostingPlanAndApiKeys = async () => { + const fetchSubscriptionAndApiKeys = async () => { try { setNextBtnLoading(true); - const hostingPlan = await fetchHostingPlan(); + const subscription = await fetchSubscription(); let apiKeys = await getUserApiKeysApi(); if (!isConsumerHostError(apiKeys.data)) { @@ -331,7 +342,7 @@ const GetEndpoint: FC = ({ deploymentId, project, actionBtn, initialOpen } } return { - hostingPlan, + subscription, apiKeys, }; } catch (e) { @@ -351,13 +362,13 @@ const GetEndpoint: FC = ({ deploymentId, project, actionBtn, initialOpen if (freeOrFlexPlan === 'free') { setCurrentStep('checkFree'); } else { - const fetched = await fetchHostingPlanAndApiKeys(); + const fetched = await fetchSubscriptionAndApiKeys(); if (fetched) { - if (!isConsumerHostError(fetched.hostingPlan.data) && !isConsumerHostError(fetched.apiKeys.data)) { + if (fetched.subscription.data && !isConsumerHostError(fetched.apiKeys.data)) { if ( fetched.apiKeys?.data.find((key) => key.name === specialApiKeyName) && - fetched.hostingPlan?.data.find((plan) => plan.deployment.deployment === deploymentId && plan.is_actived) + fetched.subscription.data.is_active ) { setCurrentStep('checkEndpointWithApiKey'); } else { @@ -390,7 +401,7 @@ const GetEndpoint: FC = ({ deploymentId, project, actionBtn, initialOpen const resetAllField = () => { setCurrentStep('select'); setFreeOrFlexPlan('flexPlan'); - setUserHostingPlan([]); + setCurrentSubscription(null); setUserApiKeys([]); beforeStep.current = 'select'; }; @@ -399,7 +410,7 @@ const GetEndpoint: FC = ({ deploymentId, project, actionBtn, initialOpen useEffect(() => { if (account && open) { resetAllField(); - fetchHostingPlanAndApiKeys(); + fetchSubscriptionAndApiKeys(); } }, [account]); @@ -413,7 +424,7 @@ const GetEndpoint: FC = ({ deploymentId, project, actionBtn, initialOpen setFreeOrFlexPlan('flexPlan'); await handleNextStep(); } - await fetchHostingPlan(); + await fetchSubscription(); setOpen(true); }} > @@ -430,7 +441,7 @@ const GetEndpoint: FC = ({ deploymentId, project, actionBtn, initialOpen setFreeOrFlexPlan('flexPlan'); await handleNextStep(); } - await fetchHostingPlan(); + await fetchSubscription(); setOpen(true); }} > @@ -447,7 +458,6 @@ const GetEndpoint: FC = ({ deploymentId, project, actionBtn, initialOpen }} className={account ? '' : 'hideModalWrapper'} footer={ - // it's kind of chaos, but I don't want to handle the action out of the component. currentStep !== 'createFlexPlan' ? (
{currentStep !== 'select' && ( diff --git a/src/hooks/useConsumerHostServices.tsx b/src/hooks/useConsumerHostServices.tsx index 5ca52daa..81b896f2 100644 --- a/src/hooks/useConsumerHostServices.tsx +++ b/src/hooks/useConsumerHostServices.tsx @@ -450,6 +450,76 @@ export const useConsumerHostServices = ( return res; }, []); + const getUserSubscriptions = useCallback(async (): Promise< + AxiosResponse + > => { + const res = await instance.get('/users/subscriptions', { + headers: authHeaders.current, + }); + + return res; + }, []); + + // 新增: 创建订阅 + const createSubscription = useCallback( + async (params: { project_id: number }): Promise> => { + const res = await instance.post('/users/subscriptions', params, { + headers: authHeaders.current, + }); + + return res; + }, + [], + ); + + // 新增: 取消订阅 + const unsubscribeProject = useCallback( + async (projectId: number): Promise> => { + const res = await instance.post<{ success: boolean; message: string } | ConsumerHostError>( + `/users/subscriptions/${projectId}/unsubscribe`, + {}, + { + headers: authHeaders.current, + }, + ); + + return res; + }, + [], + ); + + // 新增: 获取当前用户指定项目订阅信息 + const getUserSubscriptionByProject = useCallback( + async ( + projectId: number, + ): Promise> => { + const res = await instance.get( + `/users/subscriptions/${projectId}`, + { + headers: authHeaders.current, + }, + ); + + return res; + }, + [], + ); + + // 新增: 获取当前用户指定项目的托管计划 + const getUserHostingPlansByProject = useCallback( + async (projectId: number): Promise> => { + const res = await instance.get( + `/users/hosting-plans/project/${projectId}`, + { + headers: authHeaders.current, + }, + ); + + return res; + }, + [], + ); + useEffect(() => { checkIfHasLogin(); if (autoLogin) { @@ -469,6 +539,11 @@ export const useConsumerHostServices = ( getStatisticQueries: alertResDecorator(getStatisticQueries), getStatisticQueriesByPrice: alertResDecorator(getStatisticQueriesByPrice), getUserQueriesAggregation: alertResDecorator(getUserQueriesAggregation), + getUserSubscriptions: alertResDecorator(loginResDecorator(getUserSubscriptions)), + createSubscription: alertResDecorator(loginResDecorator(createSubscription)), + unsubscribeProject: alertResDecorator(loginResDecorator(unsubscribeProject)), + getUserSubscriptionByProject: alertResDecorator(loginResDecorator(getUserSubscriptionByProject)), + getUserHostingPlansByProject: alertResDecorator(loginResDecorator(getUserHostingPlansByProject)), getSpentInfo, getHostingPlanApi, getChannelLimit, @@ -604,6 +679,36 @@ export interface IIndexerFlexPlan { price_token: string; } +export interface IGetUserSubscription { + id: number; + user_id: number; + project_id: number; + auto_latest?: boolean; + max_versions?: number; + is_active: boolean; + created_at: string; + updated_at: string; + project?: { + id: number; + metadata: string; + }; +} + +// 新增: 未订阅时的返回类型 +export interface IGetUserSubscriptionNotFound { + subscribed: false; + project_id: number; + user_id: number; + message: string; +} + +// 新增: 用于类型守卫,判断是否未订阅 +export const isNotSubscribed = ( + res: IGetUserSubscription | IGetUserSubscriptionNotFound | ConsumerHostError, +): res is IGetUserSubscriptionNotFound => { + return 'subscribed' in res && res.subscribed === false; +}; + export type ConsumerHostError = | { code: '2000'; diff --git a/src/pages/consumer/MyFlexPlans/MyHostedPlan/MyHostedPlan.tsx b/src/pages/consumer/MyFlexPlans/MyHostedPlan/MyHostedPlan.tsx index ce8075d0..c2ecd5c0 100644 --- a/src/pages/consumer/MyFlexPlans/MyHostedPlan/MyHostedPlan.tsx +++ b/src/pages/consumer/MyFlexPlans/MyHostedPlan/MyHostedPlan.tsx @@ -6,11 +6,17 @@ import { AiOutlineCopy } from 'react-icons/ai'; import { LuArrowRightFromLine } from 'react-icons/lu'; import { useNavigate } from 'react-router'; import Copy from '@components/Copy'; +import { DeploymentMeta } from '@components/DeploymentInfo'; import { getHttpEndpointWithApiKey, getWsEndpointWithApiKey, specialApiKeyName } from '@components/GetEndpoint'; import { OutlineDot } from '@components/Icons/Icons'; import { useProjectMetadata } from '@containers'; import { useAccount } from '@containers/Web3'; -import { GetUserApiKeys, IGetHostingPlans, useConsumerHostServices } from '@hooks/useConsumerHostServices'; +import { + GetUserApiKeys, + IGetHostingPlans, + IGetUserSubscription, + useConsumerHostServices, +} from '@hooks/useConsumerHostServices'; import { isConsumerHostError } from '@hooks/useConsumerHostServices'; import CreateHostingFlexPlan, { CreateHostingFlexPlanRef, @@ -95,7 +101,9 @@ const MyHostedPlan: FC = () => { const navigate = useNavigate(); const { updateHostingPlanApi, - getHostingPlanApi, + getUserSubscriptions, + unsubscribeProject, + getUserHostingPlansByProject, loading: consumerHostLoading, } = useConsumerHostServices({ alert: true, @@ -111,64 +119,99 @@ const MyHostedPlan: FC = () => { const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const [fetchConnectLoading, setFetchConnectLoading] = useState(false); - const [createdHostingPlan, setCreatedHostingPlan] = useState<(IGetHostingPlans & { projectName: string | number })[]>( - [], - ); + const [subscriptions, setSubscriptions] = useState([]); + const [hostingPlansMap, setHostingPlansMap] = useState< + Map + >(new Map()); + const [expandedRowKeys, setExpandedRowKeys] = useState([]); const [currentEditInfo, setCurrentEditInfo] = useState(); const { getMetadataFromCid } = useProjectMetadata(); const ref = useRef(null); - const init = async () => { + + const initSubscriptions = async () => { try { setLoading(true); + const res = await getUserSubscriptions(); + if (!isConsumerHostError(res.data)) { + setSubscriptions(res.data); + } else { + setSubscriptions([]); + } + } catch (e) { + setSubscriptions([]); + } finally { + setLoading(false); + } + }; - const res = await getHostingPlanApi({ - account, - }); - const allMetadata = await Promise.allSettled( - res.data.map((i) => { - const cid = i.project.metadata.startsWith('Qm') - ? i.project.metadata - : bytes32ToCid(`0x${i.project.metadata}`); - return getMetadataFromCid(cid); - }), - ); - setCreatedHostingPlan( - res.data.map((raw, index) => { + const fetchHostingPlans = async (projectId: number) => { + try { + const res = await getUserHostingPlansByProject(projectId); + if (!isConsumerHostError(res.data)) { + const allMetadata = await Promise.allSettled( + res.data.map((i) => { + const cid = i.project.metadata.startsWith('Qm') + ? i.project.metadata + : bytes32ToCid(`0x${i.project.metadata}`); + return getMetadataFromCid(cid); + }), + ); + + const plansWithNames = res.data.map((raw, index) => { const result = allMetadata[index]; const name = result.status === 'fulfilled' ? result.value.name : raw.id; return { ...raw, projectName: name, }; - }), - ); + }); + + setHostingPlansMap((prev) => new Map(prev).set(projectId, plansWithNames)); + } } catch (e) { - setCreatedHostingPlan([]); + parseError(e, { alert: true }); + } + }; + + const handleExpand = async (expanded: boolean, record: IGetUserSubscription) => { + if (expanded) { + setExpandedRowKeys((prev) => [...prev, record.project_id]); + if (!hostingPlansMap.has(record.project_id)) { + await fetchHostingPlans(record.project_id); + } + } else { + setExpandedRowKeys((prev) => prev.filter((key) => key !== record.project_id)); + } + }; + + const handleUnsubscribe = async (projectId: number) => { + try { + setLoading(true); + const res = await unsubscribeProject(projectId); + if (!isConsumerHostError(res.data)) { + message.success('Unsubscribed successfully'); + await initSubscriptions(); + setHostingPlansMap((prev) => { + const newMap = new Map(prev); + newMap.delete(projectId); + return newMap; + }); + } } finally { setLoading(false); } }; - useEffect(() => { - if (account) { - init(); - } - }, [account]); + const expandedRowRender = (record: IGetUserSubscription) => { + const plans = hostingPlansMap.get(record.project_id) || []; - return ( -
+ return ( record.id} - style={{ marginTop: 40 }} - loading={loading || consumerHostLoading} - dataSource={createdHostingPlan} + dataSource={plans} + pagination={false} columns={[ { - title: 'Project', - dataIndex: 'projectName', - }, - { - width: 150, title: 'Plan', dataIndex: 'price', render: (val: string) => { @@ -209,7 +252,7 @@ const MyHostedPlan: FC = () => { fixed: 'right', dataIndex: 'spent', width: 50, - render: (_, record) => { + render: (_, planRecord) => { return (
+ /> + ); + }; + + useEffect(() => { + if (account) { + initSubscriptions(); + } + }, [account]); + + return ( +
+ record.project_id} + style={{ marginTop: 40 }} + loading={loading || consumerHostLoading} + dataSource={subscriptions} + expandable={{ + expandedRowRender, + expandedRowKeys, + onExpand: handleExpand, + }} + columns={[ + { + title: 'Project', + dataIndex: ['project', 'name'], + render: (name: string, record) => { + return ; + }, + }, + { + title: 'Auto Latest', + dataIndex: 'auto_latest', + render: (val?: boolean) => { + return {val ? 'Yes' : 'No'}; + }, + }, + { + title: 'Status', + dataIndex: 'is_active', + render: (val: boolean) => { + return {val ? 'Active' : 'Inactive'}; + }, + }, + { + title: 'Action', + fixed: 'right', + width: 120, + render: (_, record) => { + return ( + + ); + }, + }, + ]} + /> { id={`${currentEditInfo?.deployment.project_id || ''}`} deploymentId={`${currentEditInfo?.deployment.deployment || ''}`} editInformation={currentEditInfo} - onSubmit={() => init()} - > + onSubmit={async () => { + if (currentEditInfo) { + await fetchHostingPlans(currentEditInfo.deployment.project_id); + } + }} + /> Date: Mon, 3 Nov 2025 11:36:49 +0800 Subject: [PATCH 02/11] feat: sub --- .../MyFlexPlans/MyHostedPlan/MyHostedPlan.tsx | 47 ++----------------- 1 file changed, 5 insertions(+), 42 deletions(-) diff --git a/src/pages/consumer/MyFlexPlans/MyHostedPlan/MyHostedPlan.tsx b/src/pages/consumer/MyFlexPlans/MyHostedPlan/MyHostedPlan.tsx index c2ecd5c0..0d178903 100644 --- a/src/pages/consumer/MyFlexPlans/MyHostedPlan/MyHostedPlan.tsx +++ b/src/pages/consumer/MyFlexPlans/MyHostedPlan/MyHostedPlan.tsx @@ -119,6 +119,7 @@ const MyHostedPlan: FC = () => { const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const [fetchConnectLoading, setFetchConnectLoading] = useState(false); + const [expandLoading, setExpandLoading] = useState(false); const [subscriptions, setSubscriptions] = useState([]); const [hostingPlansMap, setHostingPlansMap] = useState< Map @@ -146,6 +147,7 @@ const MyHostedPlan: FC = () => { const fetchHostingPlans = async (projectId: number) => { try { + setExpandLoading(true); const res = await getUserHostingPlansByProject(projectId); if (!isConsumerHostError(res.data)) { const allMetadata = await Promise.allSettled( @@ -170,6 +172,8 @@ const MyHostedPlan: FC = () => { } } catch (e) { parseError(e, { alert: true }); + } finally { + setExpandLoading(false); } }; @@ -302,47 +306,6 @@ const MyHostedPlan: FC = () => { ); }, }, - // { - // label: ( - // - // {planRecord.price === '0' ? 'Restart' : 'Update'} - // - // ), - // key: 2, - // onClick: () => { - // setCurrentEditInfo(planRecord); - // ref.current?.showModal(); - // }, - // }, - // { - // label: ( - // - // Stop - // - // ), - // key: 3, - // onClick: async () => { - // if (planRecord.price === '0') return; - // try { - // setLoading(true); - // await updateHostingPlanApi({ - // id: planRecord.id, - // deploymentId: planRecord.deployment.deployment, - // price: '0', - // maximum: 2, - // expiration: 0, - // }); - // await fetchHostingPlans(record.project_id); - // } finally { - // setLoading(false); - // } - // }, - // }, ], }} > @@ -368,7 +331,7 @@ const MyHostedPlan: FC = () => {
record.project_id} style={{ marginTop: 40 }} - loading={loading || consumerHostLoading} + loading={loading || consumerHostLoading || expandLoading} dataSource={subscriptions} expandable={{ expandedRowRender, From 2ca5476f70eb38293c9c34732f674f1dade76ce1 Mon Sep 17 00:00:00 2001 From: Cyrbuzz Date: Mon, 3 Nov 2025 11:41:06 +0800 Subject: [PATCH 03/11] Update src/hooks/useConsumerHostServices.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/hooks/useConsumerHostServices.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useConsumerHostServices.tsx b/src/hooks/useConsumerHostServices.tsx index 81b896f2..adbe7868 100644 --- a/src/hooks/useConsumerHostServices.tsx +++ b/src/hooks/useConsumerHostServices.tsx @@ -472,7 +472,7 @@ export const useConsumerHostServices = ( [], ); - // 新增: 取消订阅 + // New: Unsubscribe from a project const unsubscribeProject = useCallback( async (projectId: number): Promise> => { const res = await instance.post<{ success: boolean; message: string } | ConsumerHostError>( From 989410d864c4afdf43e664561b754a6ad3817761 Mon Sep 17 00:00:00 2001 From: Cyrbuzz Date: Mon, 3 Nov 2025 11:41:19 +0800 Subject: [PATCH 04/11] Update src/hooks/useConsumerHostServices.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/hooks/useConsumerHostServices.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useConsumerHostServices.tsx b/src/hooks/useConsumerHostServices.tsx index adbe7868..f24f63a7 100644 --- a/src/hooks/useConsumerHostServices.tsx +++ b/src/hooks/useConsumerHostServices.tsx @@ -488,7 +488,7 @@ export const useConsumerHostServices = ( [], ); - // 新增: 获取当前用户指定项目订阅信息 + // Get user subscription for a specific project const getUserSubscriptionByProject = useCallback( async ( projectId: number, From 4560e3163e20bbada47e980395b0031df6eedacc Mon Sep 17 00:00:00 2001 From: Cyrbuzz Date: Mon, 3 Nov 2025 11:41:25 +0800 Subject: [PATCH 05/11] Update src/hooks/useConsumerHostServices.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/hooks/useConsumerHostServices.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useConsumerHostServices.tsx b/src/hooks/useConsumerHostServices.tsx index f24f63a7..a94885e6 100644 --- a/src/hooks/useConsumerHostServices.tsx +++ b/src/hooks/useConsumerHostServices.tsx @@ -694,7 +694,7 @@ export interface IGetUserSubscription { }; } -// 新增: 未订阅时的返回类型 +// Return type when subscription is not found export interface IGetUserSubscriptionNotFound { subscribed: false; project_id: number; From 8e5ea67e36bcf3cd0d7ef64086c1f80bada41a81 Mon Sep 17 00:00:00 2001 From: Cyrbuzz Date: Mon, 3 Nov 2025 11:41:35 +0800 Subject: [PATCH 06/11] Update src/components/CreateFlexPlan/index.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/components/CreateFlexPlan/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CreateFlexPlan/index.tsx b/src/components/CreateFlexPlan/index.tsx index c1497f32..ba2ea691 100644 --- a/src/components/CreateFlexPlan/index.tsx +++ b/src/components/CreateFlexPlan/index.tsx @@ -19,7 +19,7 @@ import { useSqtPrice } from '@hooks/useSqtPrice'; import { Steps, Typography } from '@subql/components'; import { formatSQT, useAsyncMemo, useGetDeploymentBoosterTotalAmountByDeploymentIdQuery } from '@subql/react-hooks'; import { parseError, TOKEN, tokenDecimals } from '@utils'; -import { Button, Checkbox, Divider, Form, InputNumber, Tooltip } from 'antd'; +import { Button, Checkbox, Divider, Form, InputNumber } from 'antd'; import BigNumberJs from 'bignumber.js'; import clsx from 'clsx'; import { parseEther } from 'ethers/lib/utils'; From b0bf3071e4ef6ed37120518036ff5c0ab294ba05 Mon Sep 17 00:00:00 2001 From: Cyrbuzz Date: Mon, 3 Nov 2025 11:41:43 +0800 Subject: [PATCH 07/11] Update src/components/GetEndpoint/index.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/components/GetEndpoint/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/GetEndpoint/index.tsx b/src/components/GetEndpoint/index.tsx index 17b0b629..c842af35 100644 --- a/src/components/GetEndpoint/index.tsx +++ b/src/components/GetEndpoint/index.tsx @@ -295,7 +295,7 @@ const GetEndpoint: FC = ({ deploymentId, project, actionBtn, initialOpen if (!isConsumerHostError(subscriptionRes.data)) { if (isNotSubscribed(subscriptionRes.data)) { - // 没有订阅,创建新订阅 + // No subscription found, create new subscription const newSubscription = await createSubscription({ project_id: projectIdNumber }); if (!isConsumerHostError(newSubscription.data)) { From a96605ef3569e3559e0b5c32a97a51af238d666f Mon Sep 17 00:00:00 2001 From: Cyrbuzz Date: Mon, 3 Nov 2025 11:41:53 +0800 Subject: [PATCH 08/11] Update src/hooks/useConsumerHostServices.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/hooks/useConsumerHostServices.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useConsumerHostServices.tsx b/src/hooks/useConsumerHostServices.tsx index a94885e6..991f20e8 100644 --- a/src/hooks/useConsumerHostServices.tsx +++ b/src/hooks/useConsumerHostServices.tsx @@ -505,7 +505,7 @@ export const useConsumerHostServices = ( [], ); - // 新增: 获取当前用户指定项目的托管计划 + // Get user hosting plans for a specific project const getUserHostingPlansByProject = useCallback( async (projectId: number): Promise> => { const res = await instance.get( From 3998160a8c067d1f9ec6c5100f29269f09b8f5e2 Mon Sep 17 00:00:00 2001 From: Cyrbuzz Date: Mon, 3 Nov 2025 11:42:06 +0800 Subject: [PATCH 09/11] Update src/hooks/useConsumerHostServices.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/hooks/useConsumerHostServices.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useConsumerHostServices.tsx b/src/hooks/useConsumerHostServices.tsx index 991f20e8..196c1da8 100644 --- a/src/hooks/useConsumerHostServices.tsx +++ b/src/hooks/useConsumerHostServices.tsx @@ -702,7 +702,7 @@ export interface IGetUserSubscriptionNotFound { message: string; } -// 新增: 用于类型守卫,判断是否未订阅 +// Type guard: checks if the user is not subscribed export const isNotSubscribed = ( res: IGetUserSubscription | IGetUserSubscriptionNotFound | ConsumerHostError, ): res is IGetUserSubscriptionNotFound => { From 2c47dc648a845bbea0b16236a5f13e3150c027c3 Mon Sep 17 00:00:00 2001 From: Cyrbuzz Date: Mon, 3 Nov 2025 11:42:14 +0800 Subject: [PATCH 10/11] Update src/components/DeploymentInfo/DeploymentInfo.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/components/DeploymentInfo/DeploymentInfo.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/DeploymentInfo/DeploymentInfo.tsx b/src/components/DeploymentInfo/DeploymentInfo.tsx index 74faf462..061491c0 100644 --- a/src/components/DeploymentInfo/DeploymentInfo.tsx +++ b/src/components/DeploymentInfo/DeploymentInfo.tsx @@ -101,9 +101,7 @@ export const DeploymentInfo: React.FC = ({ project, deploymentId, type, m - {deploymentId - ? `${deploymentId.slice(0, 5)}...${deploymentId.slice(deploymentId.length - 5, deploymentId.length)}` - : '-'} + {`${deploymentId.slice(0, 5)}...${deploymentId.slice(deploymentId.length - 5, deploymentId.length)}`} From 109ec683a7dad2231d9fd78c8857dfcae0981ecf Mon Sep 17 00:00:00 2001 From: Cyrbuzz Date: Mon, 3 Nov 2025 11:42:21 +0800 Subject: [PATCH 11/11] Update src/components/GetEndpoint/index.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/components/GetEndpoint/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/GetEndpoint/index.tsx b/src/components/GetEndpoint/index.tsx index c842af35..99e95d12 100644 --- a/src/components/GetEndpoint/index.tsx +++ b/src/components/GetEndpoint/index.tsx @@ -306,7 +306,7 @@ const GetEndpoint: FC = ({ deploymentId, project, actionBtn, initialOpen return { data: null }; } } else { - // 已有订阅 + // Subscription already exists setCurrentSubscription(subscriptionRes.data); return { data: subscriptionRes.data }; }