@fayz-ai/plugin-courses 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"CoursesListPage.d.ts","sourceRoot":"","sources":["../../src/pages/CoursesListPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAY9B,wBAAgB,eAAe,sBA2F9B;yBA3Fe,eAAe"}
1
+ {"version":3,"file":"CoursesListPage.d.ts","sourceRoot":"","sources":["../../src/pages/CoursesListPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAa9B,wBAAgB,eAAe,sBAuG9B;yBAvGe,eAAe"}
@@ -1 +1 @@
1
- {"version":3,"file":"FinancialPage.d.ts","sourceRoot":"","sources":["../../src/pages/FinancialPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAO9B,wBAAgB,aAAa,sBAmE5B;yBAnEe,aAAa"}
1
+ {"version":3,"file":"FinancialPage.d.ts","sourceRoot":"","sources":["../../src/pages/FinancialPage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAY9B,wBAAgB,aAAa,sBAmE5B;yBAnEe,aAAa"}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "fayz": {
4
4
  "status": "preview"
5
5
  },
6
- "version": "0.8.0",
6
+ "version": "0.8.1",
7
7
  "description": "[experimental] Fayz SDK — courses admin management plugin",
8
8
  "type": "module",
9
9
  "main": "./dist/index.cjs",
@@ -28,10 +28,10 @@
28
28
  "dependencies": {
29
29
  "lucide-react": "^0.400.0",
30
30
  "@tanstack/react-table": "^8.20.0",
31
- "@fayz-ai/core": "^0.8.0",
32
- "@fayz-ai/ui": "^0.8.0",
33
- "@fayz-ai/courses": "^0.8.0",
34
- "@fayz-ai/saas": "^0.8.0"
31
+ "@fayz-ai/saas": "^0.8.2",
32
+ "@fayz-ai/core": "^0.8.2",
33
+ "@fayz-ai/ui": "^0.8.1",
34
+ "@fayz-ai/courses": "^0.8.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/react": "^18.3.0",
package/src/index.ts CHANGED
@@ -33,6 +33,14 @@ export interface CoursesPluginOptions {
33
33
  navPosition?: number
34
34
  navSection?: 'main' | 'secondary' | 'settings'
35
35
  navLabel?: string
36
+ /** Nav labels for the module entries (default English). */
37
+ labels?: {
38
+ membersArea?: string
39
+ sales?: string
40
+ subscriptions?: string
41
+ financial?: string
42
+ reports?: string
43
+ }
36
44
  scope?: PluginScope
37
45
  verticalId?: VerticalId
38
46
  /**
@@ -74,13 +82,14 @@ export function createCoursesPlugin(options?: CoursesPluginOptions): PluginManif
74
82
 
75
83
  // Opt-in nav/routes per module. Position offsets stay stable relative to the
76
84
  // enabled set so hosts control exactly where the block sits.
85
+ const labels = options?.labels ?? {}
77
86
  const navigation = [
78
87
  { section, position: base, label: options?.navLabel ?? 'Courses', route: '/courses', icon: 'BookOpen', permission: read('courses') },
79
- ...(modules.membersArea ? [{ section, position: base + 1, label: 'Members area', route: '/courses/members', icon: 'GraduationCap', permission: read('courses') }] : []),
80
- ...(modules.sales ? [{ section, position: base + 2, label: 'Sales', route: '/courses/sales', icon: 'TrendingUp', permission: read('courses.sales') }] : []),
81
- ...(modules.subscriptions ? [{ section, position: base + 3, label: 'Subscriptions', route: '/courses/subscriptions', icon: 'Repeat', permission: read('courses.sales') }] : []),
82
- ...(modules.financial ? [{ section, position: base + 4, label: 'Financial', route: '/courses/financial', icon: 'CreditCard', permission: read('courses.finance') }] : []),
83
- ...(modules.reports ? [{ section, position: base + 5, label: 'Reports', route: '/courses/reports', icon: 'BarChart3', permission: read('courses.sales') }] : []),
88
+ ...(modules.membersArea ? [{ section, position: base + 1, label: labels.membersArea ?? 'Members area', route: '/courses/members', icon: 'GraduationCap', permission: read('courses') }] : []),
89
+ ...(modules.sales ? [{ section, position: base + 2, label: labels.sales ?? 'Sales', route: '/courses/sales', icon: 'TrendingUp', permission: read('courses.sales') }] : []),
90
+ ...(modules.subscriptions ? [{ section, position: base + 3, label: labels.subscriptions ?? 'Subscriptions', route: '/courses/subscriptions', icon: 'Repeat', permission: read('courses.sales') }] : []),
91
+ ...(modules.financial ? [{ section, position: base + 4, label: labels.financial ?? 'Financial', route: '/courses/financial', icon: 'CreditCard', permission: read('courses.finance') }] : []),
92
+ ...(modules.reports ? [{ section, position: base + 5, label: labels.reports ?? 'Reports', route: '/courses/reports', icon: 'BarChart3', permission: read('courses.sales') }] : []),
84
93
  ]
85
94
 
86
95
  const routes = [
@@ -113,6 +122,9 @@ export function createCoursesPlugin(options?: CoursesPluginOptions): PluginManif
113
122
  { id: 'courses.sales', label: 'Sales', group: 'Courses' },
114
123
  { id: 'courses.finance', label: 'Financial', group: 'Courses' },
115
124
  ],
125
+ declaredLimits: [
126
+ { key: 'courses', label: 'Courses', table: 'plg_courses_courses' },
127
+ ],
116
128
  navigation,
117
129
  routes,
118
130
  widgets: [],
@@ -2,6 +2,7 @@ import * as React from 'react'
2
2
  import { Button, Badge, PageHeaderActions, useModuleLayout } from '@fayz-ai/ui'
3
3
  import { useTranslation } from '@fayz-ai/core'
4
4
  import { getCoursesProvider, type Course } from '@fayz-ai/courses'
5
+ import { useLimitGuard, invalidateLimit, PermissionGate, usePermissionOptional } from '@fayz-ai/saas'
5
6
  import { navigateTo } from '../nav'
6
7
 
7
8
  function statusVariant(status: Course['status']): 'success' | 'secondary' | 'outline' {
@@ -17,6 +18,8 @@ export function CoursesListPage() {
17
18
  const shellOwnsHeader = useModuleLayout() === 'tabs'
18
19
  const [courses, setCourses] = React.useState<Course[] | null>(null)
19
20
  const [creating, setCreating] = React.useState(false)
21
+ const guardCourses = useLimitGuard('courses')
22
+ const can = usePermissionOptional()
20
23
 
21
24
  const load = React.useCallback(async () => {
22
25
  setCourses(await getCoursesProvider().listCourses())
@@ -27,12 +30,18 @@ export function CoursesListPage() {
27
30
  }, [load])
28
31
 
29
32
  async function onCreate() {
33
+ // Defense in depth: never create without the role's `create` action.
34
+ if (!can('courses', 'create')) return
35
+ // Plan quantity guard (client-side, before the provider call). Opens the
36
+ // global UpgradeModal and aborts when the plan's course cap is reached.
37
+ if ((await guardCourses()) === 'blocked') return
30
38
  setCreating(true)
31
39
  try {
32
40
  const course = await getCoursesProvider().createCourse({
33
41
  title: t('courses.editor.untitledCourse') || 'Untitled course',
34
42
  status: 'draft',
35
43
  })
44
+ invalidateLimit('courses')
36
45
  navigateTo(`/courses/${course.id}`)
37
46
  } finally {
38
47
  setCreating(false)
@@ -43,9 +52,11 @@ export function CoursesListPage() {
43
52
  <div className="mx-auto max-w-6xl p-6 md:p-8">
44
53
  {shellOwnsHeader ? (
45
54
  <PageHeaderActions>
46
- <Button onClick={onCreate} disabled={creating}>
47
- {t('courses.new') || 'New course'}
48
- </Button>
55
+ <PermissionGate feature="courses" action="create">
56
+ <Button onClick={onCreate} disabled={creating}>
57
+ {t('courses.new') || 'New course'}
58
+ </Button>
59
+ </PermissionGate>
49
60
  </PageHeaderActions>
50
61
  ) : (
51
62
  <div className="mb-6 flex items-center justify-between">
@@ -57,9 +68,11 @@ export function CoursesListPage() {
57
68
  </p>
58
69
  )}
59
70
  </div>
60
- <Button onClick={onCreate} disabled={creating}>
61
- {t('courses.new') || 'New course'}
62
- </Button>
71
+ <PermissionGate feature="courses" action="create">
72
+ <Button onClick={onCreate} disabled={creating}>
73
+ {t('courses.new') || 'New course'}
74
+ </Button>
75
+ </PermissionGate>
63
76
  </div>
64
77
  )}
65
78
 
@@ -5,6 +5,11 @@ import { getCoursesProvider, type Payout, type FinancialSummary, type CreatorAcc
5
5
  import { PageContainer, StatCard, StatGrid, SimpleTable, type SimpleColumn } from '../components/CommerceUI'
6
6
  import { formatMoney, formatDate, formatPercent } from '../lib/format'
7
7
 
8
+ // No plan gate here on purpose. The page is registered as a shell ROUTE with
9
+ // `permission: read('courses.finance')`, so the route guard already resolves
10
+ // role AND plan for it — and whether `courses.finance` is a paid feature is the
11
+ // APP's call (its billing.ts), never the plugin's. Hardcoding a gate here would
12
+ // bake one product's packaging into the SDK.
8
13
  export function FinancialPage() {
9
14
  const t = useTranslation()
10
15
  const [summary, setSummary] = React.useState<FinancialSummary | null>(null)