@fayz-ai/plugin-courses 0.9.0 → 0.10.0

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.
Files changed (79) hide show
  1. package/README.md +3 -3
  2. package/dist/components/CommerceUI.d.ts +19 -2
  3. package/dist/components/CommerceUI.d.ts.map +1 -1
  4. package/dist/components/dashboardWidgets.d.ts.map +1 -0
  5. package/dist/index.d.ts +26 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +773 -309
  8. package/dist/index.js.map +1 -1
  9. package/dist/integrations/kiwify/connectorDef.d.ts +34 -0
  10. package/dist/integrations/kiwify/connectorDef.d.ts.map +1 -0
  11. package/dist/integrations/kiwify/data.d.ts +58 -0
  12. package/dist/integrations/kiwify/data.d.ts.map +1 -0
  13. package/dist/lib/nav.d.ts.map +1 -0
  14. package/dist/locales/en.d.ts.map +1 -1
  15. package/dist/locales/pt-BR.d.ts.map +1 -1
  16. package/dist/public/createPublicCheckoutPlugin.d.ts +1 -1
  17. package/dist/public/createPublicCheckoutPlugin.d.ts.map +1 -1
  18. package/dist/public/{data.d.ts → data/index.d.ts} +1 -1
  19. package/dist/public/data/index.d.ts.map +1 -0
  20. package/dist/public/{data.supabase.d.ts → data/supabase.d.ts} +2 -2
  21. package/dist/public/data/supabase.d.ts.map +1 -0
  22. package/dist/public/index.d.ts +4 -4
  23. package/dist/public/index.d.ts.map +1 -1
  24. package/dist/public/index.js +353 -0
  25. package/dist/public/index.js.map +1 -0
  26. package/dist/public/{CheckoutScreen.d.ts → views/CheckoutScreen.d.ts} +1 -1
  27. package/dist/public/views/CheckoutScreen.d.ts.map +1 -0
  28. package/dist/views/CourseEditorPage.d.ts.map +1 -0
  29. package/dist/views/CourseOrderDetailView.d.ts +4 -3
  30. package/dist/views/CourseOrderDetailView.d.ts.map +1 -1
  31. package/dist/{pages → views}/CoursesListPage.d.ts.map +1 -1
  32. package/dist/views/FinancialPage.d.ts.map +1 -0
  33. package/dist/{pages → views}/ReportsPage.d.ts.map +1 -1
  34. package/dist/views/SalesPage.d.ts +19 -0
  35. package/dist/views/SalesPage.d.ts.map +1 -0
  36. package/dist/views/SubscriptionsPage.d.ts +7 -0
  37. package/dist/views/SubscriptionsPage.d.ts.map +1 -0
  38. package/package.json +12 -12
  39. package/dist/connectors/stripe.d.ts +0 -3
  40. package/dist/connectors/stripe.d.ts.map +0 -1
  41. package/dist/dashboardWidgets.d.ts.map +0 -1
  42. package/dist/nav.d.ts.map +0 -1
  43. package/dist/pages/CourseEditorPage.d.ts.map +0 -1
  44. package/dist/pages/FinancialPage.d.ts.map +0 -1
  45. package/dist/pages/SalesPage.d.ts +0 -6
  46. package/dist/pages/SalesPage.d.ts.map +0 -1
  47. package/dist/pages/SubscriptionsPage.d.ts +0 -6
  48. package/dist/pages/SubscriptionsPage.d.ts.map +0 -1
  49. package/dist/public/CheckoutScreen.d.ts.map +0 -1
  50. package/dist/public/data.d.ts.map +0 -1
  51. package/dist/public/data.supabase.d.ts.map +0 -1
  52. package/src/components/CommerceUI.tsx +0 -153
  53. package/src/connectors/stripe.ts +0 -34
  54. package/src/dashboardWidgets.tsx +0 -64
  55. package/src/index.ts +0 -150
  56. package/src/lib/format.ts +0 -22
  57. package/src/locales/en.ts +0 -203
  58. package/src/locales/index.ts +0 -7
  59. package/src/locales/pt-BR.ts +0 -203
  60. package/src/nav.ts +0 -9
  61. package/src/pages/CourseEditorPage.tsx +0 -1122
  62. package/src/pages/CoursesListPage.tsx +0 -137
  63. package/src/pages/FinancialPage.tsx +0 -81
  64. package/src/pages/ReportsPage.tsx +0 -59
  65. package/src/pages/SalesPage.tsx +0 -86
  66. package/src/pages/SubscriptionsPage.tsx +0 -56
  67. package/src/public/CheckoutScreen.tsx +0 -279
  68. package/src/public/createPublicCheckoutPlugin.tsx +0 -88
  69. package/src/public/data.supabase.ts +0 -63
  70. package/src/public/data.ts +0 -89
  71. package/src/public/index.ts +0 -26
  72. package/src/settings/CoursesSettings.tsx +0 -55
  73. package/src/views/CourseOrderDetailView.tsx +0 -132
  74. /package/dist/{dashboardWidgets.d.ts → components/dashboardWidgets.d.ts} +0 -0
  75. /package/dist/{nav.d.ts → lib/nav.d.ts} +0 -0
  76. /package/dist/{pages → views}/CourseEditorPage.d.ts +0 -0
  77. /package/dist/{pages → views}/CoursesListPage.d.ts +0 -0
  78. /package/dist/{pages → views}/FinancialPage.d.ts +0 -0
  79. /package/dist/{pages → views}/ReportsPage.d.ts +0 -0
