# πŸš€ ВСхничСскоС руководство: ΠŸΡ€Π΅ΠΌΠΈΡƒΠΌ инфраструктура ## πŸ“‹ Π­Ρ‚Π°ΠΏ 1: Базовая ΠΏΡ€Π΅ΠΌΠΈΡƒΠΌ систСма ### πŸ—οΈ Backend: Django ΠΌΠΎΠ΄Π΅Π»ΠΈ #### 1. МодСль подписок ```python # backend/subscriptions/__init__.py # backend/subscriptions/models.py from django.db import models from django.contrib.auth.models import User from django.utils import timezone class SubscriptionPlan(models.Model): """ΠŸΠ»Π°Π½Ρ‹ подписок""" PLAN_CHOICES = [ ('free', 'Free'), ('premium', 'Premium'), ('business', 'Business'), ] name = models.CharField(max_length=50, choices=PLAN_CHOICES, unique=True) display_name = models.CharField(max_length=100) price_monthly = models.DecimalField(max_digits=10, decimal_places=2) price_yearly = models.DecimalField(max_digits=10, decimal_places=2) description = models.TextField() features = models.JSONField(default=dict) # Бписок возмоТностСй max_collections = models.IntegerField(default=1) max_groups = models.IntegerField(default=10) max_links = models.IntegerField(default=50) analytics_enabled = models.BooleanField(default=False) custom_domain_enabled = models.BooleanField(default=False) api_access_enabled = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.display_name class UserSubscription(models.Model): """Подписка ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ""" STATUS_CHOICES = [ ('active', 'Active'), ('cancelled', 'Cancelled'), ('expired', 'Expired'), ('trial', 'Trial'), ] user = models.OneToOneField(User, on_delete=models.CASCADE) plan = models.ForeignKey(SubscriptionPlan, on_delete=models.CASCADE) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active') starts_at = models.DateTimeField(default=timezone.now) expires_at = models.DateTimeField() stripe_subscription_id = models.CharField(max_length=255, blank=True) stripe_customer_id = models.CharField(max_length=255, blank=True) auto_renew = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'user_subscriptions' def is_active(self): return self.status == 'active' and self.expires_at > timezone.now() def is_premium(self): return self.plan.name in ['premium', 'business'] and self.is_active() def days_remaining(self): if self.expires_at > timezone.now(): return (self.expires_at - timezone.now()).days return 0 def __str__(self): return f"{self.user.username} - {self.plan.display_name}" class PaymentHistory(models.Model): """Π˜ΡΡ‚ΠΎΡ€ΠΈΡ ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ΅ΠΉ""" subscription = models.ForeignKey(UserSubscription, on_delete=models.CASCADE) amount = models.DecimalField(max_digits=10, decimal_places=2) currency = models.CharField(max_length=3, default='USD') stripe_payment_id = models.CharField(max_length=255) status = models.CharField(max_length=50) payment_date = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'payment_history' ``` #### 2. МодСль мноТСствСнных списков ```python # backend/collections/models.py from django.db import models from django.contrib.auth.models import User from django.utils.text import slugify class LinkCollection(models.Model): """ΠšΠΎΠ»Π»Π΅ΠΊΡ†ΠΈΠΈ ссылок для ΠΏΡ€Π΅ΠΌΠΈΡƒΠΌ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ""" ACCESS_CHOICES = [ ('public', 'Public'), ('private', 'Private'), ('password', 'Password Protected'), ('scheduled', 'Scheduled'), ] user = models.ForeignKey(User, on_delete=models.CASCADE) name = models.CharField(max_length=100) slug = models.SlugField(unique=True, blank=True) description = models.TextField(blank=True) is_default = models.BooleanField(default=False) access_type = models.CharField(max_length=20, choices=ACCESS_CHOICES, default='public') password = models.CharField(max_length=255, blank=True) published_at = models.DateTimeField(null=True, blank=True) expires_at = models.DateTimeField(null=True, blank=True) view_count = models.IntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) # Настройки Π΄ΠΈΠ·Π°ΠΉΠ½Π° для ΠΊΠ°ΠΆΠ΄ΠΎΠΉ ΠΊΠΎΠ»Π»Π΅ΠΊΡ†ΠΈΠΈ theme_color = models.CharField(max_length=7, default='#ffffff') background_image = models.ImageField(upload_to='collections/backgrounds/', blank=True) custom_css = models.TextField(blank=True) class Meta: db_table = 'link_collections' unique_together = ['user', 'slug'] def save(self, *args, **kwargs): if not self.slug: base_slug = slugify(self.name) slug = base_slug counter = 1 while LinkCollection.objects.filter(user=self.user, slug=slug).exists(): slug = f"{base_slug}-{counter}" counter += 1 self.slug = slug super().save(*args, **kwargs) def get_absolute_url(self): if self.is_default: return f"/{self.user.username}/" return f"/{self.user.username}/{self.slug}/" def __str__(self): return f"{self.user.username}/{self.slug}" ``` #### 3. ОбновлСниС ΠΌΠΎΠ΄Π΅Π»ΠΈ Π³Ρ€ΡƒΠΏΠΏ ΠΈ ссылок ```python # backend/links/models.py - Π΄ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ ΠΏΠΎΠ»Π΅ collection class LinkGroup(models.Model): # ... ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠ΅ поля ... collection = models.ForeignKey( 'collections.LinkCollection', on_delete=models.CASCADE, related_name='groups', null=True, # Для ΠΎΠ±Ρ€Π°Ρ‚Π½ΠΎΠΉ совмСстимости blank=True ) class Link(models.Model): # ... ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠ΅ поля ... collection = models.ForeignKey( 'collections.LinkCollection', on_delete=models.CASCADE, related_name='links', null=True, # Для ΠΎΠ±Ρ€Π°Ρ‚Π½ΠΎΠΉ совмСстимости blank=True ) ``` ### πŸ”§ БистСма ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½ΠΈΠΉ #### 1. Π”Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΎΡ€Ρ‹ для ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ Π»ΠΈΠΌΠΈΡ‚ΠΎΠ² ```python # backend/subscriptions/decorators.py from functools import wraps from django.http import JsonResponse from django.contrib.auth.decorators import login_required from .models import UserSubscription def premium_required(feature_name=None): """Π”Π΅ΠΊΠΎΡ€Π°Ρ‚ΠΎΡ€ для ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ ΠΏΡ€Π΅ΠΌΠΈΡƒΠΌ подписки""" def decorator(view_func): @wraps(view_func) @login_required def wrapped_view(request, *args, **kwargs): try: subscription = UserSubscription.objects.get(user=request.user) if not subscription.is_premium(): return JsonResponse({ 'error': 'Premium subscription required', 'feature': feature_name, 'upgrade_url': '/upgrade/' }, status=403) except UserSubscription.DoesNotExist: return JsonResponse({ 'error': 'No subscription found', 'upgrade_url': '/upgrade/' }, status=403) return view_func(request, *args, **kwargs) return wrapped_view return decorator def check_limits(limit_type): """ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π»ΠΈΠΌΠΈΡ‚ΠΎΠ² ΠΏΠΎ ΠΏΠ»Π°Π½Ρƒ""" def decorator(view_func): @wraps(view_func) @login_required def wrapped_view(request, *args, **kwargs): try: subscription = UserSubscription.objects.get(user=request.user) plan = subscription.plan if limit_type == 'collections': current_count = request.user.linkcollection_set.count() if current_count >= plan.max_collections: return JsonResponse({ 'error': f'Collection limit reached ({plan.max_collections})', 'upgrade_url': '/upgrade/' }, status=403) elif limit_type == 'groups': current_count = request.user.linkgroup_set.count() if current_count >= plan.max_groups: return JsonResponse({ 'error': f'Group limit reached ({plan.max_groups})', 'upgrade_url': '/upgrade/' }, status=403) elif limit_type == 'links': current_count = request.user.link_set.count() if current_count >= plan.max_links: return JsonResponse({ 'error': f'Link limit reached ({plan.max_links})', 'upgrade_url': '/upgrade/' }, status=403) except UserSubscription.DoesNotExist: # Free ΠΏΠ»Π°Π½ ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ pass return view_func(request, *args, **kwargs) return wrapped_view return decorator ``` #### 2. БСрвисный слой для ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ Π»ΠΈΠΌΠΈΡ‚ΠΎΠ² ```python # backend/subscriptions/services.py from .models import UserSubscription, SubscriptionPlan from collections.models import LinkCollection class SubscriptionService: @staticmethod def get_user_plan(user): """ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ ΠΏΠ»Π°Π½ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ""" try: subscription = UserSubscription.objects.get(user=user) if subscription.is_active(): return subscription.plan except UserSubscription.DoesNotExist: pass # Free ΠΏΠ»Π°Π½ ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ return SubscriptionPlan.objects.get(name='free') @staticmethod def can_create_collection(user): """ΠœΠΎΠΆΠ΅Ρ‚ Π»ΠΈ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ ΠΊΠΎΠ»Π»Π΅ΠΊΡ†ΠΈΡŽ""" plan = SubscriptionService.get_user_plan(user) current_count = LinkCollection.objects.filter(user=user).count() return current_count < plan.max_collections @staticmethod def can_create_group(user): """ΠœΠΎΠΆΠ΅Ρ‚ Π»ΠΈ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ Π³Ρ€ΡƒΠΏΠΏΡƒ""" plan = SubscriptionService.get_user_plan(user) current_count = user.linkgroup_set.count() return current_count < plan.max_groups @staticmethod def can_create_link(user): """ΠœΠΎΠΆΠ΅Ρ‚ Π»ΠΈ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ ссылку""" plan = SubscriptionService.get_user_plan(user) current_count = user.link_set.count() return current_count < plan.max_links @staticmethod def get_usage_stats(user): """Бтатистика использования""" plan = SubscriptionService.get_user_plan(user) return { 'plan': plan.name, 'collections': { 'current': LinkCollection.objects.filter(user=user).count(), 'limit': plan.max_collections, 'unlimited': plan.max_collections == -1 }, 'groups': { 'current': user.linkgroup_set.count(), 'limit': plan.max_groups, 'unlimited': plan.max_groups == -1 }, 'links': { 'current': user.link_set.count(), 'limit': plan.max_links, 'unlimited': plan.max_links == -1 }, 'features': { 'analytics': plan.analytics_enabled, 'custom_domain': plan.custom_domain_enabled, 'api_access': plan.api_access_enabled, } } ``` ### 🌐 API эндпоинты #### 1. Subscription API ```python # backend/subscriptions/views.py from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from .models import UserSubscription, SubscriptionPlan from .serializers import SubscriptionSerializer, PlanSerializer from .services import SubscriptionService class SubscriptionViewSet(viewsets.ReadOnlyModelViewSet): permission_classes = [IsAuthenticated] @action(detail=False, methods=['get']) def current(self, request): """ВСкущая подписка ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ""" try: subscription = UserSubscription.objects.get(user=request.user) serializer = SubscriptionSerializer(subscription) return Response(serializer.data) except UserSubscription.DoesNotExist: # Free ΠΏΠ»Π°Π½ free_plan = SubscriptionPlan.objects.get(name='free') return Response({ 'plan': PlanSerializer(free_plan).data, 'status': 'free', 'expires_at': None, 'is_active': True }) @action(detail=False, methods=['get']) def usage(self, request): """Бтатистика использования""" stats = SubscriptionService.get_usage_stats(request.user) return Response(stats) @action(detail=False, methods=['get']) def plans(self, request): """ДоступныС ΠΏΠ»Π°Π½Ρ‹""" plans = SubscriptionPlan.objects.all() serializer = PlanSerializer(plans, many=True) return Response(serializer.data) class CollectionViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] def get_queryset(self): return LinkCollection.objects.filter(user=self.request.user) def perform_create(self, serializer): if not SubscriptionService.can_create_collection(self.request.user): plan = SubscriptionService.get_user_plan(self.request.user) raise ValidationError( f"Collection limit reached ({plan.max_collections}). " f"Upgrade to Premium for unlimited collections." ) serializer.save(user=self.request.user) ``` #### 2. URL ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚Ρ‹ ```python # backend/subscriptions/urls.py from django.urls import path, include from rest_framework.routers import DefaultRouter from .views import SubscriptionViewSet router = DefaultRouter() router.register(r'subscriptions', SubscriptionViewSet, basename='subscription') urlpatterns = [ path('api/', include(router.urls)), ] # backend/collections/urls.py from django.urls import path, include from rest_framework.routers import DefaultRouter from .views import CollectionViewSet router = DefaultRouter() router.register(r'collections', CollectionViewSet, basename='collection') urlpatterns = [ path('api/', include(router.urls)), ] ``` ### πŸ’³ Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡ со Stripe #### 1. Настройки Stripe ```python # backend/settings.py import stripe STRIPE_PUBLISHABLE_KEY = os.environ.get('STRIPE_PUBLISHABLE_KEY') STRIPE_SECRET_KEY = os.environ.get('STRIPE_SECRET_KEY') STRIPE_WEBHOOK_SECRET = os.environ.get('STRIPE_WEBHOOK_SECRET') stripe.api_key = STRIPE_SECRET_KEY ``` #### 2. Stripe сСрвисы ```python # backend/subscriptions/stripe_services.py import stripe from django.conf import settings from .models import UserSubscription, SubscriptionPlan class StripeService: @staticmethod def create_customer(user): """Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π° Π² Stripe""" customer = stripe.Customer.create( email=user.email, name=user.get_full_name() or user.username, metadata={'user_id': user.id} ) return customer.id @staticmethod def create_subscription(user, plan_name, payment_method_id): """Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ подписку""" plan = SubscriptionPlan.objects.get(name=plan_name) # ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ ΠΈΠ»ΠΈ ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π° try: user_subscription = UserSubscription.objects.get(user=user) customer_id = user_subscription.stripe_customer_id if not customer_id: customer_id = StripeService.create_customer(user) user_subscription.stripe_customer_id = customer_id user_subscription.save() except UserSubscription.DoesNotExist: customer_id = StripeService.create_customer(user) # ΠŸΡ€ΠΈΠΊΡ€Π΅ΠΏΠΈΡ‚ΡŒ способ ΠΎΠΏΠ»Π°Ρ‚Ρ‹ stripe.PaymentMethod.attach( payment_method_id, customer=customer_id, ) # Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ подписку Π² Stripe subscription = stripe.Subscription.create( customer=customer_id, items=[{ 'price': plan.stripe_price_id, }], default_payment_method=payment_method_id, metadata={ 'user_id': user.id, 'plan_name': plan_name } ) return subscription @staticmethod def cancel_subscription(stripe_subscription_id): """ΠžΡ‚ΠΌΠ΅Π½ΠΈΡ‚ΡŒ подписку""" return stripe.Subscription.cancel(stripe_subscription_id) ``` #### 3. Webhook ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ```python # backend/subscriptions/webhooks.py import json import stripe from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from django.conf import settings from .models import UserSubscription, PaymentHistory @csrf_exempt @require_POST def stripe_webhook(request): payload = request.body sig_header = request.META.get('HTTP_STRIPE_SIGNATURE') try: event = stripe.Webhook.construct_event( payload, sig_header, settings.STRIPE_WEBHOOK_SECRET ) except ValueError: return HttpResponse(status=400) except stripe.error.SignatureVerificationError: return HttpResponse(status=400) # ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° событий if event['type'] == 'invoice.payment_succeeded': handle_payment_succeeded(event['data']['object']) elif event['type'] == 'customer.subscription.deleted': handle_subscription_cancelled(event['data']['object']) elif event['type'] == 'customer.subscription.updated': handle_subscription_updated(event['data']['object']) return HttpResponse(status=200) def handle_payment_succeeded(invoice): """ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎΠ³ΠΎ ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ°""" subscription_id = invoice['subscription'] customer_id = invoice['customer'] amount = invoice['amount_paid'] / 100 # Stripe Π² Ρ†Π΅Π½Ρ‚Π°Ρ… try: user_subscription = UserSubscription.objects.get( stripe_subscription_id=subscription_id ) # ΠžΠ±Π½ΠΎΠ²ΠΈΡ‚ΡŒ срок подписки user_subscription.status = 'active' user_subscription.save() # Π—Π°ΠΏΠΈΡΠ°Ρ‚ΡŒ ΠΏΠ»Π°Ρ‚Π΅ΠΆ Π² ΠΈΡΡ‚ΠΎΡ€ΠΈΡŽ PaymentHistory.objects.create( subscription=user_subscription, amount=amount, stripe_payment_id=invoice['payment_intent'], status='succeeded' ) except UserSubscription.DoesNotExist: pass # Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ошибки ``` ### πŸ“± Frontend ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Ρ‹ #### 1. ΠšΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ управлСния подпиской ```typescript // frontend/src/app/components/SubscriptionManager.tsx import React, { useState, useEffect } from 'react'; import { useLocale } from '../contexts/LocaleContext'; interface SubscriptionStats { plan: string; collections: { current: number; limit: number; unlimited: boolean }; groups: { current: number; limit: number; unlimited: boolean }; links: { current: number; limit: number; unlimited: boolean }; features: { analytics: boolean; custom_domain: boolean; api_access: boolean; }; } const SubscriptionManager: React.FC = () => { const { t } = useLocale(); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { loadSubscriptionStats(); }, []); const loadSubscriptionStats = async () => { try { const token = localStorage.getItem('token'); const response = await fetch('/api/subscriptions/usage/', { headers: { 'Authorization': `Bearer ${token}` } }); const data = await response.json(); setStats(data); } catch (error) { console.error('Error loading subscription stats:', error); } finally { setLoading(false); } }; const getProgressColor = (current: number, limit: number, unlimited: boolean) => { if (unlimited) return 'success'; const percentage = (current / limit) * 100; if (percentage >= 90) return 'danger'; if (percentage >= 75) return 'warning'; return 'primary'; }; if (loading || !stats) { return
; } return (
{t('subscription.currentPlan')}
{stats.plan.toUpperCase()}
{/* Usage Stats */}
{stats.collections.current} / {stats.collections.unlimited ? '∞' : stats.collections.limit}
{stats.groups.current} / {stats.groups.unlimited ? '∞' : stats.groups.limit}
{stats.links.current} / {stats.links.unlimited ? '∞' : stats.links.limit}
{/* Features */}
{t('subscription.features')}
{t('subscription.analytics')} {t('subscription.customDomain')} {t('subscription.apiAccess')}
{/* Upgrade Button */} {stats.plan === 'free' && (
)}
); }; export default SubscriptionManager; ``` #### 2. ΠšΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚ Π°ΠΏΠ³Ρ€Π΅ΠΉΠ΄Π° ```typescript // frontend/src/app/components/UpgradeModal.tsx import React, { useState } from 'react'; import { loadStripe } from '@stripe/stripe-js'; import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js'; const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!); interface Plan { name: string; display_name: string; price_monthly: number; price_yearly: number; features: string[]; } interface UpgradeModalProps { isOpen: boolean; onClose: () => void; } const CheckoutForm: React.FC<{ plan: Plan; onSuccess: () => void }> = ({ plan, onSuccess }) => { const stripe = useStripe(); const elements = useElements(); const [processing, setProcessing] = useState(false); const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); if (!stripe || !elements) return; setProcessing(true); const cardElement = elements.getElement(CardElement); if (!cardElement) return; const { error, paymentMethod } = await stripe.createPaymentMethod({ type: 'card', card: cardElement, }); if (error) { console.error(error); setProcessing(false); return; } // Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ подписку Ρ‡Π΅Ρ€Π΅Π· API try { const token = localStorage.getItem('token'); const response = await fetch('/api/subscriptions/create/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ plan_name: plan.name, payment_method_id: paymentMethod.id }) }); if (response.ok) { onSuccess(); } } catch (error) { console.error('Subscription creation failed:', error); } finally { setProcessing(false); } }; return (
); }; export const UpgradeModal: React.FC = ({ isOpen, onClose }) => { const [selectedPlan, setSelectedPlan] = useState(null); const plans: Plan[] = [ { name: 'premium', display_name: 'Premium', price_monthly: 5, price_yearly: 50, features: ['Unlimited collections', 'Advanced analytics', 'Custom themes'] }, { name: 'business', display_name: 'Business', price_monthly: 15, price_yearly: 150, features: ['Everything in Premium', 'Team collaboration', 'Custom domain', 'API access'] } ]; if (!isOpen) return null; return (
Upgrade Your Plan
{!selectedPlan ? (
{plans.map((plan) => (

{plan.display_name}

${plan.price_monthly} /month
    {plan.features.map((feature, index) => (
  • {feature}
  • ))}
))}
) : ( { onClose(); window.location.reload(); // ΠžΠ±Π½ΠΎΠ²ΠΈΡ‚ΡŒ страницу послС ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎΠΉ подписки }} /> )}
); }; export default UpgradeModal; ``` ### πŸ—ƒοΈ ΠœΠΈΠ³Ρ€Π°Ρ†ΠΈΠΈ Π±Π°Π·Ρ‹ Π΄Π°Π½Π½Ρ‹Ρ… ```sql -- Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΠ»Π°Π½ΠΎΠ² подписок INSERT INTO subscription_plans (name, display_name, price_monthly, price_yearly, description, max_collections, max_groups, max_links, analytics_enabled, custom_domain_enabled, api_access_enabled) VALUES ('free', 'Free', 0, 0, 'Basic features for personal use', 1, 10, 50, false, false, false), ('premium', 'Premium', 5, 50, 'Advanced features for creators', 5, -1, -1, true, false, false), ('business', 'Business', 15, 150, 'Professional features for teams', -1, -1, -1, true, true, true); -- Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ Π΄Π΅Ρ„ΠΎΠ»Ρ‚Π½Ρ‹Ρ… подписок для ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΡ… ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ INSERT INTO user_subscriptions (user_id, plan_id, status, starts_at, expires_at) SELECT u.id, (SELECT id FROM subscription_plans WHERE name = 'free'), 'active', NOW(), '2099-12-31 23:59:59' FROM auth_user u LEFT JOIN user_subscriptions us ON u.id = us.user_id WHERE us.id IS NULL; -- Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ Π΄Π΅Ρ„ΠΎΠ»Ρ‚Π½Ρ‹Ρ… ΠΊΠΎΠ»Π»Π΅ΠΊΡ†ΠΈΠΉ для ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΡ… ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ INSERT INTO link_collections (user_id, name, slug, is_default, access_type, created_at, updated_at) SELECT u.id, 'Main Collection', 'main', true, 'public', NOW(), NOW() FROM auth_user u LEFT JOIN link_collections lc ON u.id = lc.user_id WHERE lc.id IS NULL; -- ΠŸΡ€ΠΈΠ²ΡΠ·ΠΊΠ° ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΡ… Π³Ρ€ΡƒΠΏΠΏ ΠΈ ссылок ΠΊ Π΄Π΅Ρ„ΠΎΠ»Ρ‚Π½Ρ‹ΠΌ коллСкциям UPDATE link_groups lg SET collection_id = ( SELECT lc.id FROM link_collections lc WHERE lc.user_id = lg.user_id AND lc.is_default = true ) WHERE lg.collection_id IS NULL; UPDATE links l SET collection_id = ( SELECT lc.id FROM link_collections lc WHERE lc.user_id = l.user_id AND lc.is_default = true ) WHERE l.collection_id IS NULL; ``` Π­Ρ‚ΠΎΡ‚ ΠΏΠ»Π°Π½ обСспСчиваСт ΠΏΠΎΠ»Π½ΡƒΡŽ основу для ΠΏΡ€Π΅ΠΌΠΈΡƒΠΌ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»Π° с ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΎΠΉ Π»ΠΈΠΌΠΈΡ‚ΠΎΠ², ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠ΅ΠΉ Stripe ΠΈ соврСмСнным React интСрфСйсом.