)
}
return <>{children}>
}
```
### Step 4: Use in Components
#### Option A: With Custom Feature Gate
```typescript
// In your component
import { MyFeatureGate } from '@/lib/features/my-feature'
export default function MyPage() {
return (
{/* This only renders if feature is active */}
Feature Content
)
}
```
#### Option B: With Generic FeatureGate
```typescript
import { FeatureGate } from '@/components/features/FeatureGate'
export default function MyPage() {
return (
Feature Content
)
}
```
#### Option C: Conditional Rendering with Hook
```typescript
import { useMyFeature } from '@/lib/features/my-feature'
export default function MyComponent() {
const { canUseMyFeature, loading } = useMyFeature()
if (loading) return
if (!canUseMyFeature) return null
return
Feature Content
}
```
### Step 5: Stripe Product erstellen
**WICHTIG:** Jedes Addon braucht ein Stripe Product, damit bei Studio-Erstellung keine neuen Produkte erstellt werden!
#### A) Stripe Product anlegen
```javascript
// Via Node.js Script oder Stripe Dashboard
require('dotenv').config({ path: '.env.local' });
const Stripe = require('stripe');
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const product = await stripe.products.create({
name: 'Bookicorn My Feature Name', // Prefix "Bookicorn " für Konsistenz
metadata: {
feature_code: 'my_feature_name', // Muss mit DB code übereinstimmen!
type: 'addon'
}
});
console.log('Product ID:', product.id); // z.B. prod_TnXXXXXXXXXX
```
**Oder via Stripe Dashboard:**
1. Dashboard → Products → Add Product
2. Name: `Bookicorn [Feature Name]`
3. Metadata hinzufügen: `feature_code` = `my_feature_name`, `type` = `addon`
### Step 6: Database Setup
#### A) Register Feature Definition (mit Stripe Product ID!)
```sql
INSERT INTO feature_definitions (
code,
name,
description,
category,
addon_price_monthly,
addon_price_yearly,
is_active,
metadata
) VALUES (
'my_feature_name', -- Must match FEATURE_CODE
'My Feature Name',
'Description of what this feature does',
'content', -- Category: core, content, marketing, etc.
9.99, -- Monthly price (if sold as addon)
99.99, -- Yearly price
true,
'{"status": "available", "stripe_product_id": "prod_TnXXXXXXXXXX"}'::jsonb
-- ↑ WICHTIG: Stripe Product ID hier eintragen!
);
```
#### Alternative: Bestehendes Feature updaten
```sql
UPDATE feature_definitions
SET metadata = metadata || '{"stripe_product_id": "prod_TnXXXXXXXXXX"}'::jsonb
WHERE code = 'my_feature_name';
```
#### B) Add to Subscription Plan (Optional)
```sql
-- Include feature in a plan
UPDATE subscription_plans
SET included_features = included_features || ARRAY['my_feature_name']
WHERE code = 'professional';
```
#### C) Or Create as Addon
```sql
-- Studio can buy as addon
INSERT INTO studio_feature_addons (
studio_id,
feature_id,
status,
billing_cycle,
price_override
) VALUES (
'studio-uuid',
(SELECT id FROM feature_definitions WHERE code = 'my_feature_name'),
'active',
'monthly',
NULL
);
```
## 📁 File Structure
```
src/
├── lib/
│ └── features/
│ ├── feature-context.tsx # Admin/Studio Feature Provider
│ ├── member-feature-context.tsx # Member Dashboard Feature Provider (NEW)
│ ├── my-feature.tsx # Your new feature
│ ├── chat-feature.tsx # Example: Chat (Admin)
│ ├── blog-feature.ts # Example: Blog
│ └── checkin-feature.tsx # Example: Check-in
├── components/
│ └── features/
│ └── FeatureGate.tsx # Generic Feature Gate
└── app/
└── admin/
└── my-feature/ # Admin pages for feature
└── page.tsx
```
## 👤 Member Dashboard Feature Gates
**WICHTIG:** Das Member Dashboard hat ein SEPARATES Feature System (`MemberFeatureContext`), weil:
- Ein Kunde kann bei MEHREREN Studios Mitglied sein
- Features werden über ALLE Studios aggregiert
- Feature ist aktiv wenn MINDESTENS EIN Studio es hat
### MemberFeatureContext vs FeatureContext
| Aspekt | FeatureContext (Admin) | MemberFeatureContext (Member) |
|--------|------------------------|-------------------------------|
| Scope | Einzelnes Studio | Alle Studios des Users |
| Provider | `FeatureProvider` | `MemberFeatureProvider` |
| Hook | `useFeatures()` | `useMemberFeatures()` |
| Logik | Studio hat Feature? | Irgendein Studio hat Feature? |
### Member Feature Hook erstellen
```typescript
// src/lib/features/member-feature-context.tsx enthält:
// 1. Feature Codes Definition
export const MEMBER_FEATURE_CODES = {
CHAT: 'chat_messaging',
CHECKIN: 'checkin_system',
// Neues Feature hier hinzufügen
MY_FEATURE: 'my_feature_code',
} as const
// 2. Convenience Hooks existieren bereits:
export function useMemberChatFeature() { ... }
export function useMemberCheckinFeature() { ... }
// 3. Neuen Convenience Hook hinzufügen:
export function useMemberMyFeature() {
const { hasFeature, hasFeatureInStudio, getStudiosWithFeature, loading } = useMemberFeatures()
const featureCode = MEMBER_FEATURE_CODES.MY_FEATURE
return {
isMyFeatureEnabled: hasFeature(featureCode),
hasMyFeatureInStudio: (studioId: string) => hasFeatureInStudio(featureCode, studioId),
studiosWithMyFeature: getStudiosWithFeature(featureCode),
loading,
featureCode
}
}
```
### Member Feature Gate erstellen
```typescript
// In member-feature-context.tsx oder eigene Datei
export function MemberMyFeatureGate({ children }: { children: React.ReactNode }) {
return (
{children}
)
}
```
### Verwendung im Member Dashboard
```typescript
// src/app/dashboard/page.tsx oder Member-Komponenten
import { useMemberMyFeature, MEMBER_FEATURE_CODES } from '@/lib/features/member-feature-context'
export default function MemberDashboard() {
// Option A: Mit spezifischem Hook
const { isMyFeatureEnabled } = useMemberMyFeature()
// Option B: Mit generischem Hook
const { hasFeature } = useMemberFeatures()
const hasMyFeature = hasFeature(MEMBER_FEATURE_CODES.MY_FEATURE)
// Option C: Prüfen für spezifisches Studio
const { hasFeatureInStudio } = useMemberFeatures()
const studioHasFeature = hasFeatureInStudio('my_feature_code', studioId)
return (
<>
{/* Bedingt rendern */}
{isMyFeatureEnabled && (
)}
{/* Oder mit Gate Component */}
>
)
}
```
### Navigation Items bedingt anzeigen
```typescript
// src/components/member/shared/MemberNavigation.tsx
export function MemberSidebar({ ... }: MemberNavigationProps) {
// Feature von Props oder aus Context
const hasChatAddon = props.hasChatAddon // Vom Dashboard durchgereicht
return (
)
}
```
### Wichtig: Studios mit MemberFeatureContext synchronisieren
```typescript
// src/components/member/hooks/useMemberData.ts
export function useMemberData({ userId }: UseMemberDataProps) {
// Context für Feature Sync holen
const { setStudios } = useMemberFeatures()
const loadDashboardData = async () => {
// ... Studios laden ...
const allStudios = Array.from(allStudiosMap.values())
setMyStudios(allStudios)
// WICHTIG: Studios mit MemberFeatureContext synchronisieren
setStudios(allStudios.map((s: any) => ({ id: s.id, name: s.name })))
}
}
```
## 🎨 Complete Example: Video-on-Demand Feature
```typescript
// src/lib/features/vod-feature.tsx
'use client'
import React from 'react'
import { useFeatures } from './feature-context'
import { activeTheme } from '@/config/theme'
import { H3 } from '@/components/ui/Typography'
// 1. Define Feature Code
export const VOD_FEATURE_CODE = 'video_on_demand'
// 2. Custom Hook
export function useVodFeature() {
const { hasFeature, canUse, loading } = useFeatures()
return {
isVodEnabled: hasFeature(VOD_FEATURE_CODE),
canUseVod: canUse(VOD_FEATURE_CODE),
loading: loading,
featureCode: VOD_FEATURE_CODE
}
}
// 3. User Feature Gate (simple)
export function VodFeatureGate({ children }: { children: React.ReactNode }) {
const { canUseVod, loading } = useVodFeature()
if (loading) return null
if (!canUseVod) return null
return <>{children}>
}
// 4. Admin Feature Gate (with upgrade prompt)
export function AdminVodGate({
children,
fallback
}: {
children: React.ReactNode
fallback?: React.ReactNode
}) {
const { isVodEnabled, loading } = useVodFeature()
if (loading) {
return <>{children}>
}
if (!isVodEnabled && fallback) {
return <>{fallback}>
}
if (!isVodEnabled) {
return (
Video-on-Demand nicht aktiviert
Das VOD Feature ist in Ihrem aktuellen Tarif nicht enthalten.