@@ -1,279 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // CheckoutScreen — the public "comprar curso" surface.
3
- //
4
- // Loads a published course + its default offer by slug, collects name/email/
5
- // payment method, and records a (simulated-paid) sale via the data provider.
6
- // Dependency-light: React + lucide + Tailwind only — no @fayz-ai/saas.
7
- // ---------------------------------------------------------------------------
8
-
9
- import { useEffect, useMemo, useState, type FC, type FormEvent } from 'react'
10
- import { CheckCircle2, CreditCard, Loader2, Lock, QrCode, ShieldCheck } from 'lucide-react'
11
- import type { Course, Offer } from '@fayz-ai/courses'
12
- import type { CheckoutPaymentMethod, PublicCheckoutDataProvider } from './data'
13
-
14
- export interface CheckoutScreenProps {
15
- slug: string
16
- provider: PublicCheckoutDataProvider
17
- /** Accent color (CSS) for buttons/highlights. Default emerald. */
18
- accent?: string
19
- /** Where "acessar área de membros" points after success. Default '#/'. */
20
- memberAreaHref?: string
21
- /** Fired after a successful order with the resulting order id + email. */
22
- onSuccess?: (info: { orderId: string; email: string }) => void
23
- }
24
-
25
- function formatMoney(value: number, currency = 'BRL', locale = 'pt-BR'): string {
26
- try {
27
- return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(value)
28
- } catch {
29
- return `R$ ${value.toFixed(2)}`
30
- }
31
- }
32
-
33
- export const CheckoutScreen: FC<CheckoutScreenProps> = ({
34
- slug,
35
- provider,
36
- accent = '#10b981',
37
- memberAreaHref = '#/',
38
- onSuccess,
39
- }) => {
40
- const [course, setCourse] = useState<Course | null>(null)
41
- const [offer, setOffer] = useState<Offer | null>(null)
42
- const [loading, setLoading] = useState(true)
43
- const [loadError, setLoadError] = useState<string | null>(null)
44
-
45
- const [name, setName] = useState('')
46
- const [email, setEmail] = useState('')
47
- const [method, setMethod] = useState<CheckoutPaymentMethod>('pix')
48
-
49
- const [submitting, setSubmitting] = useState(false)
50
- const [submitError, setSubmitError] = useState<string | null>(null)
51
- const [orderId, setOrderId] = useState<string | null>(null)
52
-
53
- useEffect(() => {
54
- let cancelled = false
55
- setLoading(true)
56
- setLoadError(null)
57
- ;(async () => {
58
- try {
59
- const c = await provider.getCourseBySlug(slug)
60
- if (!c) throw new Error('Curso não encontrado.')
61
- const o = await provider.getDefaultOffer(c.id)
62
- if (cancelled) return
63
- setCourse(c)
64
- setOffer(o)
65
- } catch (err) {
66
- if (!cancelled) setLoadError(err instanceof Error ? err.message : 'Falha ao carregar o curso.')
67
- } finally {
68
- if (!cancelled) setLoading(false)
69
- }
70
- })()
71
- return () => {
72
- cancelled = true
73
- }
74
- }, [slug, provider])
75
-
76
- const price = offer?.price ?? course?.price ?? 0
77
- const currency = offer?.currency ?? course?.currency ?? 'BRL'
78
- const isSubscription = offer?.kind === 'subscription'
79
- const intervalLabel = offer?.recurringInterval === 'year' ? '/ano' : '/mês'
80
-
81
- const canSubmit = useMemo(
82
- () => !!offer && !!email.trim() && /.+@.+\..+/.test(email.trim()) && !submitting,
83
- [offer, email, submitting],
84
- )
85
-
86
- async function handleSubmit(e: FormEvent) {
87
- e.preventDefault()
88
- if (!offer || !canSubmit) return
89
- setSubmitting(true)
90
- setSubmitError(null)
91
- try {
92
- const { orderId: id } = await provider.placeOrder(offer.id, {
93
- email: email.trim(),
94
- name: name.trim() || undefined,
95
- paymentMethod: method,
96
- })
97
- setOrderId(id)
98
- onSuccess?.({ orderId: id, email: email.trim() })
99
- } catch (err) {
100
- setSubmitError(err instanceof Error ? err.message : 'Não foi possível concluir a compra. Tente novamente.')
101
- } finally {
102
- setSubmitting(false)
103
- }
104
- }
105
-
106
- if (loading) {
107
- return (
108
- <div className="min-h-screen flex items-center justify-center bg-gray-50">
109
- <Loader2 className="w-6 h-6 animate-spin text-gray-400" />
110
- </div>
111
- )
112
- }
113
-
114
- if (loadError || !course) {
115
- return (
116
- <div className="min-h-screen flex items-center justify-center bg-gray-50 p-6">
117
- <div className="max-w-md text-center">
118
- <p className="text-lg font-semibold text-gray-900">Ops!</p>
119
- <p className="mt-2 text-gray-600">{loadError ?? 'Curso indisponível.'}</p>
120
- <a href="#/" className="mt-4 inline-block text-sm font-medium underline" style={{ color: accent }}>
121
- Voltar
122
- </a>
123
- </div>
124
- </div>
125
- )
126
- }
127
-
128
- // Success state ----------------------------------------------------------
129
- if (orderId) {
130
- return (
131
- <div className="min-h-screen flex items-center justify-center bg-gray-50 p-6">
132
- <div className="max-w-md w-full bg-white rounded-2xl shadow-sm border border-gray-100 p-8 text-center">
133
- <div
134
- className="mx-auto w-14 h-14 rounded-full flex items-center justify-center"
135
- style={{ backgroundColor: `${accent}1a` }}
136
- >
137
- <CheckCircle2 className="w-8 h-8" style={{ color: accent }} />
138
- </div>
139
- <h1 className="mt-4 text-xl font-bold text-gray-900">Compra confirmada!</h1>
140
- <p className="mt-2 text-gray-600">
141
- Seu acesso a <strong>{course.title}</strong> já está liberado. Enviamos os detalhes para{' '}
142
- <strong>{email.trim()}</strong>.
143
- </p>
144
- <p className="mt-1 text-xs text-gray-400">Pedido {orderId}</p>
145
- <a
146
- href={memberAreaHref}
147
- className="mt-6 inline-flex w-full items-center justify-center rounded-xl px-4 py-3 font-semibold text-white"
148
- style={{ backgroundColor: accent }}
149
- >
150
- Acessar área de membros
151
- </a>
152
- </div>
153
- </div>
154
- )
155
- }
156
-
157
- // Checkout form ----------------------------------------------------------
158
- return (
159
- <div className="min-h-screen bg-gray-50 py-10 px-4">
160
- <div className="max-w-3xl mx-auto grid gap-6 md:grid-cols-5">
161
- {/* Order summary */}
162
- <aside className="md:col-span-2 bg-white rounded-2xl border border-gray-100 shadow-sm p-6 h-fit">
163
- {course.thumbnailUrl && (
164
- <img
165
- src={course.thumbnailUrl}
166
- alt={course.title}
167
- className="w-full aspect-video object-cover rounded-xl mb-4"
168
- />
169
- )}
170
- <h2 className="text-lg font-bold text-gray-900">{course.title}</h2>
171
- {course.subtitle && <p className="mt-1 text-sm text-gray-500">{course.subtitle}</p>}
172
- <div className="mt-4 border-t border-gray-100 pt-4">
173
- <div className="flex items-baseline justify-between">
174
- <span className="text-sm text-gray-500">{offer?.name ?? 'Acesso ao curso'}</span>
175
- <span className="text-xl font-bold text-gray-900">
176
- {formatMoney(price, currency)}
177
- {isSubscription && <span className="text-sm font-normal text-gray-400"> {intervalLabel}</span>}
178
- </span>
179
- </div>
180
- </div>
181
- <div className="mt-4 flex items-center gap-2 text-xs text-gray-400">
182
- <ShieldCheck className="w-4 h-4" />
183
- Pagamento seguro · acesso imediato
184
- </div>
185
- </aside>
186
-
187
- {/* Payment form */}
188
- <form onSubmit={handleSubmit} className="md:col-span-3 bg-white rounded-2xl border border-gray-100 shadow-sm p-6">
189
- <h1 className="text-xl font-bold text-gray-900">Finalizar compra</h1>
190
- <p className="mt-1 text-sm text-gray-500">Preencha seus dados para liberar o acesso.</p>
191
-
192
- <label className="block mt-6 text-sm font-medium text-gray-700">
193
- Nome completo
194
- <input
195
- type="text"
196
- value={name}
197
- onChange={(e) => setName(e.target.value)}
198
- placeholder="Seu nome"
199
- className="mt-1 w-full rounded-xl border border-gray-200 px-3 py-2.5 focus:outline-none focus:ring-2"
200
- style={{ ['--tw-ring-color' as string]: accent }}
201
- />
202
- </label>
203
-
204
- <label className="block mt-4 text-sm font-medium text-gray-700">
205
- E-mail *
206
- <input
207
- type="email"
208
- required
209
- value={email}
210
- onChange={(e) => setEmail(e.target.value)}
211
- placeholder="voce@email.com"
212
- className="mt-1 w-full rounded-xl border border-gray-200 px-3 py-2.5 focus:outline-none focus:ring-2"
213
- style={{ ['--tw-ring-color' as string]: accent }}
214
- />
215
- </label>
216
-
217
- <fieldset className="mt-4">
218
- <legend className="text-sm font-medium text-gray-700">Forma de pagamento</legend>
219
- <div className="mt-2 grid grid-cols-2 gap-3">
220
- {(
221
- [
222
- { id: 'pix' as const, label: 'Pix', icon: QrCode },
223
- { id: 'card' as const, label: 'Cartão', icon: CreditCard },
224
- ]
225
- ).map(({ id, label, icon: Icon }) => {
226
- const active = method === id
227
- return (
228
- <button
229
- key={id}
230
- type="button"
231
- onClick={() => setMethod(id)}
232
- className="flex items-center gap-2 rounded-xl border px-3 py-2.5 text-sm font-medium transition"
233
- style={
234
- active
235
- ? { borderColor: accent, backgroundColor: `${accent}0f`, color: accent }
236
- : { borderColor: '#e5e7eb', color: '#374151' }
237
- }
238
- >
239
- <Icon className="w-4 h-4" />
240
- {label}
241
- </button>
242
- )
243
- })}
244
- </div>
245
- </fieldset>
246
-
247
- {submitError && (
248
- <p className="mt-4 text-sm text-red-600 bg-red-50 border border-red-100 rounded-xl px-3 py-2">
249
- {submitError}
250
- </p>
251
- )}
252
-
253
- <button
254
- type="submit"
255
- disabled={!canSubmit}
256
- className="mt-6 inline-flex w-full items-center justify-center gap-2 rounded-xl px-4 py-3 font-semibold text-white disabled:opacity-50"
257
- style={{ backgroundColor: accent }}
258
- >
259
- {submitting ? (
260
- <>
261
- <Loader2 className="w-4 h-4 animate-spin" /> Processando…
262
- </>
263
- ) : (
264
- <>
265
- <Lock className="w-4 h-4" /> Pagar {formatMoney(price, currency)}
266
- {isSubscription && ` ${intervalLabel}`}
267
- </>
268
- )}
269
- </button>
270
- <p className="mt-3 text-center text-xs text-gray-400">
271
- Ambiente de demonstração — pagamento simulado, sem cobrança real.
272
- </p>
273
- </form>
274
- </div>
275
- </div>
276
- )
277
- }
278
-
279
- CheckoutScreen.displayName = 'CheckoutScreen'
@@ -1,88 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // createPublicCheckoutPlugin — the public "landing → checkout" seam for courses.
3
- //
4
- // Mirrors plugin-agenda/src/public/createPublicBookingPlugin: resolve a data
5
- // provider (injected → Supabase when a tenantId is set → loud mock), expose a
6
- // ready-to-mount CheckoutScreen + a PluginManifest with a public route. A host
7
- // app (e.g. course-members) either mounts the manifest route or renders
8
- // CheckoutScreen directly in its own router.
9
- // ---------------------------------------------------------------------------
10
-
11
- import { createElement, type FC } from 'react'
12
- import type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core'
13
- import { createMockCheckoutProvider, type PublicCheckoutDataProvider } from './data'
14
- import { createSupabaseCheckoutProvider } from './data.supabase'
15
- import { CheckoutScreen, type CheckoutScreenProps } from './CheckoutScreen'
16
-
17
- export interface PublicCheckoutOptions {
18
- /** Mount path for the public checkout route. Default '/checkout'. Slug is appended as ':slug'. */
19
- basePath?: string
20
- /**
21
- * Tenant this checkout sells for. REQUIRED when a Supabase client is
22
- * configured (setGlobalSupabaseClient) — it scopes the catalog + the RPC.
23
- */
24
- tenantId?: string
25
- /** Accent color forwarded to CheckoutScreen. */
26
- accent?: string
27
- /** Inject a custom provider (overrides the mock/Supabase resolver). */
28
- dataProvider?: PublicCheckoutDataProvider
29
- scope?: PluginScope
30
- verticalId?: VerticalId
31
- }
32
-
33
- export interface PublicCheckoutPlugin {
34
- manifest: PluginManifest
35
- dataProvider: PublicCheckoutDataProvider
36
- /** CheckoutScreen bound to the resolved provider — pass `slug` (+ optional onSuccess). */
37
- CheckoutScreen: FC<Omit<CheckoutScreenProps, 'provider'>>
38
- }
39
-
40
- export function createPublicCheckoutPlugin(options: PublicCheckoutOptions = {}): PublicCheckoutPlugin {
41
- const basePath = options.basePath ?? '/checkout'
42
-
43
- // Resolution: explicit dataProvider → Supabase whenever a tenantId is declared
44
- // (app's global client if registered, else Fayz Cloud) → seeded mock (LOUD).
45
- let provider: PublicCheckoutDataProvider
46
- if (options.dataProvider) {
47
- provider = options.dataProvider
48
- } else if (options.tenantId) {
49
- provider = createSupabaseCheckoutProvider({ tenantId: options.tenantId })
50
- } else {
51
- if (typeof console !== 'undefined') {
52
- console.warn(
53
- '[plugin-courses/public] no `tenantId` and no `dataProvider` — falling back to the MOCK checkout provider. ' +
54
- 'Pass `tenantId` to sell against the real DB, or inject a dataProvider.',
55
- )
56
- }
57
- provider = createMockCheckoutProvider()
58
- }
59
-
60
- const BoundCheckoutScreen: FC<Omit<CheckoutScreenProps, 'provider'>> = (props) =>
61
- createElement(CheckoutScreen, { ...props, provider, accent: props.accent ?? options.accent })
62
- BoundCheckoutScreen.displayName = 'BoundCheckoutScreen'
63
-
64
- // Route component for manifest-driven hosts (slug injected via route params).
65
- // course-members renders BoundCheckoutScreen directly instead.
66
- const RouteScreen: FC<unknown> = (props) => {
67
- const slug = (props as { slug?: string }).slug ?? ''
68
- return createElement(BoundCheckoutScreen, { slug, accent: options.accent })
69
- }
70
- RouteScreen.displayName = 'CourseCheckoutScreen'
71
-
72
- const manifest: PluginManifest = {
73
- id: 'course-checkout',
74
- name: 'Checkout de curso',
75
- icon: 'ShoppingCart',
76
- version: '0.1.0',
77
- scope: options.scope ?? 'universal',
78
- verticalId: options.verticalId,
79
- scaffolds: ['member', 'website', 'landing_page'],
80
- defaultEnabled: true,
81
- dependencies: [],
82
- navigation: [],
83
- routes: [{ path: `${basePath}/:slug`, component: RouteScreen, guard: 'public' }],
84
- widgets: [],
85
- }
86
-
87
- return { manifest, dataProvider: provider, CheckoutScreen: BoundCheckoutScreen }
88
- }
@@ -1,63 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Supabase implementation of PublicCheckoutDataProvider — anon-safe.
3
- //
4
- // Public checkout runs with the publishable (anon) key and (usually) no user
5
- // session, so it only touches objects designed for anon access:
6
- // - plg_courses_courses / plg_courses_offers (published catalog is anon-
7
- // readable via the storefront RLS — read here through the shared
8
- // @fayz-ai/courses Supabase provider)
9
- // - course_place_order_simulated (SECURITY DEFINER RPC: places the order and
10
- // simulates a confirmed payment — paid order + enrollment [+ subscription])
11
- // Shipped by packages/courses/migrations/0009_course_place_order_simulated.sql.
12
- // ---------------------------------------------------------------------------
13
-
14
- import { getSupabaseClientOptional } from '@fayz-ai/core'
15
- import { createSupabaseCoursesProvider } from '@fayz-ai/courses'
16
- import type { PublicCheckoutDataProvider, PlaceOrderInput } from './data'
17
-
18
- export interface SupabaseCheckoutOptions {
19
- /** Tenant this checkout sells for (RPC arg — required). */
20
- tenantId: string
21
- }
22
-
23
- interface SupabaseLikeClient {
24
- rpc(fn: string, args: Record<string, unknown>): PromiseLike<{ data: unknown; error: { message: string } | null }>
25
- }
26
-
27
- function getClient(): SupabaseLikeClient {
28
- const client = getSupabaseClientOptional()
29
- if (!client) throw new Error('No Supabase client registered — call setGlobalSupabaseClient() at app startup.')
30
- return client as SupabaseLikeClient
31
- }
32
-
33
- export function createSupabaseCheckoutProvider(options: SupabaseCheckoutOptions): PublicCheckoutDataProvider {
34
- const { tenantId } = options
35
- // Catalog reads reuse the courses provider (tenant scoping comes from the
36
- // host's setCoursesTenantResolver + storefront RLS on published rows).
37
- const courses = createSupabaseCoursesProvider()
38
-
39
- return {
40
- async getCourseBySlug(slug: string) {
41
- return courses.getCourse(slug)
42
- },
43
-
44
- async getDefaultOffer(courseId: string) {
45
- const offers = await courses.listOffers(courseId)
46
- return offers.find((o) => o.isDefault) ?? offers[0] ?? null
47
- },
48
-
49
- async placeOrder(offerId: string, input: PlaceOrderInput) {
50
- const email = input.email.trim()
51
- const { data, error } = await getClient().rpc('course_place_order_simulated', {
52
- p_tenant_id: tenantId,
53
- p_offer_id: offerId,
54
- p_customer_email: email,
55
- p_customer_name: input.name?.trim() || null,
56
- p_payment_method: input.paymentMethod ?? 'pix',
57
- })
58
- if (error) throw new Error(`[plugin-courses/public] placeOrder: ${error.message}`)
59
- const orderId = Array.isArray(data) ? (data[0] as string) : (data as string)
60
- return { orderId: String(orderId) }
61
- },
62
- }
63
- }
@@ -1,89 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Data seam for the public course checkout surface.
3
- //
4
- // Mirrors plugins/plugin-agenda/src/public/data.ts: a small provider interface
5
- // with a mock default (dev without a backend) and a Supabase implementation
6
- // (data.supabase.ts) that reads the anon-visible catalog and calls the
7
- // course_place_order_simulated RPC. Kept dependency-light on purpose — the
8
- // public surface must NOT drag in @fayz-ai/saas or the courses admin graph.
9
- // ---------------------------------------------------------------------------
10
-
11
- import type { Course, Offer } from '@fayz-ai/courses'
12
-
13
- export type CheckoutPaymentMethod = 'pix' | 'card'
14
-
15
- export interface PlaceOrderInput {
16
- email: string
17
- name?: string
18
- paymentMethod?: CheckoutPaymentMethod
19
- }
20
-
21
- export interface PlaceOrderResult {
22
- orderId: string
23
- }
24
-
25
- export interface PublicCheckoutDataProvider {
26
- /** Published course by slug (or id). Null when not found / not published. */
27
- getCourseBySlug(slug: string): Promise<Course | null>
28
- /** The offer pre-selected on the checkout (isDefault, else the first). */
29
- getDefaultOffer(courseId: string): Promise<Offer | null>
30
- /** Records the (simulated-paid) sale + enrollment. Returns the order id. */
31
- placeOrder(offerId: string, input: PlaceOrderInput): Promise<PlaceOrderResult>
32
- }
33
-
34
- export interface MockCheckoutOptions {
35
- course?: Partial<Course>
36
- offer?: Partial<Offer>
37
- }
38
-
39
- /**
40
- * Loud mock: usable for a UI-only dev run with no backend. It fabricates a
41
- * single course + offer and a fake order id so the flow renders end-to-end.
42
- */
43
- export function createMockCheckoutProvider(opts: MockCheckoutOptions = {}): PublicCheckoutDataProvider {
44
- const course: Course = {
45
- id: 'mock-course',
46
- slug: 'curso-demo',
47
- title: 'Curso Demo',
48
- subtitle: 'Um curso de demonstração',
49
- description: 'Conteúdo de exemplo para validar o fluxo de checkout.',
50
- thumbnailUrl: null,
51
- price: 197,
52
- currency: 'BRL',
53
- status: 'published',
54
- sortOrder: 0,
55
- createdAt: '2026-01-01T00:00:00.000Z',
56
- updatedAt: '2026-01-01T00:00:00.000Z',
57
- ...(opts.course as Partial<Course>),
58
- } as Course
59
- const offer: Offer = {
60
- id: 'mock-offer',
61
- courseId: course.id,
62
- name: course.title,
63
- price: course.price,
64
- currency: course.currency,
65
- kind: 'one_time',
66
- recurringInterval: null,
67
- isDefault: true,
68
- isOrderBump: false,
69
- sortOrder: 0,
70
- ...(opts.offer as Partial<Offer>),
71
- } as Offer
72
-
73
- let seq = 0
74
- return {
75
- async getCourseBySlug(slug: string) {
76
- return { ...course, slug: slug || course.slug }
77
- },
78
- async getDefaultOffer() {
79
- return offer
80
- },
81
- async placeOrder() {
82
- seq += 1
83
- if (typeof console !== 'undefined') {
84
- console.warn('[plugin-courses/public] MOCK placeOrder — no sale was recorded. Pass a tenantId for the real DB.')
85
- }
86
- return { orderId: `mock-order-${seq}` }
87
- },
88
- }
89
- }
@@ -1,26 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // @fayz-ai/plugin-courses/public — customer-facing course checkout surface.
3
- //
4
- // Separate entry from the admin plugin so a website/member host imports only the
5
- // lean checkout screen + data seam (no @fayz-ai/saas / courses admin graph).
6
- // Data: PublicCheckoutDataProvider — mock by default, Supabase RPC
7
- // (course_place_order_simulated) when a tenantId + global client are configured.
8
- // ---------------------------------------------------------------------------
9
-
10
- export { createPublicCheckoutPlugin } from './createPublicCheckoutPlugin'
11
- export type { PublicCheckoutOptions, PublicCheckoutPlugin } from './createPublicCheckoutPlugin'
12
-
13
- export { CheckoutScreen } from './CheckoutScreen'
14
- export type { CheckoutScreenProps } from './CheckoutScreen'
15
-
16
- // Data seam
17
- export { createMockCheckoutProvider } from './data'
18
- export { createSupabaseCheckoutProvider } from './data.supabase'
19
- export type { SupabaseCheckoutOptions } from './data.supabase'
20
- export type {
21
- PublicCheckoutDataProvider,
22
- PlaceOrderInput,
23
- PlaceOrderResult,
24
- CheckoutPaymentMethod,
25
- MockCheckoutOptions,
26
- } from './data'
@@ -1,55 +0,0 @@
1
- import * as React from 'react'
2
- import { useTranslation } from '@fayz-ai/core'
3
- import { PluginSettingsPanel, SettingsGroup, SelectRow, ToggleRow } from '@fayz-ai/saas'
4
-
5
- // The course plugin's Settings tab. Reuses the saas settings shell
6
- // (PluginSettingsPanel + SettingsGroup/SelectRow/ToggleRow) instead of
7
- // hand-rolling a settings layout — same chrome as every other plugin. Stripe
8
- // Connect lives in the "Integrations" tab (a connector), not here.
9
-
10
- function CoursesGeneralSettings() {
11
- const t = useTranslation()
12
- const [currency, setCurrency] = React.useState('BRL')
13
- const [methods, setMethods] = React.useState({ card: true, pix: true, boleto: true })
14
-
15
- return (
16
- <div className="space-y-6">
17
- <SettingsGroup
18
- title={t('courses.settings.payments') || 'Payments'}
19
- description={t('courses.settings.paymentsHelp') || 'Connect Stripe in the Integrations tab; the platform fee applies automatically.'}
20
- >
21
- <SelectRow
22
- label={t('courses.settings.currency') || 'Default currency'}
23
- value={currency}
24
- onChange={setCurrency}
25
- options={[
26
- { value: 'BRL', label: 'BRL (R$)' },
27
- { value: 'USD', label: 'USD ($)' },
28
- { value: 'EUR', label: 'EUR (€)' },
29
- ]}
30
- />
31
- </SettingsGroup>
32
-
33
- <SettingsGroup title={t('courses.settings.methods') || 'Payment methods'}>
34
- <ToggleRow label={t('courses.editor.payCard') || 'Credit card'} checked={methods.card} onChange={(v) => setMethods((m) => ({ ...m, card: v }))} />
35
- <ToggleRow label={t('courses.editor.payPix') || 'Pix'} checked={methods.pix} onChange={(v) => setMethods((m) => ({ ...m, pix: v }))} />
36
- <ToggleRow label={t('courses.editor.payBoleto') || 'Boleto'} checked={methods.boleto} onChange={(v) => setMethods((m) => ({ ...m, boleto: v }))} />
37
- </SettingsGroup>
38
- </div>
39
- )
40
- }
41
- CoursesGeneralSettings.displayName = 'CoursesGeneralSettings'
42
-
43
- export function CoursesSettingsTab() {
44
- const t = useTranslation()
45
- return (
46
- <PluginSettingsPanel
47
- title={t('courses.settings.title') || 'Course settings'}
48
- subtitle={t('courses.settings.subtitle') || 'Payments, currency and checkout methods'}
49
- generalSettings={<CoursesGeneralSettings />}
50
- hostPluginId="courses"
51
- routeBase="/settings/courses"
52
- />
53
- )
54
- }
55
- CoursesSettingsTab.displayName = 'CoursesSettingsTab'