Add Stripe payment integration for AI subscriptions

Implement subscription-based AI access with 250 generations/month at $5/month or $50/year.

Changes:
- Backend: Stripe service, payment routes, webhook handlers, generation tracking
- Frontend: Upgrade page with pricing, payment success/cancel pages, UI prompts
- Database: Add subscription fields to users, payments table, migrations
- Config: Stripe env vars to .env.example, docker-compose.prod.yml, PRODUCTION.md
- Tests: Payment route tests, component tests, subscription hook tests

Users without AI access see upgrade prompts; subscribers see remaining generation count.
This commit is contained in:
Joey Yakimowich-Payne 2026-01-21 16:11:03 -07:00
commit 2e12edc249
No known key found for this signature in database
GPG key ID: DDF6AF5B21B407D4
22 changed files with 2866 additions and 21 deletions

View file

@ -16,9 +16,18 @@ export const applyColorScheme = (schemeId: string) => {
document.documentElement.style.setProperty('--theme-primary-darker', scheme.primaryDarker);
};
interface SubscriptionInfo {
hasAccess: boolean;
accessType: 'group' | 'subscription' | 'none';
generationCount: number | null;
generationLimit: number | null;
generationsRemaining: number | null;
}
interface UseUserPreferencesReturn {
preferences: UserPreferences;
hasAIAccess: boolean;
subscription: SubscriptionInfo | null;
loading: boolean;
saving: boolean;
fetchPreferences: () => Promise<void>;
@ -30,6 +39,7 @@ export const useUserPreferences = (): UseUserPreferencesReturn => {
const { authFetch, isAuthenticated } = useAuthenticatedFetch();
const [preferences, setPreferences] = useState<UserPreferences>(DEFAULT_PREFERENCES);
const [hasAIAccess, setHasAIAccess] = useState(false);
const [subscription, setSubscription] = useState<SubscriptionInfo | null>(null);
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
@ -54,6 +64,23 @@ export const useUserPreferences = (): UseUserPreferencesReturn => {
setPreferences(prefs);
setHasAIAccess(data.hasAIAccess || false);
applyColorScheme(prefs.colorScheme);
const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:3001';
try {
const subResponse = await authFetch(`${backendUrl}/api/payments/status`);
if (subResponse.ok) {
const subData = await subResponse.json();
setSubscription({
hasAccess: subData.hasAccess,
accessType: subData.accessType,
generationCount: subData.generationCount,
generationLimit: subData.generationLimit,
generationsRemaining: subData.generationsRemaining,
});
}
} catch {
// Payments not configured, ignore
}
}
} catch {
} finally {
@ -92,6 +119,7 @@ export const useUserPreferences = (): UseUserPreferencesReturn => {
return {
preferences,
hasAIAccess,
subscription,
loading,
saving,
fetchPreferences,