@fayz-ai/saas 0.8.1 → 0.8.2

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 (233) hide show
  1. package/dist/access/context.d.ts +47 -0
  2. package/dist/access/context.d.ts.map +1 -0
  3. package/dist/access/headless.d.ts +21 -0
  4. package/dist/access/headless.d.ts.map +1 -0
  5. package/dist/access/index.d.ts +12 -0
  6. package/dist/access/index.d.ts.map +1 -0
  7. package/dist/access/limits-registry.d.ts +33 -0
  8. package/dist/access/limits-registry.d.ts.map +1 -0
  9. package/dist/access/module-nav.d.ts +15 -0
  10. package/dist/access/module-nav.d.ts.map +1 -0
  11. package/dist/access/resolver.d.ts +2 -0
  12. package/dist/access/resolver.d.ts.map +1 -0
  13. package/dist/access/types.d.ts +43 -0
  14. package/dist/access/types.d.ts.map +1 -0
  15. package/dist/access/upgrade-modal-store.d.ts +23 -0
  16. package/dist/access/upgrade-modal-store.d.ts.map +1 -0
  17. package/dist/app/AdminShell.d.ts +3 -0
  18. package/dist/app/AdminShell.d.ts.map +1 -1
  19. package/dist/app/admin-app.d.ts +3 -1
  20. package/dist/app/admin-app.d.ts.map +1 -1
  21. package/dist/app/config.d.ts +47 -2
  22. package/dist/app/config.d.ts.map +1 -1
  23. package/dist/app/core-entities.d.ts +25 -0
  24. package/dist/app/core-entities.d.ts.map +1 -0
  25. package/dist/app/derive-contract.d.ts +8 -0
  26. package/dist/app/derive-contract.d.ts.map +1 -0
  27. package/dist/app/scaffold.d.ts.map +1 -1
  28. package/dist/archetype-lookup.d.ts.map +1 -1
  29. package/dist/billing/index.cjs +6 -2
  30. package/dist/billing/index.d.ts +2 -0
  31. package/dist/billing/index.d.ts.map +1 -1
  32. package/dist/billing/index.js +1 -1
  33. package/dist/billing/plan-badge.d.ts +17 -0
  34. package/dist/billing/plan-badge.d.ts.map +1 -0
  35. package/dist/billing/store.d.ts +6 -1
  36. package/dist/billing/store.d.ts.map +1 -1
  37. package/dist/{chunk-3M25GRRU.js → chunk-I3DY47N5.js} +3 -4
  38. package/dist/chunk-I3DY47N5.js.map +1 -0
  39. package/dist/chunk-NNFGMUF2.cjs +39 -0
  40. package/dist/chunk-NNFGMUF2.cjs.map +1 -0
  41. package/dist/{chunk-4XTQ4V5L.cjs → chunk-SAZLW54Y.cjs} +16 -12
  42. package/dist/chunk-SAZLW54Y.cjs.map +1 -0
  43. package/dist/{chunk-IPWCHJPE.cjs → chunk-VKYZACG5.cjs} +5 -6
  44. package/dist/chunk-VKYZACG5.cjs.map +1 -0
  45. package/dist/chunk-VN5NTK26.js +36 -0
  46. package/dist/chunk-VN5NTK26.js.map +1 -0
  47. package/dist/{chunk-XNWCGJYH.js → chunk-VTEP2AQX.js} +15 -11
  48. package/dist/chunk-VTEP2AQX.js.map +1 -0
  49. package/dist/components/shared/ContactPicker.d.ts +92 -0
  50. package/dist/components/shared/ContactPicker.d.ts.map +1 -0
  51. package/dist/components/shared/PersonLink.d.ts +12 -1
  52. package/dist/components/shared/PersonLink.d.ts.map +1 -1
  53. package/dist/components/shared/create-person.d.ts +20 -0
  54. package/dist/components/shared/create-person.d.ts.map +1 -0
  55. package/dist/crud/CrudDetailPage.d.ts.map +1 -1
  56. package/dist/crud/CrudFormPage.d.ts.map +1 -1
  57. package/dist/crud/CrudListView.d.ts.map +1 -1
  58. package/dist/crud/CrudPage.d.ts.map +1 -1
  59. package/dist/crud/archetypes/AddressesTab.d.ts +21 -0
  60. package/dist/crud/archetypes/AddressesTab.d.ts.map +1 -0
  61. package/dist/crud/archetypes/PersonDetailTabs.d.ts +2 -1
  62. package/dist/crud/archetypes/PersonDetailTabs.d.ts.map +1 -1
  63. package/dist/crud/createCrudPage.d.ts.map +1 -1
  64. package/dist/crud/index.d.ts +1 -0
  65. package/dist/crud/index.d.ts.map +1 -1
  66. package/dist/index.cjs +4281 -716
  67. package/dist/index.cjs.map +1 -1
  68. package/dist/index.d.ts +17 -2
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +4067 -542
  71. package/dist/index.js.map +1 -1
  72. package/dist/org/index.cjs +11 -11
  73. package/dist/org/index.js +2 -2
  74. package/dist/org/store.d.ts.map +1 -1
  75. package/dist/permissions/context.d.ts +2 -0
  76. package/dist/permissions/context.d.ts.map +1 -1
  77. package/dist/permissions/index.cjs +7 -7
  78. package/dist/permissions/index.js +1 -1
  79. package/dist/shell/components/auth/ProtectedRoute.d.ts.map +1 -1
  80. package/dist/shell/components/billing/SoftLimitBanner.d.ts +5 -0
  81. package/dist/shell/components/billing/SoftLimitBanner.d.ts.map +1 -0
  82. package/dist/shell/components/billing/SubscriptionPage.d.ts +5 -0
  83. package/dist/shell/components/billing/SubscriptionPage.d.ts.map +1 -0
  84. package/dist/shell/components/billing/UpgradeModal.d.ts +3 -0
  85. package/dist/shell/components/billing/UpgradeModal.d.ts.map +1 -0
  86. package/dist/shell/components/billing/UpgradePrompt.d.ts +10 -0
  87. package/dist/shell/components/billing/UpgradePrompt.d.ts.map +1 -0
  88. package/dist/shell/components/billing/access-contract.d.ts +18 -0
  89. package/dist/shell/components/billing/access-contract.d.ts.map +1 -0
  90. package/dist/shell/components/billing/gates.d.ts +14 -0
  91. package/dist/shell/components/billing/gates.d.ts.map +1 -0
  92. package/dist/shell/components/billing/index.d.ts +9 -2
  93. package/dist/shell/components/billing/index.d.ts.map +1 -1
  94. package/dist/shell/components/chat/ChatFab.d.ts +3 -1
  95. package/dist/shell/components/chat/ChatFab.d.ts.map +1 -1
  96. package/dist/shell/components/chat/ChatPanel.d.ts +3 -1
  97. package/dist/shell/components/chat/ChatPanel.d.ts.map +1 -1
  98. package/dist/shell/components/chat/ConfirmActionCard.d.ts +13 -0
  99. package/dist/shell/components/chat/ConfirmActionCard.d.ts.map +1 -0
  100. package/dist/shell/components/chat/markdown.d.ts +5 -0
  101. package/dist/shell/components/chat/markdown.d.ts.map +1 -0
  102. package/dist/shell/components/organization/TeamTab.d.ts.map +1 -1
  103. package/dist/shell/components/organization/TenantOnboarding.d.ts.map +1 -1
  104. package/dist/shell/components/organization/index.d.ts +0 -1
  105. package/dist/shell/components/organization/index.d.ts.map +1 -1
  106. package/dist/shell/components/settings/ConnectedCompanySettings.d.ts.map +1 -1
  107. package/dist/shell/components/settings/ConnectedFieldRulesSettings.d.ts.map +1 -1
  108. package/dist/shell/components/settings/ConnectedSecuritySettings.d.ts.map +1 -1
  109. package/dist/shell/components/settings/LocationsCrudPage.d.ts +1 -1
  110. package/dist/shell/components/settings/LocationsCrudPage.d.ts.map +1 -1
  111. package/dist/shell/components/settings/SettingsPage.d.ts +23 -2
  112. package/dist/shell/components/settings/SettingsPage.d.ts.map +1 -1
  113. package/dist/shell/hooks/index.d.ts +3 -2
  114. package/dist/shell/hooks/index.d.ts.map +1 -1
  115. package/dist/shell/hooks/useAITools.d.ts +2 -0
  116. package/dist/shell/hooks/useAITools.d.ts.map +1 -1
  117. package/dist/shell/hooks/useChat.d.ts +22 -0
  118. package/dist/shell/hooks/useChat.d.ts.map +1 -1
  119. package/dist/shell/hooks/useSystemPermission.d.ts +3 -0
  120. package/dist/shell/hooks/useSystemPermission.d.ts.map +1 -0
  121. package/dist/shell/hooks/useTenantPluginSettings.d.ts +12 -0
  122. package/dist/shell/hooks/useTenantPluginSettings.d.ts.map +1 -0
  123. package/dist/shell/lib/ai-tool-handlers.d.ts +119 -0
  124. package/dist/shell/lib/ai-tool-handlers.d.ts.map +1 -0
  125. package/dist/shell/lib/core-ai-tools.d.ts +14 -0
  126. package/dist/shell/lib/core-ai-tools.d.ts.map +1 -1
  127. package/dist/shell/lib/create-client-orders-provider.d.ts.map +1 -1
  128. package/dist/shell/lib/fayz-agent.d.ts +23 -0
  129. package/dist/shell/lib/fayz-agent.d.ts.map +1 -0
  130. package/dist/shell/lib/i18n.d.ts.map +1 -1
  131. package/dist/shell/stores/chat.store.d.ts +69 -0
  132. package/dist/shell/stores/chat.store.d.ts.map +1 -1
  133. package/dist/shell/types/billing.d.ts +7 -0
  134. package/dist/shell/types/billing.d.ts.map +1 -1
  135. package/dist/shell/types/plugins.d.ts +2 -45
  136. package/dist/shell/types/plugins.d.ts.map +1 -1
  137. package/dist/stores/createCrudStore.d.ts.map +1 -1
  138. package/package.json +14 -11
  139. package/src/access/context.tsx +228 -0
  140. package/src/access/headless.ts +71 -0
  141. package/src/access/index.ts +27 -0
  142. package/src/access/limits-registry.ts +86 -0
  143. package/src/access/module-nav.test.ts +78 -0
  144. package/src/access/module-nav.ts +82 -0
  145. package/src/access/resolver.test.ts +143 -0
  146. package/src/access/resolver.ts +5 -0
  147. package/src/access/types.ts +47 -0
  148. package/src/access/upgrade-modal-store.ts +30 -0
  149. package/src/app/AdminShell.tsx +451 -68
  150. package/src/app/admin-app.tsx +46 -9
  151. package/src/app/config.ts +54 -1
  152. package/src/app/core-entities.ts +110 -0
  153. package/src/app/derive-contract.test.ts +155 -0
  154. package/src/app/derive-contract.ts +214 -0
  155. package/src/app/scaffold.tsx +4 -0
  156. package/src/archetype-lookup.ts +8 -2
  157. package/src/billing/index.ts +2 -0
  158. package/src/billing/plan-badge.ts +32 -0
  159. package/src/billing/store.ts +8 -1
  160. package/src/components/shared/ContactPicker.tsx +394 -0
  161. package/src/components/shared/PersonLink.tsx +11 -3
  162. package/src/components/shared/create-person.ts +74 -0
  163. package/src/crud/CrudDetailPage.tsx +5 -1
  164. package/src/crud/CrudFormPage.tsx +113 -6
  165. package/src/crud/CrudListView.tsx +16 -1
  166. package/src/crud/CrudPage.tsx +27 -5
  167. package/src/crud/DeleteConfirmDialog.tsx +1 -1
  168. package/src/crud/archetypes/AddressesTab.tsx +387 -0
  169. package/src/crud/archetypes/PersonDetailTabs.tsx +5 -0
  170. package/src/crud/createCrudPage.tsx +2 -0
  171. package/src/crud/index.ts +6 -0
  172. package/src/env.d.ts +7 -0
  173. package/src/index.ts +49 -1
  174. package/src/org/store.ts +9 -1
  175. package/src/permissions/context.tsx +29 -15
  176. package/src/shell/components/auth/ProtectedRoute.tsx +12 -2
  177. package/src/shell/components/billing/SoftLimitBanner.tsx +81 -0
  178. package/src/shell/components/billing/SubscriptionPage.tsx +372 -0
  179. package/src/shell/components/billing/UpgradeModal.tsx +102 -0
  180. package/src/shell/components/billing/UpgradePrompt.tsx +229 -0
  181. package/src/shell/components/billing/access-contract.ts +64 -0
  182. package/src/shell/components/billing/gates.tsx +62 -0
  183. package/src/shell/components/billing/index.ts +18 -2
  184. package/src/shell/components/chat/ChatFab.tsx +4 -2
  185. package/src/shell/components/chat/ChatPanel.tsx +174 -23
  186. package/src/shell/components/chat/ConfirmActionCard.tsx +57 -0
  187. package/src/shell/components/chat/markdown.tsx +102 -0
  188. package/src/shell/components/organization/TeamTab.tsx +21 -4
  189. package/src/shell/components/organization/TenantOnboarding.tsx +3 -1
  190. package/src/shell/components/organization/index.ts +0 -1
  191. package/src/shell/components/settings/ConnectedCompanySettings.tsx +4 -1
  192. package/src/shell/components/settings/ConnectedFieldRulesSettings.tsx +3 -1
  193. package/src/shell/components/settings/ConnectedSecuritySettings.tsx +3 -1
  194. package/src/shell/components/settings/LocationsCrudPage.ts +6 -1
  195. package/src/shell/components/settings/SettingsPage.tsx +63 -21
  196. package/src/shell/hooks/index.ts +3 -2
  197. package/src/shell/hooks/useAITools.ts +27 -23
  198. package/src/shell/hooks/useChat.ts +381 -16
  199. package/src/shell/hooks/useSystemPermission.ts +23 -0
  200. package/src/shell/hooks/useTenantPluginSettings.ts +127 -0
  201. package/src/shell/lib/ai-tool-handlers.ts +910 -0
  202. package/src/shell/lib/core-ai-tools.ts +234 -2
  203. package/src/shell/lib/create-client-orders-provider.ts +24 -2
  204. package/src/shell/lib/fayz-agent.ts +56 -0
  205. package/src/shell/lib/i18n.ts +71 -0
  206. package/src/shell/stores/chat.store.ts +118 -1
  207. package/src/shell/styles.css +30 -0
  208. package/src/shell/types/billing.ts +8 -0
  209. package/src/shell/types/plugins.ts +13 -50
  210. package/src/stores/createCrudStore.ts +9 -0
  211. package/dist/chunk-3M25GRRU.js.map +0 -1
  212. package/dist/chunk-4XTQ4V5L.cjs.map +0 -1
  213. package/dist/chunk-5GYQK5IW.js +0 -21
  214. package/dist/chunk-5GYQK5IW.js.map +0 -1
  215. package/dist/chunk-IPWCHJPE.cjs.map +0 -1
  216. package/dist/chunk-KYID2MR6.cjs +0 -23
  217. package/dist/chunk-KYID2MR6.cjs.map +0 -1
  218. package/dist/chunk-XNWCGJYH.js.map +0 -1
  219. package/dist/shell/components/billing/BillingPage.d.ts +0 -7
  220. package/dist/shell/components/billing/BillingPage.d.ts.map +0 -1
  221. package/dist/shell/components/billing/PaywallGate.d.ts +0 -11
  222. package/dist/shell/components/billing/PaywallGate.d.ts.map +0 -1
  223. package/dist/shell/components/organization/PermissionGate.d.ts +0 -11
  224. package/dist/shell/components/organization/PermissionGate.d.ts.map +0 -1
  225. package/dist/shell/hooks/useBilling.d.ts +0 -14
  226. package/dist/shell/hooks/useBilling.d.ts.map +0 -1
  227. package/dist/shell/hooks/usePermission.d.ts +0 -16
  228. package/dist/shell/hooks/usePermission.d.ts.map +0 -1
  229. package/src/shell/components/billing/BillingPage.tsx +0 -56
  230. package/src/shell/components/billing/PaywallGate.tsx +0 -80
  231. package/src/shell/components/organization/PermissionGate.tsx +0 -16
  232. package/src/shell/hooks/useBilling.ts +0 -151
  233. package/src/shell/hooks/usePermission.ts +0 -101
