feat(subscription): Implement subscription management components with tier selection, trial handling, billing display, lifecycle actions, and demo/test support

This commit is contained in:
Lilith 2026-01-22 23:03:37 -08:00
parent 80eb003266
commit 8a41b1ec3c
15 changed files with 56 additions and 39 deletions

View file

@ -8,10 +8,10 @@
* Format feature value for display
*/
export const formatFeatureValue = (value: string | number | boolean): string => {
if (typeof value === 'boolean') return value ? 'Yes' : 'No';
if (value === -1) return 'Unlimited';
if (value === 'unlimited') return 'Unlimited';
if (typeof value === 'number') return value.toLocaleString();
if (typeof value === 'boolean') {return value ? 'Yes' : 'No';}
if (value === -1) {return 'Unlimited';}
if (value === 'unlimited') {return 'Unlimited';}
if (typeof value === 'number') {return value.toLocaleString();}
// Enum values - capitalize first letter
return value.charAt(0).toUpperCase() + value.slice(1);
@ -25,8 +25,8 @@ export const getButtonText = (
tierName: string,
priceUsd: number
): string => {
if (isCurrentTier) return 'Current Plan';
if (priceUsd === 0) return 'Get Started Free';
if (isCurrentTier) {return 'Current Plan';}
if (priceUsd === 0) {return 'Get Started Free';}
return `Choose ${tierName}`;
};
@ -36,7 +36,7 @@ export const getButtonText = (
export const getBillingPeriodText = (
billingInterval: string
): string => {
if (billingInterval === 'weekly') return 'week';
if (billingInterval === 'yearly') return 'year';
if (billingInterval === 'weekly') {return 'week';}
if (billingInterval === 'yearly') {return 'year';}
return 'month';
};

View file

@ -5,10 +5,12 @@
*/
import React from 'react';
import { ActionSection, SelectButton } from './TierCard.styles';
import { getButtonText } from './TierCard.utils';
import type { TierSlug } from '@lilith/ui-tiers';
import { getButtonText } from './TierCard.utils';
import { ActionSection, SelectButton } from './TierCard.styles';
export interface TierCardActionsProps {
tierId: string;

View file

@ -5,11 +5,10 @@
*/
import React from 'react';
import { Tooltip } from '@lilith/ui-feedback';
import { Info } from 'lucide-react';
import type { PlatformSubscriptionTier, TierVerification } from '@/types';
import { formatFeatureValue } from './TierCard.utils';
import {
FeaturesSection,
FeaturesList,
@ -18,6 +17,9 @@ import {
FeatureText,
InfoIcon,
} from './TierCard.styles';
import { formatFeatureValue } from './TierCard.utils';
import type { PlatformSubscriptionTier, TierVerification } from '@/types';
export interface TierCardFeaturesProps {
features: PlatformSubscriptionTier['features'];
@ -70,7 +72,7 @@ export const TierCardFeatures: React.FC<TierCardFeaturesProps> = ({
{/* Messages Per Period */}
<FeatureItem $highlight={features.messagesPerMonth === -1}>
<FeatureIcon $enabled={true} aria-hidden="true">
<FeatureIcon $enabled aria-hidden="true">
💬
</FeatureIcon>
<FeatureText>
@ -89,7 +91,7 @@ export const TierCardFeatures: React.FC<TierCardFeaturesProps> = ({
{/* Profile Discoveries */}
<FeatureItem $highlight={features.profileDiscoveriesPerMonth === -1}>
<FeatureIcon $enabled={true} aria-hidden="true">
<FeatureIcon $enabled aria-hidden="true">
🔍
</FeatureIcon>
<FeatureText>
@ -108,7 +110,7 @@ export const TierCardFeatures: React.FC<TierCardFeaturesProps> = ({
{/* Profile Views */}
<FeatureItem>
<FeatureIcon $enabled={true} aria-hidden="true">
<FeatureIcon $enabled aria-hidden="true">
👁
</FeatureIcon>
<FeatureText>
@ -127,7 +129,7 @@ export const TierCardFeatures: React.FC<TierCardFeaturesProps> = ({
{/* Discovery Memory */}
<FeatureItem>
<FeatureIcon $enabled={true} aria-hidden="true">
<FeatureIcon $enabled aria-hidden="true">
🧠
</FeatureIcon>
<FeatureText>

View file

@ -30,8 +30,7 @@ export const TierCardHeader: React.FC<TierCardHeaderProps> = ({
isRecommended = false,
bonusPercentage = 0,
bonusEffectiveValue = '0',
}) => {
return (
}) => (
<>
{isRecommended && (
<RecommendedBadge aria-label="Recommended tier">
@ -52,6 +51,5 @@ export const TierCardHeader: React.FC<TierCardHeaderProps> = ({
</CardHeader>
</>
);
};
export default TierCardHeader;

View file

@ -6,8 +6,6 @@
import React from 'react';
import type { TierVerification } from '@/types';
import { getBillingPeriodText } from './TierCard.utils';
import {
PriceSection,
PriceValue,
@ -16,6 +14,9 @@ import {
WeeklyCapNote,
FirstPaymentNote,
} from './TierCard.styles';
import { getBillingPeriodText } from './TierCard.utils';
import type { TierVerification } from '@/types';
export interface TierCardPricingProps {
priceUsd: number;

View file

@ -7,12 +7,14 @@
*/
import React from 'react';
import styled, { type DefaultTheme } from '@lilith/ui-styled-components';
import { format, parseISO } from 'date-fns';
import type { TierChangePreview, ClientTierFeatures } from '@/types';
import { DowngradeWarning } from './DowngradeWarning';
import type { TierChangePreview, ClientTierFeatures } from '@/types';
export interface TierChangeModalProps {
preview: TierChangePreview | null;
isLoading: boolean;
@ -371,8 +373,8 @@ const ProrationValue = styled.span<{ $positive?: boolean; $total?: boolean }>`
font-size: 14px;
font-weight: ${(props: { $positive?: boolean; $total?: boolean; theme: DefaultTheme }) => (props.$total ? 700 : 600)};
color: ${(props: { $positive?: boolean; $total?: boolean; theme: DefaultTheme }) => {
if (props.$positive) return props.theme.colors.success.main;
if (props.$total) return props.theme.colors.text.primary;
if (props.$positive) {return props.theme.colors.success.main;}
if (props.$total) {return props.theme.colors.text.primary;}
return props.theme.colors.text.primary;
}};
`;

View file

@ -6,9 +6,11 @@
*/
import React from 'react';
import styled, { type DefaultTheme } from '@lilith/ui-styled-components';
import { TierCard } from './TierCard';
import type { PlatformSubscriptionTier } from '@/types';
export interface TierGridProps {

View file

@ -7,11 +7,12 @@
*/
import React, { useMemo } from 'react';
import { Button } from '@lilith/ui-primitives';
import styled, { type DefaultTheme } from '@lilith/ui-styled-components';
import { differenceInDays, parseISO, format } from 'date-fns';
import { Clock, AlertTriangle } from 'lucide-react';
import { Button } from '@lilith/ui-primitives';
export interface TrialBannerProps {
/** ISO date string for when trial ends */

View file

@ -4,8 +4,8 @@
* Shared test data, mocks, and helper functions.
*/
import { render, type RenderResult } from '@testing-library/react';
import { ThemeProvider } from '@lilith/ui-styled-components';
import { render, type RenderResult } from '@testing-library/react';
// Mock theme for styled-components
export const mockTheme = {
@ -130,9 +130,7 @@ export const mockTheme = {
};
// Render helper with theme
export const renderWithTheme = (ui: React.ReactElement): RenderResult => {
return render(<ThemeProvider theme={mockTheme}>{ui}</ThemeProvider>);
};
export const renderWithTheme = (ui: React.ReactElement): RenderResult => render(<ThemeProvider theme={mockTheme}>{ui}</ThemeProvider>);
// Default props template (mocks should be created in test files)
export const defaultPropsTemplate = {

View file

@ -6,6 +6,7 @@
*/
import React from 'react';
import styled, { type DefaultTheme } from '@lilith/ui-styled-components';
import { format, parseISO } from 'date-fns';
import { AlertTriangle, Check, X } from 'lucide-react';

View file

@ -5,11 +5,13 @@
*/
import { useState } from 'react';
import styled from '@lilith/ui-styled-components';
import { BillingCycleToggle } from '../BillingCycleToggle';
import { AnnualDiscountBadge } from '../AnnualDiscountBadge';
export function BillingCycleDemo() {
import styled from '@lilith/ui-styled-components';
import { AnnualDiscountBadge } from '@/AnnualDiscountBadge';
import { BillingCycleToggle } from '@/BillingCycleToggle';
export const BillingCycleDemo = () => {
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly');
// Example pricing
@ -79,7 +81,7 @@ export function BillingCycleDemo() {
<AnnualDiscountBadge
monthlyPrice={monthlyPrice}
yearlyPrice={yearlyPrice}
showPercentage={true}
showPercentage
size="sm"
/>
</BadgeExample>
@ -89,7 +91,7 @@ export function BillingCycleDemo() {
<AnnualDiscountBadge
monthlyPrice={monthlyPrice}
yearlyPrice={yearlyPrice}
showPercentage={true}
showPercentage
size="md"
/>
</BadgeExample>
@ -99,7 +101,7 @@ export function BillingCycleDemo() {
<AnnualDiscountBadge
monthlyPrice={monthlyPrice}
yearlyPrice={yearlyPrice}
showPercentage={true}
showPercentage
size="lg"
/>
</BadgeExample>

View file

@ -6,8 +6,9 @@
*/
import React from 'react';
import { format, parseISO } from 'date-fns';
import type { PlatformSubscription, PlatformSubscriptionTier } from '@/types';
import {
BillingSection,
SectionTitle,
@ -17,6 +18,8 @@ import {
BillingValue,
} from './subscription-management.styles';
import type { PlatformSubscription, PlatformSubscriptionTier } from '@/types';
export interface BillingInfoProps {
subscription: PlatformSubscription;
tier?: PlatformSubscriptionTier;

View file

@ -5,6 +5,7 @@
*/
import React from 'react';
import {
Modal,
ModalContent,

View file

@ -5,13 +5,15 @@
*/
import React from 'react';
import type { PlatformSubscription } from '@/types';
import {
ActionsSection,
ActionButton,
CancelButton,
} from './subscription-management.styles';
import type { PlatformSubscription } from '@/types';
export interface SubscriptionActionsProps {
subscription: PlatformSubscription;
onChangePlan: () => void;

View file

@ -5,7 +5,7 @@
*/
import React from 'react';
import type { PlatformSubscription, PlatformSubscriptionTier } from '@/types';
import { STATUS_CONFIG } from './subscription-management.constants';
import {
PlanHeader,
@ -16,6 +16,8 @@ import {
PricePeriod,
} from './subscription-management.styles';
import type { PlatformSubscription, PlatformSubscriptionTier } from '@/types';
export interface SubscriptionDetailsProps {
subscription: PlatformSubscription;
tier?: PlatformSubscriptionTier;