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 { getSupportPricing } from '@/features/support/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 })
|
||||
|
||||
@@ -35,6 +37,17 @@ function PlanContent() {
|
||||
const statusQuery = useQuery({ queryKey: ['my-plan-status'], queryFn: getMyPlanStatus })
|
||||
const pricingQuery = useQuery({ queryKey: ['support-pricing'], queryFn: getSupportPricing })
|
||||
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 [customCount, setCustomCount] = useState('3')
|
||||
@@ -104,6 +117,19 @@ function PlanContent() {
|
||||
<p className="mt-1 text-xs text-muted-foreground">{t('plan.pageHint')}</p>
|
||||
</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">
|
||||
{plansQuery.data?.map((p) => (
|
||||
<label
|
||||
@@ -123,19 +149,28 @@ function PlanContent() {
|
||||
</label>
|
||||
))}
|
||||
|
||||
<label className="flex cursor-pointer items-center gap-3 rounded-md border border-border p-3 text-sm has-[:checked]:border-foreground">
|
||||
<input type="radio" name="plan" checked={selection === 'custom'} onChange={() => setSelection('custom')} />
|
||||
<span className="font-medium">{t('plan.custom')}</span>
|
||||
<Input
|
||||
type="number"
|
||||
min={3}
|
||||
className="w-24"
|
||||
value={customCount}
|
||||
onChange={(e) => {
|
||||
setCustomCount(e.target.value)
|
||||
setSelection('custom')
|
||||
}}
|
||||
/>
|
||||
<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')} />
|
||||
<span className="font-medium">{t('plan.custom')}</span>
|
||||
<Input
|
||||
type="number"
|
||||
min={3}
|
||||
className="w-24"
|
||||
value={customCount}
|
||||
onChange={(e) => {
|
||||
setCustomCount(e.target.value)
|
||||
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>
|
||||
{selection === 'custom' && <p className="text-xs text-muted-foreground">{t('plan.customHint')}</p>}
|
||||
</div>
|
||||
|
||||
@@ -117,6 +117,7 @@ const resources = {
|
||||
downgradeNotice: 'Новая квота меньше текущего числа конфигов, занимающих место в квоте (включая приостановленные за неуплату) — выберите, какие отозвать:',
|
||||
selectExactly: 'Нужно выбрать ровно {{count}} конфиг(ов) для отзыва.',
|
||||
topUpCreated: 'Тариф увеличен. Создана доплата за оставшийся оплаченный период.',
|
||||
paymentRequiredNotice: 'Оплата ещё не произведена (или срок истёк) — конфиги недоступны, пока не оформите оплату.',
|
||||
goToBilling: 'Перейти к оплате',
|
||||
mustSelectConfigs: 'Выберите конфиги для отзыва.',
|
||||
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:',
|
||||
selectExactly: 'Select exactly {{count}} config(s) to revoke.',
|
||||
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',
|
||||
mustSelectConfigs: 'Select configs to revoke.',
|
||||
perQuarter: '3 months: {{price}}',
|
||||
|
||||
Reference in New Issue
Block a user