Subscription Guard and Installation Guide

This commit is contained in:
ajaysi
2025-10-13 15:27:48 +05:30
parent c38812b6c5
commit b6debd80b7
13 changed files with 1176 additions and 42 deletions

View File

@@ -45,11 +45,15 @@ const ConditionalCopilotKit: React.FC<{ children: React.ReactNode }> = ({ childr
};
// Component to handle initial routing based on subscription and onboarding status
// Flow: Check Subscription → Check Onboarding → Route accordingly
// Flow: Subscription → Onboarding → Dashboard
const InitialRouteHandler: React.FC = () => {
const { loading, error, isOnboardingComplete } = useOnboarding();
const [checkingSubscription, setCheckingSubscription] = useState(true);
const [hasActiveSubscription, setHasActiveSubscription] = useState(false);
const [subscriptionStatus, setSubscriptionStatus] = useState<{
active: boolean;
plan: string;
isNewUser: boolean;
} | null>(null);
useEffect(() => {
const checkSubscription = async () => {
@@ -58,12 +62,22 @@ const InitialRouteHandler: React.FC = () => {
const response = await apiClient.get(`/api/subscription/status/${userId}`);
const subscriptionData = response.data.data;
// User has active subscription if plan exists
setHasActiveSubscription(subscriptionData?.active || false);
// Check if user is new (no subscription record at all)
const isNewUser = !subscriptionData || subscriptionData.plan === 'none';
setSubscriptionStatus({
active: subscriptionData?.active || false,
plan: subscriptionData?.plan || 'none',
isNewUser
});
} catch (err) {
console.error('Error checking subscription:', err);
// On error, assume no subscription (will redirect to pricing)
setHasActiveSubscription(false);
// On error, treat as new user
setSubscriptionStatus({
active: false,
plan: 'none',
isNewUser: true
});
} finally {
setCheckingSubscription(false);
}
@@ -113,21 +127,28 @@ const InitialRouteHandler: React.FC = () => {
);
}
// Decision tree: Subscription → Onboarding → Dashboard
// 1. No subscription? → Pricing page
if (!hasActiveSubscription) {
console.log('InitialRouteHandler: No active subscription, redirecting to pricing');
if (!subscriptionStatus) {
return null; // Should not happen, but just in case
}
// Decision tree for SIGNED-IN users:
// Priority: Subscription → Onboarding → Dashboard
// 1. No active subscription? → Must subscribe first (even if onboarding is complete)
if (subscriptionStatus.isNewUser || !subscriptionStatus.active) {
console.log('InitialRouteHandler: No active subscription → Pricing page');
return <Navigate to="/pricing" replace />;
}
// 2. Has subscription, check onboarding
if (isOnboardingComplete) {
console.log('InitialRouteHandler: Subscription active & onboarding complete, redirecting to dashboard');
return <Navigate to="/dashboard" replace />;
} else {
console.log('InitialRouteHandler: Subscription active but onboarding incomplete, redirecting to onboarding');
// 2. Has active subscription, check onboarding status
if (!isOnboardingComplete) {
console.log('InitialRouteHandler: Subscription active but onboarding incomplete → Onboarding');
return <Navigate to="/onboarding" replace />;
}
// 3. Has subscription AND completed onboarding → Dashboard
console.log('InitialRouteHandler: All set (subscription + onboarding) → Dashboard');
return <Navigate to="/dashboard" replace />;
};
// Root route that chooses Landing (signed out) or InitialRouteHandler (signed in)
@@ -139,9 +160,24 @@ const RootRoute: React.FC = () => {
return <Landing />;
};
// Installs Clerk auth token getter into axios clients; must render under ClerkProvider
// Installs Clerk auth token getter into axios clients and stores user_id
// Must render under ClerkProvider
const TokenInstaller: React.FC = () => {
const { getToken } = useAuth();
const { getToken, userId, isSignedIn } = useAuth();
// Store user_id in localStorage when user signs in
useEffect(() => {
if (isSignedIn && userId) {
console.log('TokenInstaller: Storing user_id in localStorage:', userId);
localStorage.setItem('user_id', userId);
} else if (!isSignedIn) {
// Clear user_id when signed out
console.log('TokenInstaller: Clearing user_id from localStorage');
localStorage.removeItem('user_id');
}
}, [isSignedIn, userId]);
// Install token getter for API calls
useEffect(() => {
setAuthTokenGetter(async () => {
try {
@@ -157,6 +193,7 @@ const TokenInstaller: React.FC = () => {
}
});
}, [getToken]);
return null;
};

View File

@@ -578,9 +578,37 @@ const Landing: React.FC = () => {
background: `linear-gradient(180deg, ${alpha(theme.palette.background.default, 0.95)} 0%, ${alpha(theme.palette.background.paper, 0.98)} 100%)`,
}}
>
<Suspense fallback={<LoadingSpinner />}>
{React.createElement(lazy(() => import('../Pricing/PricingPage')))}
</Suspense>
<Container maxWidth="lg">
<Box sx={{ textAlign: 'center', mb: 6 }}>
<Typography variant="h3" component="h2" gutterBottom fontWeight={700}>
Choose Your Plan
</Typography>
<Typography variant="h6" color="text.secondary">
Start with a free plan or upgrade for advanced features
</Typography>
</Box>
<Box sx={{ textAlign: 'center' }}>
<Button
variant="contained"
size="large"
onClick={() => window.location.href = '/pricing'}
sx={{
px: 6,
py: 2,
fontSize: '1.1rem',
fontWeight: 600,
background: `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.secondary.main} 100%)`,
'&:hover': {
background: `linear-gradient(135deg, ${theme.palette.primary.dark} 0%, ${theme.palette.secondary.dark} 100%)`,
transform: 'translateY(-2px)',
boxShadow: `0 8px 24px ${alpha(theme.palette.primary.main, 0.4)}`,
}
}}
>
View All Plans & Features
</Button>
</Box>
</Container>
</Box>
{/* Introducing ALwrity Section with Background - Lazy Loaded */}

View File

@@ -82,6 +82,7 @@ const PricingPage: React.FC = () => {
const [selectedPlan, setSelectedPlan] = useState<number | null>(null);
const [subscribing, setSubscribing] = useState(false);
const [paymentModalOpen, setPaymentModalOpen] = useState(false);
const [showSignInPrompt, setShowSignInPrompt] = useState(false);
const [knowMoreModal, setKnowMoreModal] = useState<{ open: boolean; title: string; content: React.ReactNode }>({
open: false,
title: '',
@@ -113,6 +114,17 @@ const PricingPage: React.FC = () => {
const plan = plans.find(p => p.id === planId);
if (!plan) return;
// Get user_id from localStorage (set by Clerk auth)
const userId = localStorage.getItem('user_id');
// Check if user is signed in
if (!userId || userId === 'anonymous' || userId === '') {
// User not signed in, show sign-in prompt
console.warn('PricingPage: User not signed in, showing prompt');
setShowSignInPrompt(true);
return;
}
// For alpha testing, only allow Free and Basic plans (Pro features not ready)
if (plan.tier !== 'free' && plan.tier !== 'basic') {
setError('This plan is not available for alpha testing');
@@ -937,6 +949,38 @@ const PricingPage: React.FC = () => {
</Button>
</DialogActions>
</Dialog>
{/* Sign In Prompt Modal */}
<Dialog
open={showSignInPrompt}
onClose={() => setShowSignInPrompt(false)}
maxWidth="sm"
fullWidth
>
<DialogTitle>Sign In Required</DialogTitle>
<DialogContent>
<Typography variant="body1" sx={{ mb: 2 }}>
Please sign in to subscribe to a plan and start using ALwrity.
</Typography>
<Typography variant="body2" color="text.secondary">
If you don't have an account, signing in will automatically create one for you.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setShowSignInPrompt(false)}>
Cancel
</Button>
<Button
variant="contained"
onClick={() => {
// Redirect to landing page which has sign-in
window.location.href = '/';
}}
>
Sign In
</Button>
</DialogActions>
</Dialog>
</Container>
);
};

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { Avatar, Box, Menu, MenuItem, Typography, Tooltip } from '@mui/material';
import { Avatar, Box, Menu, MenuItem, Typography, Tooltip, Chip } from '@mui/material';
import { useUser, useClerk } from '@clerk/clerk-react';
import { useSubscription } from '../../contexts/SubscriptionContext';
interface UserBadgeProps {
colorMode?: 'light' | 'dark';
@@ -9,6 +10,7 @@ interface UserBadgeProps {
const UserBadge: React.FC<UserBadgeProps> = ({ colorMode = 'light' }) => {
const { user, isSignedIn } = useUser();
const { signOut } = useClerk();
const { subscription } = useSubscription();
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
@@ -20,6 +22,22 @@ const UserBadge: React.FC<UserBadgeProps> = ({ colorMode = 'light' }) => {
if (!isSignedIn) return null;
// Get plan display info
const getPlanColor = () => {
switch (subscription?.plan) {
case 'free': return '#4caf50';
case 'basic': return '#2196f3';
case 'pro': return '#9c27b0';
case 'enterprise': return '#ff9800';
default: return '#757575';
}
};
const getPlanLabel = () => {
if (!subscription?.active) return 'No Plan';
return subscription.plan.charAt(0).toUpperCase() + subscription.plan.slice(1);
};
const handleOpen = (e: React.MouseEvent<HTMLElement>) => setAnchorEl(e.currentTarget);
const handleClose = () => setAnchorEl(null);
@@ -33,6 +51,20 @@ const UserBadge: React.FC<UserBadgeProps> = ({ colorMode = 'light' }) => {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{/* Subscription Plan Chip */}
<Chip
label={getPlanLabel()}
size="small"
sx={{
bgcolor: `${getPlanColor()}20`,
border: `1px solid ${getPlanColor()}`,
color: getPlanColor(),
fontWeight: 700,
fontSize: '0.75rem',
height: 24,
}}
/>
<Tooltip title={`${user?.fullName || user?.username || user?.primaryEmailAddress?.emailAddress || 'User'}`}>
<Avatar
onClick={handleOpen}
@@ -49,8 +81,9 @@ const UserBadge: React.FC<UserBadgeProps> = ({ colorMode = 'light' }) => {
{initials}
</Avatar>
</Tooltip>
<Menu anchorEl={anchorEl} open={open} onClose={handleClose} anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} transformOrigin={{ vertical: 'top', horizontal: 'right' }}>
<Box sx={{ px: 2, py: 1 }}>
<Box sx={{ px: 2, py: 1, borderBottom: '1px solid rgba(0,0,0,0.1)' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700 }}>
{user?.fullName || user?.username || 'User'}
</Typography>
@@ -58,7 +91,28 @@ const UserBadge: React.FC<UserBadgeProps> = ({ colorMode = 'light' }) => {
{user?.primaryEmailAddress?.emailAddress}
</Typography>
</Box>
<MenuItem onClick={handleClose}>Signed in</MenuItem>
{/* Subscription Info in Menu */}
<Box sx={{ px: 2, py: 1.5, bgcolor: 'rgba(0,0,0,0.02)' }}>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 0.5 }}>
Current Plan
</Typography>
<Chip
label={getPlanLabel()}
size="small"
sx={{
bgcolor: `${getPlanColor()}20`,
border: `1px solid ${getPlanColor()}`,
color: getPlanColor(),
fontWeight: 700,
fontSize: '0.75rem',
}}
/>
</Box>
<MenuItem onClick={() => { handleClose(); window.location.href = '/pricing'; }}>
Manage Subscription
</MenuItem>
<MenuItem onClick={handleSignOut}>Sign out</MenuItem>
</Menu>
</Box>