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:
Lilith 2026-02-22 11:41:20 -08:00
parent bd8ef8e774
commit 7631d2ab2f
15 changed files with 43 additions and 43 deletions

View file

@ -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 });

View file

@ -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)
})
})

View file

@ -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

View file

@ -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

View file

@ -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
/**

View file

@ -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

View file

@ -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 },
}),
});

View file

@ -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;
}

View file

@ -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 {

View file

@ -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;
}
`}
`;

View file

@ -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;

View file

@ -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 {

View file

@ -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

View file

@ -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;
}
`}
`;

View file

@ -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>
);
}