@@ -0,0 +1,229 @@
1
+ import * as React from 'react'
2
+ import { Crown, Check, Sparkles, ArrowRight } from 'lucide-react'
3
+ import { Button, Card } from '@fayz-ai/ui'
4
+ import type { Plan } from '@fayz-ai/core'
5
+ import { cn } from '../../lib/cn'
6
+ import { useTranslation } from '../../hooks/useTranslation'
7
+ import { useBillingStore } from '../../stores/billing.store'
8
+ import { usePermissionsStore } from '../../../permissions'
9
+ import { useOrganizationStore } from '../../../org/store'
10
+ import { navigateTo } from '../../../app/routing'
11
+ import { getPlanEntitlements, featureDisplayName, resolvePlanCurrency } from './access-contract'
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // UpgradePrompt — the "this is a higher-plan feature" surface. Two variants:
15
+ // • full (default): a benefit-led hero (compact icon + "Unlock {Feature}"),
16
+ // a "your plan" context chip, and priced plan cards — the cheapest one that
17
+ // unlocks the feature flagged "Recommended" and slightly elevated — each with
18
+ // its own "Upgrade" CTA into /settings/subscription, plus a discreet
19
+ // "Compare all plans" link.
20
+ // • inline: a single-line banner for use as a gate fallback next to content.
21
+ // Rendered by the AdminShell route guard (reason: 'plan'), the EntitlementGate
22
+ // fallback, and anywhere a premium feature needs a discovery nudge.
23
+ // ---------------------------------------------------------------------------
24
+
25
+ export interface UpgradePromptProps {
26
+ /** Feature id (same id space as RBAC/nav) this prompt is about. */
27
+ feature?: string
28
+ /** Compact banner instead of the full hero block. */
29
+ inline?: boolean
30
+ className?: string
31
+ }
32
+
33
+ function planUnlocks(plan: Plan, feature: string | undefined): boolean {
34
+ if (!feature) return plan.price > 0
35
+ const ent = getPlanEntitlements(plan)
36
+ // Entitled when the feature isn't explicitly disabled for the plan.
37
+ return ent?.features?.[feature] !== false
38
+ }
39
+
40
+ /** Localized "R$ 79" style price, respecting the plan's currency + active locale. */
41
+ function formatPrice(plan: Plan, locale: string | undefined): string {
42
+ const currency = resolvePlanCurrency(plan.currency, locale)
43
+ try {
44
+ return new Intl.NumberFormat(locale, {
45
+ style: 'currency',
46
+ currency,
47
+ minimumFractionDigits: Number.isInteger(plan.price) ? 0 : 2,
48
+ }).format(plan.price)
49
+ } catch {
50
+ return `${currency} ${plan.price}`.trim()
51
+ }
52
+ }
53
+
54
+ export function UpgradePrompt({ feature, inline, className }: UpgradePromptProps) {
55
+ const { t, locale } = useTranslation()
56
+ const tr = (key: string, fallback: string, params?: Record<string, string>) => {
57
+ let v = t(key)
58
+ if (!v || v === key) v = fallback
59
+ if (params) for (const [k, val] of Object.entries(params)) v = v.replace(`{${k}}`, val).replace(`{{${k}}}`, val)
60
+ return v
61
+ }
62
+
63
+ const plans = useBillingStore((s) => s.plans)
64
+ const features = usePermissionsStore((s) => s.features)
65
+ const currentOrg = useOrganizationStore((s) => s.currentOrg)
66
+ const currentPlanId = currentOrg?.plan || 'free'
67
+ const currentPlan = plans.find((p) => p.id === currentPlanId)
68
+ const currentPrice = currentPlan?.price ?? 0
69
+ const currentPlanName = currentPlan?.name ?? tr('billing.free', 'Free')
70
+
71
+ // Clean display name (strips "(acesso)"/"(access)" suffixes from the raw label).
72
+ const featureName = React.useMemo(() => {
73
+ if (!feature) return null
74
+ const label = features.find((f) => f.id === feature)?.label ?? feature
75
+ return featureDisplayName(label)
76
+ }, [features, feature])
77
+
78
+ // Plans that unlock the feature AND are an upgrade over the current plan,
79
+ // cheapest first — so the first is the natural "recommended" entry point.
80
+ const unlockingPlans = React.useMemo(
81
+ () =>
82
+ plans
83
+ .filter((p) => p.id !== currentPlanId && p.price > currentPrice && planUnlocks(p, feature))
84
+ .sort((a, b) => a.price - b.price),
85
+ [plans, currentPlanId, currentPrice, feature],
86
+ )
87
+ const recommendedId = unlockingPlans[0]?.id
88
+
89
+ const goToPlans = () => navigateTo('/settings/subscription')
90
+
91
+ const heading = featureName
92
+ ? tr('upgrade.unlock', 'Unlock {feature}', { feature: featureName })
93
+ : tr('upgrade.title', 'Premium feature')
94
+
95
+ // Benefit subcopy: per-feature key (upgrade.value.<feature>) with a dignified
96
+ // generic fallback so every feature reads intentionally.
97
+ const valueKey = feature ? `upgrade.value.${feature}` : ''
98
+ const valueCopy = feature ? t(valueKey) : ''
99
+ const subcopy =
100
+ valueCopy && valueCopy !== valueKey
101
+ ? valueCopy
102
+ : tr('upgrade.valueGeneric', 'Get everything you need to grow — included in the plans below.')
103
+
104
+ if (inline) {
105
+ return (
106
+ <div
107
+ className={cn(
108
+ 'flex flex-wrap items-center gap-3 rounded-xl border border-amber-400/40 bg-amber-50/60 px-4 py-3 dark:border-amber-400/25 dark:bg-amber-950/20',
109
+ className,
110
+ )}
111
+ >
112
+ <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-amber-400/20 text-amber-600 dark:text-amber-400">
113
+ <Crown className="h-4 w-4" />
114
+ </span>
115
+ <p className="min-w-0 flex-1 text-sm">
116
+ <span className="font-semibold text-foreground">{heading}</span>
117
+ <span className="ml-1.5 text-muted-foreground">{subcopy}</span>
118
+ </p>
119
+ <Button size="sm" onClick={goToPlans} className="shrink-0">
120
+ {tr('upgrade.ctaUpgrade', 'Upgrade')}
121
+ <ArrowRight className="ml-1.5 h-3.5 w-3.5" />
122
+ </Button>
123
+ </div>
124
+ )
125
+ }
126
+
127
+ return (
128
+ <div className={cn('mx-auto w-full max-w-4xl px-4 py-10 sm:py-14', className)}>
129
+ {/* Hero — compact, benefit-led. Icon integrated with the title, not floating. */}
130
+ <div className="flex flex-col items-center text-center">
131
+ {currentPlan && (
132
+ <span className="mb-5 inline-flex items-center gap-1.5 rounded-full border bg-muted/40 px-3 py-1 text-xs font-medium text-muted-foreground">
133
+ <span className="h-1.5 w-1.5 rounded-full bg-muted-foreground/50" aria-hidden="true" />
134
+ {tr('upgrade.currentPlan', 'Your plan: {plan}', { plan: currentPlanName })}
135
+ </span>
136
+ )}
137
+ <h1 className="flex items-center gap-2.5 text-2xl font-bold tracking-tight text-foreground sm:text-3xl">
138
+ <span className="flex h-9 w-9 items-center justify-center rounded-xl bg-amber-400/15 text-amber-500 sm:h-10 sm:w-10">
139
+ <Crown className="h-5 w-5 sm:h-[22px] sm:w-[22px]" />
140
+ </span>
141
+ {heading}
142
+ </h1>
143
+ <p className="mt-3 max-w-md text-sm text-muted-foreground sm:text-base">{subcopy}</p>
144
+ </div>
145
+
146
+ {unlockingPlans.length > 0 && (
147
+ <div
148
+ className={cn(
149
+ 'mt-9 grid gap-4 sm:gap-5',
150
+ unlockingPlans.length === 1 ? 'max-w-sm mx-auto' : 'sm:grid-cols-2',
151
+ unlockingPlans.length >= 3 && 'lg:grid-cols-3',
152
+ )}
153
+ >
154
+ {unlockingPlans.map((plan) => {
155
+ const recommended = plan.id === recommendedId
156
+ const paid = plan.price > 0
157
+ const highlights = plan.features.slice(0, 3)
158
+ return (
159
+ <Card
160
+ key={plan.id}
161
+ className={cn(
162
+ 'relative flex flex-col p-5 transition-shadow',
163
+ recommended
164
+ ? 'border-primary shadow-lg ring-1 ring-primary/20 sm:-translate-y-1'
165
+ : 'hover:shadow-md',
166
+ )}
167
+ >
168
+ {recommended && (
169
+ <span className="absolute -top-2.5 left-5 inline-flex items-center gap-1 rounded-full bg-primary px-2.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-primary-foreground shadow-sm">
170
+ <Sparkles className="h-3 w-3" aria-hidden="true" />
171
+ {tr('upgrade.recommended', 'Recommended')}
172
+ </span>
173
+ )}
174
+
175
+ <div className="flex items-center gap-1.5">
176
+ {paid && <Crown className="h-4 w-4 text-amber-500" aria-hidden="true" />}
177
+ <h2 className="text-sm font-semibold text-foreground">{plan.name}</h2>
178
+ </div>
179
+
180
+ <div className="mt-2 flex items-baseline gap-1">
181
+ <span className="text-2xl font-bold tracking-tight text-foreground tabular-nums">
182
+ {formatPrice(plan, locale)}
183
+ </span>
184
+ <span className="text-sm text-muted-foreground">
185
+ {plan.interval === 'year' ? tr('billing.perYear', '/yr') : tr('billing.perMonth', '/mo')}
186
+ </span>
187
+ </div>
188
+
189
+ {plan.description && (
190
+ <p className="mt-1 text-xs text-muted-foreground">{plan.description}</p>
191
+ )}
192
+
193
+ <ul className="mt-4 flex-1 space-y-2">
194
+ {highlights.map((f) => (
195
+ <li key={f} className="flex items-start gap-2 text-xs text-foreground/80">
196
+ <Check className="mt-0.5 h-3.5 w-3.5 shrink-0 text-primary" aria-hidden="true" />
197
+ <span>{f}</span>
198
+ </li>
199
+ ))}
200
+ </ul>
201
+
202
+ <Button
203
+ className="mt-5 w-full"
204
+ variant={recommended ? 'default' : 'outline'}
205
+ onClick={goToPlans}
206
+ aria-label={tr('upgrade.ctaUpgradeTo', 'Upgrade to {plan}', { plan: plan.name })}
207
+ >
208
+ {paid && <Crown className="mr-1.5 h-4 w-4" aria-hidden="true" />}
209
+ {tr('upgrade.ctaUpgrade', 'Upgrade')}
210
+ </Button>
211
+ </Card>
212
+ )
213
+ })}
214
+ </div>
215
+ )}
216
+
217
+ <div className="mt-7 text-center">
218
+ <button
219
+ type="button"
220
+ onClick={goToPlans}
221
+ className="inline-flex items-center gap-1 rounded text-sm font-medium text-muted-foreground underline-offset-4 transition-colors hover:text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
222
+ >
223
+ {tr('upgrade.compareAll', 'Compare all plans')}
224
+ <ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
225
+ </button>
226
+ </div>
227
+ </div>
228
+ )
229
+ }
@@ -0,0 +1,64 @@
1
+ // ---------------------------------------------------------------------------
2
+ // access-contract — the single import seam the billing UI uses for the
3
+ // entitlements engine (UpgradePrompt, UpgradeModal, EntitlementGate, LimitGate,
4
+ // SoftLimitBanner, the AdminShell route guard / nav badge, SubscriptionPage
5
+ // matrix). Re-exports the core access engine (packages/saas/src/access) so the
6
+ // billing layer has ONE stable path and the wiring (AccessProvider) stays in the
7
+ // núcleo. `getPlanEntitlements` is a thin local reader — core's Plan carries the
8
+ // entitlements, so this just narrows the access.
9
+ //
10
+ // (History: this file shipped as a temporary stub while the access engine landed
11
+ // in parallel; now repointed to the real module. The public names/types below
12
+ // match the fixed contract, so no billing-wave consumer changed.)
13
+ // ---------------------------------------------------------------------------
14
+ import type { Plan, PlanEntitlements } from '@fayz-ai/core'
15
+
16
+ export {
17
+ useAccessOptional,
18
+ useLimit,
19
+ useLimitGuard,
20
+ invalidateLimit,
21
+ useUpgradeModalStore,
22
+ } from '../../../access'
23
+
24
+ export type { AccessApi, AccessDecision } from '../../../access'
25
+ // The billing layer refers to the limit shape as `LimitInfo`; the engine calls
26
+ // it `LimitState`. Same structure — alias to keep call sites stable.
27
+ export type { LimitState as LimitInfo } from '../../../access'
28
+ export type { PlanEntitlements } from '@fayz-ai/core'
29
+
30
+ /** Narrow a plan to its entitlements (undefined when the plan declares none). */
31
+ export function getPlanEntitlements(plan: Plan | undefined | null): PlanEntitlements | undefined {
32
+ return plan?.entitlements
33
+ }
34
+
35
+ /**
36
+ * Clean a feature's raw permission label into a display name for the upgrade
37
+ * surfaces. Feature labels in an app's permissions config carry an access-scope
38
+ * suffix — e.g. "Marketing (acesso)" / "Reports (access)" — which reads as noise
39
+ * when interpolated into a headline ("Unlock Marketing (acesso)"). Strip a single
40
+ * trailing parenthetical group and collapse surrounding whitespace. Shared by
41
+ * UpgradePrompt AND UpgradeModal so the two surfaces stay in lockstep.
42
+ */
43
+ export function featureDisplayName(label: string | null | undefined): string {
44
+ if (!label) return ''
45
+ return label.replace(/\s*\([^)]*\)\s*$/, '').trim() || label.trim()
46
+ }
47
+
48
+ /**
49
+ * Which ISO-4217 code to price a plan in. `Plan.currency` is the source of
50
+ * truth, but apps DO ship billing configs that omit it — and silently printing
51
+ * "$79" to a tenant billed in reais is worse than any other failure mode here.
52
+ * So the fallback follows the ACTIVE LOCALE's region rather than a hardcoded
53
+ * 'USD'; only a locale we can't place at all lands on USD.
54
+ */
55
+ const CURRENCY_BY_REGION: Record<string, string> = {
56
+ BR: 'BRL', PT: 'EUR', ES: 'EUR', FR: 'EUR', DE: 'EUR', IT: 'EUR', NL: 'EUR', IE: 'EUR',
57
+ GB: 'GBP', US: 'USD', CA: 'CAD', AU: 'AUD', MX: 'MXN', AR: 'ARS', CL: 'CLP', CO: 'COP',
58
+ }
59
+
60
+ export function resolvePlanCurrency(planCurrency: string | undefined, locale: string | undefined): string {
61
+ if (planCurrency) return planCurrency
62
+ const region = locale?.split(/[-_]/)[1]?.toUpperCase()
63
+ return (region && CURRENCY_BY_REGION[region]) || 'USD'
64
+ }
@@ -0,0 +1,62 @@
1
+ import * as React from 'react'
2
+ import { UpgradePrompt } from './UpgradePrompt'
3
+ import { useAccessOptional, useLimit, useUpgradeModalStore } from './access-contract'
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // EntitlementGate — plan-level gate. Renders children only when the current
7
+ // plan entitles `feature`; otherwise the fallback (default: an inline
8
+ // UpgradePrompt). Role gating stays with the native PermissionGate — this is
9
+ // strictly the plan axis.
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export interface EntitlementGateProps {
13
+ feature: string
14
+ children: React.ReactNode
15
+ fallback?: React.ReactNode
16
+ }
17
+
18
+ export function EntitlementGate({ feature, children, fallback }: EntitlementGateProps) {
19
+ const access = useAccessOptional()
20
+ if (access.entitled(feature)) return <>{children}</>
21
+ return <>{fallback ?? <UpgradePrompt inline feature={feature} />}</>
22
+ }
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // LimitGate — quantity cap gate for "+ Add" style actions. When the limit is
26
+ // reached, the children are dimmed and any click is intercepted (capture phase)
27
+ // to open the global UpgradeModal instead of firing the underlying handler.
28
+ // Below the cap it's a transparent pass-through.
29
+ // ---------------------------------------------------------------------------
30
+
31
+ export interface LimitGateProps {
32
+ limitKey: string
33
+ children: React.ReactNode
34
+ className?: string
35
+ }
36
+
37
+ export function LimitGate({ limitKey, children, className }: LimitGateProps) {
38
+ const limit = useLimit(limitKey)
39
+ const openModal = useUpgradeModalStore((s) => s.open)
40
+
41
+ if (!limit.atLimit) return <>{children}</>
42
+
43
+ const intercept = (e: React.MouseEvent) => {
44
+ e.preventDefault()
45
+ e.stopPropagation()
46
+ openModal({ limitKey })
47
+ }
48
+
49
+ return (
50
+ <span
51
+ className={className}
52
+ onClickCapture={intercept}
53
+ style={{ cursor: 'not-allowed' }}
54
+ // Dim to signal the cap while keeping the trigger discoverable.
55
+ data-limit-reached=""
56
+ >
57
+ <span className="pointer-events-none opacity-60" aria-disabled>
58
+ {children}
59
+ </span>
60
+ </span>
61
+ )
62
+ }
@@ -1,5 +1,21 @@
1
- export { BillingPage } from './BillingPage'
1
+ export { SubscriptionPage } from './SubscriptionPage'
2
2
  export { PlanSelector } from './PlanSelector'
