fix pricing at home page
Build & Push / Pipeline Tests (push) Successful in 1m51s
Test / Type Check (all packages) (push) Successful in 51s
Build & Push / Build & Push Docker Image (push) Failing after 4m11s
Test / API Unit Tests (push) Successful in 1m15s
Test / Homepage Unit Tests (push) Successful in 49s
Test / Carplace Unit Tests (push) Successful in 43s
Test / Admin Unit Tests (push) Successful in 42s
Test / Dashboard Unit Tests (push) Successful in 43s
Test / API Integration Tests (push) Successful in 1m5s
Build & Push / Pipeline Tests (push) Successful in 1m51s
Test / Type Check (all packages) (push) Successful in 51s
Build & Push / Build & Push Docker Image (push) Failing after 4m11s
Test / API Unit Tests (push) Successful in 1m15s
Test / Homepage Unit Tests (push) Successful in 49s
Test / Carplace Unit Tests (push) Successful in 43s
Test / Admin Unit Tests (push) Successful in 42s
Test / Dashboard Unit Tests (push) Successful in 43s
Test / API Integration Tests (push) Successful in 1m5s
This commit is contained in:
+8
-5
@@ -33,11 +33,7 @@ CLERK_SECRET_KEY=placeholder
|
|||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
CORS_ORIGINS=http://localhost:3000,http://localhost:3001,http://localhost:3002,http://localhost:4000,http://127.0.0.1:3000,http://127.0.0.1:3001,http://127.0.0.1:3002,http://127.0.0.1:4000
|
CORS_ORIGINS=http://localhost:3000,http://localhost:3001,http://localhost:3002,http://localhost:4000,http://127.0.0.1:3000,http://127.0.0.1:3001,http://127.0.0.1:3002,http://127.0.0.1:4000
|
||||||
|
|
||||||
# Stripe subscription checkout
|
|
||||||
# STRIPE_API_KEY must be a Stripe secret/restricted key (sk_ or rk_).
|
|
||||||
# STRIPE_WEBHOOK_SECRET must be a Stripe webhook signing secret (whsec_).
|
|
||||||
STRIPE_API_KEY=placeholder
|
|
||||||
STRIPE_WEBHOOK_SECRET=placeholder
|
|
||||||
# Email — Resend (primary) with SMTP fallback
|
# Email — Resend (primary) with SMTP fallback
|
||||||
# Get your API key at https://resend.com/api-keys
|
# Get your API key at https://resend.com/api-keys
|
||||||
RESEND_API_KEY=re_PLACEHOLDER
|
RESEND_API_KEY=re_PLACEHOLDER
|
||||||
@@ -53,3 +49,10 @@ MAIL_FROM_ADDRESS=rentaldrivego@gmail.com
|
|||||||
MAIL_FROM_NAME=RentalDriveGo
|
MAIL_FROM_NAME=RentalDriveGo
|
||||||
MAIL_REPLY_TO_ADDRESS=rentaldrivego@gmail.com
|
MAIL_REPLY_TO_ADDRESS=rentaldrivego@gmail.com
|
||||||
MAIL_REPLY_TO_NAME=RentalDriveGo
|
MAIL_REPLY_TO_NAME=RentalDriveGo
|
||||||
|
|
||||||
|
|
||||||
|
# Stripe subscription checkout
|
||||||
|
# STRIPE_API_KEY must be a Stripe secret/restricted key (sk_ or rk_).
|
||||||
|
# STRIPE_WEBHOOK_SECRET must be a Stripe webhook signing secret (whsec_).
|
||||||
|
STRIPE_API_KEY=sk_test_51TvTsb9SpDRZn9yJyBAlUSXcTp9zpwQfhJYNKxfyYaZbqT6NN8W4pXu0zOUvpunrDPdtC0I6OZPzq0B5RRI1Ybub00OcYvj28K
|
||||||
|
STRIPE_WEBHOOK_SECRET=whsec_c5e0a6b2dd5e2f6ac804b428fe46f04e3af9b55c562f4124de20c52866f3c211
|
||||||
@@ -14,14 +14,15 @@
|
|||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pricingContainer {
|
||||||
|
inline-size: min(100%, 104rem);
|
||||||
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1.5fr) minmax(16rem, 0.5fr);
|
grid-template-columns: minmax(0, 1.5fr) minmax(16rem, 0.5fr);
|
||||||
gap: var(--space-5);
|
gap: var(--space-5);
|
||||||
position: relative;
|
inline-size: 100%;
|
||||||
left: 50%;
|
|
||||||
inline-size: min(100vw - (var(--layout-gutter) * 2), 104rem);
|
|
||||||
transform: translateX(-50%);
|
|
||||||
padding: var(--space-5);
|
padding: var(--space-5);
|
||||||
border: 1px solid var(--border-standard);
|
border: 1px solid var(--border-standard);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
@@ -111,12 +112,9 @@
|
|||||||
|
|
||||||
.planGrid {
|
.planGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
position: relative;
|
inline-size: 100%;
|
||||||
left: 50%;
|
|
||||||
inline-size: min(100vw - (var(--layout-gutter) * 2), 104rem);
|
|
||||||
transform: translateX(-50%);
|
|
||||||
gap: var(--space-5);
|
gap: var(--space-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ interface PricingSectionProps {
|
|||||||
const accountSubscriptionPlanByPricingPlan: Record<Exclude<PricingPlanId, 'enterprise'>, AccountSubscriptionPlan> = {
|
const accountSubscriptionPlanByPricingPlan: Record<Exclude<PricingPlanId, 'enterprise'>, AccountSubscriptionPlan> = {
|
||||||
launch: 'STARTER',
|
launch: 'STARTER',
|
||||||
growth: 'GROWTH',
|
growth: 'GROWTH',
|
||||||
pro: 'PRO',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function analyticsContext(
|
function analyticsContext(
|
||||||
@@ -105,7 +104,7 @@ export function PricingSection({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Section id="pricing" className={styles.section} aria-label={content.title}>
|
<Section id="pricing" className={styles.section} aria-label={content.title}>
|
||||||
<Container>
|
<Container className={styles.pricingContainer}>
|
||||||
<Stack gap="large">
|
<Stack gap="large">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
className={styles.header ?? ''}
|
className={styles.header ?? ''}
|
||||||
|
|||||||
@@ -200,27 +200,12 @@
|
|||||||
],
|
],
|
||||||
"cta": "احجز عرضًا لباقة Growth"
|
"cta": "احجز عرضًا لباقة Growth"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "pro",
|
|
||||||
"name": "Pro",
|
|
||||||
"audience": "للأساطيل القائمة التي تحتاج إلى تحكم متقدم عبر عمليات أكبر.",
|
|
||||||
"features": [
|
|
||||||
"كل ما تتضمنه Growth",
|
|
||||||
"حتى 150 مركبة نشطة",
|
|
||||||
"تقارير متقدمة",
|
|
||||||
"أدوار وصلاحيات الفريق",
|
|
||||||
"مسارات عمل ذات أولوية",
|
|
||||||
"وصول API",
|
|
||||||
"دعم مخصص"
|
|
||||||
],
|
|
||||||
"cta": "احجز عرضًا لباقة Pro"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "enterprise",
|
"id": "enterprise",
|
||||||
"name": "مؤسسات",
|
"name": "مؤسسات",
|
||||||
"audience": "للأساطيل الكبيرة والامتيازات والعمليات ذات احتياجات التكامل المعقدة.",
|
"audience": "للأساطيل الكبيرة والامتيازات والعمليات ذات احتياجات التكامل المعقدة.",
|
||||||
"features": [
|
"features": [
|
||||||
"كل ما تتضمنه Pro",
|
"كل ما تتضمنه Growth",
|
||||||
"150+ مركبة نشطة",
|
"150+ مركبة نشطة",
|
||||||
"تكاملات مخصصة ووصول إلى API",
|
"تكاملات مخصصة ووصول إلى API",
|
||||||
"أدوار وصلاحيات متقدمة",
|
"أدوار وصلاحيات متقدمة",
|
||||||
|
|||||||
@@ -212,27 +212,12 @@
|
|||||||
],
|
],
|
||||||
"cta": "Book a Growth demo"
|
"cta": "Book a Growth demo"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "pro",
|
|
||||||
"name": "Pro",
|
|
||||||
"audience": "For established fleets that need advanced controls across larger operations.",
|
|
||||||
"features": [
|
|
||||||
"Everything in Growth",
|
|
||||||
"Up to 150 active vehicles",
|
|
||||||
"Advanced reports",
|
|
||||||
"Team roles and permissions",
|
|
||||||
"Priority workflows",
|
|
||||||
"API access",
|
|
||||||
"Dedicated support"
|
|
||||||
],
|
|
||||||
"cta": "Book a Pro demo"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "enterprise",
|
"id": "enterprise",
|
||||||
"name": "Enterprise",
|
"name": "Enterprise",
|
||||||
"audience": "For large fleets, franchises, and operations with complex integration needs.",
|
"audience": "For large fleets, franchises, and operations with complex integration needs.",
|
||||||
"features": [
|
"features": [
|
||||||
"Everything in Pro",
|
"Everything in Growth",
|
||||||
"150+ active vehicles",
|
"150+ active vehicles",
|
||||||
"Custom integrations and API access",
|
"Custom integrations and API access",
|
||||||
"Advanced roles and permissions",
|
"Advanced roles and permissions",
|
||||||
|
|||||||
@@ -231,27 +231,12 @@
|
|||||||
],
|
],
|
||||||
"cta": "Réserver une démo Growth"
|
"cta": "Réserver une démo Growth"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "pro",
|
|
||||||
"name": "Pro",
|
|
||||||
"audience": "Pour les parcs établis qui ont besoin de contrôles avancés sur une exploitation plus large.",
|
|
||||||
"features": [
|
|
||||||
"Tout ce qui est inclus dans Growth",
|
|
||||||
"Jusqu’à 150 véhicules actifs",
|
|
||||||
"Rapports avancés",
|
|
||||||
"Rôles et autorisations d’équipe",
|
|
||||||
"Flux prioritaires",
|
|
||||||
"Accès API",
|
|
||||||
"Support dédié"
|
|
||||||
],
|
|
||||||
"cta": "Réserver une démo Pro"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "enterprise",
|
"id": "enterprise",
|
||||||
"name": "Entreprise",
|
"name": "Entreprise",
|
||||||
"audience": "Pour les grands parcs, franchises et exploitations ayant des besoins d’intégration complexes.",
|
"audience": "Pour les grands parcs, franchises et exploitations ayant des besoins d’intégration complexes.",
|
||||||
"features": [
|
"features": [
|
||||||
"Tout ce qui est inclus dans Pro",
|
"Tout ce qui est inclus dans Growth",
|
||||||
"150+ véhicules actifs",
|
"150+ véhicules actifs",
|
||||||
"Intégrations personnalisées et accès API",
|
"Intégrations personnalisées et accès API",
|
||||||
"Rôles et autorisations avancés",
|
"Rôles et autorisations avancés",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export const fleetBandIds = ['small', 'growing', 'scale', 'enterprise'] as const;
|
export const fleetBandIds = ['small', 'growing', 'scale', 'enterprise'] as const;
|
||||||
export type FleetBandId = (typeof fleetBandIds)[number];
|
export type FleetBandId = (typeof fleetBandIds)[number];
|
||||||
|
|
||||||
export const pricingPlanIds = ['launch', 'growth', 'pro', 'enterprise'] as const;
|
export const pricingPlanIds = ['launch', 'growth', 'enterprise'] as const;
|
||||||
export type PricingPlanId = (typeof pricingPlanIds)[number];
|
export type PricingPlanId = (typeof pricingPlanIds)[number];
|
||||||
|
|
||||||
export type BillingPeriod = 'monthly' | 'annual';
|
export type BillingPeriod = 'monthly' | 'annual';
|
||||||
@@ -35,12 +35,6 @@ export const pricingCommercialConfig: PricingCommercialConfig = {
|
|||||||
scale: 299,
|
scale: 299,
|
||||||
enterprise: 299,
|
enterprise: 299,
|
||||||
},
|
},
|
||||||
pro: {
|
|
||||||
small: 399,
|
|
||||||
growing: 399,
|
|
||||||
scale: 399,
|
|
||||||
enterprise: 399,
|
|
||||||
},
|
|
||||||
enterprise: {
|
enterprise: {
|
||||||
small: null,
|
small: null,
|
||||||
growing: null,
|
growing: null,
|
||||||
@@ -51,7 +45,7 @@ export const pricingCommercialConfig: PricingCommercialConfig = {
|
|||||||
recommendedPlanByFleetBand: {
|
recommendedPlanByFleetBand: {
|
||||||
small: 'launch',
|
small: 'launch',
|
||||||
growing: 'growth',
|
growing: 'growth',
|
||||||
scale: 'pro',
|
scale: 'growth',
|
||||||
enterprise: 'enterprise',
|
enterprise: 'enterprise',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const analyticsContextSchema = z
|
|||||||
reason: safeText.optional(),
|
reason: safeText.optional(),
|
||||||
fleetBand: z.enum(['small', 'growing', 'scale', 'enterprise']).optional(),
|
fleetBand: z.enum(['small', 'growing', 'scale', 'enterprise']).optional(),
|
||||||
billingPeriod: z.enum(['monthly', 'annual']).optional(),
|
billingPeriod: z.enum(['monthly', 'annual']).optional(),
|
||||||
plan: z.enum(['launch', 'growth', 'pro', 'enterprise']).optional(),
|
plan: z.enum(['launch', 'growth', 'enterprise']).optional(),
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ test('pricing recommends a plan from the selected fleet band', async ({ page })
|
|||||||
'true',
|
'true',
|
||||||
);
|
);
|
||||||
await page.getByLabel('76–150 vehicles').check();
|
await page.getByLabel('76–150 vehicles').check();
|
||||||
await expect(page.locator('#pricing [data-plan="pro"]')).toHaveAttribute(
|
await expect(page.locator('#pricing [data-plan="growth"]')).toHaveAttribute(
|
||||||
'data-recommended',
|
'data-recommended',
|
||||||
'true',
|
'true',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ describe('homepage content model', () => {
|
|||||||
expect(content.modules.items).toHaveLength(6);
|
expect(content.modules.items).toHaveLength(6);
|
||||||
expect(content.faq.items).toHaveLength(6);
|
expect(content.faq.items).toHaveLength(6);
|
||||||
expect(content.pricing.fleetBands).toHaveLength(4);
|
expect(content.pricing.fleetBands).toHaveLength(4);
|
||||||
expect(content.pricing.plans).toHaveLength(4);
|
expect(content.pricing.plans).toHaveLength(3);
|
||||||
expect(content.hero.preview.rows).toHaveLength(2);
|
expect(content.hero.preview.rows).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -39,11 +39,11 @@ describe('PricingSection', () => {
|
|||||||
|
|
||||||
await user.click(screen.getByLabelText('76–150 vehicles'));
|
await user.click(screen.getByLabelText('76–150 vehicles'));
|
||||||
|
|
||||||
expect(container.querySelector('[data-plan="pro"]')).toHaveAttribute(
|
expect(container.querySelector('[data-plan="growth"]')).toHaveAttribute(
|
||||||
'data-recommended',
|
'data-recommended',
|
||||||
'true',
|
'true',
|
||||||
);
|
);
|
||||||
expect(container.querySelector('[data-plan="growth"]')).not.toHaveAttribute('data-recommended');
|
expect(container.querySelector('[data-plan="launch"]')).not.toHaveAttribute('data-recommended');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders approved public prices except for Enterprise custom pricing', async () => {
|
it('renders approved public prices except for Enterprise custom pricing', async () => {
|
||||||
@@ -62,15 +62,14 @@ describe('PricingSection', () => {
|
|||||||
expect(screen.queryByText('Starting price pending approval')).not.toBeInTheDocument();
|
expect(screen.queryByText('Starting price pending approval')).not.toBeInTheDocument();
|
||||||
expect(screen.getByText(/149/)).toBeInTheDocument();
|
expect(screen.getByText(/149/)).toBeInTheDocument();
|
||||||
expect(screen.getByText(/299/)).toBeInTheDocument();
|
expect(screen.getByText(/299/)).toBeInTheDocument();
|
||||||
expect(screen.getByText(/399/)).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Custom pricing')).toBeInTheDocument();
|
expect(screen.getByText('Custom pricing')).toBeInTheDocument();
|
||||||
|
|
||||||
await user.click(screen.getByLabelText(/Annual/));
|
await user.click(screen.getByLabelText(/Annual/));
|
||||||
|
|
||||||
expect(screen.getByText(/Save 20%/)).toBeInTheDocument();
|
expect(screen.getByText(/Save 20%/)).toBeInTheDocument();
|
||||||
expect(screen.getByText(/1,430/)).toBeInTheDocument();
|
expect(screen.getByText(/1,430/)).toBeInTheDocument();
|
||||||
expect(screen.getAllByText('/ year')).toHaveLength(3);
|
expect(screen.getAllByText('/ year')).toHaveLength(2);
|
||||||
expect(screen.getAllByText('billed annually after full-payment discount')).toHaveLength(3);
|
expect(screen.getAllByText('billed annually after full-payment discount')).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('links self-serve plan CTAs to account creation and opens a pricing form for Enterprise', () => {
|
it('links self-serve plan CTAs to account creation and opens a pricing form for Enterprise', () => {
|
||||||
@@ -93,10 +92,6 @@ describe('PricingSection', () => {
|
|||||||
'href',
|
'href',
|
||||||
'/dashboard/sign-up?lang=en&theme=dark&plan=GROWTH',
|
'/dashboard/sign-up?lang=en&theme=dark&plan=GROWTH',
|
||||||
);
|
);
|
||||||
expect(screen.getByRole('link', { name: 'Book a Pro demo' })).toHaveAttribute(
|
|
||||||
'href',
|
|
||||||
'/dashboard/sign-up?lang=en&theme=dark&plan=PRO',
|
|
||||||
);
|
|
||||||
expect(screen.getByRole('button', { name: 'Talk to sales' })).toHaveAttribute(
|
expect(screen.getByRole('button', { name: 'Talk to sales' })).toHaveAttribute(
|
||||||
'data-demo-source',
|
'data-demo-source',
|
||||||
'pricing',
|
'pricing',
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ describe('homepage pricing configuration', () => {
|
|||||||
expect(pricingCommercialConfig.annualDiscountPercent).toBe(20);
|
expect(pricingCommercialConfig.annualDiscountPercent).toBe(20);
|
||||||
expect(pricingCommercialConfig.monthlyPrices.launch.small).toBe(149);
|
expect(pricingCommercialConfig.monthlyPrices.launch.small).toBe(149);
|
||||||
expect(pricingCommercialConfig.monthlyPrices.growth.growing).toBe(299);
|
expect(pricingCommercialConfig.monthlyPrices.growth.growing).toBe(299);
|
||||||
expect(pricingCommercialConfig.monthlyPrices.pro.scale).toBe(399);
|
|
||||||
expect(pricingCommercialConfig.monthlyPrices.enterprise.enterprise).toBeNull();
|
expect(pricingCommercialConfig.monthlyPrices.enterprise.enterprise).toBeNull();
|
||||||
|
expect(pricingCommercialConfig.recommendedPlanByFleetBand.scale).toBe('growth');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('maps every fleet band and plan from localized content', () => {
|
it('maps every fleet band and plan from localized content', () => {
|
||||||
@@ -24,6 +24,6 @@ describe('homepage pricing configuration', () => {
|
|||||||
'scale',
|
'scale',
|
||||||
'enterprise',
|
'enterprise',
|
||||||
]);
|
]);
|
||||||
expect(pricing.plans.map((plan) => plan.id)).toEqual(['launch', 'growth', 'pro', 'enterprise']);
|
expect(pricing.plans.map((plan) => plan.id)).toEqual(['launch', 'growth', 'enterprise']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user