feat: validate podcast cost estimation accuracy, document per-token costs, and fix subscription/plan enforcement
Issue #543 — Validate Estimated Cost Accuracy (UI vs Backend) Backend: - cost_estimator.py uses pricing catalog (APIProviderPricing) as single source of truth - All 7 cost components: analysis, research (search+LLM), script, TTS, voice clone, avatar, video - initialize_default_pricing() runs on every app startup for auto-sync Frontend cost estimation fixes: - Added missing analysisCost, scriptCost, voiceCloneCost to PodcastEstimate type - toPodcastEstimate() now extracts all 7 backend fields (was dropping 3) - headerCostEst maps analysisCost->Analyze, scriptCost->Write, voiceCloneCost->Produce - EstimateCard shows 5 chips: Analysis, Research, Script, Voice(TTS+clone), Visuals(avatar+video) - Chip sum now equals backend total for all configurations Subscription & plan fixes: - Removed Stripe re-verification from checkSubscription() (downgrade regression fix #539) - Added verifyCheckoutRef pattern for reliable mount-time checkout polling - One-time Stripe sync effect with pending_subscription_change flag for Customer Portal returns - Free plan limits: stability_calls 3->10, audio_calls 5->10 (supports 2 podcasts) - Image enforcement uses actual provider (GPT_PROVIDER), not hardcoded Stability - Billing/pricing pages bypass onboarding check in ProtectedRoute - Gradient buttons + loading spinner on plan chip in UserBadge - Added metadata-based Stripe lookup fallback (Issue #538) Documentation: - TESTING_GUIDE.md: comprehensive testing instructions for non-technical testers - Free plan limits, usage tracking, cost estimation formulas - 10 test cases for UI verification - Troubleshooting guide - Quick-reference cost formulas with all default rates Cleanup: removed legacy ToBeMigrated directory (70+ files, ~22K LOC) GSC Brainstorm: service, hook, modal, and UI components for blog topic brainstorming
This commit is contained in:
@@ -39,7 +39,12 @@ const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
|
||||
const isFeatureLimited = shouldSkipOnboarding();
|
||||
const defaultRoute = getDefaultLandingRoute();
|
||||
const isOnDefaultRoute = typeof location?.pathname === 'string' && location.pathname.startsWith(defaultRoute);
|
||||
const allowAccess = isOnboardingComplete || localComplete || (isFeatureLimited && isOnDefaultRoute);
|
||||
|
||||
// Allow access to utility pages regardless of onboarding status
|
||||
const bypassRoutes = ['/billing', '/pricing', '/onboarding'];
|
||||
const isBypassRoute = typeof location?.pathname === 'string' && bypassRoutes.some(route => location.pathname.startsWith(route));
|
||||
|
||||
const allowAccess = isOnboardingComplete || localComplete || (isFeatureLimited && isOnDefaultRoute) || isBypassRoute;
|
||||
|
||||
// Wait for Clerk to load before any redirect decisions
|
||||
if (!isLoaded) {
|
||||
|
||||
@@ -20,7 +20,7 @@ interface UserBadgeProps {
|
||||
const UserBadge: React.FC<UserBadgeProps> = ({ colorMode = 'light' }) => {
|
||||
const { user, isSignedIn } = useUser();
|
||||
const { signOut } = useClerk();
|
||||
const { subscription, refreshSubscription } = useSubscription();
|
||||
const { subscription, refreshSubscription, loading } = useSubscription();
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
const [systemStatus, setSystemStatus] = useState<'healthy' | 'warning' | 'critical' | 'unknown'>('unknown');
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
@@ -131,12 +131,18 @@ const UserBadge: React.FC<UserBadgeProps> = ({ colorMode = 'light' }) => {
|
||||
label={getPlanLabel()}
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: `${getPlanColor()}20`,
|
||||
border: `1px solid ${getPlanColor()}`,
|
||||
color: getPlanColor(),
|
||||
bgcolor: loading ? '#e5e7eb' : `${getPlanColor()}20`,
|
||||
border: loading ? '1px solid #d1d5db' : `1px solid ${getPlanColor()}`,
|
||||
color: loading ? '#9ca3af' : getPlanColor(),
|
||||
fontWeight: 700,
|
||||
fontSize: '0.75rem',
|
||||
height: 24,
|
||||
minWidth: loading ? 60 : 'auto',
|
||||
animation: loading ? 'plan-pulse 1.5s ease-in-out infinite' : 'none',
|
||||
'@keyframes plan-pulse': {
|
||||
'0%, 100%': { opacity: 1 },
|
||||
'50%': { opacity: 0.4 },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -236,13 +242,13 @@ const UserBadge: React.FC<UserBadgeProps> = ({ colorMode = 'light' }) => {
|
||||
<IconButton
|
||||
onClick={handleRefreshPlan}
|
||||
size="small"
|
||||
disabled={isRefreshing}
|
||||
disabled={isRefreshing || loading}
|
||||
sx={{
|
||||
color: '#6b7280',
|
||||
'&:hover': { bgcolor: '#e5e7eb' },
|
||||
}}
|
||||
>
|
||||
{isRefreshing ? <CircularProgress size={16} /> : <RefreshIcon fontSize="small" />}
|
||||
{(isRefreshing || loading) ? <CircularProgress size={16} /> : <RefreshIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
@@ -289,10 +295,10 @@ const UserBadge: React.FC<UserBadgeProps> = ({ colorMode = 'light' }) => {
|
||||
|
||||
<Divider sx={{ mx: 2 }} />
|
||||
|
||||
<MenuItem onClick={() => { handleClose(); saveNavigationState(window.location.pathname); window.location.href = '/pricing'; }} sx={{ mx: 1, borderRadius: 1, color: '#374151', '&:hover': { bgcolor: '#f3f4f6' } }}>
|
||||
<MenuItem onClick={() => { handleClose(); saveNavigationState(window.location.pathname); sessionStorage.setItem('pending_subscription_change', 'true'); window.location.href = '/pricing'; }} sx={{ mx: 1, borderRadius: 1, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', color: '#ffffff', fontWeight: 600, mb: 0.5, '&:hover': { background: 'linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%)', boxShadow: '0 2px 8px rgba(99,102,241,0.4)' } }}>
|
||||
Manage Subscription
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => { handleClose(); window.location.href = '/billing'; }} sx={{ mx: 1, borderRadius: 1, color: '#374151', '&:hover': { bgcolor: '#f3f4f6' } }}>
|
||||
<MenuItem onClick={() => { handleClose(); window.location.href = '/billing'; }} sx={{ mx: 1, borderRadius: 1, background: 'linear-gradient(135deg, #06b6d4 0%, #3b82f6 100%)', color: '#ffffff', fontWeight: 600, '&:hover': { background: 'linear-gradient(135deg, #0891b2 0%, #2563eb 100%)', boxShadow: '0 2px 8px rgba(6,182,212,0.4)' } }}>
|
||||
View Costing Details
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleSignOut} sx={{ mx: 1, borderRadius: 1, color: '#6b7280', '&:hover': { bgcolor: '#fef2f2', color: '#ef4444' } }}>
|
||||
|
||||
Reference in New Issue
Block a user