Enhance billing status handling in PlanContent component
- Integrated billing status query to determine if payment is required for users with active plans. - Added conditional rendering for a payment notice card, guiding users to the billing page if payment is needed. - Updated i18n resource files with new translations for payment-related messages, improving user clarity on billing status.
This commit is contained in:
@@ -13,6 +13,8 @@ import { applyDiscount, resolveDiscountPercent } from '@/shared/lib/pricing'
|
|||||||
import { getMyConfigs } from '@/features/configs/api'
|
import { getMyConfigs } from '@/features/configs/api'
|
||||||
import { getSupportPricing } from '@/features/support/api'
|
import { getSupportPricing } from '@/features/support/api'
|
||||||
import { changePlan, getMyPlanStatus, listPlans } from '@/features/plans/api'
|
import { changePlan, getMyPlanStatus, listPlans } from '@/features/plans/api'
|
||||||
|
import { getMyBillingStatus } from '@/features/billing/api'
|
||||||
|
import { getDaysRemaining } from '@/features/billing/remaining'
|
||||||
|
|
||||||
export const Route = createFileRoute('/plan')({ component: PlanPage })
|
export const Route = createFileRoute('/plan')({ component: PlanPage })
|
||||||
|
|
||||||
@@ -35,6 +37,17 @@ function PlanContent() {
|
|||||||
const statusQuery = useQuery({ queryKey: ['my-plan-status'], queryFn: getMyPlanStatus })
|
const statusQuery = useQuery({ queryKey: ['my-plan-status'], queryFn: getMyPlanStatus })
|
||||||
const pricingQuery = useQuery({ queryKey: ['support-pricing'], queryFn: getSupportPricing })
|
const pricingQuery = useQuery({ queryKey: ['support-pricing'], queryFn: getSupportPricing })
|
||||||
const configsQuery = useQuery({ queryKey: ['my-configs'], queryFn: getMyConfigs })
|
const configsQuery = useQuery({ queryKey: ['my-configs'], queryFn: getMyConfigs })
|
||||||
|
const billingStatusQuery = useQuery({ queryKey: ['my-billing-status'], queryFn: getMyBillingStatus })
|
||||||
|
|
||||||
|
// Если оплата включена для роли, но сейчас нет действующего оплаченного периода (ни разу не
|
||||||
|
// платил, либо срок истёк) — не ждём, пока это всплывёт как 403 при попытке создать конфиг:
|
||||||
|
// сразу предлагаем оформить оплату прямо на странице тарифа. Не показываем, если заявка уже
|
||||||
|
// отправлена и ждёт решения админа (activeRequest) — тогда достаточно билинг-страницы.
|
||||||
|
const billingData = billingStatusQuery.data
|
||||||
|
const needsPayment =
|
||||||
|
!!billingData?.billingEnabled &&
|
||||||
|
!billingData.activeRequest &&
|
||||||
|
(!billingData.paidUntil || getDaysRemaining(billingData.paidUntil) <= 0)
|
||||||
|
|
||||||
const [selection, setSelection] = useState<string>('')
|
const [selection, setSelection] = useState<string>('')
|
||||||
const [customCount, setCustomCount] = useState('3')
|
const [customCount, setCustomCount] = useState('3')
|
||||||
@@ -104,6 +117,19 @@ function PlanContent() {
|
|||||||
<p className="mt-1 text-xs text-muted-foreground">{t('plan.pageHint')}</p>
|
<p className="mt-1 text-xs text-muted-foreground">{t('plan.pageHint')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{needsPayment && (
|
||||||
|
<Card className="border-destructive/50">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">{t('plan.paymentRequiredNotice')}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Link to="/billing" className="font-medium text-foreground underline underline-offset-2">
|
||||||
|
{t('plan.goToBilling')}
|
||||||
|
</Link>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{plansQuery.data?.map((p) => (
|
{plansQuery.data?.map((p) => (
|
||||||
<label
|
<label
|
||||||
@@ -123,7 +149,8 @@ function PlanContent() {
|
|||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<label className="flex cursor-pointer items-center gap-3 rounded-md border border-border p-3 text-sm has-[:checked]:border-foreground">
|
<label className="flex cursor-pointer items-center justify-between gap-3 rounded-md border border-border p-3 text-sm has-[:checked]:border-foreground">
|
||||||
|
<span className="flex items-center gap-3">
|
||||||
<input type="radio" name="plan" checked={selection === 'custom'} onChange={() => setSelection('custom')} />
|
<input type="radio" name="plan" checked={selection === 'custom'} onChange={() => setSelection('custom')} />
|
||||||
<span className="font-medium">{t('plan.custom')}</span>
|
<span className="font-medium">{t('plan.custom')}</span>
|
||||||
<Input
|
<Input
|
||||||
@@ -136,6 +163,14 @@ function PlanContent() {
|
|||||||
setSelection('custom')
|
setSelection('custom')
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</span>
|
||||||
|
{selection === 'custom' && (
|
||||||
|
<span className="flex flex-col items-end text-xs text-muted-foreground">
|
||||||
|
<span>{t('plan.perQuarter', { price: priceFor(pricingQuery.data?.pricePerConfigPerQuarter, targetCount, 3) })}</span>
|
||||||
|
<span>{t('plan.perHalfYear', { price: priceFor(pricingQuery.data?.pricePerConfigPerHalfYear, targetCount, 6) })}</span>
|
||||||
|
<span>{t('plan.perYear', { price: priceFor(pricingQuery.data?.pricePerConfigPerYear, targetCount, 12) })}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
{selection === 'custom' && <p className="text-xs text-muted-foreground">{t('plan.customHint')}</p>}
|
{selection === 'custom' && <p className="text-xs text-muted-foreground">{t('plan.customHint')}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ const resources = {
|
|||||||
downgradeNotice: 'Новая квота меньше текущего числа конфигов, занимающих место в квоте (включая приостановленные за неуплату) — выберите, какие отозвать:',
|
downgradeNotice: 'Новая квота меньше текущего числа конфигов, занимающих место в квоте (включая приостановленные за неуплату) — выберите, какие отозвать:',
|
||||||
selectExactly: 'Нужно выбрать ровно {{count}} конфиг(ов) для отзыва.',
|
selectExactly: 'Нужно выбрать ровно {{count}} конфиг(ов) для отзыва.',
|
||||||
topUpCreated: 'Тариф увеличен. Создана доплата за оставшийся оплаченный период.',
|
topUpCreated: 'Тариф увеличен. Создана доплата за оставшийся оплаченный период.',
|
||||||
|
paymentRequiredNotice: 'Оплата ещё не произведена (или срок истёк) — конфиги недоступны, пока не оформите оплату.',
|
||||||
goToBilling: 'Перейти к оплате',
|
goToBilling: 'Перейти к оплате',
|
||||||
mustSelectConfigs: 'Выберите конфиги для отзыва.',
|
mustSelectConfigs: 'Выберите конфиги для отзыва.',
|
||||||
perQuarter: '3 мес: {{price}}',
|
perQuarter: '3 мес: {{price}}',
|
||||||
@@ -708,6 +709,7 @@ const resources = {
|
|||||||
downgradeNotice: 'The new quota is lower than your current number of configs that still count against it (including ones suspended for non-payment) — choose which to revoke:',
|
downgradeNotice: 'The new quota is lower than your current number of configs that still count against it (including ones suspended for non-payment) — choose which to revoke:',
|
||||||
selectExactly: 'Select exactly {{count}} config(s) to revoke.',
|
selectExactly: 'Select exactly {{count}} config(s) to revoke.',
|
||||||
topUpCreated: 'Plan increased. A top-up was created for the remaining paid period.',
|
topUpCreated: 'Plan increased. A top-up was created for the remaining paid period.',
|
||||||
|
paymentRequiredNotice: "You haven't paid yet (or your paid period expired) — configs stay unavailable until you complete a payment.",
|
||||||
goToBilling: 'Go to payment',
|
goToBilling: 'Go to payment',
|
||||||
mustSelectConfigs: 'Select configs to revoke.',
|
mustSelectConfigs: 'Select configs to revoke.',
|
||||||
perQuarter: '3 months: {{price}}',
|
perQuarter: '3 months: {{price}}',
|
||||||
|
|||||||
Reference in New Issue
Block a user