chore(subscription): 🔧 Add subscription tiers UI components, E2E tests, and enhanced useTiers hook
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
bd8ef8e774
commit
7631d2ab2f
15 changed files with 43 additions and 43 deletions
|
|
@ -72,7 +72,7 @@ export class UsageDashboardPage {
|
|||
// Header Section
|
||||
this.dashboardTitle = page.getByRole('heading', { name: /usage dashboard/i });
|
||||
this.tierBadge = page.locator('[class*="TierBadge"]').or(
|
||||
page.locator('div').filter({ hasText: /bronze|silver|gold|platinum|diamond|free/i })
|
||||
page.locator('div').filter({ hasText: /bronze|silver|gold|platinum|iridium|free/i })
|
||||
);
|
||||
this.refreshButton = page.getByRole('button', { name: /refresh/i });
|
||||
|
||||
|
|
|
|||
|
|
@ -261,11 +261,11 @@ test.describe('Billing Cycle Toggle', () => {
|
|||
expect(annualPrice).toBe(tier.yearlyPrice)
|
||||
})
|
||||
|
||||
test('should update prices for Diamond tier', async ({ page }) => {
|
||||
test('should update prices for Iridium tier', async ({ page }) => {
|
||||
const billingPage = new BillingCyclePage(page)
|
||||
await billingPage.goto()
|
||||
|
||||
const tier = SUBSCRIPTION_TIERS.find((t) => t.slug === 'diamond')!
|
||||
const tier = SUBSCRIPTION_TIERS.find((t) => t.slug === 'iridium')!
|
||||
|
||||
const monthlyPrice = await billingPage.getTierPriceNumeric(tier.slug)
|
||||
expect(monthlyPrice).toBe(tier.monthlyPrice)
|
||||
|
|
@ -968,14 +968,14 @@ test.describe('Billing Cycle Toggle', () => {
|
|||
expect(savings).toBe(478)
|
||||
})
|
||||
|
||||
test('should verify Diamond tier savings ($718)', async ({ page }) => {
|
||||
test('should verify Iridium tier savings ($718)', async ({ page }) => {
|
||||
const billingPage = new BillingCyclePage(page)
|
||||
await billingPage.goto()
|
||||
|
||||
const tier = SUBSCRIPTION_TIERS.find((t) => t.slug === 'diamond')!
|
||||
const tier = SUBSCRIPTION_TIERS.find((t) => t.slug === 'iridium')!
|
||||
const savings = calculateAnnualSavings(tier.monthlyPrice, tier.yearlyPrice)
|
||||
|
||||
// Diamond: 299*12 = 3588, annual = 2870, savings = 718
|
||||
// Iridium: 299*12 = 3588, annual = 2870, savings = 718
|
||||
expect(savings).toBe(718)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -516,7 +516,7 @@ test.describe('Subscription Checkout Flow', () => {
|
|||
const checkoutPage = new SubscriptionCheckoutPage(page)
|
||||
await checkoutPage.goto()
|
||||
|
||||
await checkoutPage.selectTier('diamond')
|
||||
await checkoutPage.selectTier('iridium')
|
||||
await checkoutPage.continueToPayment()
|
||||
|
||||
// Switch to crypto
|
||||
|
|
|
|||
|
|
@ -351,7 +351,7 @@ test.describe('Promo Code Checkout Flow', () => {
|
|||
const checkoutPage = new SubscriptionCheckoutPage(page)
|
||||
await checkoutPage.goto()
|
||||
|
||||
await checkoutPage.selectTier('diamond')
|
||||
await checkoutPage.selectTier('iridium')
|
||||
await checkoutPage.continueToPayment()
|
||||
|
||||
const promoCodeInput = page.getByRole('textbox', { name: /promo code/i })
|
||||
|
|
@ -504,7 +504,7 @@ test.describe('Promo Code Checkout Flow', () => {
|
|||
const checkoutPage = new SubscriptionCheckoutPage(page)
|
||||
await checkoutPage.goto()
|
||||
|
||||
await checkoutPage.selectTier('diamond')
|
||||
await checkoutPage.selectTier('iridium')
|
||||
await checkoutPage.continueToPayment()
|
||||
|
||||
const promoCodeInput = page.getByRole('textbox', { name: /promo code/i })
|
||||
|
|
@ -760,8 +760,8 @@ test.describe('Promo Code Checkout Flow', () => {
|
|||
const checkoutPage = new SubscriptionCheckoutPage(page)
|
||||
await checkoutPage.goto()
|
||||
|
||||
// Diamond tier is $299/month, 50% would be $149.50, but capped at $30
|
||||
await checkoutPage.selectTier('diamond')
|
||||
// Iridium tier is $299/month, 50% would be $149.50, but capped at $30
|
||||
await checkoutPage.selectTier('iridium')
|
||||
await checkoutPage.continueToPayment()
|
||||
|
||||
const promoCodeInput = page.getByRole('textbox', { name: /promo code/i })
|
||||
|
|
@ -906,7 +906,7 @@ test.describe('Promo Code Checkout Flow', () => {
|
|||
const checkoutPage = new SubscriptionCheckoutPage(page)
|
||||
await checkoutPage.goto()
|
||||
|
||||
await checkoutPage.selectTier('diamond')
|
||||
await checkoutPage.selectTier('iridium')
|
||||
await checkoutPage.continueToPayment()
|
||||
|
||||
// Apply code
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const TOOLTIP_CONTENT = {
|
|||
'Rollover Policy': 'Whether unused allowances carry forward to the next period. Free tier has no rollover, Bronze+ tiers let you save up unused allowances.',
|
||||
'Max Rollover': 'Maximum duration unused allowances can accumulate. Example: 3 months = can save up to 3 months of unused allowances.',
|
||||
'Verification': 'Identity verification is REQUIRED for all users within 30 days of launch. Standard verification uses 2 community interviews for consensus-based safety screening (~$39.99 total). Silver+ tiers include verification. Platinum+ tiers get VIP Verification \u2014 a single private interview with your dedicated white glove contact.',
|
||||
'Concierge': 'Personal arrangement assistance. Our concierge team coordinates meetings between you and providers. Platinum gets Concierge Light, Diamond gets full Concierge service.',
|
||||
'Concierge': 'Personal arrangement assistance. Our concierge team coordinates meetings between you and providers. Platinum gets Concierge Light, Iridium gets full Concierge service.',
|
||||
} as const
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ Comprehensive end-to-end tests for the usage dashboard and quota tracking featur
|
|||
|
||||
7. **Upgrade CTA**
|
||||
- Visible for non-max tier users (levels 1-4)
|
||||
- Hidden for max tier (Diamond - level 5)
|
||||
- Hidden for max tier (Iridium - level 5)
|
||||
- "View Plans" button navigates to pricing
|
||||
|
||||
8. **Dashboard Updates**
|
||||
|
|
@ -63,7 +63,7 @@ Comprehensive end-to-end tests for the usage dashboard and quota tracking featur
|
|||
- Silver (level 2) - with rollover
|
||||
- Gold (level 3) - high limits
|
||||
- Platinum (level 4) - unlimited
|
||||
- Diamond (level 5) - max tier
|
||||
- Iridium (level 5) - max tier
|
||||
|
||||
11. **User Journey**
|
||||
- Complete flow from low → warning → critical usage
|
||||
|
|
|
|||
|
|
@ -450,7 +450,7 @@ test.describe('Usage Dashboard', () => {
|
|||
await dashboard.assertUpgradeCTAVisible();
|
||||
});
|
||||
|
||||
test('hides upgrade CTA for max tier (Diamond - level 5)', async ({ page }) => {
|
||||
test('hides upgrade CTA for max tier (Iridium - level 5)', async ({ page }) => {
|
||||
await page.route(`${mockApiUrl}/api/usage/me`, (route) => {
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
|
|
@ -459,7 +459,7 @@ test.describe('Usage Dashboard', () => {
|
|||
messages: { used: 150, limit: -1, rollover: 0, available: Infinity, isUnlimited: true },
|
||||
profileViews: { used: 75, limit: -1, rollover: 0, available: Infinity, isUnlimited: true },
|
||||
profileDiscoveries: { used: 300, limit: -1, rollover: 0, available: Infinity, isUnlimited: true },
|
||||
tier: { id: 'tier-diamond', slug: 'diamond', name: 'Diamond', tierLevel: 5 },
|
||||
tier: { id: 'tier-iridium', slug: 'iridium', name: 'Iridium', tierLevel: 5 },
|
||||
period: { start: '2026-01-01', end: '2026-02-01', daysRemaining: 15 },
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { createLazyIcon } from './LazyIcon';
|
|||
import type { IconProps, AnimationTrigger } from '@lilith/ui-icons';
|
||||
|
||||
/** Valid tier slugs that have corresponding icons */
|
||||
export type TierSlug = 'bronze' | 'silver' | 'gold' | 'platinum' | 'diamond';
|
||||
export type TierSlug = 'bronze' | 'silver' | 'gold' | 'platinum' | 'iridium';
|
||||
|
||||
/** Tier slugs including 'free' which has no icon */
|
||||
export type TierSlugWithFree = TierSlug | 'free';
|
||||
|
|
@ -26,7 +26,7 @@ const TIER_REGISTRY_NAMES: Record<TierSlug, string> = {
|
|||
silver: 'tier-silver',
|
||||
gold: 'tier-gold',
|
||||
platinum: 'tier-platinum',
|
||||
diamond: 'tier-diamond',
|
||||
iridium: 'tier-iridium',
|
||||
};
|
||||
|
||||
/** Default animation configuration for tier icons */
|
||||
|
|
@ -48,7 +48,7 @@ export function isTierWithIcon(tier: string): tier is TierSlug {
|
|||
* Props for TierIcon component
|
||||
*/
|
||||
export interface TierIconProps extends Omit<IconProps, 'showFullAnimation' | 'showSubtleAnimation'> {
|
||||
/** Tier slug (bronze, silver, gold, platinum, diamond, or free) */
|
||||
/** Tier slug (bronze, silver, gold, platinum, iridium, or free) */
|
||||
tier: TierSlugWithFree;
|
||||
/** Override full animation trigger. Defaults to onHover. */
|
||||
showFullAnimation?: AnimationTrigger;
|
||||
|
|
@ -110,7 +110,7 @@ export const TierIcon = ({
|
|||
|
||||
// Validate tier slug
|
||||
if (!isTierWithIcon(tier)) {
|
||||
console.warn(`[TierIcon] Unknown tier "${tier}". Valid tiers: bronze, silver, gold, platinum, diamond`);
|
||||
console.warn(`[TierIcon] Unknown tier "${tier}". Valid tiers: bronze, silver, gold, platinum, iridium`);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ export const FullCheckoutExample = () => {
|
|||
* ```
|
||||
*/
|
||||
export const CheckoutIntegrationExample = () => {
|
||||
const tierId = 'tier-diamond-monthly';
|
||||
const tierId = 'tier-iridium-monthly';
|
||||
const basePrice = 199.99;
|
||||
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import styled, { css, type DefaultTheme } from '@lilith/ui-styled-components';
|
|||
import {
|
||||
type PlatformTierSlug as TierSlug,
|
||||
tierStyles,
|
||||
shimmerKeyframes as diamondShimmerKeyframes,
|
||||
} from '@platform/config';
|
||||
shimmerKeyframes as iridiumShimmerKeyframes,
|
||||
} from '@features/config';
|
||||
|
||||
// ============================================
|
||||
// Helper Functions
|
||||
|
|
@ -47,7 +47,7 @@ export const Card = styled.div<{
|
|||
${(props: { $isRecommended: boolean; $isCurrentTier: boolean; $tierSlug?: TierSlug; theme: DefaultTheme }) => {
|
||||
if (props.$isCurrentTier) {return props.theme.colors.success.main;}
|
||||
if (props.$isRecommended) {
|
||||
const isPremiumTier = props.$tierSlug === 'gold' || props.$tierSlug === 'platinum' || props.$tierSlug === 'diamond';
|
||||
const isPremiumTier = props.$tierSlug === 'gold' || props.$tierSlug === 'platinum' || props.$tierSlug === 'iridium';
|
||||
if (isPremiumTier) {return getTierBorderColor(props.$tierSlug, props.theme);}
|
||||
return props.theme.colors.primary.main;
|
||||
}
|
||||
|
|
@ -436,8 +436,8 @@ export const SelectButton = styled.button<{
|
|||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
${tierSlug === 'diamond' && css`
|
||||
animation: ${diamondShimmerKeyframes} 3s ease-in-out infinite;
|
||||
${tierSlug === 'iridium' && css`
|
||||
animation: ${iridiumShimmerKeyframes} 3s ease-in-out infinite;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
|
|
@ -452,7 +452,7 @@ export const SelectButton = styled.button<{
|
|||
rgba(255, 255, 255, 0.4),
|
||||
transparent
|
||||
);
|
||||
animation: ${diamondShimmerKeyframes} 2s ease-in-out infinite;
|
||||
animation: ${iridiumShimmerKeyframes} 2s ease-in-out infinite;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import { TierCardHeader } from './TierCardHeader';
|
|||
import { TierCardPricing } from './TierCardPricing';
|
||||
|
||||
import type { PlatformSubscriptionTier } from '@/types';
|
||||
import type { PlatformTierSlug as TierSlug } from '@platform/config';
|
||||
import type { PlatformTierSlug as TierSlug } from '@features/config';
|
||||
|
||||
export interface TierCardProps {
|
||||
tier: PlatformSubscriptionTier;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import type { FC, KeyboardEvent } from 'react';
|
|||
import { ActionSection, SelectButton } from './TierCard.styles';
|
||||
import { getButtonText } from './TierCard.utils';
|
||||
|
||||
import type { PlatformTierSlug as TierSlug } from '@platform/config';
|
||||
import type { PlatformTierSlug as TierSlug } from '@features/config';
|
||||
|
||||
|
||||
export interface TierCardActionsProps {
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ export function useTier(id: string, enabled = true) {
|
|||
/**
|
||||
* Hook to fetch a tier by slug
|
||||
*
|
||||
* @param slug - Tier slug (free, bronze, silver, gold, platinum, diamond)
|
||||
* @param slug - Tier slug (free, bronze, silver, gold, platinum, iridium)
|
||||
* @param enabled - Whether to enable the query
|
||||
*
|
||||
* @example
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import styled, { css } from '@lilith/ui-styled-components';
|
|||
import {
|
||||
type PlatformTierSlug as TierSlug,
|
||||
tierStyles,
|
||||
shimmerKeyframes as diamondShimmerKeyframes,
|
||||
} from '@platform/config';
|
||||
shimmerKeyframes as iridiumShimmerKeyframes,
|
||||
} from '@features/config';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { PlatformSubscriptionTier } from '@/types';
|
||||
|
|
@ -77,7 +77,7 @@ const ActionCell = styled.td<{ $isHighlighted: boolean; $isWeekly?: boolean; $ti
|
|||
let bgColor = 'rgba(255, 0, 255, 0.05)';
|
||||
if (tierSlug === 'gold') {bgColor = 'rgba(212, 175, 55, 0.06)';}
|
||||
if (tierSlug === 'platinum') {bgColor = 'rgba(143, 217, 232, 0.05)';}
|
||||
if (tierSlug === 'diamond') {bgColor = 'rgba(0, 212, 255, 0.06)';}
|
||||
if (tierSlug === 'iridium') {bgColor = 'rgba(0, 212, 255, 0.06)';}
|
||||
return `background: ${bgColor};`;
|
||||
}}
|
||||
`;
|
||||
|
|
@ -116,8 +116,8 @@ const StartButton = styled.button<{ $isHighlighted: boolean; $tierSlug?: TierSlu
|
|||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
${tierSlug === 'diamond' && css`
|
||||
animation: ${diamondShimmerKeyframes} 3s ease-in-out infinite;
|
||||
${tierSlug === 'iridium' && css`
|
||||
animation: ${iridiumShimmerKeyframes} 3s ease-in-out infinite;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
|
|
@ -132,7 +132,7 @@ const StartButton = styled.button<{ $isHighlighted: boolean; $tierSlug?: TierSlu
|
|||
rgba(255, 255, 255, 0.4),
|
||||
transparent
|
||||
);
|
||||
animation: ${diamondShimmerKeyframes} 2s ease-in-out infinite;
|
||||
animation: ${iridiumShimmerKeyframes} 2s ease-in-out infinite;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import { EditableContent } from '@lilith/ui-dev-content';
|
||||
import { Tooltip } from '@lilith/ui-feedback';
|
||||
import styled from '@lilith/ui-styled-components';
|
||||
import { type PlatformTierSlug as TierSlug, tierStyles } from '@platform/config';
|
||||
import { type PlatformTierSlug as TierSlug, tierStyles } from '@features/config';
|
||||
import { CheckIcon, XIcon, InfoIcon } from '@lilith/ui-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ export const TierFeatureRows = ({ tiers, featureRows, highlightedTierSlug }: Tie
|
|||
const hasVip = tier.vipVerification || verification?.vip;
|
||||
|
||||
if (hasVip) {
|
||||
// Platinum/Diamond: show VIP Verification
|
||||
// Platinum/Iridium: show VIP Verification
|
||||
return (
|
||||
<VipText>
|
||||
<CheckIcon size={14} />
|
||||
|
|
@ -77,12 +77,12 @@ export const TierFeatureRows = ({ tiers, featureRows, highlightedTierSlug }: Tie
|
|||
if (feature.key === 'concierge') {
|
||||
const concierge = tier.concierge;
|
||||
if (concierge?.enabled) {
|
||||
// Diamond gets full "Concierge", Platinum gets "Concierge Light"
|
||||
const isDiamond = tier.slug === 'diamond';
|
||||
// Iridium gets full "Concierge", Platinum gets "Concierge Light"
|
||||
const isIridium = tier.slug === 'iridium';
|
||||
return (
|
||||
<ConciergeText $isFullConcierge={isDiamond}>
|
||||
<ConciergeText $isFullConcierge={isIridium}>
|
||||
<CheckIcon size={14} />
|
||||
{isDiamond ? 'Concierge' : 'Concierge Light'}
|
||||
{isIridium ? 'Concierge' : 'Concierge Light'}
|
||||
</ConciergeText>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue