update subscription module
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -417,3 +417,74 @@ export function upsertPricingConfig(plan: string, billingPeriod: string, amount:
|
||||
create: { id: `prc_${plan.toLowerCase()}_${billingPeriod.toLowerCase()}`, plan, billingPeriod, amount, updatedBy },
|
||||
})
|
||||
}
|
||||
|
||||
export function listPlanFeatures() {
|
||||
return prisma.planFeature.findMany({
|
||||
orderBy: [{ plan: 'asc' }, { sortOrder: 'asc' }, { createdAt: 'asc' }],
|
||||
})
|
||||
}
|
||||
|
||||
export function createPlanFeature(data: { plan: 'STARTER' | 'GROWTH' | 'PRO'; label: string; sortOrder?: number }) {
|
||||
return prisma.planFeature.create({
|
||||
data: {
|
||||
plan: data.plan,
|
||||
label: data.label,
|
||||
sortOrder: data.sortOrder ?? 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function updatePlanFeature(id: string, data: Partial<{ plan: 'STARTER' | 'GROWTH' | 'PRO'; label: string; sortOrder: number }>) {
|
||||
return prisma.planFeature.update({
|
||||
where: { id },
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export function deletePlanFeature(id: string) {
|
||||
return prisma.planFeature.delete({ where: { id } })
|
||||
}
|
||||
|
||||
// ─── Pricing promotions ────────────────────────────────────────────────────
|
||||
|
||||
export function listPromotions() {
|
||||
return (prisma as any).pricingPromotion.findMany({ orderBy: { createdAt: 'desc' } })
|
||||
}
|
||||
|
||||
export function createPromotion(data: {
|
||||
code: string; name: string; description?: string | null
|
||||
discountType: string; discountValue: number
|
||||
plans: string[]; periods: string[]
|
||||
maxUses?: number | null; validFrom: string; validUntil?: string | null
|
||||
isActive: boolean; createdBy?: string | null
|
||||
}) {
|
||||
return (prisma as any).pricingPromotion.create({
|
||||
data: {
|
||||
...data,
|
||||
validFrom: new Date(data.validFrom),
|
||||
validUntil: data.validUntil ? new Date(data.validUntil) : null,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function updatePromotion(id: string, data: Partial<{
|
||||
code: string; name: string; description: string | null
|
||||
discountType: string; discountValue: number
|
||||
plans: string[]; periods: string[]
|
||||
maxUses: number | null; validFrom: string; validUntil: string | null
|
||||
isActive: boolean
|
||||
}>) {
|
||||
const { validFrom, validUntil, ...rest } = data
|
||||
return (prisma as any).pricingPromotion.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...rest,
|
||||
...(validFrom ? { validFrom: new Date(validFrom) } : {}),
|
||||
...(validUntil !== undefined ? { validUntil: validUntil ? new Date(validUntil) : null } : {}),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function deletePromotion(id: string) {
|
||||
return (prisma as any).pricingPromotion.delete({ where: { id } })
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
invoicesQuerySchema, adminCompanyUpdateSchema, companyStatusSchema,
|
||||
createAdminSchema, adminRoleSchema, adminPermissionsSchema,
|
||||
homepageUpdateSchema, idParamSchema, companyIdParamSchema, invoiceIdParamSchema,
|
||||
pricingUpdateSchema,
|
||||
pricingUpdateSchema, planFeatureCreateSchema, planFeatureUpdateSchema, planFeatureIdParamSchema,
|
||||
promotionCreateSchema, promotionUpdateSchema, promotionIdParamSchema,
|
||||
} from './admin.schemas'
|
||||
import { z } from 'zod'
|
||||
|
||||
@@ -235,6 +236,66 @@ router.patch('/pricing', requireAdminAuth, requireAdminRole('FINANCE'), async (r
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.get('/pricing/features', requireAdminAuth, requireAdminRole('FINANCE'), async (_req, res, next) => {
|
||||
try {
|
||||
ok(res, await service.listPlanFeatures())
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.post('/pricing/features', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
|
||||
try {
|
||||
const data = parseBody(planFeatureCreateSchema, req)
|
||||
created(res, await service.createPlanFeature(data, req.admin.id, req.ip))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.patch('/pricing/features/:featureId', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
|
||||
try {
|
||||
const { featureId } = parseParams(planFeatureIdParamSchema, req)
|
||||
const data = parseBody(planFeatureUpdateSchema, req)
|
||||
ok(res, await service.updatePlanFeature(featureId, data, req.admin.id, req.ip))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.delete('/pricing/features/:featureId', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
|
||||
try {
|
||||
const { featureId } = parseParams(planFeatureIdParamSchema, req)
|
||||
await service.deletePlanFeature(featureId, req.admin.id, req.ip)
|
||||
ok(res, { success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// ─── Promotions ────────────────────────────────────────────────────────────
|
||||
|
||||
router.get('/pricing/promotions', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
|
||||
try {
|
||||
ok(res, await service.listPromotions())
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.post('/pricing/promotions', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
|
||||
try {
|
||||
const data = parseBody(promotionCreateSchema, req)
|
||||
created(res, await service.createPromotion(data as any, req.admin.id, req.ip))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.patch('/pricing/promotions/:promotionId', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
|
||||
try {
|
||||
const { promotionId } = parseParams(promotionIdParamSchema, req)
|
||||
const data = parseBody(promotionUpdateSchema, req)
|
||||
ok(res, await service.updatePromotion(promotionId, data as any, req.admin.id, req.ip))
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.delete('/pricing/promotions/:promotionId', requireAdminAuth, requireAdminRole('FINANCE'), async (req, res, next) => {
|
||||
try {
|
||||
const { promotionId } = parseParams(promotionIdParamSchema, req)
|
||||
await service.deletePromotion(promotionId, req.admin.id, req.ip)
|
||||
ok(res, { success: true })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// ─── Subscription admin overrides ─────────────────────────────
|
||||
|
||||
router.get('/subscriptions/:subscriptionId/events', requireAdminAuth, requireAdminRole('SUPPORT'), async (req, res, next) => {
|
||||
|
||||
@@ -182,3 +182,30 @@ export const pricingUpdateSchema = z.object({
|
||||
amount: z.number().int().positive(),
|
||||
})).min(1),
|
||||
})
|
||||
|
||||
const planFeatureSchema = z.object({
|
||||
plan: z.enum(['STARTER', 'GROWTH', 'PRO']),
|
||||
label: z.string().min(1).max(120),
|
||||
sortOrder: z.number().int().min(0).default(0),
|
||||
})
|
||||
|
||||
export const planFeatureCreateSchema = planFeatureSchema
|
||||
export const planFeatureUpdateSchema = planFeatureSchema.partial()
|
||||
export const promotionCreateSchema = z.object({
|
||||
code: z.string().min(2).max(30).regex(/^[A-Z0-9_-]+$/, 'Code must be uppercase A-Z, 0-9, _ or -'),
|
||||
name: z.string().min(1).max(100),
|
||||
description: z.string().max(500).optional(),
|
||||
discountType: z.enum(['PERCENTAGE', 'FIXED']),
|
||||
discountValue: z.number().int().positive(),
|
||||
plans: z.array(z.enum(['STARTER', 'GROWTH', 'PRO'])),
|
||||
periods: z.array(z.enum(['MONTHLY', 'ANNUAL'])),
|
||||
maxUses: z.number().int().positive().nullable().optional(),
|
||||
validFrom: z.string().datetime(),
|
||||
validUntil: z.string().datetime().nullable().optional(),
|
||||
isActive: z.boolean().default(true),
|
||||
})
|
||||
|
||||
export const promotionUpdateSchema = promotionCreateSchema.partial()
|
||||
|
||||
export const planFeatureIdParamSchema = z.object({ featureId: z.string().min(1) })
|
||||
export const promotionIdParamSchema = z.object({ promotionId: z.string().min(1) })
|
||||
|
||||
@@ -315,3 +315,78 @@ export async function updatePricingConfigs(entries: { plan: string; billingPerio
|
||||
})
|
||||
return results
|
||||
}
|
||||
|
||||
export function listPlanFeatures() {
|
||||
return repo.listPlanFeatures()
|
||||
}
|
||||
|
||||
export async function createPlanFeature(data: Parameters<typeof repo.createPlanFeature>[0], adminId: string, ip?: string) {
|
||||
const feature = await repo.createPlanFeature(data)
|
||||
await repo.createAuditLog({
|
||||
adminUserId: adminId,
|
||||
action: 'CREATE',
|
||||
resource: 'PlanFeature',
|
||||
after: toAuditJson(feature),
|
||||
ipAddress: ip,
|
||||
userAgent: undefined,
|
||||
})
|
||||
return feature
|
||||
}
|
||||
|
||||
export async function updatePlanFeature(id: string, data: Parameters<typeof repo.updatePlanFeature>[1], adminId: string, ip?: string) {
|
||||
const feature = await repo.updatePlanFeature(id, data)
|
||||
await repo.createAuditLog({
|
||||
adminUserId: adminId,
|
||||
action: 'UPDATE',
|
||||
resource: 'PlanFeature',
|
||||
resourceId: id,
|
||||
after: toAuditJson(feature),
|
||||
ipAddress: ip,
|
||||
userAgent: undefined,
|
||||
})
|
||||
return feature
|
||||
}
|
||||
|
||||
export async function deletePlanFeature(id: string, adminId: string, ip?: string) {
|
||||
await repo.deletePlanFeature(id)
|
||||
await repo.createAuditLog({
|
||||
adminUserId: adminId,
|
||||
action: 'DELETE',
|
||||
resource: 'PlanFeature',
|
||||
resourceId: id,
|
||||
ipAddress: ip,
|
||||
userAgent: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Promotions ────────────────────────────────────────────────────────────
|
||||
|
||||
export function listPromotions() {
|
||||
return repo.listPromotions()
|
||||
}
|
||||
|
||||
export async function createPromotion(data: Parameters<typeof repo.createPromotion>[0], adminId: string, ip?: string) {
|
||||
const promo = await repo.createPromotion({ ...data, createdBy: adminId })
|
||||
await repo.createAuditLog({
|
||||
adminUserId: adminId, action: 'CREATE', resource: 'PricingPromotion',
|
||||
after: toAuditJson(promo), ipAddress: ip, userAgent: undefined,
|
||||
})
|
||||
return promo
|
||||
}
|
||||
|
||||
export async function updatePromotion(id: string, data: Parameters<typeof repo.updatePromotion>[1], adminId: string, ip?: string) {
|
||||
const promo = await repo.updatePromotion(id, data)
|
||||
await repo.createAuditLog({
|
||||
adminUserId: adminId, action: 'UPDATE', resource: 'PricingPromotion',
|
||||
after: toAuditJson(promo), ipAddress: ip, userAgent: undefined,
|
||||
})
|
||||
return promo
|
||||
}
|
||||
|
||||
export async function deletePromotion(id: string, adminId: string, ip?: string) {
|
||||
await repo.deletePromotion(id)
|
||||
await repo.createAuditLog({
|
||||
adminUserId: adminId, action: 'DELETE', resource: 'PricingPromotion',
|
||||
entityId: id, ipAddress: ip, userAgent: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PLAN_FEATURES, PLAN_PRICES } from '@rentaldrivego/types'
|
||||
import { Router } from 'express'
|
||||
import { parseBody, parseParams } from '../../http/validate'
|
||||
import { ok, created } from '../../http/respond'
|
||||
@@ -16,6 +17,17 @@ router.get('/platform/homepage', async (_req, res, next) => {
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.get('/platform/pricing', async (_req, res, next) => {
|
||||
try {
|
||||
ok(res, await service.getPlatformPricing())
|
||||
} catch (err) {
|
||||
if (isDatabaseUnavailableError(err)) {
|
||||
return res.json({ data: { prices: PLAN_PRICES, planFeatures: PLAN_FEATURES } })
|
||||
}
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/:slug/brand', async (req, res, next) => {
|
||||
try {
|
||||
const { slug } = parseParams(slugParamSchema, req)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PLAN_FEATURES, PLAN_PRICES } from '@rentaldrivego/types'
|
||||
import { AppError, NotFoundError } from '../../http/errors'
|
||||
import { getVehicleAvailabilitySummary } from '../../services/vehicleAvailabilityService'
|
||||
import { applyInsurancesToReservation } from '../../services/insuranceService'
|
||||
@@ -5,6 +6,7 @@ import { applyAdditionalDriversToReservation } from '../../services/additionalDr
|
||||
import { applyPricingRules } from '../../services/pricingRuleService'
|
||||
import { validateLicense, validateAndFlagLicense } from '../../services/licenseValidationService'
|
||||
import { getMarketplaceHomepageContent } from '../../services/platformContentService'
|
||||
import { prisma } from '../../lib/prisma'
|
||||
import * as amanpay from '../../services/amanpayService'
|
||||
import * as paypal from '../../services/paypalService'
|
||||
import * as repo from './site.repo'
|
||||
@@ -14,6 +16,32 @@ export async function getPlatformHomepage() {
|
||||
return getMarketplaceHomepageContent()
|
||||
}
|
||||
|
||||
export async function getPlatformPricing() {
|
||||
const [configs, features] = await Promise.all([
|
||||
prisma.pricingConfig.findMany(),
|
||||
prisma.planFeature.findMany({
|
||||
orderBy: [{ plan: 'asc' }, { sortOrder: 'asc' }, { createdAt: 'asc' }],
|
||||
}),
|
||||
])
|
||||
|
||||
const prices = configs.length === 0
|
||||
? PLAN_PRICES
|
||||
: configs.reduce<Record<string, Record<string, Record<string, number>>>>((acc, config) => {
|
||||
if (!acc[config.plan]) acc[config.plan] = {}
|
||||
acc[config.plan]![config.billingPeriod] = { MAD: config.amount }
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const planFeatures = Object.fromEntries(
|
||||
Object.entries(PLAN_FEATURES).map(([plan, fallback]) => {
|
||||
const labels = features.filter((feature) => feature.plan === plan).map((feature) => feature.label)
|
||||
return [plan, labels.length > 0 ? labels : fallback]
|
||||
}),
|
||||
)
|
||||
|
||||
return { prices, planFeatures }
|
||||
}
|
||||
|
||||
export async function getBrand(slug: string) {
|
||||
const company = await repo.findCompanyBySlug(slug)
|
||||
return presentBrand(company)
|
||||
|
||||
@@ -2,15 +2,24 @@
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { PLAN_FEATURES, PLAN_PRICES } from '@rentaldrivego/types'
|
||||
import { useMarketplacePreferences } from '@/components/MarketplaceShell'
|
||||
import { marketplaceFetchOrDefault } from '@/lib/api'
|
||||
import { resolveBrowserAppUrl } from '@/lib/appUrls'
|
||||
|
||||
type Billing = 'monthly' | 'annual'
|
||||
|
||||
const PRICES: Record<string, { monthly: number; annual: number }> = {
|
||||
STARTER: { monthly: 299, annual: 2990 },
|
||||
GROWTH: { monthly: 399, annual: 3990 },
|
||||
PRO: { monthly: 499, annual: 4990 },
|
||||
type PricingMatrix = Record<string, Record<string, Record<string, number>>>
|
||||
type PlanFeatureMap = Record<string, string[]>
|
||||
|
||||
type PlatformPricing = {
|
||||
prices: PricingMatrix
|
||||
planFeatures: PlanFeatureMap
|
||||
}
|
||||
|
||||
export const DEFAULT_PRICING: PlatformPricing = {
|
||||
prices: PLAN_PRICES,
|
||||
planFeatures: PLAN_FEATURES,
|
||||
}
|
||||
|
||||
const PLANS = [
|
||||
@@ -18,38 +27,18 @@ const PLANS = [
|
||||
key: 'STARTER',
|
||||
name: 'Starter',
|
||||
tagline: 'Launch your fleet online',
|
||||
features: [
|
||||
'Up to 10 vehicles',
|
||||
'1 user account',
|
||||
'Basic analytics',
|
||||
'Marketplace listing',
|
||||
],
|
||||
highlight: false,
|
||||
},
|
||||
{
|
||||
key: 'GROWTH',
|
||||
name: 'Growth',
|
||||
tagline: 'Scale with confidence',
|
||||
features: [
|
||||
'Up to 50 vehicles',
|
||||
'5 user accounts',
|
||||
'Full analytics',
|
||||
'Priority marketplace placement',
|
||||
'Custom branding',
|
||||
],
|
||||
highlight: true,
|
||||
},
|
||||
{
|
||||
key: 'PRO',
|
||||
name: 'Pro',
|
||||
tagline: 'Enterprise-grade power',
|
||||
features: [
|
||||
'Unlimited vehicles',
|
||||
'Unlimited user accounts',
|
||||
'Advanced reports',
|
||||
'API access',
|
||||
'Dedicated support',
|
||||
],
|
||||
highlight: false,
|
||||
},
|
||||
]
|
||||
@@ -93,20 +82,32 @@ const copy = {
|
||||
},
|
||||
} as const
|
||||
|
||||
function annualSavingsPct(plan: string): number {
|
||||
const { monthly, annual } = PRICES[plan]
|
||||
const wouldPay = monthly * 12
|
||||
return Math.round(((wouldPay - annual) / wouldPay) * 100)
|
||||
function getPlanPrice(prices: PricingMatrix, plan: string, billingPeriod: 'MONTHLY' | 'ANNUAL') {
|
||||
return (prices[plan]?.[billingPeriod]?.MAD ?? PLAN_PRICES[plan]?.[billingPeriod]?.MAD ?? 0) / 100
|
||||
}
|
||||
|
||||
export default function PricingClient() {
|
||||
function annualSavingsPct(prices: PricingMatrix, plan: string): number {
|
||||
const monthly = getPlanPrice(prices, plan, 'MONTHLY')
|
||||
const annual = getPlanPrice(prices, plan, 'ANNUAL')
|
||||
const wouldPay = monthly * 12
|
||||
if (wouldPay <= 0) return 0
|
||||
return Math.max(0, Math.round(((wouldPay - annual) / wouldPay) * 100))
|
||||
}
|
||||
|
||||
export default function PricingClient({ initialPricing = DEFAULT_PRICING }: { initialPricing?: PlatformPricing }) {
|
||||
const { language } = useMarketplacePreferences()
|
||||
const dict = copy[language]
|
||||
const [billing, setBilling] = useState<Billing>('monthly')
|
||||
const [pricing, setPricing] = useState<PlatformPricing>(initialPricing)
|
||||
|
||||
const dashboardUrl = resolveBrowserAppUrl(process.env.NEXT_PUBLIC_DASHBOARD_URL ?? 'http://localhost:3000/dashboard')
|
||||
|
||||
const savings = annualSavingsPct('STARTER')
|
||||
useEffect(() => {
|
||||
marketplaceFetchOrDefault<PlatformPricing>('/site/platform/pricing', DEFAULT_PRICING)
|
||||
.then(setPricing)
|
||||
}, [])
|
||||
|
||||
const savings = annualSavingsPct(pricing.prices, 'STARTER')
|
||||
|
||||
return (
|
||||
<div className="space-y-10">
|
||||
@@ -141,10 +142,12 @@ export default function PricingClient() {
|
||||
{/* Plan cards */}
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
{PLANS.map((plan) => {
|
||||
const price = PRICES[plan.key]
|
||||
const monthlyPrice = getPlanPrice(pricing.prices, plan.key, 'MONTHLY')
|
||||
const annualPrice = getPlanPrice(pricing.prices, plan.key, 'ANNUAL')
|
||||
const displayPrice = billing === 'annual'
|
||||
? Math.round(price.annual / 12)
|
||||
: price.monthly
|
||||
? Math.round(annualPrice / 12)
|
||||
: monthlyPrice
|
||||
const features = pricing.planFeatures[plan.key] ?? PLAN_FEATURES[plan.key] ?? []
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -177,13 +180,13 @@ export default function PricingClient() {
|
||||
</div>
|
||||
{billing === 'annual' && (
|
||||
<p className="mt-1 text-xs text-stone-400 dark:text-stone-500">
|
||||
{dict.billedAs} {price.annual} MAD {dict.yearly}
|
||||
{dict.billedAs} {annualPrice} MAD {dict.yearly}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ul className="mt-8 flex-1 space-y-3">
|
||||
{plan.features.map((f) => (
|
||||
{features.map((f) => (
|
||||
<li key={f} className="flex items-start gap-2.5 text-sm text-stone-700 dark:text-stone-300">
|
||||
<svg
|
||||
className="mt-0.5 h-4 w-4 shrink-0 text-orange-500 dark:text-orange-400"
|
||||
|
||||
@@ -42,7 +42,7 @@ const copy = {
|
||||
},
|
||||
} as const
|
||||
|
||||
export default function PricingPageContent() {
|
||||
export default function PricingPageContent({ initialPricing }: { initialPricing?: React.ComponentProps<typeof PricingClient>['initialPricing'] }) {
|
||||
const { language } = useMarketplacePreferences()
|
||||
const dict = copy[language]
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function PricingPageContent() {
|
||||
</div>
|
||||
|
||||
<div className="mt-16">
|
||||
<PricingClient />
|
||||
<PricingClient initialPricing={initialPricing} />
|
||||
</div>
|
||||
|
||||
<div className="site-panel mx-auto mt-24 max-w-3xl divide-y divide-stone-200/80 dark:divide-stone-800">
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { PLAN_FEATURES, PLAN_PRICES } from '@rentaldrivego/types'
|
||||
import { marketplaceFetchOrDefault } from '@/lib/api'
|
||||
import PricingPageContent from './PricingPageContent'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -6,6 +8,12 @@ export const metadata: Metadata = {
|
||||
description: 'Simple, transparent pricing for every fleet size.',
|
||||
}
|
||||
|
||||
export default function PricingPage() {
|
||||
return <PricingPageContent />
|
||||
const DEFAULT_PRICING = {
|
||||
prices: PLAN_PRICES,
|
||||
planFeatures: PLAN_FEATURES,
|
||||
}
|
||||
|
||||
export default async function PricingPage() {
|
||||
const initialPricing = await marketplaceFetchOrDefault('/site/platform/pricing', DEFAULT_PRICING)
|
||||
return <PricingPageContent initialPricing={initialPricing} />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user