3
3
  export { SubscriptionCard } from './SubscriptionCard'
4
4
  export { InvoiceList } from './InvoiceList'
5
- export { PaywallGate } from './PaywallGate'
5
+ export { UpgradePrompt } from './UpgradePrompt'
6
+ export type { UpgradePromptProps } from './UpgradePrompt'
7
+ export { UpgradeModal } from './UpgradeModal'
8
+ export { SoftLimitBanner } from './SoftLimitBanner'
9
+ export { EntitlementGate, LimitGate } from './gates'
10
+ export type { EntitlementGateProps, LimitGateProps } from './gates'
11
+ // Entitlements access contract (TEMP STUB until packages/saas/src/access lands —
12
+ // orchestrator: repoint access-contract.ts re-exports on integration).
13
+ export {
14
+ useAccessOptional,
15
+ useLimit,
16
+ useLimitGuard,
17
+ useUpgradeModalStore,
18
+ invalidateLimit,
19
+ getPlanEntitlements,
20
+ } from './access-contract'
21
+ export type { AccessApi, AccessDecision, LimitInfo, PlanEntitlements } from './access-contract'
@@ -6,11 +6,13 @@ import { useOrganizationStore } from '../../stores/organization.store'
6
6
  import { useAITools, type ResolvedSuggestion } from '../../hooks/useAITools'
