feat(subscription): ✨ Implement subscription management components with tier selection, trial handling, billing display, lifecycle actions, and demo/test support
This commit is contained in:
parent
80eb003266
commit
8a41b1ec3c
15 changed files with 56 additions and 39 deletions
|
|
@ -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';
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}};
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Modal,
|
||||
ModalContent,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue