@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.
- package/dist/access/context.d.ts +47 -0
- package/dist/access/context.d.ts.map +1 -0
- package/dist/access/headless.d.ts +21 -0
- package/dist/access/headless.d.ts.map +1 -0
- package/dist/access/index.d.ts +12 -0
- package/dist/access/index.d.ts.map +1 -0
- package/dist/access/limits-registry.d.ts +33 -0
- package/dist/access/limits-registry.d.ts.map +1 -0
- package/dist/access/module-nav.d.ts +15 -0
- package/dist/access/module-nav.d.ts.map +1 -0
- package/dist/access/resolver.d.ts +2 -0
- package/dist/access/resolver.d.ts.map +1 -0
- package/dist/access/types.d.ts +43 -0
- package/dist/access/types.d.ts.map +1 -0
- package/dist/access/upgrade-modal-store.d.ts +23 -0
- package/dist/access/upgrade-modal-store.d.ts.map +1 -0
- package/dist/app/AdminShell.d.ts +3 -0
- package/dist/app/AdminShell.d.ts.map +1 -1
- package/dist/app/admin-app.d.ts +3 -1
- package/dist/app/admin-app.d.ts.map +1 -1
- package/dist/app/config.d.ts +47 -2
- package/dist/app/config.d.ts.map +1 -1
- package/dist/app/core-entities.d.ts +25 -0
- package/dist/app/core-entities.d.ts.map +1 -0
- package/dist/app/derive-contract.d.ts +8 -0
- package/dist/app/derive-contract.d.ts.map +1 -0
- package/dist/app/scaffold.d.ts.map +1 -1
- package/dist/archetype-lookup.d.ts.map +1 -1
- package/dist/billing/index.cjs +6 -2
- package/dist/billing/index.d.ts +2 -0
- package/dist/billing/index.d.ts.map +1 -1
- package/dist/billing/index.js +1 -1
- package/dist/billing/plan-badge.d.ts +17 -0
- package/dist/billing/plan-badge.d.ts.map +1 -0
- package/dist/billing/store.d.ts +6 -1
- package/dist/billing/store.d.ts.map +1 -1
- package/dist/{chunk-3M25GRRU.js → chunk-I3DY47N5.js} +3 -4
- package/dist/chunk-I3DY47N5.js.map +1 -0
- package/dist/chunk-NNFGMUF2.cjs +39 -0
- package/dist/chunk-NNFGMUF2.cjs.map +1 -0
- package/dist/{chunk-4XTQ4V5L.cjs → chunk-SAZLW54Y.cjs} +16 -12
- package/dist/chunk-SAZLW54Y.cjs.map +1 -0
- package/dist/{chunk-IPWCHJPE.cjs → chunk-VKYZACG5.cjs} +5 -6
- package/dist/chunk-VKYZACG5.cjs.map +1 -0
- package/dist/chunk-VN5NTK26.js +36 -0
- package/dist/chunk-VN5NTK26.js.map +1 -0
- package/dist/{chunk-XNWCGJYH.js → chunk-VTEP2AQX.js} +15 -11
- package/dist/chunk-VTEP2AQX.js.map +1 -0
- package/dist/components/shared/ContactPicker.d.ts +92 -0
- package/dist/components/shared/ContactPicker.d.ts.map +1 -0
- package/dist/components/shared/PersonLink.d.ts +12 -1
- package/dist/components/shared/PersonLink.d.ts.map +1 -1
- package/dist/components/shared/create-person.d.ts +20 -0
- package/dist/components/shared/create-person.d.ts.map +1 -0
- package/dist/crud/CrudDetailPage.d.ts.map +1 -1
- package/dist/crud/CrudFormPage.d.ts.map +1 -1
- package/dist/crud/CrudListView.d.ts.map +1 -1
- package/dist/crud/CrudPage.d.ts.map +1 -1
- package/dist/crud/archetypes/AddressesTab.d.ts +21 -0
- package/dist/crud/archetypes/AddressesTab.d.ts.map +1 -0
- package/dist/crud/archetypes/PersonDetailTabs.d.ts +2 -1
- package/dist/crud/archetypes/PersonDetailTabs.d.ts.map +1 -1
- package/dist/crud/createCrudPage.d.ts.map +1 -1
- package/dist/crud/index.d.ts +1 -0
- package/dist/crud/index.d.ts.map +1 -1
- package/dist/index.cjs +4281 -716
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +17 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4067 -542
- package/dist/index.js.map +1 -1
- package/dist/org/index.cjs +11 -11
- package/dist/org/index.js +2 -2
- package/dist/org/store.d.ts.map +1 -1
- package/dist/permissions/context.d.ts +2 -0
- package/dist/permissions/context.d.ts.map +1 -1
- package/dist/permissions/index.cjs +7 -7
- package/dist/permissions/index.js +1 -1
- package/dist/shell/components/auth/ProtectedRoute.d.ts.map +1 -1
- package/dist/shell/components/billing/SoftLimitBanner.d.ts +5 -0
- package/dist/shell/components/billing/SoftLimitBanner.d.ts.map +1 -0
- package/dist/shell/components/billing/SubscriptionPage.d.ts +5 -0
- package/dist/shell/components/billing/SubscriptionPage.d.ts.map +1 -0
- package/dist/shell/components/billing/UpgradeModal.d.ts +3 -0
- package/dist/shell/components/billing/UpgradeModal.d.ts.map +1 -0
- package/dist/shell/components/billing/UpgradePrompt.d.ts +10 -0
- package/dist/shell/components/billing/UpgradePrompt.d.ts.map +1 -0
- package/dist/shell/components/billing/access-contract.d.ts +18 -0
- package/dist/shell/components/billing/access-contract.d.ts.map +1 -0
- package/dist/shell/components/billing/gates.d.ts +14 -0
- package/dist/shell/components/billing/gates.d.ts.map +1 -0
- package/dist/shell/components/billing/index.d.ts +9 -2
- package/dist/shell/components/billing/index.d.ts.map +1 -1
- package/dist/shell/components/chat/ChatFab.d.ts +3 -1
- package/dist/shell/components/chat/ChatFab.d.ts.map +1 -1
- package/dist/shell/components/chat/ChatPanel.d.ts +3 -1
- package/dist/shell/components/chat/ChatPanel.d.ts.map +1 -1
- package/dist/shell/components/chat/ConfirmActionCard.d.ts +13 -0
- package/dist/shell/components/chat/ConfirmActionCard.d.ts.map +1 -0
- package/dist/shell/components/chat/markdown.d.ts +5 -0
- package/dist/shell/components/chat/markdown.d.ts.map +1 -0
- package/dist/shell/components/organization/TeamTab.d.ts.map +1 -1
- package/dist/shell/components/organization/TenantOnboarding.d.ts.map +1 -1
- package/dist/shell/components/organization/index.d.ts +0 -1
- package/dist/shell/components/organization/index.d.ts.map +1 -1
- package/dist/shell/components/settings/ConnectedCompanySettings.d.ts.map +1 -1
- package/dist/shell/components/settings/ConnectedFieldRulesSettings.d.ts.map +1 -1
- package/dist/shell/components/settings/ConnectedSecuritySettings.d.ts.map +1 -1
- package/dist/shell/components/settings/LocationsCrudPage.d.ts +1 -1
- package/dist/shell/components/settings/LocationsCrudPage.d.ts.map +1 -1
- package/dist/shell/components/settings/SettingsPage.d.ts +23 -2
- package/dist/shell/components/settings/SettingsPage.d.ts.map +1 -1
- package/dist/shell/hooks/index.d.ts +3 -2
- package/dist/shell/hooks/index.d.ts.map +1 -1
- package/dist/shell/hooks/useAITools.d.ts +2 -0
- package/dist/shell/hooks/useAITools.d.ts.map +1 -1
- package/dist/shell/hooks/useChat.d.ts +22 -0
- package/dist/shell/hooks/useChat.d.ts.map +1 -1
- package/dist/shell/hooks/useSystemPermission.d.ts +3 -0
- package/dist/shell/hooks/useSystemPermission.d.ts.map +1 -0
- package/dist/shell/hooks/useTenantPluginSettings.d.ts +12 -0
- package/dist/shell/hooks/useTenantPluginSettings.d.ts.map +1 -0
- package/dist/shell/lib/ai-tool-handlers.d.ts +119 -0
- package/dist/shell/lib/ai-tool-handlers.d.ts.map +1 -0
- package/dist/shell/lib/core-ai-tools.d.ts +14 -0
- package/dist/shell/lib/core-ai-tools.d.ts.map +1 -1
- package/dist/shell/lib/create-client-orders-provider.d.ts.map +1 -1
- package/dist/shell/lib/fayz-agent.d.ts +23 -0
- package/dist/shell/lib/fayz-agent.d.ts.map +1 -0
- package/dist/shell/lib/i18n.d.ts.map +1 -1
- package/dist/shell/stores/chat.store.d.ts +69 -0
- package/dist/shell/stores/chat.store.d.ts.map +1 -1
- package/dist/shell/types/billing.d.ts +7 -0
- package/dist/shell/types/billing.d.ts.map +1 -1
- package/dist/shell/types/plugins.d.ts +2 -45
- package/dist/shell/types/plugins.d.ts.map +1 -1
- package/dist/stores/createCrudStore.d.ts.map +1 -1
- package/package.json +14 -11
- package/src/access/context.tsx +228 -0
- package/src/access/headless.ts +71 -0
- package/src/access/index.ts +27 -0
- package/src/access/limits-registry.ts +86 -0
- package/src/access/module-nav.test.ts +78 -0
- package/src/access/module-nav.ts +82 -0
- package/src/access/resolver.test.ts +143 -0
- package/src/access/resolver.ts +5 -0
- package/src/access/types.ts +47 -0
- package/src/access/upgrade-modal-store.ts +30 -0
- package/src/app/AdminShell.tsx +451 -68
- package/src/app/admin-app.tsx +46 -9
- package/src/app/config.ts +54 -1
- package/src/app/core-entities.ts +110 -0
- package/src/app/derive-contract.test.ts +155 -0
- package/src/app/derive-contract.ts +214 -0
- package/src/app/scaffold.tsx +4 -0
- package/src/archetype-lookup.ts +8 -2
- package/src/billing/index.ts +2 -0
- package/src/billing/plan-badge.ts +32 -0
- package/src/billing/store.ts +8 -1
- package/src/components/shared/ContactPicker.tsx +394 -0
- package/src/components/shared/PersonLink.tsx +11 -3
- package/src/components/shared/create-person.ts +74 -0
- package/src/crud/CrudDetailPage.tsx +5 -1
- package/src/crud/CrudFormPage.tsx +113 -6
- package/src/crud/CrudListView.tsx +16 -1
- package/src/crud/CrudPage.tsx +27 -5
- package/src/crud/DeleteConfirmDialog.tsx +1 -1
- package/src/crud/archetypes/AddressesTab.tsx +387 -0
- package/src/crud/archetypes/PersonDetailTabs.tsx +5 -0
- package/src/crud/createCrudPage.tsx +2 -0
- package/src/crud/index.ts +6 -0
- package/src/env.d.ts +7 -0
- package/src/index.ts +49 -1
- package/src/org/store.ts +9 -1
- package/src/permissions/context.tsx +29 -15
- package/src/shell/components/auth/ProtectedRoute.tsx +12 -2
- package/src/shell/components/billing/SoftLimitBanner.tsx +81 -0
- package/src/shell/components/billing/SubscriptionPage.tsx +372 -0
- package/src/shell/components/billing/UpgradeModal.tsx +102 -0
- package/src/shell/components/billing/UpgradePrompt.tsx +229 -0
- package/src/shell/components/billing/access-contract.ts +64 -0
- package/src/shell/components/billing/gates.tsx +62 -0
- package/src/shell/components/billing/index.ts +18 -2
- package/src/shell/components/chat/ChatFab.tsx +4 -2
- package/src/shell/components/chat/ChatPanel.tsx +174 -23
- package/src/shell/components/chat/ConfirmActionCard.tsx +57 -0
- package/src/shell/components/chat/markdown.tsx +102 -0
- package/src/shell/components/organization/TeamTab.tsx +21 -4
- package/src/shell/components/organization/TenantOnboarding.tsx +3 -1
- package/src/shell/components/organization/index.ts +0 -1
- package/src/shell/components/settings/ConnectedCompanySettings.tsx +4 -1
- package/src/shell/components/settings/ConnectedFieldRulesSettings.tsx +3 -1
- package/src/shell/components/settings/ConnectedSecuritySettings.tsx +3 -1
- package/src/shell/components/settings/LocationsCrudPage.ts +6 -1
- package/src/shell/components/settings/SettingsPage.tsx +63 -21
- package/src/shell/hooks/index.ts +3 -2
- package/src/shell/hooks/useAITools.ts +27 -23
- package/src/shell/hooks/useChat.ts +381 -16
- package/src/shell/hooks/useSystemPermission.ts +23 -0
- package/src/shell/hooks/useTenantPluginSettings.ts +127 -0
- package/src/shell/lib/ai-tool-handlers.ts +910 -0
- package/src/shell/lib/core-ai-tools.ts +234 -2
- package/src/shell/lib/create-client-orders-provider.ts +24 -2
- package/src/shell/lib/fayz-agent.ts +56 -0
- package/src/shell/lib/i18n.ts +71 -0
- package/src/shell/stores/chat.store.ts +118 -1
- package/src/shell/styles.css +30 -0
- package/src/shell/types/billing.ts +8 -0
- package/src/shell/types/plugins.ts +13 -50
- package/src/stores/createCrudStore.ts +9 -0
- package/dist/chunk-3M25GRRU.js.map +0 -1
- package/dist/chunk-4XTQ4V5L.cjs.map +0 -1
- package/dist/chunk-5GYQK5IW.js +0 -21
- package/dist/chunk-5GYQK5IW.js.map +0 -1
- package/dist/chunk-IPWCHJPE.cjs.map +0 -1
- package/dist/chunk-KYID2MR6.cjs +0 -23
- package/dist/chunk-KYID2MR6.cjs.map +0 -1
- package/dist/chunk-XNWCGJYH.js.map +0 -1
- package/dist/shell/components/billing/BillingPage.d.ts +0 -7
- package/dist/shell/components/billing/BillingPage.d.ts.map +0 -1
- package/dist/shell/components/billing/PaywallGate.d.ts +0 -11
- package/dist/shell/components/billing/PaywallGate.d.ts.map +0 -1
- package/dist/shell/components/organization/PermissionGate.d.ts +0 -11
- package/dist/shell/components/organization/PermissionGate.d.ts.map +0 -1
- package/dist/shell/hooks/useBilling.d.ts +0 -14
- package/dist/shell/hooks/useBilling.d.ts.map +0 -1
- package/dist/shell/hooks/usePermission.d.ts +0 -16
- package/dist/shell/hooks/usePermission.d.ts.map +0 -1
- package/src/shell/components/billing/BillingPage.tsx +0 -56
- package/src/shell/components/billing/PaywallGate.tsx +0 -80
- package/src/shell/components/organization/PermissionGate.tsx +0 -16
- package/src/shell/hooks/useBilling.ts +0 -151
- 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 {
|
|
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 {
|
|
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')
|