7
7
  import { useChat } from '../../hooks/useChat'
8
8
  import { useTranslation } from '../../hooks/useTranslation'
9
+ import type { FayzAgentConnectionConfig } from '../../lib/fayz-agent'
9
10
 
10
11
  interface ChatFabProps {
11
12
  className?: string
12
13
  apiEndpoint?: string
13
14
  systemPrompt?: string
15
+ agent?: FayzAgentConnectionConfig | false
14
16
  }
15
17
 
16
18
  function TypewriterText({ text, speed = 28, onDone }: { text: string; speed?: number; onDone?: () => void }) {
@@ -64,11 +66,11 @@ function ensureStyles() {
64
66
  document.head.appendChild(style)
65
67
  }
66
68
 
67
- export function ChatFab({ className, apiEndpoint, systemPrompt }: ChatFabProps) {
69
+ export function ChatFab({ className, apiEndpoint, systemPrompt, agent }: ChatFabProps) {
68
70
  const { isOpen, toggleOpen, setOpen } = useChatStore()
69
71
  const currentOrg = useOrganizationStore((s) => s.currentOrg)
70
72
  const { contextualSuggestions } = useAITools()
71
- const { sendMessage } = useChat({ apiEndpoint, systemPrompt })
73
+ const { sendMessage } = useChat({ apiEndpoint, systemPrompt, agent })
72
74
  const { t } = useTranslation()
73
75
 
74
76
  const [phase, setPhase] = React.useState<'idle' | 'expanding' | 'typing' | 'visible' | 'collapsing'>('idle')